背景
项目上用到protoBuffer对模块间通信的消息做序列化/反序列化,对如何将这些做编码的很感兴趣,所以看看官方文档的介绍。
一条简单消息(A Simple Message)
假如有一条简单消息定义如下
message Test1 {
optional int32 a = 1;
}
如果在一个应用里,创建了一个Test1消息,然后将a设为150.那么将消息序列化(serialize)到一个输出流(output stream)。如果你检查编码消息的话,你会看到三个字节(bytes):
08 96 01
Base 128 Varints
如果要理解简单的protoBuffer编码就要理解varints。Varints是用一个或多个字节(bytes)序列化整形(integers)。小的数字占的字节数就小。
varint中的每个字节(byte),除了最后一位(bit)有一个最高有效位集(the most significant bit)。最后一位为1表明后面还有更多直接。每个字节的低7位用于存储7字节组合的补码(two’s complement representation),低有效位的字节组和在前(least significant group first)。
例如,数字1,单个字节,没有设置msb
0000 0001
然后是300,它的二进制表示复杂些(complicated):
1010 1100 0000 0010
怎么识别出是300的?首先把msb位去掉,它只是告诉我们是否到了数字的结尾(可以看到第一个字节设置了msb表示varint有不止一个字节):
1010 1100 0000 0010
→ 010 1100 000 0010
然后把两个7字节的组合(group of 7 bit)颠倒(varint是按低有效字节组优先(least significant group first)存储的)。然后就可以得到最终值了:
000 0010 010 1100
→ 000 0010 ++ 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300
消息结构(Message Structure)
如你所知,protocol buffer消息是一系列键值对(key-value pairs)。二进制版本的消息使用字段的序号(field’s number)作为主键—每个字段声明的类型和名称只有在通过参考(reference)消息类型的定义(即proto文件)解码后才能决定(be determined)
当一个消息编码时,主键和值被拼接到(concatenated)到字节串中(byte stream)。当消息解码时,解析器需要能够跳过不能被识别(recognize)的字段。这样,就能在消息中加入新的字段而不破坏不认识新字段的老程序了。最后,在有线格式消息(wire-format message)每个“主键”实际上是两个值—proto文件中字段的序号和后面对应wire type类型的值的长度(提供足够信息的的wire type)。大多数语言实现这个组件时将其作为一个tag。
可用的wire type如下:
Type | Meaning | Userd 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 (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
更多数值类型(More Value Types)
有符号整型(Signed Integers)
从前面的部分可以看到,所有结合了wire type 0的protoBuffer类型被编码为varints。然而有符号整型(signed int type) (sint32 and sint64)与“标准”的有符号整形(int32 and int64)在对负数编码时有重要差别。如果使用int32或int64作为负数的类型,结果的varint总是10直接长度—它实际上被当成一个很大的无符号整型。如果使用有符号整形(signed types),varint的结果会使用效率更高的ZigZag编码。
ZigZag编码将有符号整数标记为(sign)无符号整数,因此绝对值较小的数字(例如-1)的varint编码值也会比较小。通过反复地在正整数和负整数间只字型,比如-1被编码为1,1被编码为2,-2被编码为3,如此,如下表所示:
Signed Original | Encoded As |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
换言之,每个值用如下公式进行编码
(n << 1) ^ (n >> 31)
对 sint32s, 或
(n << 1) ^ (n >> 63)
对 64-bit 版本.
注意第二个右移(n >> 31部分)是算术位移。所以整数的结果必然为0,负数的结果必然为1。(该公司的意思是将待编码数字左移一位后与右移n-1位的数字做与运算)
当解析sint32 or sint64时,它的值被解码回原先的有符号版本 。
非varint数字(Signed Integers)
非varint数字类型很简单—double和fixed64的wire type为1,会告诉解析器会期望一个fixed 64位的数据块(lump of data);类似地,float和fixed32的wire type为5,告知期望32位。两种情况的值都是以小端(little-endian)字节顺序存储。
字体(string)
wire type 2(长度分隔)意味着值是varint编码长度跟随着特点长度字节的数据。
message Test2 {
optional string b = 2;
}
将b的值设定为"testing"得到:
12 07 74 65 73 74 69 6e 67
标红的字节是"testing"的utf8编码。这里的key是0x12,字段顺序是2,类型是2.varint的长度为7。
嵌入消息(Embedded Messages)
以下是嵌入消息Test1的protoBuffer消息的例子:
message Test3 {
optional Test1 c = 3;
}
然后下面是编码版本,同事 Test1’s 的字段被设为150:
1a 03 08 96 01
可以看到最后的3字节与第一个例子中一致,嵌入消息被和strings一样对待,所以可以看到前面的03。
可选及重复元素(Optional And Repeated Elements)
proto 2的消息定义如果有repeated元素(没加[packed=true]选项),编码消息有0或多个有同样字段序号(field number)键值对。这些repeated值不一定连续显示;有可能被其他字段交错(interleave)。元素的对应顺序在解析时保存,相对其他字段的顺序会丢失。在proto 3中,repeated字段采用包编码(packed encoding),如下。
对于proto 3中任何非repeated字段,或proto 2中的optional字段, 编码消息可能有或没有那个字段的键值对。
通常,编码消息应该不会有超过一个的非repeated字段的实体。然而,解析器被希望能处理这样的情况。对数值类型和字符串,如果相同字段重复出现多次,解析器接受收到的最后一个值。对嵌入消息字段,解析器融合(merge)相同字段的多个实体,像Message::MergeFrom方法—所有单个标量在最新的实体中替代先前的,单个(singular)嵌入消息被融合了,repeated字段被拼接了。这些规则的效果使得解析两条编码消息的拼接产生的效果与分别解析两个消息并融合结果一直,即:
MyMessage message;
message.ParseFromString(str1 + str2);
等同于:
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
这个性质有时很用用,你可以融合两条消息,即使你不是确切知道他们的类型。
打包的重复元素(Packed Repeated Fields)
2.1.0版本引入了包重复字段(packed repeated fields),proto 2里声明与repeated字段一致但是要添加 [packed=true]说明选项。在proto 3里repeated字段默认打包(packed by default)。其功能与repeated字段类似,但是编码不同。包重复字段如果没消息,那么包含0个元素。否则所有字段元素会打包成一个wire type 2类型的键值对。每个元素的编码还是相同的,除了前面没有了主键(key)。
例如想象消息类型:
message Test4 {
repeated int32 d = 4 [packed=true];
}
现在建立一个 Test4, 提供值3, 270, 和86942给d. 编码形式如下:
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)
只有原始数值类型(primitive numeric types)(使用varint, 32-bit, 或64-bit的wire type)能被声明为packed。
字段顺序(Field Order)
proto文件中的字段顺序可以是任何顺序,当消息序列化后应按照字段顺序写。这就允许解析代码能使用介于字段顺序连续的优化。
消息如果有未知字段,现在java和c++的实现会将其以任意顺序写到已知顺序排序的字段后。目前python实现不会跟踪未知字段。
Reference
[1].https://developers.google.com/protocol-buffers/docs/encoding
[2].https://blog.youkuaiyun.com/zxhoo/article/details/53228303