Protocol Buffer原理解密

背景

Protocol Buffer是Google出品的数据传输协议,目前已经普遍用于客户端和服务器之间的数据交互,清晰理解Protocol Buffer原理颇有必要,本文主要解密Protocol Buffer为何更小,更快,不了解Protocol Buffer的能够看下以前对Protocol Buffer的介绍java

原理

Protocol Buffer更快,更小的主要缘由以下:算法

  • 序列化数据时,不序列化key的name,使用key的编号替代,减少数据
    例如定义以下数据结构:
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
复制代码

上述数据在序列化时,query,page_number以及result_per_page的key不会参与,由编号1,2,3替代,这样在反序列的时候能够直接经过编号找到对应的key,这样作确实能够减少传输数据,可是编号一旦肯定就不可更改bash

  • 没有赋值的key,不参与序列化
    序列化时只会对赋值的key进行序列化,没有赋值的不参与,在反序列化的时候直接给默认值便可
  • 可变长度编码
    可变长度编码,主要缩减整数占用字节实现,例如java中int占用4个字节,可是大多数状况下,咱们使用的数字都比较小,使用1个字节就够了,这就是可变长度编码完成的事
  • TLV
    TLV全称为Tag_Length_Value,其中Tag表示后面数据的类型,Length不必定有,根据Tag的值肯定,Value就是数据了,TLV表示数据时,减小分隔符的使用,更加紧凑

数据结构

Protocol Buffer的数据组成方式为TLV,数据结构图以下: 服务器

image
其中Length不必定有,依据Tag肯定,例如int类型的数据就只有Tag-Value,string类型的数据就必须是Tag-Length-Value

数据类型

Protocol Buffer定义了以下的数据类型,其中部分数据类型已经再也不使用:数据结构

类型 释义 备注
0 可变长度编码 int32 int64 uint32 uint64 sint32 sint64 bool enum
1 64位长度 fixed64 sfixed64 double
2 value 的长度 string bytes message packed repeated fiels
3 Start Group 废弃
4 End Group 废弃
5 32位长度 fixed32 sfixed32 float

Tag

上面已经介绍了Protocol Buffer的数据结构及Tag的类型,可是Tag块并非只表示数据类型,其中数据编号也在Tag块中,Tag的生成规则以下:工具

(field_number << 3) | wire_type
复制代码

其中Tag块的后3位表示数据类型,其余位表示数据编号post

可变长度编码

Java中整数类型的长度都是肯定的,如int类型的长度为4个字节,可表示的整数范围为-2^31——2^31-1,可是实际开发中用到的数字均比较小,会形成字节浪费,可变长度编码就能很好的解决这个问题,可变长度编码规则以下:ui

  • 字节最高位表示数据是否结束,若是最高位为1,则表示后面的字节也是该数据的一部分

举个例子: 编码

image
其中第一个字节因为最高位为1,则后面的字节也是前面的数据的一部分,第二个字节最高位为0,则表示数据计算终止,因为Protocol Buffer是低位在前,总体的转换过程以下:
image

10000001 00000011 ——> 00000110000001 表示的10进制数为:2^0 + 2^7 + 2^8 = 385
经过上面的例子能够知道一个字节表示的数的范围0-128,上面介绍的Tag生成算法中因为后3位表示数据类型,因此Tag中1-15编号只占用1个字节,因此确保编号中1-15为经常使用的,减小数据大小spa

可变长度编码惟一的缺点就是当数很大的时候int32须要占用5个字节,可是从统计学角度来讲,通常不会有这么大的数

负数

Java中最高位表示整数的正负,经过上面可变长度编码介绍,最高位被用来做为数据结束标识符了,因此无法经过最高位来表示数据的正负,使用int32或者int64表示负数的时候占用10个字节,这是Protocol Buffer源码中规定的,因此若是要使用负数强烈不建议使用int32和int64,建议使用sint32和sint64,sint32和sint64先使用zigZag编码,生成的数再使用可变长度编码,下面介绍一下zigzag编码.

zigZag

zigzag编码的代码以下:

Zigzag(n) = (n << 1) ^ (n >> 31), n 为 sint32 时

Zigzag(n) = (n << 1) ^ (n >> 63), n 为 sint64 时
复制代码

按照这种编码方式,对应的数字以下:

image
正数和负数交叉出现,这样编码后的数字再使用可变长度编码也能缩减数据的占用

定长编码

定长编码其实没什么说的,double float等数据结构的长度是肯定的,当解析到这种类型的数据时,直接取对应长度的数据便可

案例分析

上面介绍了Protocol Buffer的原理,如今经过实例来展现分析过程,咱们定义的proto文件以下:

message Person {
  string name = 1;
  int32 id = 2;
}
复制代码

经过Protocol Buffer提供的工具,建立对应的源文件而且设置对应的值:name=test id=1,序列化后的字节以下:

image
前面介绍过Protocol Buffer的 数据结构为TLV,其中L不是必须的,根据T的类型来肯定
先看下第一个字节:
image
这里字节最高位为0,因此该Tag就用这一个字节表示,其中后3位表示类型,前面表示字段编号,因此:
file_num = 0001 = 1
type = 010 = 2
上面介绍过type=2,则后面有Length,按照可变长度编码规则,知道表示长度的字节为:
image
因此Length=4,则value的长度是4个字节,直接取出后面4个字节:
image
这4个字节对应的就是test
再看下一组:
image
由上面的Tag知道: file_num=2 type=0
前面介绍过type=0,后面没有Length,直接就是value,
image
value=1,经过上面的解析能够知道

  1. file_num=1 value=test
  2. file_num=2 value=1
    这样解析就结束了

总结

上面介绍了Protocol Buffer的原理,解释了为何Protocol Buffer更快,更小,这里再总结一下:

  1. 序列化的时候,不序列化key的name,只序列化key的编号
  2. 序列化的时候,没有赋值的key,不参与序列化,反序列化的时候直接使用默认值填充
  3. 可变长度编码,减少字节占用
  4. TLV编码,去除没有的符号,使数据更加紧凑
相关文章
相关标签/搜索