RPC 核心概念 02:IDL 与 Protobuf 详解
RPC 的精髓之一就是接口先行——双方先约定好接口长什么样,再各自实现。这份"约定"的载体就是IDL(Interface Definition Language,接口定义语言)。
一、为什么需要 IDL?
设想一个场景:A 团队提供 UserService,B 团队要调用。
方案一:B 团队对照文档手写客户端代码,调试发现字段名拼写错了,浪费一天。
方案二:A、B 共用一份 IDL 文件,工具自动生成两端代码,类型与签名编译期保证。
IDL 解决的问题:
- 接口契约:字段名、类型、方法签名权威定义;
- 代码自动生成:避免手写客户端/服务端骨架;
- 跨语言:一份 IDL 生成 Go/Java/Python/C++ 代码;
- 版本演进:增删字段有规则可循。
二、主流 IDL 一览
| IDL | 代表框架 | 特点 |
|---|---|---|
| Protobuf | gRPC、tRPC | 二进制紧凑,跨语言生态最强 |
| Thrift | Apache Thrift | Facebook 出品,自带服务定义 |
| Avro | Hadoop 生态 | 内嵌 schema 适合数据存储 |
| FlatBuffers | 游戏、移动端 | 零拷贝反序列化 |
本文聚焦Protobuf,因为它是事实上的工业标准。
三、Protobuf 简介
Protobuf(Protocol Buffers)是 Google 在 2008 年开源的二进制序列化协议。当前主流版本是proto3。
3.1 一个最简单的 .proto 文件
syntax = "proto3"; package user.v1; option go_package = "github.com/example/user/v1"; service UserService { rpc GetUser (GetUserRequest) returns (GetUserResponse); rpc CreateUser (CreateUserRequest) returns (CreateUserResponse); } message GetUserRequest { int64 id = 1; } message GetUserResponse { int64 id = 1; string name = 2; string email = 3; } message CreateUserRequest { string name = 1; string email = 2; } message CreateUserResponse { int64 id = 1; }四、字段与编号
每个字段必须有一个唯一的整数编号(field number):
message User { int64 id = 1; string name = 2; }编号规则:
- 1~15:占用 1 字节,应分配给最常用字段;
- 16~2047:占用 2 字节;
- 19000~19999:保留给 protobuf 自己使用;
- 一旦发布,编号永远不要复用!
五、数据类型
5.1 标量类型
| Protobuf | Go | Java |
|---|---|---|
| double | float64 | double |
| float | float32 | float |
| int32 / int64 | int32 / int64 | int / long |
| uint32 / uint64 | uint32 / uint64 | int / long |
| sint32 / sint64 | int32 / int64 | int / long |
| bool | bool | boolean |
| string | string | String |
| bytes | []byte | ByteString |
小贴士:
sintXX用 ZigZag 编码,对负数更紧凑;fixedXX总是占固定字节,适合大概率较大的数值。
5.2 复合类型
message Address { string city = 1; string country = 2; } message User { int64 id = 1; string name = 2; Address address = 3; // 嵌套消息 repeated string tags = 4; // 数组(slice) map<string, string> meta = 5; // map }5.3 枚举
enum Gender { GENDER_UNKNOWN = 0; // proto3 要求第一个值为 0 GENDER_MALE = 1; GENDER_FEMALE = 2; }5.4 oneof(多选一)
message Result { oneof payload { string text = 1; bytes binary = 2; int32 code = 3; } }六、Protobuf 的二进制格式
Protobuf 使用TLV(Tag-Length-Value)编码:
[field_number << 3 | wire_type] [length] [value]例如int32 id = 1; id = 150:
08 96 0108=(1 << 3) | 0:field 1, wire type 0(varint)96 01= varint 编码的 150
二进制紧凑性是 protobuf 比 JSON 快 5~10 倍的根本原因。
七、proto3 的关键特性
7.1 默认值与缺省字段
proto3 中每个字段都有默认值(int=0, string=“”),反序列化时无法区分"未设置"与"显式设置为 0",这在某些场景会带来困扰。
解决方案:
- 使用
google.protobuf.Int32Value等包装类型; - proto3 后续支持
optional关键字(推荐):
message User { optional int32 age = 1; // 可区分未设置和 0 }7.2 字段保留(reserved)
删除字段时务必保留编号防止后人复用:
message Foo { reserved 2, 5, 9 to 11; reserved "old_name"; }八、版本演进规则
为了前后向兼容:
✅可以做的:
- 新增字段(旧客户端会忽略);
- 删除字段并
reserved编号; - 把
int32改为int64(同 wire type)。
❌不能做的:
- 修改字段编号;
- 修改字段类型(除非兼容);
- 删除字段不 reserved。
九、protoc 与代码生成
9.1 安装 protoc
brewinstallprotobuf brewinstallprotoc-gen-go brewinstallprotoc-gen-go-grpc9.2 生成 Go 代码
protoc--go_out=.--go_opt=paths=source_relative\--go-grpc_out=. --go-grpc_opt=paths=source_relative\user/v1/user.proto生成的user.pb.go包含 message 结构体和序列化代码;user_grpc.pb.go包含服务端、客户端骨架。
9.3 工程化推荐
使用 buf 管理 proto 文件:
buf generate buf lint buf breaking--against'.git#branch=main'buf breaking能自动检测破坏性变更,强烈推荐 CI 集成。
十、Service 定义与四种调用模式
service Chat { // 1. Unary:一来一回 rpc Send (Msg) returns (Ack); // 2. Server Streaming:客户端发一次,服务端流式返回 rpc Subscribe (SubReq) returns (stream Msg); // 3. Client Streaming:客户端流式上传,服务端一次返回 rpc Upload (stream Chunk) returns (UploadAck); // 4. Bidirectional Streaming:双向流 rpc Talk (stream Msg) returns (stream Msg); }十一、与 JSON 的转换
Protobuf 可以序列化为 JSON 形式,便于调试:
import"google.golang.org/protobuf/encoding/protojson"data,_:=protojson.Marshal(user)fmt.Println(string(data))但生产环境强烈建议使用二进制格式,性能更好、流量更省。
十二、小结
- IDL 是 RPC 的接口契约,是跨团队协作的基石;
- Protobuf = IDL + 高效二进制序列化协议;
- 字段编号一经发布永不复用;
- proto3 的
optional解决了零值歧义问题; - 配合
buf可以做工程化版本管理。
下一篇我们看看 RPC 的"血液循环":序列化与传输协议。