本文档描述了协议缓冲区消息的二进制线格式。您不需要理解这一点就可以在应用程序中使用协议缓冲区,但是了解不同的协议缓冲区格式如何影响编码消息的大小可能非常有用。
一个简单的消息
假设你有以下非常简单的消息定义:
message Test1 {
required int32 a = 1;
}
在应用程序中,您创建一个Test1
消息并设置a
为150.然后,将消息序列化为输出流。如果您能够检查编码的消息,您会看到三个字节:
08 96 01
到目前为止,你看到这么小的几个数字 - 但是这是什么意思?继续阅读…
Base 128 Varints
要了解您的简单协议缓冲区编码,您首先需要了解varints
。Varints是使用一个或多个字节序列化整数的方法。较小的数字占用较少的字节数。
除了最后一个字节,varint中的每个字节都以最高有效位(msb)置位 - 表示后面是否还有字节出现。
每个字节的低7位用于以7位组的形式存储数字的二进制补码表示,最低有效组优先。
所以,例如,这里是数字1 - 它是一个单字节,所以msb没有设置:
0000 0001
而这里是300 - 这有点复杂:
1010 1100 0000 0010
你怎么知道这是300?首先从每个字节中删除msb,因为这只是告诉我们是否还有后继字节(可以看到,它在第一个字节的最高位设置为1,是因为该varint中有多个字节):
1010 1100 0000 0010
→ 010 1100 000 0010
你需要反转这两个7位组,因为你记得,varints首先存储具有最低有效组的数字。然后连接它们以获得最终的值:
000 0010 010 1100
→ 000 0010 ++ 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300
消息结构
如您所知,协议缓冲区消息是一系列键值对。消息的二进制版本只是使用字段的号码作为key - 每个字段的名称和声明类型只能在解码端通过引用消息类型的定义(即.proto文件)来确定。
当消息被编码时,键和值被连接成字节流。当消息被解码时,解析器需要能够跳过它不能识别的字段。这样,新的字段可以添加到消息中,而不会破坏不了解它们的旧程序。
为此,线格式消息中每对的“key”实际上是两个值 - 来自.proto文件的字段编号, 加上一个线型,它提供足够的信息来查找以下值的长度。
可用的线型如下:
类型 | 含义 | 用于 |
---|---|---|
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 |
在流式传输的消息中每个key都是一个带线型值的varint(field_number << 3) | wire_type
(字段编号左移3位后与线型按位或) - 换句话说,数字的最后三位存储线型。
现在再来看一下我们的简单例子。你现在知道流中的第一个数字总是一个varint键,这里是08,(丢弃msb如下):
000 1000
您可以取最后三位获得线型(0),然后右移三位以获得字段编号(1)。所以你现在知道字段标签是1,下面的值是一个varint。使用上一节中的varint-decoding知识,可以看到接下来的两个字节存储值为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
更多值类型
Signed Integers
如上一节所述,与线型0相关联的所有协议缓冲区类型都被编码为varints。然而,在编码负数时,签名的int类型(sint32和sint64)和“标准”int类型(int32和int64)之间存在重要的区别。
如果使用int32或int64作为负数的类型,则生成的varint始终为10个字节长 - 它被有效地视为非常大的无符号整数。如果您使用其中一种签名类型,则生成的varint使用ZigZag编码,效率更高。
ZigZag编码将有符号整数映射到无符号整数,以使具有小绝对值(例如-1)的数字也具有小的varint编码值。它以一种通过正整数和负整数“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编码, 对于sint32s
使用:
(n << 1) ^ (n >> 31)
,或对于64位
版本使用:
(n << 1) ^ (n >> 63)
注意,第二个移位 - (n >> 31)
部分是一个算术移位。因此,换句话说,移位的结果是全零位(如果n为正)或全1位(如果n为负)的数字。
当sint32
或sint64
被解析时,其值被解码回原始的签名版本。
// 示例: n为1
0000 0001 原
0000 0001 反
0000 0001 补
00000000 00000000 00000000 00000001 << 1 → 00000000 00000000 00000000 00000010
^
00000000 00000000 00000000 00000001 >> 31 → 00000000 00000000 00000000 00000000
→ 00000000 00000000 00000000 00000010
= 2
示例: n为-2
1000 0010 原
1111 1101 反
1111 1110 补
11111111 11111111 11111111 11111110 << 1 → 11111111 11111111 11111111 11111100
^
11111111 11111111 11111111 11111110 >> 31 → 11111111 11111111 11111111 11111111
→ 00000000 00000000 00000000 00000011
= 3
非varint数字
非varint数字类型是简单的 - double
和fixed64
具有线型1,它告诉解析器期望固定的64位数据块;类似地,float和fixed32具有线型5,这告诉它预期32位。
在这两种情况下,值都以低位优先的字节顺序存储。
字符串
线型2(长度分隔)意味着该值是一个varint编码长度,后跟指定的数据字节数。
message Test2 {
required string b = 2;
}
将b的值设置为“testing”:
12 07 74 65 73 74 69 6e 67
74 65 73 74 69 6e 67
字节是“testing”的UTF8表示。
这里的key是0x12 → tag = 2,type = 2
。值的长度varint编码为7,并且我们发现我们的字符串后面有七个字节。
0x12 转换成2进制 → 0001 0010
丢弃msb → 001 0010
后三位为线型2: 010
剩下位>>3为字段编号2: 000 0010
注意 消息的编码是以16进制格式表示的
嵌套消息
这是一个包含我们示例类型的嵌入消息的消息定义,Test1:
message Test3 {
required Test1 c = 3;
}
这里是编码版本,Test1的一个字段再次设置为150:
1a 03 08 96 01
如您所见,最后三个字节与我们的第一个例子(08 96 01)完全相同,它们之前是03 - 嵌入式消息的处理方式与字符串(线型=2)完全相同。
……
参考链接: Encoding & Third-Party Add-ons