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代码

springcloud开发GRPC接口 springcloud集成grpc_springcloud开发GRPC接口

然后创建测试服务

@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到一段落

springcloud开发GRPC接口 springcloud集成grpc_springcloud开发GRPC接口_02

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. 这个源码实现的真多…还没全部捋明白…