Google Protocol Buffers 原理【解读】

最近在了解对象的序列化,知道google的Protocol Buffers定义了结构的序列化,想了解一下,因此边学边记录,错误之处还请指正,共同进步。

最简单的message定义

首先看一下一个简单的message定义:

message Test1 {
  optional int32 a = 1;
}

我们在程序中创建一个Test1 and Test1.a = 150,然后序列化,得到如下3个字节

08 96 01

然后一个int32应该是4字节,那么到底是怎么样做到的呢?

基于128位8字节Varints

理解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

Message Structure

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

More Value Types

在什么说道,所有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补齐

Non-varint Numbers

非可变的数字类型很简单——double和fixed64属于1线型,这告诉解析器期望固定的64位数据块;类似地,float 和fixed32具有线型5,这告诉它期望32位。在这两种情况下,这些值都以小端字节顺序存储。

Strings

线型为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表示后面跟的字符串的长度

Embedded Messages

内嵌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编码值

Optional And Repeated Elements

如果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);

这个属性有时很有用,因为它允许您合并两个消息,即使您不知道它们的类型。

Packed Repeated Fields

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解析器必须能够解析像未打包一样打包的重复字段,反之亦然。以增加兼容性。

Field Order

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

当消息被序列化时,对于如何写入其已知或未知字段没有保证顺序。序列化顺序是一个实现细节,任何特定实现的细节都可能在将来发生变化。因此,协议缓冲区解析器必须能够以任何顺序解析字段。

NOTICE

  • 不要假设序列化消息的字节输出是稳定的。对于具有表示其他序列化协议缓冲区消息的可传递字节字段的消息尤其如此。
  • 默认情况下,对同一协议缓冲区消息实例重复调用序列化方法可能不会返回相同的字节输出;即默认串行化不是确定性的 ,确定性序列化只保证特定二进制文件的相同字节输出。字节输出可能在二进制文件的不同版本之间发生变化。
  • 对于协议缓冲区消息,以下检查可能会失败,实例foo.
foo.SerializeAsString() == foo.SerializeAsString()
Hash(foo.SerializeAsString()) == Hash(foo.SerializeAsString())
CRC(foo.SerializeAsString()) == CRC(foo.SerializeAsString())
FingerPrint(foo.SerializeAsString()) == FingerPrint(foo.SerializeAsString())
  •  这里有几个示例场景,其中逻辑等价的协议缓冲消息foo和bar可以序列化为不同的字节输出。
  1. bar由旧服务器序列化,旧服务器将某些字段视为未知字段。
  2. bar由用不同编程语言实现的服务器序列化,并以不同的顺序序列化字段。
  3. bar有一个以非确定性方式序列化的字段。
  4. bar有一个存储协议缓冲区消息序列化字节输出的字段,该消息以不同方式序列化。
  5. bar由新的服务器序列化,由于实现的变化,该服务器以不同的顺序序列化字段。
  6. foo和bar都是单个消息的串联,但是顺序不同

你可能感兴趣的:(架构)