Protobuf编码指南

这个文档会介绍protocol buffer的二进制有线格式(binary wire format)。你并非须要理解这些后才能在应用里使用protocol buffer,可是当你想知道不一样的protocol buffer格式是如何影响编码后的消息体的体积时,这些知识会很是有用。bash

一个简单的消息

假设有一个很是简单的消息定义:post

message Test1 {
  optional int32 a = 1;
}
复制代码

在应用中,你建立了一个Test1消息并把a设置为150。而后你把消息序列化到输出流中,若是你能查看编码后的消息,你会看到三个字节:ui

08 96 01
复制代码

到目前为止,如此小并且都是数字-可是这是什么意思呢?继续往下看编码

Varint编码

要理解上面protocol buffer编码的数据,你须要先理解vaintsVarints是一种使用一个或多个字节编码整数的方法。较小的数字使用较少的字节。spa

除了最后一个字节外,varint编码中的每一个字节都设置了最高有效位(most significant bit - msb)–msb为1则代表后面的字节仍是属于当前数据的,若是是0那么这是当前数据的最后一个字节数据。每一个字节的低7位用于以7位为一组存储数字的二进制补码表示,最低有效组在前,或者叫最低有效字节在前。这代表varint编码后数据的字节是按照小端序排列的。code

举例来讲,对于数字1-它占用单个字节,因此字节的最高位上是0orm

0000 0001
复制代码

对于数字300会有一点复杂,它占用俩个字节对象

1010 1100 0000 0010
复制代码

那么是怎么计算出来是300的呢?首先你须要把每一个字节的msb去掉,由于它只用来告诉咱们是否已经到达数字的最后一个字节(本例的varint占用俩个字节因此第一个字节的msb为1)element

1010 1100 0000 0010
→ 010 1100  000 0010
复制代码

将两组7位反转,由于你记得,varint存储的数字最低有效组在前。而后,将它们链接起来以得到最终值文档

000 0010  010 1100 (去掉最高有效位,并反转7位组)
→  000 0010 ++ 010 1100
→  100101100
→  256 + 32 + 8 + 4 = 300
复制代码

:varint编码理解起来有点难,能够看以前写的varint编码原理解析

消息的组成

如你所知,一个protocol buffer是一系列键值对。消息的二进制格式只使用消息字段的字段编号做为键--字段名和声明的类型只能在解析端经过引用参考消息类型定义(即.proto文件)才能肯定。

当一个消息被编码时,键和值会被链接放入字节流中。当消息被解码时,分析器须要可以跳过未识别的字段。这样,新加入消息的字段就不会破坏不知道他们存在的那些老程序。为此,有线格式消息中每一个对的“键”其实是两个值-.proto文件中的字段编号,加上一种有线类型,该类型仅提供足够的信息来查找随后的值的长度。在大多数语言实现中,这个键称为标签。

可用的有线类型以下:

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 messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float

在消息流中的每一个键都是varint,使用(filed_number << 3) | wire_type 得到--也就是说字节的后三位存储的是有线类型。

如今让咱们再回到上面的消息示例。你如今知道字节流中的首个字节永远都是一个varint键,在咱们的例子中它是08或者下面的二进制(去掉了msb)。

000 1000
复制代码

经过后三位得出有线类型(0),而后右移三位获得字段编号(1)。如今你知道字段的编号是1对应的值是一个varint。使用前面学到的解码varint的知识,你能够看到下面的两个字节存储着值150。

96 01 = 1001 0110  0000 0001
       → 000 0001  ++  001 0110 (去掉最高有效位,并反转7位组)
       → 10010110
       → 128 + 16 + 4 + 2 = 150
复制代码

更多值类型

有符号整数

就像你在上一部分看到的那样,protocol buffer中全部与有线类型0关联的类型都会被编码为varint。可是,在编码负数时,带符号的int类型(sint32和sint64)与“标准” int类型(int32和int64)之间存在着巨大区别。若是将int32或int64用做负数的类型,则结果varint老是十个字节长––实际上,它被视为一个很是大的无符号整数。若是使用带符号类型(sint32和sint64)之一,则生成的varint使用ZigZag编码,效率更高

