本文档介绍了protocol buffer消息的二进制格式。在你的应用程序中使用protocol buffers的时候,你不需要理解这些,但是对于你想知道不同的protocol buffer格式如何影响你的消息编码的大小是非常有用的。

 

一个简单的消息

假设你有如下的一个简单的消息定义:


message Test1 { required int32 a = 1; }


 在一个应用程序中,你创建了一个Test1消息并且设置a的值为150。然后你将这个消息序列化到一个输出流。如果你想检车编码后的消息,你将看到如下的三个字节:


08 96 01


 

Base 128 Varints

为了理解protocol buffer的编码,首先需要理解varints。Varints是一个将整数序列化成一个或多个字节的方法。数字越小占用的字节数也越小。

一个varint中的每个字节,除了最后一个字节,都要设置一个most significant bit (msb)用来标识接下来还有字节。每个字节的低7位用来存储7个字节表示的数字的二进制补码,least significant group first。

例如,数字1,它是单个字节,所以不需要设置msb:


0000 0001


如果是300,就要复杂一点:


1010 1100 0000 0010


你是如何推断这是300呢?首先你丢掉每个字节的msb,因为msb只是用来告诉我们是否到达数字的末尾:


1010 1100 0000 0010 → 010 1100 000 0010


接着反转两组7位字节,因为varints存储数字的原则是the least significant group first。然后可以计算最终值了:


000 0010 010 1100 → 000 0010 ++ 010 1100 → 100101100 → 256 + 32 + 8 + 4 = 300


 

消息结构

一个protocol buffer消息是一系列的key-value对。一个消息的二进制版本就是使用字段的数字作为key——每个字段的名字和声明的类型在解码结束的时候通过引用消息的类型定义(比如.proto文件)来决定。

当一个消息被编码的时候,keys和values被连结成一个字节流。当消息被解码的时候,解析器需要跳过它不能识别的字段。这样的话,新的字段可以被添加到一个消息中,而不会中断不知道这些新字段的旧程序。每个key-value对中的key实际上包含了两个值——来自.proto文件的字段数字,加上一个类型用来提供足够的信息决定接下来值得长度。

可用的wire类型如下:


int32, int64, uint32, uint64, sint32, sint64, bool, enum

Type

Meaning

Used For

0

Varint

 

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——换句话说,数字的最后三个bit用来存储wire type的。

现在再看一个简单的例子。你现在已经知道流中第一个数字总是一个varint key,这里是08,或者(丢掉msb):


000 1000


你通过后三位bit得到wire type是0,然后右移三位得到字段数字是1。所以你现在知道tag是1,随后的值是一个varint。使用前面的varint解码知道,我们可以知道随后的两个字节存储的值是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


更多的值类型

有符号整数

所有的protocol buffer类型为0的被编码成varints。但是,当编码负数的时候,有符号整型(sint32和sint64)和标准整型(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对于sint32被编码如下:

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

对于sint64,被编码:

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

注意(n>>31)部分是一个算术移位。换句话说,这个移位的结果数字要么所有的位全0(n是正数),要么全1(n是负数)。

当sint32或sint64被解析的时候,其值被解码成有符号的原始值。

 

非varint数字

非varint数字类型也非常简单——double和fixed64其wire type为1,告诉解析器获取一个固定的64位的数据;float和fixed32其wire type为5,告诉解析器获取一个32位的数据。这两种情况下,其对应的值都是以小端字节顺序存储的。

 

字符串

wire type为2意味着其值是一个varint编码长度,后面接着的是指定字节数的数据。

message Test2 {
required string b = 2;
}


设置b的值为"testing",会得到:

12 07 74 65 73 74 69 6e 67

红色的字节部分是UTF-8编码的"testing"。key是0x12->tag=2,type2。长度的值是7,随后的7个字节就是我们的字符串。

 

嵌套的消息

如下的例子中有一个嵌套的消息:


message Test3 {
required Test1 c = 3;
}


编码后的结果如下,Test1的字段a的值设为150:

1a 03 08 96 01

可以看到,后三个字节和我们前面第一个例子相同(08 96 10),表示数字150,其前面是数字3,嵌套消息被当做字符串来看待(wire type=2)。

 

可选和重复元素

如果你的消息中有repeated元素(没有[packed=true]选项),编码后的消息有0个或多个key-value对有相同的tag数字。这些重复的值不需要连续的出现,他们可能与其他的字段交错出现。元素的顺序在解析的时候确定。

如果元素是optional,编码后的消息可能有或没有key-value对。

正常情况下,一个编码的消息拥有的一个optional或required字段的实例不会超过一个。但是,解析器也会处理这种情况。对于数值类型和字符串,如果同样的值出现了多次,解析器只接受最后的值。对于嵌套的消息字段,解析器合并相同字段的多个实例,就像Message::MergeFrom方法做的一样——那就是,后面的实例的单个字段会替换掉前面出现的,单个嵌套消息被合并,重复字段被连结。这些规则的效果就是解析串联出现的两个编码的消息产生的结果和单独解析两个消息然后合并它们的结果是相同的。示例如下:


MyMessage message;
message.ParseFromString(str1 + str2);


等价于:


MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);


 

Packed Repeated字段

版本2.1.0中介绍了packed repeated字段,同时定义了repeated和[packed=true]选项。一个包含0个元素的packed repeated字段不会出现在编码消息中,除此之外,字段的所有元素被打包到一个key-value对中,用wire type 2标识。每个元素按照自身的类型编码。

例如,假设你有如下的消息类型:


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


构造一个Test4,d字段包含的值有3,270,86942。然后,编码的结果如下:


22        // tag (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)


仅仅repeated的基础数值类型(varint, 32-bit, 64-bit)才可以声明"packed"。

 

字段顺序

你可以在一个.proto文件中以任何的顺序来使用字段数字,当一个消息被序列化的时候,它的已知的字段会按照字段数字顺序连续写入。这个允许解析代码依赖于字段数字做优化。但是,protocol buffer的解析器应该能够以任何顺序解析字段,因为并不是所有的消息是通过序列化一个对象创建的。