【微服务】protocol buffer 编码机制

编码机制

  • 消息示例
  • Base 128 Varints
  • 消息结构
  • 其他数据类型
  • 嵌入式的消息
  • Optional And Repeated Elements
  • 字段顺序

这篇文档介绍了 protocol buffer 消息的二进制传输格式。理解与否不影响对 protocol buffer 的使用,
但是对于了解消息编码后的不同类型数据的大小区别会很有帮助。

消息示例

假设我们已经定义好了下面的消息:

message Test1 {
  optional int32 a = 1;
}

在应用中我们给消息 Test1 的 a 赋值 150 ,然后在发送消息的过程中抓包会看到下面三个字节的数据:

08 96 01

这代表些什么意思?接下来会慢慢告诉你。

Base 128 Varints

要理解 protocol buffer 的编码机制,首先需要了解 varints。varints 用一个或多个字节序列化整型数据,越小的数需要的字节越少。
在 varint 中每一个字节(除了最后一个字节)的最高位(msb)都会被用来表示该字节后面还有没有其它字节,剩下的低七位用来表示
被拆分的这两部分数值,优先表示低位的数值。举例来说,单字节数值 1 的 msb 为 0,表示后面没有其他字节来共同表示 1:

0000 0001

稍复杂的例子如 300:

1010 1100 0000 0010
  1. 先去掉每个字节的最高位(代表该字节后面还有没有字节)
1010 1100 0000 0010
→ 010 1100  000 0010
  1. 然后把这两组七位二进制反转(因为 varint 先表示低位数值),拼接反转后的两组二进制得到原先的 300
000 0010  010 1100
→  000 0010 ++ 010 1100
→  100101100
→  256 + 32 + 8 + 4 = 300

消息结构

Protocol buffer 消息是由一系列的键值对定义的。上面用于传输的二进制消息只用到了定义中字段的编号 —— 字段的名称和类型是在
二进制消息解码时通过参考消息类型的定义(即 .proto 定义文件)来最终确定的。
消息在编码时,键和值被连接成了字节流。消息在解码时,解释器会过滤掉它无法识别的字段。如此说来,在消息中添加新的字段并不会影响旧版本应用的正常运行。
为了不影响旧版本应用的正常运行,传输消息中代表键值对的 key 实际上包含两部分 —— 在 .proto 文件中的字段编号和一个可以表示数据长度的传输类型。
在很多程序语言的设计和实现中,这个 key 被称为 tag。下面是可用消息传输数据类型和消息定义数据类型的对照表:

TypeMeaningUsed For
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,这个 varint 是由字段编号左移三位和传输类型做或运算得到的,换言之,这个 varint 的后三个比特位
代表的就是传输类型。让我们再来看一下上面的例子,在传输流中的第一个数值代表的是 varint 类型的 key,在例子中是 08,去掉 msb 位为:

000 1000

从后三位可以得出传输类型(0),右移三位得到该字段的编号为 1 ,用上一节的 varint-decoding 方法解码剩下的两个字节得到该字段的值为 150:

96 01 = 1001 0110  0000 0001
       → 000 0001  ++  001 0110 (去掉 msb 位并反转两组7位二进制)
       → 10010110
       → 128 + 16 + 4 + 2 = 150

其他数据类型

  • Signed Integers

上一小节已经介绍过了,所有关联传输类型 0 的 protocol buffer 数据类型都被用 varints 来编码。
然而,在对负数进行编码时,有符号的整型(sint32 和 sint64)和 “标准” 整型(int32 和 int64)是有很大区别的。
如果定义一个负数的类型为 int32 或 int64,编码后的 varint 长度总是 10 个字节 —— 实际上,这个负数会被当作一个非常大的无符号整数来处理。
如果这个负数的类型被定义成一种有符号的整型,在编码时会用更高效的 ZigZag 方式。ZigZag 编码方式对有符号的整型和无符号的整型做了映射,
所以说,绝对值较小的数值(比如说 -1)也会对应较小的 varint 编码值。
ZigZag 是通过来回地对正负值做 “zig-zags” 实现的,比如说 -1 被编码成 1 ,1 被编码成 2 ,-2 被编码成 3 等等,见下表:

Signed OriginalEncoded As
00
-11
12
-23
21474836474294967294
-21474836484294967295

换言之,每一个数值 n 的编码方式如下:
对于 sint32 系来说

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

对于 64 位来说

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

注意第二个右移 —— n >> 31部分 —— 是算数右移(算数右移左边添加的数字和符号位有关,逻辑右移左边统一添加 0;
算数左移和逻辑左移一样,在右边统一添加 0)。所以说,右移的结果要么是全 0 位(如果 n 是一个正数),要么是全 1 位(如果 n 是一个负数)。
sint32 或 sint64 类型的数据在解析时,他们的值会被解码成对应的原始有符号数值。

  • Non-varint Numbers

