varints
在了解protobuf编码原理之前,首先要了解的是varints
。
varints
是一种用一个或多个字节来序列化整数的方法。整数的值越小,占用的字节数就越少。
varints
的每个字节的第一个bit最高有效位
(msb
)是用来指示下一个字节是不是还是用来表示这个整数的。可以将它看成一个单链表的next指针,为1则指向下一个节点,为0则指向null。而varints
较低位的7个bit以二进制补码、低字节序(小端)的形式存储整数。
例如:
有以下varints表示的整数:
1010 1100 0000 0010
要解码得到原来的数值,首先要去掉每个字节的msb(msb只是用来告诉这个整数是不是已经到结尾了,计算数值时并没有用),得到了一下没有msb的形式:
010 1100 000 0010
然后,由于varints是低字节序的,因此需要先对字节进行一个反转(reverse)操作,得到:
000 0010 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300
Message
protobuf message是一系列的键值对,message的二进制形式使用字段的tag数值作为key,而其的字段名以及实际的数据类型则只能在解码端根据message的类型定义来决定。
编码message时,key和value被连接成字节流。当解码message的时候,解释器要有能力跳过那些他不能识别的字段,这样子,才能够添加新的字段进message而且不会影响不能识别这些新字段的老程序。
为此,每个字节流上的每个key实际上是由两个值组成的——一个是字段的tag number,另一个是
该字段的 wire type
,这是用来提供足够的信息来明确该字段的值得长度的。
下面是可选的wire type
:
Type | Meaning | Used For |
---|---|---|
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(已废弃) |
4 | End group | groups(已废弃) |
5 | 32-bit | fixed32, sfixed32, float |
字节流上的每个key都是varint。key的值是(字段tag number<<3 | wire_type),即最后3个bit用来存储该字段的wire type
。
举个例子,有以下message的定义:
message Test1 {
required int32 a = 1;
}
在某个应用中,产生了一个Test1 message,a的值为150。将这条message序列化成二进制形式后,得到3个字节:
08 96 01
则二进制中key为08
,即:
0000 1000
①从最后3个bit能判断出该字段的wire type为0(Varint)
②然后将该key右移3个bit,得到0000 0001
,
得知这个字段的tag number为1。
根据varint来解析后面两个字节存储的值:
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (去掉msb并对字节进行反转)
→ 10010110
→ 150
更多数据类型
Signed Integers
有符号整数类型(sint32
、sint64
)和“标准”整数类型(int32
、int64
)都是编码成varints,但是,当他们的值是一个负数时,会有很重要的不同。
当你使用int32
和int64
存储负数时,总会产生一个长度为10个字节的varint——它被看做是一个十分大的无符号整型。
而如果使用其中一种有符号整型(sint32
、sing64
),则会使用ZigZag编码产生varint,效率比int32
、int64
高。
ZigZag 编码使用”zig-zags”在正负数间回退前进的方法,使-1编码成1,1编码成2,-2编码成3,如此类推:
Signed Original | Encoded As |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
换言之,每个值n都会根据以下公式进行编码:
对于sint32:
(n << 1) ^ (n >> 31)
对于sint64:
(n << 1) ^ (n >> 63)
注意,这里的第二个移位——n >> 31
部分,是算术移位,也就是说,如果n是正数,则移位的结果是所有位都是0,如果n是负数,则所有位都是1。
分析sint32
或sint64
时,其值会被解码成原始的有符号的形式。
举个例子:
a = -1 → 0xffffffff(计算机用补码表示负数)
a << 1 → 0xfffffffe → -2
a >> 31 → 0xffffffff → -1
(a << 1) ^ (a >> 31) → 0xfffffffe ^ 0xffffffff → 0x00000001 → 1
Strings
wire type 2(length-delimited)表示一个这个字段的值由一个varint n和其后跟随的n个字节的数据组成。
message Test2 {
required string b = 2;
}
设b的值为”testing”,序列化得到:
12 07 73 74 69 6e 67
红色的字节是UTF8编码的“testing”,key是0x12 → tag = 2, type = 2,。长度有一个varint表示,即0x07→7,所以后面的7个字节就是字符串“testing”。
Embbed Messages
在序列化时,对于内嵌message,实际上和对string的处理方式是一样的。
message Test3 {
required Test1 c = 3;
}
设Test1 c的a字段为150,序列化将得到:
1a 03 08 96 01
可以看到,最后3个字节实际上和我们之前举得例子(08 96 01
)是一样的。
Optional And Repeated Elements
正常来说,一条序列化的message,不会多次出现同一个non-repeated字段。但是,分析器对这种情况设置了处理的方法。
对数值型和字符串类型,如果同一个字段多次出现,则分析器以最后一次出现的值为最终结果。
对于内嵌message,分析器将同一个字段多个值合并,就像使用Message::MergeFrom
方法一样。
这些规则使得分析两条连接在一起的序列化的message实际等于分别解析这两条message再对他们执行merge后得到最终结果。即:
MyMessage message;
message.ParseFromString(str1 + str2);
和这样是相同的:
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
Packed Repeated Fields
对于proto2 message,如果一个repeated字段没有设置[packed=true]
,那么序列化将会有0个或多个键值对,每个键中都包含tag,就像上面提到的其他类型字段一样。这些repeated的键值对不一定是连续的,可能会和其他字段交错出现。但每个repeated的键值对出现的位置和他们实际应该的顺序是保持一致的。
从2.1.0版本起,protobuf引入了一种packed repeated fields
,声明方式与repeated fields一样,只是需要设置[packed=true]
选项。
在proto3中,repeated fields默认就是packed repeated fields。
packed repeated field和一般的repeated field不同,对于多个值,序列化后,将只会产生一个键值对,对于每个值,都会根据其类型进行序列化(但是没有tag),然后将所有的值拼接后以wire type 2(length-delimited)的方式打包成一个值。
例如,
message Test4 {
repeated int32 d = 4 [packed=true];
}
生成一个Test4,d为{3, 270, 86942}。将该message序列化将得到:
22 // tag number 4, wire type 2
06 // 长度为6个字节
03 // 第一个元素为varint 3
8E 02 // 第二个元素为varint 270
9E A7 05 // 第3个元素为varint 86942
只有原始数值型(varint, 32-bit, 64-bit)的repeated字段可以被声明为“packed”。