Base 128 Varints
Varints是一种使用一个或多个字节表示整型数据的方法。其中数值本身越小,其所占用的字节数越少。
在Varint中,除了最后一个字节之外,其余每个字节中都包含一个MSB(最高位),字节中的其余七位将用于存储数据本身。在解码的时候,如果读到的字节的 MSB 是 1 话,则表示还有后序字节,一直读到 MSB 为 0 的字节为止。
通常而言,整数数值都是由字节表示,其中每个字节为8位,即Base 256。然而在Protocol Buffer的编码中,最高位成为了msb,只有后面的7位存储实际的数据,因此我们称其为Base 128。
将数据转换成Varints编码有如下五个步骤:
- 将数据转换为二进制。
- 从 LSB 到 MSB 每七位分一组,不足七位高位补零。
- 最左边一组高位补0,其他组高位补1。
- 转换成十六进制。
- 从 LSB 到 MSB 输出结果(即小端的表示方法)。
举个例子,整数624485 可以作如下编码:
MSB ------------------ LSB
10011000011101100101 <1> 二进制 20bit
0100110 0001110 1100101 <2> 从 LSB 到 MSB 每七位分一组,不足七位高位补零
00100110 10001110 11100101 <3> 最左边一组高位补零,其他组高位补一
0x26 0x8E 0xE5 <4> 转换成十六进制
→ 0xE5 0x8E 0x26 <5> 从 LSB 到 MSB 输出结果
编码方式
protobuf3每个字段编码后从逻辑上分为四个部分:
<tag> <type> [<length>] <data>
其中<tag>
表示字段(或变量)的唯一标识符,<type>
是该字段的类型,<length>
表示字段的长度,<data>
是经过Varints编码(字符串用UTF-8 编码)的数据。
<tag>
和<type>
共占1个字节,但是<tag>
超过 16 时,就需要用两个以上的字节表示了,<length>
是可选的。
protobuf3定义了4种类型的type:
0 VarInt 表示int32,int64,uint32,uint64,sint32,sint64,bool,enum
1 64-bit 表示fixed64,sfixed64,double
2 Length-delimited 表示string,bytes,embedded messages,repeated字段
5 32-bit 表示fixed32,sfixed32,float
其中 3 和 4 表示的类型已经废弃,不多讨论。因为类型比较少,所以 protubuf3在编码type的时候只用了3比特,实际传输的时候是以 (tag<<3)|type 的方式传输的,即<tag>
和<type>
共占1个字节。但是,单字节的最高位是零,而最低3位表示类型,所以只剩下 4 位可用了,因此<tag>
超过 16 时,就需要用两个以上的字节表示了。
使用<tag>
的优点是不用重复传输字段名,但也是它的缺点。因为没有字段名,所以编码和解码的代码必须持有一份字段名和 tag 的映射关系,这是在生成代码的时候自动完成的。也就是说,没有 proto 文件,你是没法对 Protocol Buffers 数据进行解码的。
正数的编码
message Foo {
int32 foo = 1;
}
Foo 的 foo 字段取值为 1 的话,则对应的编码是:0x08 0x01。foo 的类型是 int32,对应的 type 取 0。而它的 tag 又是 1,所以第一个字节是 (1<<3)|0 = 0x08,第二个字节是数字 1 的 VarInts 编码,即 0x01。
7 0 7 0
+-----+---+--------+
|00001|000|00000001|
+-----+---+--------+
tag type data
字符串的编码
message Foo {
string bar = 2;
}
Foo 的 bar 字段取值为 吕 的话,则对应的编码是:0x12 0x03 0xe5 0x90 0x95。bar 的类型是 string,对应的 type 取 2。而它的 tag 又是 2,所以第一个字节是 (2<<3)|2 = 0x12,第二个字节表示字符串的长度为 3,再后面 3 个字节是汉字吕 UTF-8 编码。
7 0 7 0 25 0
+-----+---+--------+===========+
|00010|010|00000011|0xe50x90x95|
+-----+---+--------+===========+
tag type length utf-8
嵌套消息的编码
message Baz {
int32 b = 1;
}
message Bar {
repeated int32 a = 1;
Baz b = 2;
}
如果我们让 Bar 的 a 字段取 [1,2,3],让 b 字段取 {4},则对应的编码为 0x0a 0x03 0x01 0x02 0x03 0x12 0x02 0x08 0x04。这段数据可以拆成两部分:
1. 0x0a 0x03 0x01 0x02 0x03
2. 0x12 0x02 0x08 0x04
先说第一部分。因为 a 的类型为 repeated int32,所以对应 type 取 2;又 a 的 tag 为 1,所以第一个字节应该是 (1<<3|2) = 0x0a。第二个字节表示数组长度,所以是 0x03,接下来三个字节分别是 1, 2, 3 的 VarInts 编码。
再说第二部分。因为 b 的类型为 Bar,所以对应的 type 也是 2;又 b 的 tag 为 2,所以第一个字节应该是(2<<3|2) = 0x12。第二个字节表示 message 的长度,所以是 0x02,接下来两个字节表示 Baz 的编码,Baz中 b 的类型是 int32,对应的 type 取 0。而它的 tag 又是 1,所以第一个字节是 (1<<3)|0 = 0x08,第二个字节是数字 4 的 VarInts 编码,即 0x04。
7 0 7 0 25 0 7 0 7 0 7 0 7 0
+-----+---+--------+===========+-----+---+--------+=====+===+========+
|00001|010|00000011|0x010x02x03|00010|010|00000010|00001|000|00000100|
+-----+---+--------+===========+-----+---+--------+=====+===+========+
tag type length utf-8 tag type length tag type data
|<----- baz.b ---->|
|<---------- Bar.a ----------->|<-------------- Bar.b -------------->|
单从数据来看,我们无法区分 string,repeated 和 message。要想解析这类数据,必须依赖 proto 定义。
负数的编码——ZigZag
VarInts 不太适合表示负数。因为负数在计算机使用补码表示,转成 unit64 是一个很大的数。当你使用 VarInts 表示的时候,-1 居然要占用 10 个字节!为此,Protocol Buffers 引入了 sint32 和 sint64 两种类型,在编码的时候先将数字转化成 ZigZag 编码。ZigZag 思想也很简单,就是用正数来表示负数,映射规则如下:
(n << 1) ^ (n >> 31) //sint32
(n << 1> ^ (n >> 63) //sint64
protobuf3在实现上述位移操作时均采用的算术位移,因此对于(n >> 31)和(n >> 63)而言,如果n为负值位移后的结果就是-1,否则就是0。
ZigZag对照表如下:
Original ZigZag
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295