最近在了解对象的序列化,知道google的Protocol Buffers定义了结构的序列化,想了解一下,因此边学边记录,错误之处还请指正,共同进步。
首先看一下一个简单的message定义:
message Test1 { optional int32 a = 1; }
我们在程序中创建一个Test1 and Test1.a = 150,然后序列化,得到如下3个字节
08 96 01
然后一个int32应该是4字节,那么到底是怎么样做到的呢?
理解protocol buffer编码之前,先要了解varints编码,varints编码是一种使用一个或多个字节序列化整数的方法,数字越小占用的字节数也就越少。
在varints中,对于每一个字节,除了最后一位是一个重要的bitset外(msb) ---表面之后是否还有字节,其余的低7位用于存储一个数的二进制补码,低位优先。
例如,数字1 - ---------- 0000 0001,
低7位存储1的值 : 000 0001, 最后一位0,表示没有下一个字节表示
数组300------------1010 1100 0000 0010
先看第一个字节:1010 1100,数值010 1100 然后最后一位1表示有下一个字节
第二个字节:0000 0010,数值000 0010,然后最后一位0表示没有下一个字节
因此300的值读取为(低位优先) 000 0010 010 1100计算一下值得到256 + 32 +8 + 4 = 300
protocol buffer message是一系列的键值对。二进制版本的message仅仅用域的数字作为key,域的类型只有在解码的时候通过引用message类型定义知道。
在编码的时候,键和值链接成字节流。当消息被解码时,解析器需要能够跳过它不能识别的字段。这样,当新字段添加到消息中,就不会破坏识别不了它们的旧程序。因此,线性格式消息中每对的key实际上是两个值——域的值来自.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 |
每一个key在message流中是一个varint 用域值(field_number << 3) | wire_type
,其实就是最低三位表示的线型。
我们在看一下之前的例子,数字1,域的值总是varint:
000 0001 左移三位,000 1000 线型表示varint 000 进行与运算 得到:000 1000
可以看到,这里域的值1,线型为0,接下来就是varint类型的值。比方说之前的150:
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
在什么说道,所有0型的线型都是以varint编码,然而,当编码负数的时候,sint32 与 标准的int32有一个大的不同。如果用标准的int32编码负数,总是10字节长,课件标准int32仅仅对于无符号数是非常有效的。varint ZigZag 锯齿编码,对于负数是更好的:
ZigZag 编码采用把有符号数编码为无符号数,让数值拥有更小的绝对值和更小的varint编码值。方法是在正负数之间来回曲折, -1 is encoded as 1, 1 is encoded as 2, -2 is encoded as 3, ……
Signed Original | Encoded As |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
简单说,编码n采用如下格式:
for sint32 version (n << 1) ^ (n >> 31) for the 64-bit version:(n << 1) ^ (n >> 63)
注意:上面的第二个移位属于算术移位(n >> 31),如果是正数,则用0补齐,如果是负数则用1补齐
非可变的数字类型很简单——double和fixed64属于1线型,这告诉解析器期望固定的64位数据块;类似地,float
和fixed32
具有线型5,这告诉它期望32位。在这两种情况下,这些值都以小端字节顺序存储。
线型为2(长度分隔)意味着该值是一个可变的编码长度,后跟指定的数据字节数。
message Test2 {
optional string b = 2;
}
例如"testing"这个字符串:
12 07 74 65 73 74 69 6e 67
红色的部分为UTF8 的"testing",
key 0x12 → 001 0010 可得 field number = 2, type = 2.
0x07表示后面跟的字符串的长度
内嵌message的定义在上面Tset1中
message Test3 {
optional Test1 c = 3;
}
假设Test1.a = 150,下面是编码后的字节:
1a 03 08 96 01
1a 03-> 001 1010 可得3域类型,2线型,长度分割的,之后的03表示有3个字节,红色部分即上面150的varint编码值
如果proto2消息定义具有repeated
元素(没有 [packed=true]
)选项,则编码消息具有零个或多个具有相同字段编号的键值对。这些重复值不必连续出现;它们可以与其他字段交错。在解析时,元素相对于彼此的顺序被保留,尽管相对于其他字段的顺序丢失了。在proto3中,重复字段使用打包编码,您可以在下面阅读。
对于proto3中的任何非重复字段或proto2中的可选字段,编码消息可能具有也可能不具有与该字段编号的键值对。
通常,一个编码的消息绝不会有一个以上的不重复字段实例。然而,解析器应该被期待处理这样的情况。对于数字类型和字符串,如果同一个字段出现多次,解析器将接受它看到的最后一个值。对于嵌入的消息字段,解析器将合并同一个字段的多个实例,like Message::MergeFrom
方法。后一种情况下的所有单个标量字段替换前一种情况下的字段,合并单个嵌入消息,并将重复的字段连接起来。这些规则的效果是,解析两个编码消息的连接会产生完全相同的结果,就像您分别解析了这两个消息并合并了结果对象一样。like:
MyMessage message;
message.ParseFromString(str1 + str2);
is equivalent to this:
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
这个属性有时很有用,因为它允许您合并两个消息,即使您不知道它们的类型。
2.1.0版引入了打包的重复字段,在proto2中,这些字段被声明为类似于重复字段,但带有特殊的[packed=true]
option.
在proto3中,默认情况下,标量数值类型的重复字段被打包。这些功能类似于重复的字段,但编码方式不同。包含零元素的打包重复字段不会出现在编码消息中。否则,字段的所有元素都被打包成一个具有导线类型2(长度分隔)的键值对。每个元素都以正常方式编码,除了前面没有键
For example,
message Test4 { repeated int32 d = 4 [packed=true]; }
我们建立一个Test4 然后添加值3270, 86942,encode得到如下字节:
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, or 64-bit wire types的repeated字段可以声明为"packed"
请注意,虽然通常没有理由为一个打包的重复字段编码多个键值对,但是编码器必须准备好接受多个键值对。在这种情况下,有效载荷应该连接在一起。每对必须包含整数个元素。
Protocol buffer解析器必须能够解析像未打包一样打包的重复字段,反之亦然。以增加兼容性。
在. proto文件中,字段编号可以以任何顺序使用。选择的顺序对消息的序列化方式没有影响。
当消息被序列化时,对于如何写入其已知或未知字段没有保证顺序。序列化顺序是一个实现细节,任何特定实现的细节都可能在将来发生变化。因此,协议缓冲区解析器必须能够以任何顺序解析字段。
foo
.foo.SerializeAsString() == foo.SerializeAsString()
Hash(foo.SerializeAsString()) == Hash(foo.SerializeAsString())
CRC(foo.SerializeAsString()) == CRC(foo.SerializeAsString())
FingerPrint(foo.SerializeAsString()) == FingerPrint(foo.SerializeAsString())