本文翻译自Protocol Buffer官方文档: http://code.google.com/apis/protocolbuffers/docs/encoding.html
http://ptsolmyr.com/2010/09/16/protocol-buffer-decode/
Base 128 Varints
要了解ProtoBuf中的编码问题,首先要了解varints。Varints是用一个或多个byte来序列化整数的一种方法。越小的整数占用越少的bytes。
除了最后一个byte,在varint中的每个byte,都把最高有效位(msb)set为1 — 这表明了后面还会跟着其他的bytes。除了最高有效位,剩下的7个bit用来存储该数字以7位为一组的补码形式。最后的有效组在最前面。
例如,对于number 1 — 它只有一位,所以msb没有set为1:
0000 0001
对于300 — 就稍微复杂了:
1010 1100 0000 0010
额滴神啊,怎么才能看出来这是300?首先,把每个byte的msb丢弃,因为它仅仅用来判断是否到达了最后一个byte:
1010 1100 0000 0010
→ 010 1100 000 0010
然后把两组7bit补码反转位置,记得么,varints把最后的significant group存在最前面。然后把它们连接起来,得到最后的值:
000 0010 010 1100
→ 000 0010 ++ 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300
Message Structure
你懂得,一个Protocol Buffer Message实际上是一组Key-Value对。一个message的二进制版本就是用field的名字作为key — 名字和其声明的类型只能在decoding段指定。(也就是在.proto文件中指定)
在一个message被编码后,key和value都被连接在一个byte流中。当message被解码,parser需要能够跳过它不认识的field。这样才能在不破坏旧程序的前提下,在message中添加新的field,否则,旧程序将会因为不认识新field而报错。为了达到这个目的,每个KV对的”key”实际上由两个值决定:.proto文件中的field number,加上能够提供于key相对应的value到底有多长的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 message, packed repeated fields |
3 | Start group | groups(deprecated) |
4 | End group | groups(deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
在流状message中的每个key都是一个varint,它的值是这么算出来的:(field_number << 3) | wire_type
—换句话说,最后三个bit用来存储wire type。
让我们再来看一下简单的例子。现在我们已经知道流中的第一个number永远都是一个varint key了,而且这个数字是0×08:
000 1000
后三个bit是wire type (0), 然后右移3个bit,得到field number (1). 现在真相大白了,tag是1,而且后面跟着的是一个varint。运用我们之前的关于varint-decoding的知识,你就知道下面的两个byte存储的是150。
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (drop the msb and reverse the groups of 7 bits)
→ 10010110
→ 2 + 4 + 16 + 128 = 150
More Value Types
Signed Integers
正如在前面所见到的,所有的和wire type 0相对应的protocol buffer type都被编码成varint。不过,在面对负数的情况中,signed int type(sint32, sint64)和standard int type(int32, int64)还是有很大区别的. 对于int32或int64,如果值为负数,那么生成的varint总是10个byte长度—实际上是把这个负数当作一个很大的unsigned integer处理了。如果你用的是signed type,那么protobuf使用ZigZag编码来生成varint,效率会更高。
ZigZag编码把signed integers映射到unsigned integer,力图使绝对值比较小,这样小负数也会有一个比较小的varint编码值。它的方法是在正数和负数之间曲折的前进,即:-1编码为1,1编为2,-2编为3……
Signed Original | Encoded As |
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
换句话说,每个值n都按下式编码:
(n << 1) ^ (n >> 31)
如果是64bit,那么用下面的式子:
(n << 1) ^ (n >> 63)
注意,第二个位移运算,也就是(n >> 31)这部分,是一个算术位移,也就是说,位移的结果要么是全0(n是正数),或者全1(n是负数)。
Non-varint Numbers
Non-varint numeric type就简单多了–double和fixed64,wire type是1,告诉parser后面跟着一个64bit的data;float和fixed32,wire type是5,告诉parser后面跟着一个32bit的data。这两种情况下,value都是以little-endian字节序存储的。
Strings
wire type 2(length-delimited)意味着后面跟的value是一个用varint编码的length,然后跟着一个length指定长度的bytes。
message Test2 {
required string b = 2;
}
如果b被set为“testing”, 那么你会得到下面这个序列:
12 07 74 65 73 74 69 6e 67
标红色的bytes是UTF8编码下的“testing”。这个stream中的key是0×12 –> tag=2, type=2. 表示长度的varint是7,随后跟着7个bytes,就是我们的string。
Embedded Messages
下面定义了一个message,里面又包含了一个Test1类型的message:
message Test3 {
required Test1 c = 3;
}
下面是编码后的版本
1a 03 08 96 01
后三个byte正是第一个例子中的结果(08 96 01), 而且他们前面的byte是3(也就是length)。可以看出embedded message的处理方法和string是完全一样的(wiretype = 2).
Optional And Repeated Elements
如果message中包含repeated类型(没有加[packed=true]选项),那么编码后的massage就会存在0个或多个tag值相同的K-V对。这些重复的values并不一定要连续出现;他们中间有可能掺着其他的fields。这些repeated元素之间的顺序会被保存下来,不过不会记录它们中间掺杂的其他field的顺序。
Packed Repeated Fields
在version 2.1.0中,引入了packed repeated fields,声明的时候和repeated fields一样,但是跟着一个特殊的[packed=true]选项。它和普通的repeated fields的编码方式不同。如果一个packed repeated field里面没有任何element,那么它不会出现在encoded message中,只要这个packed repeated field中包含1个或1个以上的element,那么这些element全部被包入一个key-value对中,而且wire type = 2(length-delimited). 每个element的编码方式都和之前讲的一样,但是他们的value前面没有tag。
例如,这样的一个message type:
message Test4 {
repeated int32 d = 4[packed=true];
}
假设你创建了一个Test4对象,repeated field d中存入了3,270,86942. 那么,编码后的形式将会是:
22 // tag (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)
只有类型为原始数字类型的repeated field才能被声明为“packed”。
注意,尽管没有理由把packed repeated field编码成多于一个的key-value对,encoder必须时刻准备着接受multiple key-value pairs。在这种情况下,负荷被累加在了一起。每个pair都一定包含着所有的elements。
Field Order
在.proto文件中,你可以以任意顺序安排field number,但是,序列化一个message时,它的fields应该按照field number的顺序写入。这样可以让负责parse的code可以使用一些依赖于field number顺序的优化技术。当然,protocol buffer的parser必须能够以任意顺序解析field,因为并不是所有的message都是仅仅经过简单的序列化生成的。