Protocol Buffers(常简称 Protobuf)是一种语言无关、平台无关的结构化数据序列化机制,用 .proto schema 定义消息结构,再生成不同语言的读写代码,把结构化对象编码成稳定的 Wire Format。
核心问题
程序内部的数据结构不能直接跨语言、跨进程或跨机器传输。JSON 容易读,但类型弱、字段名重复、体积较大;直接传内存结构又会受语言运行时、字节序、对齐和版本影响。Protocol Buffers 解决的是:用一份 schema 约定字段编号和类型,让多语言程序都能用同一种二进制格式读写同一类消息。
核心对象
| 对象 | 作用 | 工程关注点 |
|---|---|---|
.proto 文件 | schema 源文件 | package、message、service、import |
| Message | 一种结构化记录类型 | 字段编号、字段类型、嵌套结构 |
| Field number | 字段在 wire format 中的稳定编号 | 一旦发布不能随便复用 |
| Field type | 字段值类型 | int、string、bool、message、repeated 等 |
| Cardinality | 字段出现次数语义 | singular、optional、repeated |
| Generated code | 由 protoc 生成的语言绑定 | builder、getter、parser、serializer |
| Wire format | 实际写入网络或文件的字节布局 | varint、length-delimited、未知字段 |
核心机制
一个最小 .proto 可以写成:
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}这里 name = 1 的 1 不是默认值,而是字段编号。Protobuf 的二进制编码主要靠字段编号和 wire type 识别字段,而不是靠字段名。因此改字段名通常不破坏已发布数据,但复用或改错字段编号会破坏兼容性。
编码时,每个字段大致变成:
field key = (field_number << 3) | wire_type
field bytes = encode(field key) + encode(value)field_number是.proto里写的编号,例如1、2、3。wire_type表示底层编码类别,例如 varint、fixed32、fixed64、length-delimited。<< 3是按位左移 3 位,给低 3 位留出 wire type 的位置。|是按位或,把字段编号和 wire type 合成一个 key。encode(value)按字段类型把值编码成字节,例如整数常用 varint,字符串常用长度前缀加 UTF-8 字节。
读消息时,解析器不需要字段名。它读到 field key 后拆出字段编号和 wire type,再根据当前 schema 把值填入生成代码里的对象。如果遇到未知字段,较新的实现通常可以跳过或保留,从而支持“新发送方给旧接收方多发字段”的演进模式。
工程用途
- 定义跨语言通信协议,例如 gRPC service 和 OTLP schema。
- 保存结构化二进制文件或队列消息。
- 用稳定 schema 支撑接口演进、测试向量和代码生成。
- 降低手写解析器的错误率,尤其是多语言 SDK 和后端服务之间。
观察 protobuf 使用质量时,重点看字段编号是否稳定、未知字段策略、消息大小、解析失败率、schema 版本、生成代码版本和跨语言测试覆盖。
边界与常见坑
- Protobuf 不是自描述格式:只拿到字节通常不能完整理解含义,接收方还需要对应
.protoschema。 - 不要复用已删除字段编号:旧数据或旧客户端可能仍使用该编号,应使用
reserved保留。 - 字段名不是 wire-level 身份:二进制格式核心依赖字段编号,改名和改编号的风险完全不同。
- 默认值和 presence 要小心:特别是 proto3 中,字段“没出现”和“出现但等于默认值”的语义可能影响业务判断。
- 不是压缩或加密方案:protobuf 比很多文本格式更紧凑,但不等于压缩;也不提供保密性或完整性。
- 不适合所有大数组场景:大量浮点矩阵、图像或科学计算数据可能需要专门格式,否则解析和内存开销不一定理想。