Google--Proto Buffer的序列化原理

Google–Proto Buffer的序列化原理

这篇文档将讨论 protocol buffer 消息的二进制编码格式,了解不同的 protocol buffer 格式如何影响编码消息的大小可能非常有用!!!

简单示例

让我们来看一看非常简单的消息定义:

message Person {
  string user_name = 1;
  int64  favorite_number = 2;
  repeated string interests = 3;
}
## 赋值:
oldData := &proto.Person{
		UserName:       "Martin",
		FavoriteNumber: 1337,
		Interests:      []string{"daydreaming", "hacking"},
	}

在应用程序中,创建Person 消息赋值为上述的情况。然后使用 proto.Marshal()函数,消息序列化为输出流。查看序列化后的编码消息,会看到下面这些字节:

## 十进制表示
[10 6 77 97 114 116 105 110 16 185 10 26 11 100 97 121 100 114 101 97 109 105 110 103 26 7 104 97 99 107 105 110 103]
-------------------------------------------------------------------------------------------
## 十六进制
0a 06 4d 61 72 74 69 6e 10 b9 0a 1a 0b 64 61 79 64 72 65 61 6d 69 6e 67 1a 07 68 61 63 6b 69 6e 67  

而这,就是消息传输过程中的二进制传输格式。他们是怎么来的呢?我们最后将给出解答。

存储方式

二进制

对于仅在组织内部使用的数据,使用最小公约数式的编码格式压力较小。例如,可以选择更紧凑或更快的解析格式。虽然对小数据集来说,收益可以忽略不计;但一旦达到TB级别,数据格式的选型就会产生巨大的影响。在占用空间方面,二进制格式比JSON和XML编码相比优势巨大。

TLV格式编码

首先,我们需要了解的是,protocol buffer在传输过中,是使用T-L-V 的格式对数据进行存储的。如下图所示:

在这里插入图片描述

编码方式

在Protocol Buffer中,主要是采用两种方式编码:Varint和UTF8格式。其中,Varint主要是对 int 型数据进行编码,而 UTF8 主要是对 string 型数据进行编码。

Base 128 Varints

为了理解 protocol buffer 缓冲编码原理,我们首先需要了解什么是varint。Varints是一种使用一个或多个字节序列化整数的方法,较小的数字占用较少的字节数

除了最后一个字节外,varint中的每个字节都设置了最高有效位(most significant bit --msb)––这表明还会有其他字节。

每个字节的低7位用于以7位为一组存储数字的二进制补码表示,最低有效组在前。

下面,我们看一个示例,对于数字300,使用varint如何编码呢?

如下图所示,是其编码过程:
在这里插入图片描述

最终,我们得到了300的编码:

1010 1100 0000 0010

其解码过程就是这个编码的逆操作,即:

首先,从每个字节(8 bit)中删除msb,因为这是在告诉我们是否已到达数字的末尾(如您所见,它设置在第一个字节中,因为varint中有多个字节):

1010 1100 0000 0010
--> 010 1100 000 0010

然后,反转两组7位的值,因为 varint 存储数字的最低有效组在前。 然后,将它们连接起来以获得最终值:

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

使用golang 编码,就是:
在这里插入图片描述

wire types

一个protocol buffer消息是一组键值对。二进制版本的protocol buffer消息仅使用 字段的编号 作为关键字,即,每个字段的名称和声明的类型只能在解码端通过引用消息类型的定义(即.proto文件)来确定。

当对消息进行编码时,键和值被串联到一个字节流中(Key 相当于上图中的Tag,V相当于上图中的Lenght(可选)+Value),如上图所示。 在对消息进行解码时,解析器需要能够跳过无法识别的字段。 这样,可以将新字段添加到消息中,而不会破坏不知道它们的旧程序。

为此,最终的格式中,每对Key 实际上是包含两个值的,即一个是.proto文件中的字段编号,一个是wire type(数据类型编号)。事实上,在大多数语言实现中,这里说的Key就是Tag

wire types 类型如下所示:

TypeMeaningUsed For
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages, packed repeated fields
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, sfixed32, float

在编码的时候,我们一般是这样的:

(field_number << 3) | wire_type

例子:

message Person {
  string user_name = 1;
}
有:field_number=1,wire_type=2   ====>二进制:
   field_number=1,wire_type=10  ====>1000+10
   最后,就是 1010
也就是说,user_name的Tag是1010,转换为十进制就是10,十六进制就是 a

其他类型的编码

ZigZag

正如上一节中所看到的,与 wire_type=0 关联的所有 protocol buffer 消息类型都被编码为varint。

但是,在对 负数 进行编码时,带符号的int类型(sint32和sint64)与“标准” int类型(int32和int64)之间存在重要区别。 如果将int32或int64用作负数的类型,则结果varint总是十个字节长–实际上,它被视为一个非常大的无符号整数。 如果使用带符号类型之一,则生成的varint使用ZigZag编码,效率更高。

ZigZag 编码将 有符号整数 映射为无符号整数,以便具有较小绝对值(例如-1)的数字也具有较小的varint编码值。 这样做的方式是通过正整数和负整数来回“曲折”,以便将-1编码为1,将1编码为2,将-2编码为3,依此类推。

以-11为例子,-11的补码为11110101,其处理过程为:

  1. 补码左移一位:11101010
  2. 符号位放到最后一位:11101011
  3. 除最后一位外全部取反:00010101
    在这里插入图片描述
UTF8编码Strings类型

string 类型是使用UTF-8进行编码的,其UTF-8编码可以在线查看:查看字符编码(UTF-8)

其他类型

万变不离其宗,嵌套或者枚举、数组等,都是使用上述的类似编码方式

小结

在这里插入图片描述

ProtoBuffer序列化示例

下面,我们将对开头给出的示例进行详细的说明:

对消息体赋值:

message Person {
  string user_name = 1;            ---->"Martin"
  int64  favorite_number = 2;      ---->1337
  repeated string interests = 3;   ---->[]string{"daydreaming", "hacking"}
}

对此,其编码过程和结果如下图:
Proto Buffer序列化编码过程

总结

  • Proto Buffer序列化编码方式主要有:
    1. varint、ZigZag和 utf-8 编码
    2. 采用T-L-V 方式存储数据

这也是他序列化和反序列化快的原因,采用的编码方式就相当于位移操作(快),而且Proto Buffer编译器完成编码编译。

参考文献

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值