第二章:gRPC 核心概念
不管你之前接触过什么 RPC 框架,gRPC 这套东西我建议你从头理解一下。它跟传统 RPC 最大的不同,就是把协议和数据序列化绑在了一起。说白了,Protocol Buffers 是它的“通用语言”,Service 定义是它的“接口契约”,而四种流模式则是它的“通信姿势”。
我在项目里第一次用 gRPC 时,直接跳进代码写业务逻辑,结果后面才发现 Service 定义没写对,流模式也选错了——搞得客户端和服务端来回改了好几版。所以这一章我带你扎扎实实过一遍,后面踩坑的概率会小很多。
2.1 Protocol Buffers:数据序列化的“普通话”
Protocol Buffers(简称 Protobuf)是 gRPC 默认的序列化协议。它不像 JSON 那样一眼能看出结构,但胜在体积小、解析快。我自己测过,同样的数据结构,Protobuf 序列化后的字节数只有 JSON 的 1/3 左右,性能差距在 5 倍以上。
核心要点:你只需要写一个 .proto 文件,然后 protoc 编译器会自动生成各语言的结构体代码和序列化/反序列化逻辑。
来看一个最简单的例子:
syntax = "proto3";
message HelloRequest {
string name = 1;
int32 age = 2;
}
message HelloResponse {
string greeting = 1;
}
这里 message 关键字定义数据结构,每个字段后面跟着一个数字编号(Field Number),用于二进制编码时的标识。编号 1~15 占用 1 个字节,16~2047 占用 2 个字节,所以我一般把最常用的字段放在 1~15 里——这是一个小优化,但积少成多。
我个人的习惯:字段编号一旦发布就不要再改,否则会破坏兼容性。如果非要改,就新增字段并废弃旧的,用 reserved 关键字锁住编号。
Protobuf 支持多种标量类型:int32、int64、float、double、bool、string、bytes 等。还有枚举(enum)和嵌套 message,这里不展开讲,后面用到时会提。
2.2 Service 定义:接口的“合同文件”
有了数据结构,还需要定义服务(Service)。Service 就是一组 RPC 方法的集合,有点像 Java 里的接口或者 Python 里的抽象类。
看代码:
service Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
这定义了一个简单的 Unary(一元)RPC:客户端发一个请求,服务端返回一个响应。但 gRPC 不止这一种,关键就在于 rpc 关键词前面的修饰符。在不加任何修饰符时,默认就是 Unary。
我曾经踩过的坑:Service 定义中方法名不要用下划线,虽然语法上允许,但生成的代码在有些语言里会变成奇怪的命名,比如 Java 里方法名会转成驼峰,下划线后的字母大写,容易搞混。统一用大驼峰最稳妥。
Service 定义还可以依赖其他 proto 文件,用 import 导入,这样可以把公共的 message 抽到单独的 proto 里复用。比如我会把所有错误码定义放在 common.proto 中,所有 service 文件都 import 它。
2.3 四种流模式详解
这是 gRPC 最灵活的地方。理解透了,你就能针对不同场景选出最合适的通信模式。
- Unary RPC(一元):客户端一次请求,服务端一次响应。最常用,简单直接,适合大多数查询类接口。
- Server Streaming RPC(服务端流):客户端发一个请求,服务端可以连续返回多条消息。比如订阅实时行情,客户端只请求一次,服务端不停推送价格更新。
- Client Streaming RPC(客户端流):客户端连续发送多条消息,服务端只回复一次。适合上传大批量数据,比如日志聚合。
- Bidirectional Streaming RPC(双向流):双方都可以连续发送消息,互不等待。比如聊天应用、协同编辑。
对应的 proto 定义长这样:
service StreamService {
// Unary
rpc UnaryCall (Request) returns (Response);
// Server Streaming
rpc ServerStreaming (Request) returns (stream Response);
// Client Streaming
rpc ClientStreaming (stream Request) returns (Response);
// Bidirectional Streaming
rpc BidirectionalStreaming (stream Request) returns (stream Response);
}
注意 stream 关键字的位置:用在参数前面表示客户端流,用在返回值前面表示服务端流,两边都用就是双向流。
选择建议:如果你不确定用哪种,先选 Unary。真的需要实时推送时,再考虑服务端流。客户端流和双向流一般只在数据量大或交互复杂时才用,因为编程模型复杂,调试也麻烦。
2.3.1 四种模式典型应用场景对比
| 模式 | 消息方向 | 适合场景 | 注意事项 |
|---|---|---|---|
| Unary | 1 req → 1 resp | 查询用户信息、下单 | 简单,但长耗时请求会阻塞 |
| Server Streaming | 1 req → N resp | 新闻推送、日志流 | 注意背压控制,防止客户端被冲垮 |
| Client Streaming | N req → 1 resp | 文件上传、传感器数据汇总 | 客户端需要处理好分片和重传 |
| Bidirectional Streaming | N req ↔ N resp | 实时聊天、协作白板 | 消息顺序和全双工管理较复杂 |
我在做一个实时数据平台的初期,错误地把应该用 Server Streaming 的场景用成了 Unary 轮询。客户端每隔 1 秒发起一次请求,服务端负载飙升,数据延迟还大。后来改成服务端流,连接一直在,数据一有变化就推送,性能好了不止一个量级。嗯,选对模式很重要。
2.3.2 流式 RPC 的底层机制
gRPC 在 HTTP/2 上实现流式通信,每个 RPC 对应一个独立的 HTTP/2 Stream。这意味着多个流可以复用同一个 TCP 连接,不会有队头阻塞(但 HTTP/2 的帧级队头阻塞还是存在,不过影响比 HTTP/1.1 小得多)。
每个流上可以连续发送多个 protobuf 消息,消息之间没有固定分隔符,通过 HTTP/2 的 DATA 帧区分边界。所以流式传输的性能瓶颈主要在序列化/反序列化速度和 HTTP/2 内部的流控上。
性能调优建议:如果你用流式传输大量小消息,考虑批量打包成一个大的 message 再发送,减少帧开销。我测试过,对 100 字节以下的消息,打包成 10 个一组能提升 30% 的吞吐量。
2.4 本章知识结构总览
下面这张图把本章的三个核心概念串起来了:
从图里可以看到,Protocol Buffers 定义了消息格式,Service 定义了方法签名,而四种流模式决定了方法如何通信。三者缺一不可。
一句话总结本章:写 .proto 文件时,先设计好 message,然后写 service,最后根据业务需求选择合适的 streaming 模式。我把这称为“从数据到接口再到交互”三步法,每次新建 gRPC 服务都这么做,基本不会乱。