Protobuf编码规则详解
- 1 Message 结构
- 1.1 tag
- 1.1.1 字段编号(field_num)
- 1.1.2 传输类型(wire_type)
- 1.2 字段顺序
- 1.3 默认值
- 2 编码
- 2.1 Varint编码
- 2.1.1 Varint编码过程
- 2.1.2解码过程
- 2.1.3 存储
- 2.1.4 小结
- 2.2 有符号整数(sint32和sint64)编码的问题与zigzag优化
- 3 编码实践
- 3.1测试数据
- 3.2 write_type=0 编码
- 3.2.1 int32, int64, uint32, uint64 编码
- 3.2.2 sint32 sint64 编码
- 3.2.3 enum 编码
- 3.2.4 bool编码
- 3.3 write_type=5 编码
- 3.3.1 float 编码
- 3.3.2 fixed32编码
- 3.3.3 sfixed32编码
- 3.3 write_type=1 编码
- 3.3.1 double编码
- 3.3.2 fixed64编码
- 3.3.3 sfixed64编码
- 3.4 write_type=2 编码
- 3.4.1 string编码
- 3.4.2 repeated [packed] 原始数字类型 编码
- 3.4.3 嵌套Message编码
- 3.4.2 repeated Message
- 3.4.2 optional 不设置
- 4 使用建议
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 和 sint64 这种类型,采用 ZigZag 编码。将所有整数映射成无符号整数,然后再采用 Varints 编码方式编码,这样绝对值小的整数,编码后也会有一个较小的 varint 编码值。ZigZag 映射函数为:
Zigzag(n) = (n << 1) ^ (n >> 31), n 为 sint32 时
Zigzag(n) = (n << 1) ^ (n >> 63), n 为 sint64 时
或简单理解:
正数 Zigzag(n) =2n
负数 Zigzag(n) =2abs(n)-1
映射结果如下:
Signed Original | Encoded As |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2 | 4 |
-3 | 5 |
… | … |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
3 编码实践
3.1测试数据
message S2
{optional int32 s2_1 = 1;optional string s2_2 = 2 ;
}enum E1
{E1_0 = 0;E1_1 = 1;E1_3 = 3;E1_5 = 5;
}message S3
{optional int32 s3_1 = 1; //设置为0x88optional int32 s3_2 = 2; //设置为0x8888optional uint32 s3_3 = 3; //设置为0xE8E8E8optional uint32 s3_4 = 4; //设置为0xE8E8E8E8optional int64 s3_5 = 5; //设置为0x8888optional int64 s3_6 = 6; //设置为0xE8E8E8E8optional uint64 s3_7 = 7; //设置为0xE8E8E8E8optional uint64 s3_8 = 8; //设置为0xE8E8E8E8E8E8E8E8optional sint32 s3_9 = 9; //设置为0x8888optional sint32 s3_10 = 10; //设置为-0x8888optional sint64 s3_64 = 64; //注意这个tag id 设置为0xE8E8E8E8optional sint64 s3_65 = 65; //注意这个tag id 设置为-0xE8E8E8E8optional E1 s3_11 = 11; //设置为E1_5optional bool s3_12 = 12; //设置为trueoptional float s3_13 = 13; //设置 float,设置为88.888optional fixed32 s3_14 = 14; //设置为 0x8888optional sfixed32 s3_15 = 15; //设置为 -0x8888optional double s3_16 = 16; //设置 double,设置为8888.8888optional fixed64 s3_17 = 17; //设置为 0x8888888888optional sfixed64 s3_18 = 18; //设置为 -0x8888888888optional string s3_19 = 19; //设置为 "I love you,C++!"optional bytes s3_20 = 20; //设置为 "I hate you,C++!"repeated int32 s3_21 = 21 [packed = false]; //设置为3, 270, and 86942, 用google文档的例子repeated int32 s3_22 = 22 [packed = true]; //设置为3, 270, and 86942repeated string s3_23 = 23; //设置为"love","hate","C++"optional S2 s3_24 = 24; //设置为 0x1,"love"repeated S2 s3_25 = 25; //设置为 0x16,"love" and 0x16,"hate"repeated fixed32 s3_26 = 26 [packed = false]; //设置为1,2,3optional int32 s3_27 = 27; //不设置
}
分类说明 | 定义 | FieldNum | WriteType | 值 | 编码后(16进制) |
---|---|---|---|---|---|
VALUE用VARINT表示 | optional int32 | 1 | 0 | 0x88 | 08 88 01 |
optional int32 | 2 | 0 | 0x8888 | 10 88 91 02 | |
optional uint32 | 3 | 0 | 0xE8E8E8 | 18 e8 d1 a3 07 | |
optional uint32 | 4 | 0 | 0xE8E8E8E8 | 20 e8 d1 a3 c7 0e | |
optional int64 | 5 | 0 | 0x8888 | 28 88 91 02 | |
optional int64 | 6 | 0 | 0xE8E8E8E8 | 30 e8 d1 a3 c7 0e | |
optional int64 | 7 | 0 | 0xE8E8E8E8 | 38 e8 d1 a3 c7 0e | |
optional int64 | 8 | 0 | 0xE8E8E8E8E8E8E8E8 | 40 e8 d1 a3 c7 8e 9d ba f4 e8 01 | |
optional sint32 | 9 | 0 | 0x8888 | 48 90 a2 04 | |
optional sint32 | 10 | 0 | -0x8888 | 50 8f a2 04 | |
optional E1(enum) | 11 | 0 | E1_5 | 58 05 | |
optional bool | 12 | 0 | true | 60 01 | |
VALUE固定4个字节 FIXED32(5) | optional float | 13 | 5 | 88.888 | 6d a8 c6 b1 42 |
optional fixed32 | 14 | 5 | 0x8888 | 75 88 88 00 00 | |
optional sfixed32 | 15 | 5 | 0x8888 | 7d 88 88 00 00 | |
VALUE固定8个字节 FIXED64(1) | optional double | 16 | 1 | 8888.8888 | 81 01 58 ca 32 c4 71 5c c1 40 |
optional fixed32 | 17 | 1 | 0x8888888888 | 89 01 88 88 88 88 88 00 00 00 | |
optional sfixed32 | 18 | 1 | -0x8888888888 | 91 01 78 77 77 77 77 ff ff ff | |
string, bytes embedded messages packed repeated fields LENGTH_DELIMITED(2) | optional string | 19 | 2 | "I love you,C++!" | 9a 01 of 49 20 6c 6f 76 65 20 79 6f 75 2c 43 2b 2b 21 |
optional bytes | 20 | 2 | "I love you,C++!" | 9a 01 of 49 20 6c 6f 76 65 20 79 6f 75 2c 43 2b 2b 21 | |
optional repeated int32 [packed=false] | 21 | 0 | 3,270,86942 | a8 01 03 a8 01 8e 02 a8 01 9e a7 05 | |
optional repeated int32 [packed=true] | 22 | 2 | 3,270,86942 | b2 01 06 03 8e 02 9e a7 05 | |
optional repeated string | 23 | 2 | "love","hate","C++" | ba 01 04 6c 6f 76 65 ba 01 04 68 61 74 65 ba 01 03 43 2b 2b | |
optional S2(message) | 24 | 2 | 0x1,"love" | c2 01 08 08 01 12 04 6c 6f 76 65 | |
repeated S2 | 25 | 2 | S2{0x16,"love"} ,S2{0x16,"hate"} | ca 01 08 08 16 12 04 6c 6f 76 65 ca 01 08 08 16 12 04 68 61 74 65 | |
repeated fixed32[packed=false] | 26 | 5 | 1,2,3 | d5 0101 00 00 00 d5 0102 00 00 00 d5 0103 00 00 00 | |
可选没有设置 | optional int32 | 27 | . | 没有设置 | 没有数据 |
所有tag 转换过程都一样:值->二进制->分组->加标志位->翻转->转16进制
3.2 write_type=0 编码
Filed 为Tag-Value结构,没有Length,
Tag,Value 都采用 Varint编码(小端字节序)
3.2.1 int32, int64, uint32, uint64 编码
这四种类型的值直接对其做Varint编码
转换过程:值->二进制->分组->加标志位->翻转->转16进制
- s3_1
- tag :1<<3|0-> 0001000-> 0000 1000-> 0000 1000-> 0000 1000->->0x08
- value: 0x88-> 10001000->000 0001,000 1000->0000 0001,1000 1000->1000 1000 ,0000 0001->88 01
- s3_2
- tag :2<<3|0->00010000->001 0000->0001 0000->0001 0000->10
- value: 0x8888->1000100010001000->000 0010 ,001 0001 ,000 1000->0000 0010 ,1001 0001 ,1000 1000->1000 1000,1001 0001,0000 0010 ->88,91,02
- s3_3
- tag :3<<3|0->00011000->001 1000->0001 1000->0001 1000->18
- value: 0xE8E8E8->111010001110100011101000->000 0111,010 0011 ,101 0001 ,110 1000->0000 0111,1010 0011 ,1101 0001 ,1110 1000->1110 1000,1101 0001,1010 0011,0000 0111-> E8 D1 A3 07
- s3_4
- tag :4<<3|0->00100000->010 0000->0010 0000->0010 0000->20
- value:0xE8E8E8E8->000 1110,100 0111,010 0011,101 0001 ,110 1000->0000 1110,1100 0111,1010 0011,1101 0001 ,1110 1000->1110 1000,1101 0001,1010 0011,1100 0111,0000 1110->E8 D1 A3 C7 0E
3.2.2 sint32 sint64 编码
sint32 sint64 需要首先做一个zigzag 转化,转化后对值做Varint编码
tag 转换过程:值->二进制->分组->加标志位->翻转->转16进制
value转换过程:值->zigzag 转化->二进制->分组->加标志位->翻转->转16进制
- s3_9
- tag: 9<<3|0= 01001000-> 100 1000->0100 1000->0100 1000->48
- value 0x8888-> 0x11110->00010001000100010000->000 0100,010 0010,001 0000->0000 0100,1010 0010,1001 0000->1001 0000 ,1010 0010,0000 0100->90 A2 04
- s3_10
- tag: 10<<3|0=01010000->101 0000->0101 0000->0101 0000->50
- value 0x8888-> 0x1110F->00010001000100001111->000 0100,010 0010,000 1111->0000 0100,1010 0010,1000 1111->1000 1111,1000 1111,1010 0010,0000 0100->8F 8F A2 04
3.2.3 enum 编码
enum 是对其对应的数值做Varint编码
tag 转换过程:值->二进制->分组->加标志位->翻转->转16进制
value转换过程:值->对应数值->二进制->分组->加标志位->翻转->转16进制
- s3_11
- tag: 11<<3|0= 01011000-> 101 1000->0101 1000->0101 1000->58
- value E1_5->0x5->0101-> 000 0101->0000 0101->0000 0101->05
3.2.4 bool编码
**bool,是对其对应的数值做Varint编码 ,true:1,false:0 **
value转换过程:值->对应数值->二进制->分组->加标志位->翻转->转16进制
- s3_12
- value true->0x1->0001-> 000 0001->0000 0001->0000 0001->01
- value false->0x0->0000-> 000 0000->0000 0000->0000 0000->00
3.3 write_type=5 编码
Filed 为Tag-Value结构,没有Length,
Tag,采用 Varint编码(小端字节序)
Value 长度为固定定4个字节,小端字节序(非Varint编码
),编码过程只是转成小端字节序即可
3.3.1 float 编码
**
编码过程只是转成小端字节序即可
非Varint编码
),固定长度4字节
**
- s3_13
- tag:13<<3|5->01101101-> 0110 1101 -> 0110 1101-> 0110 1101 ->6D
- value:88.888f->1000010101100011100011010101000->100 0010,1011 0001,1100 0110 ,1010 1000->1010 1000,1100 0110,1011 0001,100 0010->A8 C6 B1 42
3.3.2 fixed32编码
这个用于处理32位整形的数值 和int32的区别就是
- fixed32 长度为固定定4个字节,小端字节序(
非Varint编码
)- int32 变长,小端字节序(
Varint编码
)
- s3_14
- value:0x8888>1000100010001000->0000 0000 0000 0000 1000 1000 1000 1000->1000 1000 1000 1000 0000 0000 0000 0000->88 88 00 00
3.3.3 sfixed32编码
和fixed32 编码完全一样(不明白为什么弄了两个类型)
3.3 write_type=1 编码
Filed 为Tag-Value结构,没有Length,
Tag,采用 Varint编码(小端字节序)
Value 长度为固定定8个字节,小端字节序(非Varint编码
),编码过程只是转成小端字节序即可
3.3.1 double编码
编码过程只是转成小端字节序即可(
非Varint编码
),固定长度8字节
3.3.2 fixed64编码
这个用于处理64位整形的数值 和int64的区别就是
- fixed64 长度为固定定8个字节,小端字节序(
非Varint编码
)- int64 变长,小端字节序(
Varint编码
)
3.3.3 sfixed64编码
和fixed64编码完全一样(不明白为什么弄了两个类型)
3.4 write_type=2 编码
消息结构
Field含有Length
3.4.1 string编码
字段类型为 string 类型,字段值采用 UTF-8 编码,value 编码为正常字符串编码
- s3_19
- tag:19<<3|2-> 10011010->1 ,001 1010->0000 0001 ,1001 1010->1001 1010,0000 0001->9A 01
- length: 15->1111->0f
- value 正常字符串编码
3.4.2 repeated [packed] 原始数字类型 编码
- repeated字段, proto2 默认 [packed=false],proto3中 默认 [packed=true]
- 只有原始数字类型(使用varint,32位或64位)的repeated 字段才可以声明为 [packed=true]。
- [packed=false] 时,编码后的 message 有一个或者多个包含相同tag(filed_num 和 **
write_type 为对应的数值类型write_type**
)的 field。这些 filed 不需要连续的出现。他们可能与其他的字段交错出现。尽管它们是无序的,但是在解析后它们是需要有序的。 - [packed=true] 的repeated 字段所有的元素会被打包到单一一个 field 对中,且它的
wire_type=2
,长度为所有数值编码后的长度之和。value编码按其定义类型的write_type编码,无缝排列到一起。
- s3_21[packed=false] repeated int32
- 三个值,在Message中会有三个完整的field,三个field 的tag 都相同,write_type(因值类型为int32,所以write_type=0)
- tag: 21<<3|0->10101000->1,0101000->0000 0001,10101000->1010 1000,0000 0001->A8 01
- value1:3->0000 0011->03
- value2:270->000100001110-> 0 0010 ,000 1110->0000 0010 ,1000 1110->1000 1110,0000 0010->8E 02
- value3:86942->00010101001110011110->00 0101,010 0111,001 1110->0000 0101,1010 0111,1001 1110->1001 1110,1010 0111,0000 0101->9E A7 05
- s3_22[packed=true] repeated int32
- 三个值放在一个字段,value紧密排列(value的编码方式 因值类型为int32,所以是varint编码),‘write_type=2’
- tag: 22<<3|2->10110010->1,011 0010->0000 0001,1011 0010->1011 0010,0000 0001->B2 01
- length->6->0110->06 (长度值为value 实际编码后长度)
- value
- 3->0000 0011->03
- 240->8E 02
- 86942->9E A7 05
- s3_23 repeated string
- 三个值,在Message中会有三个完整的field,三个field 的tag 都相同,因值类型为String,所以write_type=2,并有length
3.4.3 嵌套Message编码
嵌套Message就是value又是一个Message,外层消息存储采用 TLV 存储,外层write_type=2 ,它的 value 又是一个 TLV 存储。整个编码结构如下图所示
s3_24
嵌套自定义类型 的write_type=2
- tag: 24<<3|2->11000010->1,100 0010=0000 0001, 1100 0010->1100 0010,0000 0001->C2 01
- length-> (嵌套数据编码后实际长度)8->08
- value:
- s2_1:
- write_type=0
- tag: 1<<3|0=>1000->08
- value:0x1->0001->01
- s2_2:
- write_type=2
- tag: 2<<3|2=00010010->12
- length:4->0100->04
- -value: 6c 6f 76 65
- s2_1:
3.4.2 repeated Message
- [packed=false] 只能为false
- write_type=2
- 消息结构如下图,repeated Message 相当于 嵌套 多个Message并且这些Tag都一样(field_num ,write_type=2 都相同)
3.4.2 optional 不设置
不设置 就不会出现在Message中
4 使用建议
- 版本兼容宝典:字段只新增,不删除,任何时候tag不要变动(即不要修改类型和field_num)
- 如果要出现负数,不要使用int32,int64,而应该使用sint32,sint64。
- repeated的原始数字类型(使用varint,32位或64位)的重复字段可以声明为 [packed=true ]减小占用空间,但有低版本不兼容的风险