Non-varint 数值类型是易于理解的 —— double 和 fixed64 对应传输类型 1 ,这表明解释器期望解析的是一个固定的 64 位数据块;
类似地,float 和 fixed32 对应传输类型 5 ,它表明期望解析 32 位的数据块。
在这所有的情况下,数据都是采用小端存储(低尾端存储,即尾端存储在低地址)。

  • Strings

传输类型 2 (length-delimited)表示该值是一个 varint 编码的长度后面紧跟着指定数量字节的数据。

message Test2 {
  optional string b = 2;
}

给 b 赋值 “testing”:

12 07 74 65 73 74 69 6e 67

后七个字节代表 UTF8 编码的 “testing”。在这里 key 是 0x12,代表字段编号为 2,传输类型为 2 。
数据长度是 7 个字节,在其后面正好是 7 个字节的字符串 “testing”。

嵌入式的消息

定义一个嵌入 Test1 的嵌入式消息如下:

message Test3 {
  optional Test1 c = 3;
}

下面是该消息在传输中的编码格式(还是给 Test1 的 a 字段赋值 150):

1a 03 08 96 01

后三个字节(08 96 01)和我们上面 Test1 的例子一样,并且在它们前面是长度 3 。这表示嵌入式消息的编码方式和 strings(传输类型为 2)是一样的。

Optional And Repeated Elements

如果在 proto2 消息中定义了不带[packed=true]选项的 repeated 元素,编码后的消息数据会含有 0 或多个相同字段编号的键值对。
这些重复的键值对没有必要连续出项,它们有可能被其它字段错开。在解析时,元素相对于彼此的顺序会被保留,尽管这些顺序会丢失。
在 proto3 中,repeated 字段会采用 packed 编码方式,我们会在下面讲解。
对任何 proto3 中的 non-repeated 字段或 proto2 中的 optional 字段来讲,编码后的键值对表示不一定会包含它们。
通常来说,non-repeated 字段在编码后的消息中不会存在多个实例。然而,解释器已经可以处理这种重复实例的情况。
对于 numeric types 和 strings 来讲,如果一个字段出现了多次,解释器只会保留它的最后一个值。对于嵌入式的消息字段来讲,
解释器会合并同一字段的值,就好像使用了 Message::MergeFrom 方法一样 —— 用所有 singular 标准字段后来的值替换了
former 中相同字段的值,合并了 singular 嵌入式的消息,连接了 repeated 字段。
这些规则将会使以下两种方式产生一样的效果:

  1. 解析两个嵌入式消息的连接体
MyMessage message;
message.ParseFromString(str1 + str2);
  1. 分别解析两个单独的消息之后再合并结果
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

这种特性在合并两种未知类型的消息时会有用武之地。

Packed Repeated Fields

2.1.0 版本引入了 packed repeated 字段,在 proto2 中被关键字 repeated 修饰并带有[packed=true]选项,
在 proto3 中 repeated 字段默认就是 packed。这些功能和 repeated 字段类似,但是在编码上有区别。
包含 0 个元素的 packed repeated 字段不会出现在编码消息中,否则,这个字段的所有元素都会被 packed 成
一个 single 的键值对并且它的 wire type 为 2 (length-delimited),每个元素的编码方式与以往相同,只是在前面没有 key。
举个例子:

message Test4 {
  repeated int32 d = 4 [packed=true];
}

为 d 赋值 3, 270, 和 86942,编码如下:

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)

只有那些原生数字类型(对应传输类型为 varint、32-bit、64-bit)的 repeated 字段才能被声明为 “packed”。
注意:通常不会用多个键值对来编码一个 packed repeated 字段,但是编码器必须能接收多个键值对。在这种情况下,
payloads 应该被连接,每一个键值对应该包含全部数量的元素。
Protocol buffer 解释器能够解析以 packed 方式编译的 repeated 字段,就像它们没被以 packed 方式编译过一样,
反之亦然。这使得在已有的字段上添加[packed=true]选项有了向前和向后的兼容性。

字段顺序

虽然可以在 .proto 文件中以任意的顺序使用字段的编号,但是就如同我们给出的 C++、Java 和 Python 序列化示例代码中一样:消息在序列化时,已知字段会按它们的编号顺序地被写入。这样做允许解析代码能够依赖顺序的字段编号做优化。然而,protocol buffer 解释器可以解析按任意顺序排列的字段,因为不是所有的消息都会被简单地序列化 —— 比如说,有时通过简单地连接两个消息来合并它们会大有裨益。如果一个消息包含未知字段,目前 Java 和 C++ 的实现是在按编号顺序排列的已知字段后面以任意顺序写入它们,而在 Python 的实现中没有跟踪这些未知字段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值