Protobuf编码规则详解
1 Message 结构
Message由一系列field组成,每个字段都使用TLV(Tag-Length-Value)结构形式。每个字段都有一个 tag 值,length 表示 value 数据编码后的长度
,length 不是必须的,对于固定长度的和使用Varint编码的 value,是没有 length 的。value 是数据本身的内容。
1.1 tag
- tag 有 field_number 和 wire_type 两部分组成,组成格式:field_num << 3 | wire_type
tag使用Varint编码
- tag是要占空间的,如果tag>16时,KEY的编码就会占用2个字节了
结构如下图
1.1.1 字段编号(field_num)
就是.proto文件中定义的字段编号
1.1.2 传输类型(wire_type)
每个字段都有一个对应的字段(传输)类型,如下表:
Type | Meaning | Used For | Structure | value的字节序 | value编码格式 | Length |
---|---|---|---|---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum | Tag-Value | 小端字节序 | Varint编码 | 变长 无Length值 |
1 | 64-bit | fixed64, sfixed64, double | Tag-Value | 小端字节序 | 非Varint编码 | 固定8字节 |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields | Tag-Length-Value | 变长,有Length值 | ||
3 | Start group | groups (deprecated) | ||||
4 | End group | groups (deprecated) | ||||
5 | 32-bit | fixed32, sfixed32, float | Tag-Value | 小端字节序 | 非Varint编码 | 固定4字节 |
- 消息的二进制格式只使用消息字段的字段编号(field_num)和write_type(根据proto文件定义的类型对应而来)作为Tag的一部分,字段名和声名类型只能在解析端通过引用参考消息类型的定义(即.proto文件)才能确定。
- 解码的时候解码程序(解码器)读入二进制的字节流,解析出每一个field;如果解码过程中遇到识别不出来的filed_num就直接跳过。这样的机制保证了即使该消息(message)添加了新的字段,也不会影响旧的编/解码程序正常工作。
1.2 字段顺序
字段编号可以在 .proto 文件中以任何顺序使用,编码 / 解码与字段顺序无关。
序列化 message 时,对于如何写入其已知字段或未知字段没有保证的顺序。解析消息不能认为filed_num=1 的消息一定在最前。
1.3 默认值
编码时如果没有对字段设置值,protobuf就不会把该字段编码到消息中。
解析数据时,如果编码的消息不包含特定的字段,则解析将对象中的相应字段将设置为该字段的默认值
不同类型的默认值不同,具体如下:
- 对于字符串,默认值为null
- 对于字节,默认值为空字节
- 对于bool,默认值为false
- 对于数字类型,默认值为零
- 对于枚举,默认值是第一个定义的枚举值,该值必须为0。
- repeated字段默认值是空列表
- message字段的默认值为空对象
2 编码
2.1 Varint编码
Varint是一种将一个整数序列化为一个或者多个Byte的方法。越小的整数,使用的Bytes越少。Varint规则如下
- 每个Byte的最高位是标志位(msb, most significant bit)。如果值为1,表示该Bytes后面还有其他Byte;如果该位为0,表示该Byte是最后一个Byte。
- 每个Byte的低7位是用来存数值的位。
Varint方法使用小端字节序(低位在前编码)
,通常都是大端字节序(高位在前)
2.1.1 Varint编码过程
步骤/值(10进制) | 65 | 128 | |
---|---|---|---|
1 | 大端字节序二进制(低位在后/右) | 1000001 | 10000000 |
2 | 7位一分隔 (从低开始计数分隔) | 1000001 | 0000001,0000000 |
3 | 补标志位 | 0 1000001,后边高位没1了,标志位补0 |
0 0000001(后边高位没1了,标志位补0),1 0000000(后边高位有1标志位补1) |
4 | 翻转变为小端字节序(低位在前/左) | 0 1000001 |
1 0000000 0 0000001 |
2.1.2解码过程
Varints 的解码就是对编码的逆操作
10进制数字 | 65 | 128 | |
---|---|---|---|
1 | pb编码值(小端字节序,低位在前/左),从低位(左)8位一分格 | 0 1000001 |
1 0000000 0 0000001 |
2 | 去补标志位(最高位) | 1000001 | 0000000 0000001 |
3 | 翻转为大端字节序(低位在后/右) | 1000001 | 0000001 0000000 |
2.1.3 存储
一个字节的 Varints 编码有 7 位可以存储数据(最高位为 msb),则可以传输 [ 0 , 2^7 -1] 以此类推,两个字节就是 [ 2 ^7 , 2^14 − 1 ]
2.1.4 小结
Varint 确实是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。比如对于 int32 类型的数字,一般需要 4 个字节来表示。但是采用 Varints,对于很小的 int32 类型的数字,则可以用 1 个字节来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个字节来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。如果确定传输大的数字,可以考虑fixed32/fixed64 类型
2.2 有符号整数(sint32和sint64)编码的问题与zigzag优化
protocol buffer中 write_type=0 的都使用Varint编码。当数值为负数时,有符号整型(sint32, sint64)和标准整型(int32, int64)有一个重要的差别。如果使用 int32 或 int64 存储负数,那么 Varints 编码后的结果一定是 10 个字节(int32 类型的负数也是占用10个字节)。而如果使用 sint32 或 sint64 存储负数,则会使用效率更高的 ZigZag 编码。
为此 Protobuf 定义了 sint32 和 sint