Protobuf¶
简介¶
Protobuf 是 google 推出的一种序列化格式, 应用非常广泛,如 grpc、tfrecord。
proto schema 案例¶
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
字段后面的数字是 Field Number , 用于编码索引,其范围为 1~(2^29 - 1)。 推荐将 1~15 范围的数值预留用于频繁访问的属性, 这个范围的 Field Number 进行 Varint 编码后只需要 1 字节, 16~2047 耗费 2 字节,越大的 Field Number 编码后字节越多。
probobuf 编码¶
类型 | 含义 | 适用类型 |
---|---|---|
0 | Varint(变长数值) | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit(64位数值) | fixed64, sfixed64, double |
2 | Length-delimited(指定长度) | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit(32位数值) | fixed32, sfixed32, float |
每一个字段首字节表示类型, byte 末3位是主类型, 前5位是 Field Number, 其中首位 msb (most significant bit) 有特殊用途。
上图表示主类型为0(Varint), Field Number 为 2 。
变长类型
这里的变长类型特指变长数值 - Varint 。 Varint 数值区域中每个 byte 首位标识后面还有没有内容(此标识即 msb)。 当首位为1,表示后面还有内容,为 0 表示当前已经是最后一字节。
这种变长策略是 Protobuf 的亮点, 使整数类型的存储非常紧凑,小整数高位为0,这些无效位可以省略, 所以通常只会占 1 byte,大整数才会占据更多空间 。基于统计,越小的整数使用越频繁,这种策略是合理的。
int32 类型的 -1 二进制是
0xffffffff
,由于补码的特性,绝对值很小的负数有效位非常多。 也许你已经猜到了,对于这个小整数,使用 Varint 编码却需要占据 5 字节。 所以,对于有符号整数, Protobuf 先采用 ZigZag 映射为无符号整数,再进行编码。
定长类型
当 type 字段是定常类型, 只需要从后面取固定长度字节解释为对应类型, 1、5 两种主类型都是定常类型。
指定长度
字符串、结构体等类型长度没有上限,都需要手动指定长度,所以除了首字节type,还有第二段内容说明数据长度, 再后面才是真正的数据。
Optional & Repeated
由于每个字段都有 Field Number 作为索引, Optional 字段可以不出现在编码中,即缺失。
Repeated 字段比较有意思。 Protobuf 存储的策略是相同的 Field Number 可以出现多次, 并且可以不连续, 如下是包含了2个元素的数组(Field Number索引为3);
schema¶
Protobuf 编码有 type 的概念,但是作用很弱, 数据解释很依赖于 schema , 甚至基于 schema 可以对相同的数据做出不同解释。
比如 protobuf 支持的 option、默认值、数组、结构体等概念, 都是在 .proto 文件中描述的,还有涉及到向后兼容性等功能, schema 层承载了大量业务逻辑。
如以下 schema (v1):
message TestMessage {
string query = 1;
}
此时新增一个字段 (v2):
message TestMessage {
string query = 1;
optional string page_number = 2 [default = 10];
}
这种情况,如果 v2 客户端接收到 v1 协议数据, schema 层可以把这个字段补全。 v1 客户端接收到 v2 协议的消息,也能正确忽略这个字段.