spring-cloud集成grpc笔记-1
- 致谢
- 流程
- 1. 创建eureka服务器
- 2. 创建GRPC服务端
- 3. 创建GRPC客户端
- 4. 踩过的坑
- 总结
致谢
本文部分参考来自于
感谢大佬的分享。
流程
1. 创建eureka服务器
就是创建一个通用的eureka服务器就好了,部分配置可有可无的
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
application.yml
server:
port: 8761
eureka:
instance:
hostname: localhost
prefer-ip-address: true
lease-expiration-duration-in-seconds: 30
lease-renewal-interval-in-seconds: 30
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
#关闭自我保护机制,在测试阶段不停的重启,容易触发自我保护
enable-self-preservation: false
endpoints:
shutdown:
enabled: true
EurekaServerApplication.java
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
至此,一个普通的eureka服务器就搞定了。
2. 创建GRPC服务端
就像平时一样创建一个grpc服务器就可以了,在springcloud+eureka调用的实际使用阶段,服务器的创建基本没有区别。
首先,将pom依赖引入。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-all</artifactId>
<version>1.40.1</version>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.12.0.RELEASE</version>
</dependency>
为了方便proto文件生成java文件,在build中加入对应依赖。
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
在引入上述依赖后,在maven编译中应该出现protobuf:compile等相关操作,此时点击maven的compile即可将/src/main/proto下的proto文件生成对应的java代码。如,我参考大佬的范例编写的hello.proto
syntax = "proto3";
package com.example.grpcserver;
option java_package = "com.example.grpcserver";
message HelloRequest {
string name = 1;
}
message HelloResponse {
string name = 1;
string status = 2;
}
// rpc 服务
service HelloService {
rpc hello(HelloRequest) returns(HelloResponse) {}
}
在编译过后,会在target中显示proto文件编译后的java代码
然后创建测试服务
@GrpcService
@Slf4j
public class TestGrpcService extends HelloServiceGrpc.HelloServiceImplBase {
@Override
public void hello(Hello.HelloRequest request, StreamObserver<Hello.HelloResponse> responseObserver) {
log.info("hello:{}", request.getName());
final Hello.HelloResponse.Builder replyBuilder = Hello.HelloResponse.newBuilder().setName(request.getName()).setStatus("success");
responseObserver.onNext(replyBuilder.build());
responseObserver.onCompleted();
}
}
并对应修改application.yml,并将自己注册到eureka服务器上
server: #服务的端口
port: 8089
grpc: #grpc端口
server:
port: 9090
eureka:
instance:
prefer-ip-address: true
instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: cloud-grpc-server
将服务启动并注册到eureka上就ok了
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class GrpcServerApplication {
public static void main(String[] args) {
SpringApplication.run(GrpcServerApplication.class, args);
}
}
3. 创建GRPC客户端
pom基本一致,就是把server修改一下,变成client就好。还有把服务端的proto文件复制到项目中去,同样编译一下生成java代码。
首先创建客户端的调用
@Service
public class GrpcClientService {
@GrpcClient("cloud-grpc-server")
private Channel channel;
public String hello(String name) {
HelloServiceGrpc.HelloServiceBlockingStub stub = HelloServiceGrpc.newBlockingStub(channel);
Hello.HelloRequest.Builder builder = Hello.HelloRequest.newBuilder().
setName(name);
Hello.HelloResponse response = stub.hello(builder.build());
return "{'responseStatus':'" + response.getStatus() + "','name':'" + response.getName() + "'}";
}
}
这个地方我参考大神写的文章,将@GrpcClient
中的value设置为服务端注册到eureka的服务名,实测下来会报unknown host
错误,后续会进行修改。此处需要在配置文件中获取对应address才可使用。
然后在配置文件中对grpc和eureka进行对应的配置:
server:
port: 8086
spring:
application:
name: cloud-grpc-client
eureka:
instance:
prefer-ip-address: true
instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka/
grpc:
client:
cloud-grpc-server:
address: static://localhost:9090 #指定grpc服务端地址
enableKeepAlive: true
keepAliveWithoutCalls: true
negotiationType: plaintext
到此,创建一个客户端已经完成,剩下就是创建一个测试类去实际试一下了。
创建一个普通的测试接口实际试一下:
@RestController
@Slf4j
public class Controller {
@Autowired
private GrpcClientService service;
@GetMapping(value = "/hello")
public String test(String name) {
try {
String result = service.hello(name);
log.info("respString : {}", result);
return result;
} catch (Throwable e) {
log.error("hello error", e);
}
return null;
}
}
在访问该测试接口后,可以发现已经通过grpc访问到服务端并返回的对应信息。将grpc集成到springcloud到一段落
4. 踩过的坑
就是上面所说到unkownhost
问题,如下图。这解析不了eureka服务名这可如何是好…面对这个问题,我还以为是我本机无法解析这个地址,于是还修改了host文件…保存后发现并不好用。然后我就顺手写了个RestTemplate测试接口,发现http://cloud-grpc-server是可以调用的…得了…从初始化开始看起吧…
2021-09-15 23:06:06.317 WARN 8994 --- [ault-executor-0] io.grpc.internal.ManagedChannelImpl : [Channel<1>: (cloud-grpc-server)] Failed to resolve name. status=Status{code=UNAVAILABLE, description=Unable to resolve host cloud-grpc-server, cause=java.lang.RuntimeException: java.net.UnknownHostException: cloud-grpc-server
at io.grpc.internal.DnsNameResolver.resolveAddresses(DnsNameResolver.java:223)
at io.grpc.internal.DnsNameResolver.doResolve(DnsNameResolver.java:282)
at io.grpc.grpclb.GrpclbNameResolver.doResolve(GrpclbNameResolver.java:63)
at io.grpc.internal.DnsNameResolver$Resolve.run(DnsNameResolver.java:318)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.net.UnknownHostException: cloud-grpc-server
at java.net.InetAddress.getAllByName0(InetAddress.java:1281)
at java.net.InetAddress.getAllByName(InetAddress.java:1193)
at java.net.InetAddress.getAllByName(InetAddress.java:1127)
at io.grpc.internal.DnsNameResolver$JdkAddressResolver.resolveAddress(DnsNameResolver.java:631)
at io.grpc.internal.DnsNameResolver.resolveAddresses(DnsNameResolver.java:219)
... 6 more
根据Channel的源代码,可以看到@GrpcClient会在这里实例化一个ManageChannelImpl
,在io.grpc.internal包下。这里实例化的时候会将target等一系列参数送入,其中发现一个名称解析器nameResolverFactory
,继续debug跟踪下去,发现默认的nameResolver
是一个dnsNameResolver。感觉上是解析器无法将eureka服务名解析出来造成的。于是,根据NameResolver这个父类向下找寻它的实现,看看有没有蛛丝马迹,果然有一个看起来就非常亲切的实现DiscoveryClientNameResolver
。看了一下构造器,只需要一个DiscoveryClient,这简直就是送的,于是自己开始手动创建Channel。
将客户端调用代码修改如下:
@Service
@Slf4j
public class GrpcClientService {
@Autowired
private DiscoveryClient discoveryClient;
private ManagedChannel channel = null;
private Object lock = new Object();
public String hello(String name) {
if (channel == null){
synchronized (lock){
if (channel == null){
log.info("channel已被初始化");
channel = ManagedChannelBuilder.forTarget("http://cloud-grpc-server")
.defaultLoadBalancingPolicy("round-robin")
.nameResolverFactory(new DiscoveryClientResolverFactory(discoveryClient))
.usePlaintext()
.build();
}
}
}
HelloServiceGrpc.HelloServiceBlockingStub stub = HelloServiceGrpc.newBlockingStub(channel);
Hello.HelloRequest.Builder builder = Hello.HelloRequest.newBuilder().
setName(name);
Hello.HelloResponse response = stub.hello(builder.build());
return "{'responseStatus':'" + response.getStatus() + "','name':'" + response.getName() + "'}";
}
}
并去除application.yml中相关配置(手写已经不读这里了,去不去的无所谓)
到此完成所有的步骤,可以使用eureka进行调用了,负载均衡也实现了,搞定。
其实还想将DiscoveryClientNameResolver作为默认的名称解析器来着…但是看到了代码上有行注释// Do not add this to the NameResolverProvider service loader list
呃,作者都这么说了,虽然还不知道原因,接下来有空写进去再看看有什么坑。
总结
grpc在集成进spring-cloud中并不难,重点是搞定服务端和客户端的创建即可。但是部署在容器上,服务端的ip和端口不一定是总保持一致的,所以首先就想到了服务注册与发现,在这里踩了一个小坑。至于为什么不能加到默认的列表里,后续再研究一下。
p.s. 这个源码实现的真多…还没全部捋明白…