protocol buffer 使用和原理
Protocol Buffer 介绍
Protocol Buffer 是一种轻便高效的结构化数据格式,可以用于结构化数据的序列号,适合做数据存储和RPC数据交换格式,他是平台无关,语言无关,可扩展的序列号结构数据格式
结构化数据格式
- XML, 通过标签定义的
- JSON,通过键值对定义的
- DB,数据库
序列化
- Serilizable
- Parseable
用途
- RPC数据交换格式 即网络通讯
- 数据存储
跨平台,语言
如json,不限操作平台和编程语言同可以使用json
优点
- 序列化后的体积相比json和xml很小,适合网络传输 40M的json数据 17M Protobuffer
- 支持跨平台,多语言
- 消息格式升级和兼容性不错
- 序列化反序列化快 40M的json数据 10s, Protobuffer 0.8s
使用
Android studio 环境配置
配置应用build
1 | buildscript { |
配置module build.gradle
1 | apply plugin: 'com.android.application' |
构建消息
消息由至少一个字段组合而成:字段 = 字段修饰符 + 字段类型 + 字段名 + 标识符
标识符TAG:每个字段的唯一标识数字,用于说明二进制文件的对应关系,不能修改,
1 | syntax = "proto3"; |
Studio 自动生成代码
通过Android Studio build,Protobuf插件会帮助我们自动生成TestProto类,类结构如下

Protobuf帮助我们自动生成了testOrBuilder接口,主要定义了个字段的get,set方法,并生成了test类,核心逻辑,通过writeTo(CodedOutputStream)接口序列化到CodedOutputStream,通过parseFrom(InputStream) 接口从InputStream中反序列化

ProtoBuffer 原理
ProtoBuffer不管在时间还是空间上更加高效,是怎么做到的?
消息经过ProtoBuffer序列化后会成为二进制数据流,通过key-Value组成方式写入到二进制数据流。
编码机制
Base 128 Varints
是一种可变字节序列化整形的方法
- 每个byte的最高位是标志位(msb), 如果是1,则表示后面还有byte,否则为结束byte
- 每个byte的低7位用来存储数值的位
- Varints方法用Litte-Endian(小端)字节序列
消息结构
编码类型
| Type | Meaning | Used For |
|---|---|---|
| 0 | Varint | int32,int64,uinit32,uint64,sint32,sint64,bool,enum |
| 1 | 64-bit | fixed64,sfixed64,double |
| 2 | Length-delimited | string,bytes,embedded messages |
| 3 | ||
| 4 | ||
| 5 | 32-bit | fixed32,sfixed32,float |
key
key的具体值为 (field_number << 3) | wire_type,
以上面的例子来说,如字段id定义:
1 | int32 id = 2; // 150 |
在序列化时,并不会把字段id写进二进制流中,而是把field_number=2通过上述Key的定义计算后写进二进制流中,这就是Protobuf可读性差的原因,也是其高效的主要原因。
1 | key = (field_number << 3) | wire_type = 2 << 3 | 0 = 10000 |0 = 16 = 0x10 |
key的范围:wire_type只有六种类型,用3bit表示,在一个byte里,去掉mbs,以及3bit的wire_type,只剩下4bit来表示field_number,因此一个Byte里,field_number只能表达0-15,超过15个需要多个byte表示
负数
所谓ZigZag编码即将负数转换成正数,而所有正数都乘2,如0编码成0,-1编码成1,1编码成2,-2编码成3,以此类推,因而它对负数的编码依然保持比较高的效率。
优缺点
- Varint适用于表达比较小的整形,当数字很大时,采用定长编码类型(64bit,32bit)
- 不利于表达负数,负数采用补码表示,会占用更多字节,确定出现负数用sint32,sint64,他会采用ZigZig编码将负数映射成整数,之后再使用Varint编码
Length-delimited
Length-delimited编码格式会将数据的length也编码进最终的数据,编码格式有string,bytes,自定义消息
在消息中将str = “testing”,
序列化的打印结果 为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21private void test() {
TestProto.test.Builder test = TestProto.test.newBuilder();
TestProto.test test1 = test.setStr("testing").build();
// 序列化
byte[] bytes = test1.toByteArray();
for (byte aByte : bytes) {
System.out.print(aByte +" ");
}
}
// 18 7 116 101 115 116 105 110 103
str 的field_number = 2; str = "testing"
key = (field << 3) | wire_type = 2 << 3 | 2 = 10000 | 10 = (十进制) 18 = (16进制)0x12
7 为 value的长度testing长度为7
然后计算value的每个字母 t 在ASCII中的 十进制数是 116, e 为101, s为115 以此类推
发现每个byte存储的是计算后的10进制数字,和网上说16进制 byte存储方式不一致 12 07 74 65 73 74 69 6e 67
现在按照16进制存储计算
key = 12 已经确定
t 的二进制为 0111 0100 16 进制为74
e 则为65 ..
所以string的转换方式是 key的16进制 + 字符串长度16进制 + 字符串的每个字母的16进制结论
通过原理破解,在通过观察源码Protobuffer的序列化和反序列化同时通过几个位运算实现的,所以他的效率高,体积小
使用指南
- 尽量不要修改tag
- 字段数量不要超过16个,否则会采用2个字节编码, (1个字节最大值为128, key的计算会通过field_number << 3 | wire_type, 当field_number 为16时刚好使用了1个字节计算,否则就需要两个字节计算)
- 如果确定使用负数,采用sint32/sint64