Google Protocol Buffers,简称:protobuf
protobuf是什么?
protobuf是Google的语言中立、平台中立、可扩展的结构化数据序列化机制——想想XML,但更小、更快、更简单。您可以定义一次数据的结构化方式,然后您可以使用特殊生成的源代码轻松地使用各种语言在各种数据流中写入和读取结构化数据。
protobuf目前支持在Java、Python、Objective-C和C++中生成的代码。使用我们新的proto3语言版本,您还可以使用Dart、Go、Ruby和C#,以及更多的语言。
Message Structure
protobuf中message是一系列键值对。message的二进制版本只是使用字段号(field's number和type)作为key。每个字段的名称和声明类型只能在解码端通过引用消息类型的定义(即 .proto 文件)来确定。
如果没有数据结构描述.proto文件,拿到数据以后是无法解释成正常的数据的。
要了解protobuf编码之前,先需要了解varint。变体是一种使用一个或多个字节序列化整数的方法。较小的数字占用较少的字节数。
varint中的每个字节,除了最后一个字节,都设置了最高有效位 (msb)——这表明还有更多的字节要来。每个字节的低7位用于以7位为一组存储数字的二进制补码表示,最低有效组在前。
例如300,如果用Varint表示的话:1010 1100 0000 0010
取出尾7位010 1100 000 0010
前后调换顺序1 0010 1100
即256 + 32 + 8 + 4 = 300
protobuf中字段类型如下:(3、4已被废弃,只剩下0、1、2、5)
TypeMeaningUsed For
0 Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bitfixed64, sfixed64, double
2 Length-delimitedstring, bytes, embedded messages, packed repeated fields
3 Start groupgroups (deprecated)
4 End groupgroups (deprecated)
5 32-bitfixed32, sfixed32, float
key 的计算方法是field_number << 3) | type,换句话说,key的最后3位表示的就是type。
一般message的字段号都是1开始的,所以对应的tag可能是这样的:0000 1000,即:08。
末尾3位表示的是value的类型,这里是000,即0,代表的是varint值。右移3位,即0001,这代表的就是字段号field number
varint
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (drop the msb and reverse the groups of 7 bits)
→ 10010110
→ 128 + 16 + 4 + 2 = 150
96 01代表的数据就是150。
message Test1 {
optional int32 a = 1;
}
如果存在上面这样的一个message的结构,如果存入150,在Protobuf中显示的二进制应该为08 96 01。
sint32/sint64
一个负数一般会被表示为一个很大的整数,因为计算机定义负数的符号位为数字的最高位。如果采用varint表示一个负数,那么一定需要 10 个 byte 长度。(因为源码里面是这么规定的。32 位的有符号数都会转换成 64 位无符号来处理。 )
protobuf定义了 sint32 这种类型,采用zigzag编码。将所有整数映射成无符号整数,然后再采用varint编码方式编码,这样,绝对值小的整数,编码后也会有一个较小的varint编码值。
zigzag编码将有符号整数映射为无符号整数,因此具有较小绝对值(例如,-1)的数字也具有较小的变量编码值。它以一种通过正整数和负整数来回“之字形”的方式实现,因此-1编码为1,1编码为2,-2编码为3,依此类推,如下表所示:
Signed Original Encoded As
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295
当sint32或sint64被解析时,它的值被解码回原始的带符号的版本
Non-varint Numbers
非可变数字类型很简单–double和fixed64具有type 1,它告诉解析器需要一个固定的64位数据块;类似地,float和fixed32的导线类型为5,这告诉它需要32位。在这两种情况下,值都以小端字节顺序存储。
string
Type为2(长度分隔)表示该值是可变编码长度,后跟指定的数据字节数。
message Test2 {
optional string b = 2;
}
假如b的值是"testing":12 07 [74 65 73 74 69 6e 67]
0x12
→ 0001 0010 (binary representation)
→ 00010 010 (regroup bits)
→ field_number = 2, wire_type = 2
值中的长度变量是7,我们发现它后面有7个字节——我们的字符串。
Type为 2 的数据,是一种指定长度的编码方式:key + length + content,key 的编码方式是统一的,length 采用 varints 编码方式,content 就是由 length 指定长度的 Bytes。(T-L-V (Tag - Length - Value))
repeated
Tag - Value - Tag - Value - Tag - Value ……
message Test4 {
repeated int32 d = 4;
}
20 // tag (field number 4, wire type 0)
03 // first element (varint 3)
20 // tag (field number 4, wire type 0)
8E 02 // second element (varint 270)
20 // tag (field number 4, wire type 0)
9E A7 05 // third element (varint 86942)
Tag - Length - Value - Value - Value ……
message Test4 {
repeated int32 d = 4 [packed=true];
}
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”
嵌入式message
message Test3 {
optional Test1 c = 3;
}
设置字段为整数150,编码后的字节为:1a 03 08 96 01 08 96 01这三个代表的是150,上面讲解过,这里就不再赘述了。 1a -> 0001 1010,后三位010为type = 2,0001 1010右移三位为0000 0011,即tag = 3。length为3,代表后面有3个字节,即08 96 01。 需要转变为T - L - V形式的有string, bytes, embedded messages, packed repeated fields(即type为2的形式都会转变成T - L - V形式)
map
map
projects = N; 是语法糖 message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
key_type不能是float、double、bytes和enum。