本文已经与(1)整合后发在了我的个人网站,欢迎访问:http://www.wendev.site/article/25
写在前面
前一段时间学习SpringCloud时尝试了SpringCloud整合Dubbo,感觉非常棒。那比Dubbo更快、还有着跨语言特性的gRPC与SpringCloud又会碰撞出什么样的火花呢?今天就来试一试。
当然,一开始写的也并不复杂,还是从最简单的一个服务提供者,一个服务消费者,发送一条HelloWorld并显示端口号开始。
服务注册与发现中心选择了Consul
,本来用的是Nacos
,结果出了莫名其妙的Bug(这个在下面会写),换Consul之后一下子就好了。。。
版本
- Java:11
- Consul:1.6.3
- Spring Cloud:Hoxton.RELEASE
- Spring Boot:2.2.4.RELEASE
- gRPC:1.25.0
- grpc-spring-boot-strater:2.6.2.RELEASE GitHub地址
使用Docker启动Consul
以前一直是使用直接运行可执行文件启动Consul,而使用docker比直接运行jar包更方便管理,所以这次就使用Docker来运行。
这里顺便记录一下Nacos的Docker启动方式:
Nacos
docker pull nacos/nacos-server
docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server
Consul
只启动一个实例的话可以这样启动:
docker pull consul
docker run --name consul -p 8500:8500 -d consul
运行完毕就可以通过localhost:8500
访问了。
开始!
整个项目的目录结构:
父级依赖管理工程
4.0.0
site.wendev.grpc
grpc-spring-cloud-final
pom
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-parent
2.2.4.RELEASE
wendev-api
wendev-provider
wendev-consumer
UTF-8
UTF-8
11
Hoxton.RELEASE
0.9.0.RELEASE
1.18.10
1.3.2
1.25.0
2.6.2.RELEASE
site.wendev.grpc
wendev-api
${project.version}
org.springframework.cloud
spring-cloud-dependencies
${spring.cloud.version}
pom
import
org.springframework.cloud
spring-cloud-starter-alibaba-nacos-config
${nacos.version}
org.springframework.cloud
spring-cloud-starter-alibaba-nacos-discovery
${nacos.version}
org.springframework.cloud
spring-cloud-starter-netflix-ribbon
io.grpc
grpc-all
${grpc.version}
io.grpc
grpc-netty-shaded
${grpc.version}
io.grpc
grpc-protobuf
${grpc.version}
io.grpc
grpc-stub
${grpc.version}
javax.annotation
javax.annotation-api
${java.annotation.api.version}
provided
net.devh
grpc-server-spring-boot-starter
${grpc.spring.boot.starter.verison}
net.devh
grpc-client-spring-boot-starter
${grpc.spring.boot.starter.verison}
这个grpc-spring-boot-starter
貌似是国人写的,赞!
公共API模块
Maven依赖,主要是gRPC
的依赖:
4.0.0
grpc-spring-cloud-final
site.wendev.grpc
1.0-SNAPSHOT
wendev-api
wendev-api
jar
1.6.2
3.10.0
0.6.1
io.grpc
grpc-all
javax.annotation
javax.annotation-api
org.springframework.boot
spring-boot-autoconfigure
kr.motd.maven
os-maven-plugin
${os.plugin.version}
org.xolstice.maven.plugins
protobuf-maven-plugin
${protobuf.plugin.version}
com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}
grpc-java
io.grpc:protoc-gen-grpc-java:1.25.0:exe:${os.detected.classifier}
/Users/jiangwen/tools/protoc-3.10.0/bin/protoc
false
compile
compile
compile-custom
首在main
文件夹下建立一个proto
文件夹,写个proto
文件:
hello_world.proto
syntax = "proto3";
option java_package = "site.wendev.grpc.api";
option java_multiple_files = true;
option java_outer_classname = "HelloProto";
service HelloWorld {
rpc Hello (HelloRequest) returns (HelloResponse) {
}
}
message HelloRequest {
string message = 1;
}
message HelloResponse {
string response = 1;
}
然后像上一篇讲的那样把代码生成出来,放到相应目录下,再执行mvn clean install
把这个模块安装在本地,供其他服务调用。
服务提供者
Maven依赖:
4.0.0
grpc-spring-cloud-final
site.wendev.grpc
1.0-SNAPSHOT
wendev-provider
${project.version}
wendev-provider
11
site.wendev.grpc
wendev-api
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-consul-discovery
net.devh
grpc-server-spring-boot-starter
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-maven-plugin
配置:
bootstrap.yml
server:
port: 8763
grpc:
server:
port: 0
spring:
application:
name: wendev-provider
cloud:
consul:
host: 127.0.0.1
port: 8500
discovery:
hostname: 127.0.0.1
gRPC的端口是0,这样写是“端口号随机”的意思,可以保证每次启动端口都不同,就不用手动修改了。server.port
写死了是因为需要把这个值注入,返回给服务消费者,如果不需要返回端口号也可以写0。
然后编写服务提供者:
/**
* 服务提供者:返回服务消费者发送的信息和端口号
* 使用@GrpcService
注解声明这个服务提供者
*
* @author 江文
* @date 2020/2/7 5:12 上午
*/
@GrpcService
public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
@Value("${server.port}")
private String port;
@Override
public void hello(HelloRequest request, StreamObserver responseObserver) {
String message = String.format("Welcome to WenDev, your message is %s, from port %s."
+ "From: Spring Cloud + gRPC.",
request.getMessage(), port);
HelloResponse response = HelloResponse.newBuilder().setResponse(message).build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
题外话:大家可以从注释中看到创建时间是昨天早上,所以我踩坑踩了整整一天。。。写此文的目的除了总结,就是希望大家不要继续踩我踩过的坑。
逻辑很简单,主要就是生成一条响应信息,然后返回响应。代码并不比使用Dubbo
复杂多少。
这里遇到个大坑,差点没把我坑死。。。
最新的gRPC版本是1.27.0
,然而grpc-spring-boot-starter
只支持到1.25,还不兼容1.27。。。晕死,为了换个版本还得重新下一个3.10版本的protoc
(1.27是用的3.11.3版本的),然后重新生成代码。。。说不定我应该去grpc-spring-boot-starter
的仓库里提个issue?
服务消费者
首先是依赖,与服务提供者非常相似,就是grpc-spring-boot-starter
换成了服务消费者的:
4.0.0
grpc-spring-cloud-final
site.wendev.grpc
1.0-SNAPSHOT
wendev-consumer
${project.version}
wendev-consumer
11
site.wendev.grpc
wendev-api
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-consul-discovery
net.devh
grpc-client-spring-boot-starter
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-maven-plugin
配置,也没什么区别。因为没有CA证书,用OpenSSL生成又一直报各种奇奇怪怪的错误,就设定为不加密的plaintext
了:
bootstrap.yml
server:
port: 8720
spring:
application:
name: wendev-consumer
cloud:
consul:
host: 127.0.0.1
port: 8500
discovery:
hostname: 127.0.0.1
grpc:
client:
GLOBAL:
security:
enable-keep-alive: true
keep-alive-without-calls: true
negotiation-type: plaintext
业务逻辑代码,首先是Service:
/**
* 服务消费者:远程调用服务提供者,并且接收消息返回给Controller。
*
* @author 江文
* @date 2020/2/7 5:33 上午
*/
@Service
public class HelloWorldService {
@GrpcClient("wendev-provider")
private HelloWorldGrpc.HelloWorldBlockingStub stub;
public String rpc(String message) {
HelloRequest request = HelloRequest.newBuilder().setMessage(message).build();
return stub.hello(request).getResponse();
}
}
这个@GrpcClient
加在stub
上,或者如同网上绝大多数资料那样加在channel
上都可以,不过官方推荐加在stub
上,就按照推荐的来。
代码同样也不复杂。
然后是Controller层,调用Service里的rpc
方法,返回调用结果:
/**
* Controller层,只有一个方法。
*
* @author 江文
* @date 2020/2/7 5:36 上午
*/
@RestController
public class HelloWorldController {
final HelloWorldService service;
@GetMapping("/{message}")
public String helloWorld(@PathVariable String message) {
return service.rpc(message);
}
HelloWorldController(HelloWorldService service) {
this.service = service;
}
}
运行
先说一下,这里又一个巨坑!比上一个要大得多!排查这个花了我整整一天,最后发现居然不是我的错(虽说选了不合适的注册中心也算是我的错吧)。。。
本来是用的Nacos做的服务注册与发现中心,结果请求总是出错,报network closed for unknown reason
的异常。后来换了Consul,就改了依赖和加上了Consul服务注册与发现的配置代码,业务代码改都没改一下子就好了。。。
看来这是Nacos的bug,具体原因不明。不过用Consul不会出问题,似乎用Eureka也不会。
先启动服务提供者,修改server.port
多启动几份,然后启动服务消费者。在Consul里可以看到,启动成功了:
不过这个健康检查失败也不知道是哪里出了问题,但是服务间调用正常,可能因为我没在yml
文件里配置?
可以看到Tags
里出现了gRPC
的端口号,gRPC客户端(服务消费者)就是通过这个Tag找到gRPC服务端(服务提供者)的端口的。
请求127.0.0.1:8720/HelloWorld
可以发现消息成功返回了:
多刷新几次,可以看到Consul提供的负载均衡效果:
通过这个例子,我们发现gRPC和Spring Cloud在grpc-spring-boot-strater
的帮助下可以配合得非常好。虽然坑有些多,但毕竟gRPC不是Dubbo那种无缝集成的,出现一些坑也是可以理解的。