前情提要
之前咱们聊过SpringBoot下开启多端口有3个思路,并分析了第一种开启独立management端口的实现细节,今天咱们聊聊开启gRPC端口的实现。
一、gRPC的特别之处
基于SpringBoot的微服务,其通信大多基于HTTP+JSON。前者为通信协议,后者为数据内容格式。更进一步说,这HTTP大多也是HTTP/1.1,随着HTTP协议的发展HTTP/2, HTTP/3也已经出来了。此外, 数据格式也未必都是JSON, XML、Protobuf、Thrift, Avro等结构化格式都是可行的。gRPC使用的默认是HTTP/2+Protobuf,其特别之处主要在HTTP/2与HTTP/1.1的区别。
HTTP/1.1 | HTTP/2 | |
---|---|---|
内容编码 | 文本编码 | 二进制编码, 相同信息比文本编码报文更短 |
网络请求方式 | 在同一链接上,多个请求依次发送和接收 | 在同一连接上,多个情况可以流水线发送 |
此外, HTTP/2还新增了header压缩和服务端push等特性。由此可见,gRPC依赖的HTTP/2与HTTP/1的有截然不同的要求, 在实际使用中也要专门处理,其中有独立的HTTP/2相关的协议编解码,应用层的消息编解码等。不过,这些也都被gRPC框架给屏蔽了。关于gRPC本身的实现细节在此不做讨论,我们关注在与SpringBoot的集成。
二、粗暴方案
手动创建相关对象,暴露到Spring容器中。
原始的GrpcObservabilityServer
此处为官网原始的GrpcObservabilityServer
public class GcpObservabilityServer {
private Server server;
private void start() throws IOException {
int port = 50051;
server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create())
.addService(new GreeterImpl())
.build()
.start();
logger.info("Server started, listening on " + port);
}
private void stop() throws InterruptedException {
if (server != null) {
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
}
}
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
/**
* Main launches the server from the command line.
*/
public static void main(String[] args) throws IOException, InterruptedException {
final GcpObservabilityServer server = new GcpObservabilityServer();
server.start();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.err.println("*** shutting down gRPC server since JVM is shutting down");
try {
server.stop();
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
// Shut down observability
observability.close();
System.err.println("*** server shut down");
}
});
server.blockUntilShutdown();
}
static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
}
集成支持
@SpringBootApplication
public class Starter {
public static void main(String[] args) {
SpringApplication.run(Starter.class, args);
}
@Bean
public Server newGrpcServer() {
final GcpObservabilityServer server = new GcpObservabilityServer();
server.start();
// 最好再实现DisposableBean,当容器关闭时,关闭掉server实例。此处仅为演示
}
}
方案评价
这个玩法实在过于暴力:
- 配置与代码耦合:端口指定完全可以依赖配置文件;
- 纯手工组装(容易出错):GreeterImpl如果依赖容器中的其他类,无法自动组装,同样的问题Server对象的组装也是存在的;
- 扩展性上来说,每新增一个grpcService都要记得在server端注册;
三、改进方案
基于grpc-spring-boot-starter,Maven依赖声明如下
<dependency>
<groupId>io.github.lognet</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>4.2.2</version>
</dependency>
基本原理
- 通过SpringBoot的AutoConfiguration机制创建Bean GrpcServerRunner;
- GrpcServerRunner implements CommandLineRunner接口, 容器初始化完成后执行;
- 扫描容器中所有带@GrpcService注解并且类型为BindableService的Bean, 加入到ServerBuilder中;
- 结合最终GrpcServerProperties对象, 构造server并启动;
到这里整个Application就默认开放两个端口, 默认的HTTP/1.1端口和GRPC使用的HTTP/2.0端口。
改造结果
- 实际的service增加注解
// 使当前类被容器自动装配,后续就可以由Spring容器注入依赖对象
@Component
// 被GrpcRunner识别,组装GrpcServer用
@GrpcService
public class GreeterImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
- 增加必要的配置
grpc:
port: 8092
netty-server:
max-inbound-message-size: 10485760
-
保证GreeterImpl会被正确扫描到即可;
-
测试用环境说明
SpringBoot 2.7.2
Grpc 1.33.0
四、小结
本文介绍了SpringBoot下开启gRPC端口的两种方案,前者仅由容器管理声明周期,后者由框架负责对象的配置,生成和生命周期,开发者可以更多关注自身业务。实际工作中如何处理,还请读者“量体裁衣”,感谢你的阅读。