模式、代码与数据各司其职

Protocol Buffers,常简称 protobuf,以 .proto 文件描述消息及字段。开发者声明字段的类型、名称和编号,再用 protoc 等工具生成目标语言代码,获得读写字段、序列化和解析消息的接口。模式不是某一条消息的内容,而是解释许多消息的约定。本文以 proto3 规则为主,并在涉及 Editions 时另作区分。

例如消息可以把编号 1 留给整数计数、编号 2 留给字符串名称。接收方要使用相容的定义理解它们,而不是依赖发送方的程序语言。序列化解决的是对象怎样表示为字节,不会单凭这些字节建立连接、认证对方或提供机密性;这些要求须由承载它的系统另行处理。

参考:[1] [2]

字段编号怎样进入字节流

二进制记录的标签把字段编号和线型合在一起,计算方式是字段编号左移三位,再并入线型。线型帮助解析器识别变长整数、定长值或带长度的数据,但并不携带完整字段名称和业务含义;解释这些内容仍需要模式。字符串、嵌套消息等使用带长度的表示,未知字段因此也能按编码边界被跳过。

官方编码示例把编号 1 的 int32 字段设为 150,结果是十六进制 08 96 01。08 表示字段 1、变长整数线型;后两字节分段保存数值,每字节用七位承载数据,以最高位指示是否继续。这解释了小整数常能较紧凑表示的原因,却不能推导出任意消息都比其他格式更小。

参考:[2]

零值不一定表示没有设置

proto3 中,未加 optional 的单值基本类型通常使用隐式存在性:读取未设置的整数得到零,而显式写入零也通常不进入序列化结果。两种情况的值相同,应用却可能想分别表达“保留原值”和“把它改为零”。因此不能只凭读取结果推断发送者是否提供了字段。

给这些字段加 optional 可以显式记录是否设置;已设置的默认值也可序列化。消息字段和 oneof 本来就具有存在性,而 repeated 和 map 不区分“空”与“未出现”。Edition 2023 所属的 Editions 体系默认采用显式存在性,但支持的特性设置仍须核对。新旧程序若分别使用显式和隐式规则,即使字节可解析,往返传递也可能丢掉“曾设置为零”的信息。

参考:[3]

可解析不等于业务兼容

二进制消息通常允许增加字段:旧程序不认识的字段按未知字段处理,并可在再次序列化时保留。删除字段后应保留其编号,避免后来把同一编号分配给不同含义;修改已有编号则不是安全升级。即使编码规则允许某种变动,调用代码或业务判断仍可能被破坏,部署前必须分别检查。

ProtoJSON 是另一套映射,而非二进制消息换一种显示方式。它把字段名和枚举名称写进文本,使用十进制字符串表示 int64,使用 Base64 表示字节,因此名称变化也会影响互通。它不保留二进制格式中的未知字段;把消息转成 JSON 再转回去,可能丢失旧程序不认识的数据。选择格式时,应把可读性、模式演进和实际数据路径一起考虑。

参考:[1] [4]