ZigZag编码将有符号数映射到无符号数以便具备较小绝对值的数字(好比-1)也具备较小的varint编码值。这样作的方式是经过正整数和负整数来回“曲折”,将-1编码为1,将1编码为2,将-2编码为3,依此类推,能够在下表中看到:

Signed Original Encoded As
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295

非varint数字

对与非可varint编码的数字来讲比较简单--doublefixed64使用有线类型1,这会告诉解析器指望固定的64-bit的数据块。类似地floatfixed32使用有线类型5,这会告诉解析器指望固定的32-bit数据块。这两种状况都是使用小端序排列字节存储数据的。

字符串

有线类型2(长度分隔)表示该值是varint编码的长度值,后跟长度值指定数量的数据字节。

message Test2 {
  optional string b = 2;
}
复制代码

设置b的值为"testing"后消息对应的二进制有线格式为

12 07 74 65 73 74 69 6e 67

红色的字节是UTF-8编码后的"testing"

这里的键是0x12→0001 0010→字段号= 2,类型=2(第一个字节的后三位表示有线类型的编号,而后右移三位变成000 0010获得字段号)。值中的varint表示的数据字节长度是7,如你所见咱们在它后面找到的七个字节–就是解析器要找的字符串。

内嵌消息

下面是一个拥有内嵌消息的消息定义Test3,内嵌的消息类型是咱们上面示例中定义的Test1

message Test3 {
  optional Test1 c = 3;
}
复制代码

下面则是内嵌的Test1中的a设置为150,Test3`被编码后的版本

1a 03 08 96 01

如你所见,最后三个字节和咱们第一个例子编码后的结果同样(08 96 01),在他们以前是数字3,--内嵌消息会像字符串同样被对对待(有线格式=2)。

可选和可重复元素

若是proto2消息定义具备重复的元素(不带[packed = true]选项),则编码消息具备零个或多个具备相同字段编号的键值对。这些重复的值没必要连续出现。它们可能与其余字段交错。解析时,元素之间的顺序会保留下来,尽管其余字段的顺序会丢失。在proto3中,重复字段使用packed编码,能够在下面看到相关编码。

一般,编码消息永远不会有一个以上非重复字段的实例。可是,解析器能处理这种实际状况,对于数字类型和字符串,若是同一字段屡次出现,则解析器将接受它看到的最后一个值。对于嵌入式消息字段,解析器将合并同一字段的多个实例,就像使用Message :: MergeFrom方法同样-也就是说,后一个实例中的全部单个标量字段将替换前一个实例中的单个标量字段,可重复字段会被串联到一块。这些规则的做用是,解析两个编码的消息的链接所产生的结果与您分别解析两个消息并合并结果对象的结果彻底相同。也就是说:

MyMessage message;
message.ParseFromString(str1 + str2);
复制代码

等同于

MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
复制代码

这个特性有时颇有用,由于即便您不知道它们的类型,也容许你合并两个消息。

压缩重复字段

proto版本2.1.0引入了压缩重复字段,在proto2中声明为重复字段,并使用特殊的[packed = true]选项。在proto3中,默认状况下压缩标量数字类型的重复字段。这些功能相似于重复的字段,但编码方式不一样。包含零元素的压缩重复字段不会出如今编码的消息中。不然,该字段的全部元素都将打包为有线类型为2(定界)的单个键值对。每一个元素的编码方式与一般相同,不一样之处在于元素以前没有键。

举例来讲,你有如下消息类型:

message Test4 {
  repeated int32 d = 4 [packed=true];
}
复制代码

如今假设您构造一个Test4,为重复的字段d提供值三、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位或64位线型的类型)的重复字段声明为“packed”。

字段顺序

字段编号能够在.proto文件中以任何顺序使用。选择使用的顺序对消息的序列化方式没有影响。

序列化消息时,对于如何写入其已知字段或未知字段没有保证的顺序。序列化顺序是一个实现细节,未来任何特定实现的细节均可能更改。所以,protocol buffer解析器必须可以以任何顺序解析字段。

相关文章
相关标签/搜索