3. gRPC 基础:Protocol Buffers、服务定义、流式通信
聊完 RESTful,咱们该看看 gRPC 了。我最早接触 gRPC 是在一个微服务改造项目里,那时团队被 JSON 序列化的性能折腾得够呛。
gRPC 跟 RESTful 最大的不同,就是它默认用了 Protocol Buffers 来做序列化协议。说白了,它把接口定义和数据格式硬绑定在了一起。这招挺聪明,但也让初学者有点不适应。
3.1 Protocol Buffers:不只是序列化
Protocol Buffers,简称 protobuf,是 Google 搞出来的一种结构化数据序列化方式。我习惯叫它 pb。
为什么说它不只是序列化?因为它还定义了消息的结构、字段的类型、版本控制规则。你用 .proto 文件写好结构,它就能自动生成各种语言的代码。
syntax = "proto3";
message SecretItem {
string key = 1;
string value = 2;
int64 version = 3;
map<string, string> metadata = 4;
}
这段代码定义了四个字段。每个字段都有编号,从1开始。这是关键:编号不能重复,也不能改。我踩过坑,曾经把一个字段编号从2改成5,结果新旧版本反序列化全乱了。
reserved 关键字,防止别人误用。
3.2 服务定义:用接口驱动开发
gRPC 的服务定义也是写在 .proto 文件里的。本质上就是定义 RPC 方法——请求的消息类型、响应的消息类型。你可以把 .proto 文件看作一个严格的合同。
service SecretManager {
rpc GetSecret (GetSecretRequest) returns (GetSecretResponse);
rpc ListSecrets (ListSecretsRequest) returns (ListSecretsResponse);
rpc WatchSecret (WatchSecretRequest) returns (stream SecretEvent);
rpc BatchUpdate (stream BatchUpdateRequest) returns (BatchUpdateResponse);
rpc ChatPipeline (stream ChatMessage) returns (stream ChatMessage);
}
这段代码里定义了五种调用模式。gRPC 一共支持四种流式通信模式,上面这段正好全用上了。
| 模式 | 示例方法 | 客户端 | 服务端 |
|---|---|---|---|
| 一元 RPC | GetSecret |
请求一次 | 响应一次 |
| 服务端流 RPC | WatchSecret |
请求一次 | 多次响应 |
| 客户端流 RPC | BatchUpdate |
多次请求 | 一次响应 |
| 双向流 RPC | ChatPipeline |
多次请求 | 多次响应 |
3.3 流式通信:核心优势的体现
gRPC 的流式通信建立在 HTTP/2 的长连接上。它跟 WebSocket 不太一样,WebSocket 是双向字节流,而 gRPC 流是带了 protobuf 消息边界的流。嗯,这样说可能有点抽象。
拿 WatchSecret 举例吧——客户端发一个请求说「我要监听 secret_a 的变化」,服务端持续推送事件。每次推送都是一个完整的 protobuf 消息。客户端收完一个就解析一个,不粘连。
// 服务端编写流式逻辑
public void watchSecret(WatchSecretRequest request,
StreamObserver<SecretEvent> responseObserver) {
// 挂起线程,监听变化
watcher.addListener(event -> {
SecretEvent reply = SecretEvent.newBuilder()
.setKey(event.getKey())
.setAction(event.getAction())
.setTimestamp(System.currentTimeMillis())
.build();
responseObserver.onNext(reply);
});
// 不要忘记标记完成
// responseObserver.onCompleted();
}
我在实际项目里见过有人忘调 onCompleted(),结果连接一直挂着,客户端以为还有数据要等。这是一个很隐蔽的 bug。
onCompleted()——比如连接数到达上限、授权过期、或者监听器给出完成信号。不然连接不会自动回收,最终泄漏。
双向流 ChatPipeline 模式,我用的最多的是做智能客服的实时对话系统。客户端持续发消息,服务端持续回消息。这种模式下,两边可以同时读写,因为 HTTP/2 是多路复用的。
为什么会有人觉得双向流难?因为很容易把并发写和顺序搞混。记住一点:gRPC 保证消息在一条流内的有序性,但不保证不同流的先后顺序。真要做全序,你得在应用层加序号。
3.4 本章知识体系总览
下面这张图把本章的核心脉络画出来了。你看一眼就能明白 gRPC 的函数调用关系:proto 文件定义服务和消息,编译器生成客户端和服务端存根,然后通过 HTTP/2 实现四种通信模式。
你看这个图,左边是客户端,右边是服务端,中间四个盒子就是 gRPC 的四种通信模式。每种模式都用不同的颜色区分了。
我在这张图里刻意把双向流的说明写成「双工、无序」。你想想看,双向流是真的双工——两边可以同时读写,不一定要等对方。但正因为如此,才更容易出现消息顺序的问题。我在一个对接第三方支付的项目里就吃过这个亏。
3.5 流式通信的最佳实践
讲几点我在实战中总结的经验:
- 务必设置超时:gRPC 默认是不超时的。一条服务端流挂上,如果服务端挂了,客户端就卡死了。自己设个
withDeadlineAfter把超时限制住。 - 少量控制消息:双向流里,不要频繁发送小消息。HTTP/2 的帧头开销不小,小消息多了,带宽都浪费在协议头上了。我一般会攒够一定的量或时间再发送。
- 注意 goroutine/携程安全:服务端处理多条流时,每个流都是独立的协程。在实现回调时一定要考虑并发写入,该加锁的加锁,该用并发容器的用并发容器。
.proto 文件里给每个 RPC 方法写注释,说明它是什么模式、预期行为、超时建议、还有示例调用。这样无论是生成文档还是 debug 都非常顺手。
嗯,gRPC 基础的三个核心要点就这些了。Protocol Buffers 定义数据结构,服务定义描述接口契约,流式通信提供四种交互方式。下次碰到需要实时推送、大量数据传输、或者低延迟的场景,你想想看,gRPC 是不是一个更好的选择?