Protobuf-数据编码规则

Charlesbases 2019-11-03

参考文档: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
标志位字段编号字段类型标志位低位字段值标志位高位字段值
000010001001011000000001

protobuf都是以8bit(1byte)为一个解析单元。

标志位 0:表示解析单元结束,后一个字节是新的解析单元,1:表示解析单元未结束,后一个字节是这个解析单元的高位部分。

字段编号:protobuf的消息体的字段编号,如上所示,转换为十进制是1

字段类型:protobuf的消息体的字段类型,转换为十进制是0

字段类型对照表

类型值类型名使用场景
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages, packed repeated fields
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, sfixed32, float

我们进一步对其解析,剔除标志位,交换字段值的高低位

字段编号字段类型高位字段值低位字段值
000100000000010010110
10高低位合并去除左侧010010110
10计算十进制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

原始值编码值
00
-11
12
-23
21474836474294967294
-21474836484294967295

从表格中可以看出,小数值的正负数,二进制编码,高位将都是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"

具体编码:

字段编号&字段类型字段长度字段值
120774 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报文特征做下说明:

  1. 所有数组元素可能不是连续的,中间可能穿插其他字段的报文
    2.所有数组元素的顺序是可保持的,解码后,数组元素的展示顺序将和编码前一致。
  2. 数组元素,将是多对key-value的格式,在网络传递。

相关推荐