背景
Protocol Buffer是Google出品的数据传输协议,目前已经广泛用于客户端和服务器之间的数据交互,清晰理解Protocol Buffer原理很有必要,本文主要解密Protocol Buffer为什么更小,更快,不了解Protocol Buffer的可以看下之前对Protocol Buffer的介绍
原理
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,这样做确实可以减小传输数据,但是编号一旦确定就不可更改
- 没有赋值的key,不参与序列化
序列化时只会对赋值的key进行序列化,没有赋值的不参与,在反序列化的时候直接给默认值即可 - 可变长度编码
可变长度编码,主要缩减整数占用字节实现,例如java中int占用4个字节,但是大多数情况下,我们使用的数字都比较小,使用1个字节就够了,这就是可变长度编码完成的事 - TLV
TLV全称为Tag_Length_Value,其中Tag表示后面数据的类型,Length不一定有,根据Tag的值确定,Value就是数据了,TLV表示数据时,减少分隔符的使用,更加紧凑
数据结构
Protocol Buffer的数据组成方式为TLV,数据结构图如下:

数据类型
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位表示数据类型,其他位表示数据编号
可变长度编码
Java中整数类型的长度都是确定的,如int类型的长度为4个字节,可表示的整数范围为-2^31——2^31-1,但是实际开发中用到的数字均比较小,会造成字节浪费,可变长度编码就能很好的解决这个问题,可变长度编码规则如下:
- 字节最高位表示数据是否结束,如果最高位为1,则表示后面的字节也是该数据的一部分
举个例子:


10000001 00000011 ——> 00000110000001
表示的10进制数为:2^0 + 2^7 + 2^8 = 385
通过上面的例子可以知道一个字节表示的数的范围0-128,上面介绍的Tag生成算法中由于后3位表示数据类型,所以Tag中1-15编号只占用1个字节,所以确保编号中1-15为常用的,减少数据大小
可变长度编码唯一的缺点就是当数很大的时候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 时
按照这种编码方式,对应的数字如下:

定长编码
定长编码其实没什么说的,double float等数据结构的长度是确定的,当解析到这种类型的数据时,直接取对应长度的数据即可
案例分析
上面介绍了Protocol Buffer的原理,现在通过实例来展示分析过程,我们定义的proto文件如下:
message Person {
string name = 1;
int32 id = 2;
}
通过Protocol Buffer提供的工具,创建对应的源文件并且设置对应的值:name=test id=1,序列化后的字节如下:

先看下第一个字节:

file_num = 0001 = 1
type = 010 = 2
上面介绍过type=2,则后面有Length,按照可变长度编码规则,知道表示长度的字节为:


再看下一组:

前面介绍过type=0,后面没有Length,直接就是value,

- file_num=1 value=test
- file_num=2 value=1
这样解析就结束了
总结
上面介绍了Protocol Buffer的原理,解释了为什么Protocol Buffer更快,更小,这里再总结一下:
- 序列化的时候,不序列化key的name,只序列化key的编号
- 序列化的时候,没有赋值的key,不参与序列化,反序列化的时候直接使用默认值填充
- 可变长度编码,减小字节占用
- TLV编码,去除没有的符号,使数据更加紧凑