proto buffer 是google的一种结构化数据格式标准,已经有不少人做过protobuf与thrift等其他结构化数据格式标准的性能比较。由于正好之前也用过protobuf,对其压缩协议的方式比较感兴趣,所以做了一番探索,本篇的主要内容是protobuf的数据压缩格式,而不关注具体使用。
概括
protobuf的主要是将数据进行序列化和反序列化,目的在提供一种轻便高效的结构化数据格式
主要的应用场合:
- 1.数据存储格式。
- 2.RPC数据通信。
使用
- 首先编写协议文件 .proto文件,例如一个协议定义文件:test.proto,文件内容如下
message test{
required int32 a = 1;
optional int32 b = 2;
}
- 解析.proto协议文件,针对每个协议生成对应的编解码代码
这样做的好处在哪里?
与xml和json这样的数据格式标准对比,xml和json里都需要写字段名称。而protobuf对协议做了预先处理,每个协议都有各自的解析代码,将字段名称这些都数据都节省掉,有效提高压缩率。
编码
键值对
所有的protobuf处理过的数据都会转换成如上Key->Value的扁平化数据格式,一个Field就是一个字段。
注意事项:- 这里可以看出protobuf将一个协议压缩到这个地步,但是并没有指明是哪一个协议,所以使用者一般都需要加上一个协议标识。
- 可以从上图看出数据是没有一个起点终点标识符的,所以,使用者需要给数据加上长度标识,否则协议解析时无法知道是否已经接收完一整个数据包。
Base 128 Varints
特点:
- 每个字节的第一位有特殊的作用:表示是否需要继续读取下一位作为完整的数据内容
- 小字节序。低位存在内存的低地址端,高位存在内存的高地址端
- 每个字节使用7bit的空间存储有效数据,即数据描述范围是0-127
- 由于每个字节第一位已经被使用,所以我们正常一个整数,大多数小于4个字节的描述,在protobuf极端情况下需要5个字节描述。
Key编码方式
官方定义是field_number<<3|wire_type
就是上文说的字段标识号左移3位,然后拼上字段的数据类型(wire_type)
数据类型如下:
Type=3或者4表示的是“组”的概念,组主要是为了描述一些组合数据,但是其他数据结构已经能起到这个作用,所以现在“组”已经被弃用。
以刚才的数据结构为例子,将test.a=1,test.b=2
通过protobuffer编码后的二进制数据是:0000 1000 0000 0001 0001 0000 0000 0010
第一个字节和第三个字节解析如下
一个数字只需要一个字节即可描述,对于我们的业务来说大部分都是小数字,所以带来的压缩效果非常明显。
但这里的标识号只有4位,能描述的最大标示号为15,当标识号大于15时,它是如何描述的?
假设标识号为16,编码后得到的二进制数据是:1000 0000 0000 0001
解析过程如下图:
value的编码方式
例如有以下结构:
message test{
repeated int32 a=1;
optional int32 b=2;
}
设 a的值为空,b的值为1
编码后的二进制数据是 0001 0000 0000 0001
通过上面的解析方式得知,第一个字节的值是16,描述的是Key标识号为2的数据,第二个字节的值是1,表示Value=1设a的值是内容为1,2,3的数组,b的值为4
编码后得到二进制数据:0000 1000 0000 0001 0000 1000 0000 0010 00001000 0000 0011 0001 0000 0000 0001
按字节转换为十进制得:8,1,8,2,8,3,16,4
此处是将数组的每个字段平铺,可以理解为:k=1,v=1,k=1,v=2,k=1,v=3,k=2,v=4多层嵌套的数据结构,例如新增下面这个结构,数组里包含了数组:
message test2{
repeated test a=1;
optional int32 b=2;
}
随便填入一些数据,将编译后的二进制数据按字节转换成十进制得:
10,6,8,1,8,1,16,1,10,6,8,2,8,2,16,2,16,4
这里主要为了解释前两个字节:
第一个字节10:0 0001 010 标示第一个字段,后面跟的val是个列表结构
第二个字节6:标示后面需要读取6个字节作为value紧凑型的数组数据
message test{
repeated int32 a=1[packed=true];
optional int32 b=2;
}
给数组字段增加一个[packed=true]的属性,可以是数组类型的数据压缩率更高。
也是设a的值是内容为1,2,3的数组,b的值为4,得到的字节数据是:10,3,1,2,3,16,4
第一个字节10表示后面是列表结构
第二个字节3表示需要读取后面多少个字节
明显比之前的方式压缩比率更高zigzag编码解决负数问题
对于负数,常见的处理方式是用符号位去标识,这样就导致了数据极大
-1的编码结果:8,255,…. 255,255,1
zigzag如图,就是正负转换成正数交替递增
-1的编码结果:8,1 效果显著IEEE754编码
protobuf还使用了IEEE754标准作为数据编码方式,主要应对长整型浮点数等,既然protobuf有用到,这里做个大概分析
以单精度为例:- Sign:符号位
- Exponent:指数偏移值(2^(e-1)-1),单精度是8位,那么指数偏移值是2^(8-1)-1=127(因为指数有可能是正数有可能是负数,需要保证上下范围基本对称,所以需要偏移值)
Fraction:有效数字
现在假设原始数据是:10.625
- 转换为二进制是1010.101
- 科学技术法 1.010101*2^3
- 按照IEEE转换数据
Sign:0
Exponent:127+3=>130=>10000010
Fraction:01010100000000000000000
拼凑起来可得数据:65,42,0,0 - little-endian
最终数据:0,0,42,65
综上所述:
撇开ProtocolBuffer的性能不谈,ProtocolBuffer压缩率还是很明显的,比较适用于大型结构化数据,可以通过其编码特性对不使用的字段以及常用数据进行高度压缩,对于传输大串的字符数据则不赞成用这种方式。