Protobuf-数据编码规则
参考文档:https://developers.google.cn/protocol-buffers/docs/encoding
文章是本人对官方文档的理解,可能理解有误,望指正。^^
1.A Simple Message 简单消息格式
protobuf中的最简单的消息定义:
message Test1 { optional int32 a = 1; }
如果将a赋值150,它的字节流(16进制表示)如下:
08 96 01
转换为二进制表示如下:
0 8 9 6 0 1 → 0000 1000 1001 0110 0000 0001
标志位 | 字段编号 | 字段类型 | 标志位 | 低位字段值 | 标志位 | 高位字段值 |
---|---|---|---|---|---|---|
0 | 0001 | 000 | 1 | 0010110 | 0 | 0000001 |
protobuf都是以8bit(1byte)为一个解析单元。
标志位 0:表示解析单元结束,后一个字节是新的解析单元,1:表示解析单元未结束,后一个字节是这个解析单元的高位部分。
字段编号:protobuf的消息体的字段编号,如上所示,转换为十进制是1
字段类型:protobuf的消息体的字段类型,转换为十进制是0
字段类型对照表
类型值 | 类型名 | 使用场景 |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | 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 | fixed32, sfixed32, float |
我们进一步对其解析,剔除标志位,交换字段值的高低位
字段编号 | 字段类型 | 高位字段值 | 低位字段值 |
---|---|---|---|
0001 | 000 | 0000001 | 0010110 |
1 | 0 | 高低位合并去除左侧0 | 10010110 |
1 | 0 | 计算十进制 | 150 |
这样得到字段编号=1的int32类型值为150
2.Base 128 Varints
varints是整数类型的编码规则,大小是1byte的整数倍
如果是1byte的Varints,能表示0-127的正整数,这里就会奇怪,1byte不是8bit,实际上能表示0-255的正整数,为什么少了一半。
这里就要提到前面讲的标志位,Varints类型的每个字节都有两部分组成,高位的1bit是标志位,剩下的7bit可用用于表示整数。如果高位的1bit是1,下一个byte也被视为Varints的一部分,直至下一个byte的高位1bit是0,Varints的解析单元结束。
注意:如果varints是大于1byte,需要做高低位置换,因为前面表示整数的低位部分,往后,表示整数的高位,总的来说,计算数值时,需要去除标记位后,7bit一组倒转
比如300这个数值,我们看看其二进制
1010 1100 0000 0010 → 010 1100 000 0010 → 000 0010 ++ 010 1100 (高低位倒转) → 100101100 → 256 + 32 + 8 + 4 = 300
3.More Value Types 其他数据类型
3.1有符号整数
前面章节,我们讲到字段类型=0,都用varints的编码表示。但是sint32,sint64会比int32,int64表示负数上面,更节省空间。
就拿int32举例,它表示负数,一般需要5byte,如果采用sint32,其采用ZigZag编码,可以少于5byte,注意,只是可以,不是绝对,特定数值,比如大数值就会达到5byte
原始值 | 编码值 |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
从表格中可以看出,小数值的正负数,二进制编码,高位将都是0,32位的整型数据可以将高位去除后,进行传输,解码时,在高位补0,凑足32位。减少传输数据的量。
ZigZag编解码公式
之所以可以用这个公式编码,原因在于:
在原始值的二级制结构,正数的最高位都是0,负数的最高位都是1
在编码后的二级制结构,正数的最低位都是1,负数的最低位都是0
通过这个特性,我们可以知道当前数值时正数还是负数,同时采用不同的编解码方式。
n表示数值 正数编码:n<<1 正数解码:n>>>1 负数编码:(n<<1)^~(n&0) 负数解码:(n>>>1)^~(n&0)
举个例子,比如2和-2的编码按照公式计算下
未采用ZigZag的传输
2 → 0000 0000 0000 0000 0000 0000 0000 0010 → 10(压缩值) → 0000 0010(protobuf传输内容,注意每8bit的最高位是标志位,不是数值部分)
这里传输2只需要1byte
未采用ZigZag的传输
-2 → 1111 1111 1111 1111 1111 1111 1111 1110 (-2是2取反补码得到的,这是负数在二进制中的表示规则) → 1111 1111 1111 1111 1111 1111 1111 1110(压缩值,可以看出无法压缩,高位都是1) → 1000 1110 1111 1111 1111 1111 1111 1111 0111 1111 (protobuf传输内容,注意每8bit的最高位是标志位,不是数值部分,注意超过1byte,需要每个字节进行高低位倒转)
这里传输-2需要5byte
采用ZigZag的传输
2 → 0000 0000 0000 0000 0000 0000 0000 0010 → 0000 0000 0000 0000 0000 0000 0000 0100(编码) → 100(压缩值) → 0000 0100(protobuf传输内容,注意每8bit的最高位是标志位,不是数值部分)
这里传输2只需要1byte
采用ZigZag的传输
-2 → 1111 1111 1111 1111 1111 1111 1111 1110 (-2是2取反补码得到的,这是负数在二进制中的表示规则) → 0000 0000 0000 0000 0000 0000 0000 0011(编码) → 11(压缩值) → 0000 0011 (protobuf传输内容,注意每8bit的最高位是标志位,不是数值部分)
这里传输-2需要1byte
从如上两个的对比,可以看出负数在ZigZag编码的传输中,可以节省空间。具备更高的传输效率。但是,如果对大数值的正负数,压缩的空间就很小了。
3.2Non-varint Numbers 非varint型数值
fixed64, sfixed64, double,fixed32, sfixed32, float都是Non-varint Numbers,
可以从字面看出,fixed64, sfixed64, double大小是64bit(也就是8byte),fixed32, sfixed32, float大小是32bit(也就是4byte),
但是在实际的传输过程中可能超过,因为有标志位存在。
这块的部分没有找到详细的资料说明,只是说编码采用标志位+高低位逆序编码(就是varint编码规则),没太懂!!!!!!!!!!
Strings 字符型
字符串都用length-delimited长度限定的编码格式,编码中,有一个部分采用varint类型表示长度。
message Test2 { optional string b = 2; }
b="testing"
具体编码:
字段编号&字段类型 | 字段长度 | 字段值 |
---|---|---|
12 | 07 | 74 65 73 74 69 6e 67 |
0x12 → field number = 2, type = 2
0x07→ 7个字节
Embedded Messages 嵌套消息
如下是我们说的嵌套消息结构,Test3的c字段是Test1类型,一样,我们把a设置为150
message Test1 { optional int32 a = 1; } message Test3 { optional Test1 c = 3; }
如下是实际编码,我们来解析下,
前文提到 Test1.a=150,它的编码是08 96 01,你会发现下面的编码后半部分正好是一样的。
1a 03 08 96 01
剩下的1a 03是Test3.c的编码,我们解析下
1a 03 → 0001 1010 0000 0011 → 0(标志位)0011(字段编号)010(字段类型)0(标志位)0000011(字段长度) → 3(字段编号)2(字段类型)3(字段长度)
对照字段类型表,我们可以确认嵌套消息的类型采用length-delimited长度限定类型,字段值按照长度截取,解析它的时候,再按照子消息格式,再次解码。
3.3Packed Repeated Fields 列表字段的压缩
在proto2版本中,repeated默认采用[packed=false],不进行压缩。
在proto3版本中,对于数值类型(指字段类型是0,1,5)默认采用 [packed=true],在grpc报文中,它是(字段编号+字段类型+元素个数+元素1+元素2....)结构,具体如下所示:
message Test4 { repeated int32 d = 4 [packed=true]; }
22 // key (field number 4, wire type 2) 06 // payload size (6 bytes) 03 // first element (varint 3) 8E 02 // second element (varint 270) 9E A7 05 // third element (varint 86942)
剩下的Length-delimited类型,就无法采用这种压缩方式,它们的grpc报文组织结构是这样子(字段编号+字段类型+长度限定值1+字符串1+字段编号+字段类型+长度限定值2+字符串2。。。。。)
从报文可以看出,字段编号和字段类型都是重复要素,需要占用一定的字节。
注意:这边有一个官方对于protobuf解码器的要求,比如你传递的grpc的报文,列表类型数据,采用packed=false,不进行压缩,但是接收者的protobuf定义的又是配置了packed=true,这时候,解码器需要兼容这种情况,对报文做正确解析。
这边额外对packed=false的编码grpc报文特征做下说明:
- 所有数组元素可能不是连续的,中间可能穿插其他字段的报文
2.所有数组元素的顺序是可保持的,解码后,数组元素的展示顺序将和编码前一致。 - 数组元素,将是多对key-value的格式,在网络传递。