Protocol Buffers Encoding

本文详细介绍了协议缓冲区消息的二进制线格式,包括Base128 Varints的使用,不同类型的数值如何编码,以及字符串和嵌套消息的处理方式。通过具体的例子帮助读者更好地理解协议缓冲区的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文档描述了协议缓冲区消息的二进制线格式。您不需要理解这一点就可以在应用程序中使用协议缓冲区,但是了解不同的协议缓冲区格式如何影响编码消息的大小可能非常有用。

一个简单的消息

假设你有以下非常简单的消息定义:

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文件的字段编号, 加上一个线型,它提供足够的信息来查找以下值的长度。

可用的线型如下:

类型含义用于
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages, packed repeated fields
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, 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 0001000 0001  ++  001 0110 (drop the msb and reverse the groups of 7 bits)
       → 100101102 + 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 OriginalEncoded As
00
-11
12
-23
21474836474294967294
-21474836484294967295

换句话说,每个值n编码, 对于sint32s使用:

(n << 1) ^ (n >> 31)

,或对于64位版本使用:

(n << 1) ^ (n >> 63)

注意,第二个移位 - (n >> 31)部分是一个算术移位。因此,换句话说,移位的结果是全零位(如果n为正)或全1位(如果n为负)的数字。

sint32sint64被解析时,其值被解码回原始的签名版本。

// 示例: 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数字类型是简单的 - doublefixed64具有线型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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值