Protocal Buffers(简称protobuf)是Google的一项技术,用于结构化的数据序列化、反序列化。
Protobuf的使用比较广泛,常用于RPC 系统(Remote Procedure Call Protocol System)和持续数据存储系统。其主要优点是空间开销小和性能比较好,类似于XML生成和解析,但protobuf的效率高于XML,不过protobuf生成的是字节码,可读性比XML差。
相关官方文档:
Protocol Buffers官网:https://developers.google.com/protocol-buffers/
Protocol Buffers官网(中文):https://developers.google.com/protocol-buffers/?hl=zh-CN
Git地址:https://github.com/google/protobuf
Java API:https://developers.google.com/protocol-buffers/docs/reference/java/?hl=zh-CN
proro文件的编写指南:https://developers.google.com/protocol-buffers/docs/style?hl=zh-CN
Java使用指南:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN
一.protobuf的基本应用
使用protobuf开发的基本步骤为:
1.配置开发环境 安装protocol 代码编译器
2.编写对应的.proto文件,定义序列化对象结构
3.使用编译器生成对应的序列化工具类
4.编写自己的应用
使用github上的protoc-3.5.1-win32.zip https://github.com/google/protobuf/releases
每个Protobuf 的 字段 都有一定的格式:
限定修饰符 | 数据类型 | 字段名称 | = | 字段编码值 | [字段默认值]
1.限定修饰符: required/optional/repeated
Required:表示是一个必须字段,缺失该字段会引发编解码异常,导致消息被丢弃。
Optional:表示是一个可选字段。
Repeated:表示该字段可重复,每次可以包含0~N个值,表示集合
2.数据类型
protobuf定义了一些基本的数据类型
string/bytes/bool/int32/int64/float/double
enum 枚举类 message 自定义类
3.字段名称
字段名称的命名方式与C、Java等语言的变量命名方式几乎是相同的。
protobuf建议字段的命名采用以下划线分割的驼峰式。例如 建议使用first_name 而不是firstName.
4.字段编码值
编码值的取值范围为 1~2^32(4294967296)。
相同的编码值,其限定修饰符和数据类型必须相同。
消息中的字段的编码值无需连续,只要是合法的,并且不能在同一个消息中有字段包含相同的编码值。不过通常习惯使用连续的字段编码值,比较易于理解
⑤.默认值。
在发送数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递。当接受数据时,对于optional字段,如果没有接收到对应值,则设置为默认值
以下是一个简单的User对象的proto文件
syntax="proto2";
option java_package = "com.chenpp.serializer.protobuf";
option java_outer_classname="UserProto";
message User {
//1,2表示当前序列化后的字段顺序
required int32 age = 2; //年龄
required string name = 1;//姓名
}
使用proto自己的编译器进行编译,生成实体类
protoc.exe --java_out=./java ./user.proto
–java_out 后面是生成java文件存放地址
最后的参数是proto文件的名称,可以写绝对地址,也可以直接写proto文件名称
引入protobuf对应的dependency,打印对应的序列化结果:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.5.0</version>
</dependency>
public class ProtoSerializer implements ISerializer {
public <T> byte[] serialize(T obj) {
UserProto.User user = UserProto.User.newBuilder().setAge(21).setName("chenpp").build();
return user.toByteArray();
}
public <T> T deserialize(byte[] data, Class<T> clazz) {
try {
UserProto.User user = UserProto.User.parseFrom(data);
return (T) user;
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
return null;
}
}
==================protobuf========================
protobuf {age:21}序列化后的byte[]的length: 10
10 6 99 104 101 110 112 112 16 21
name: "chenpp"
age: 21
二.protobuf序列化的原理
之前那篇文章,讲过Json里的序列化结果为: { "name":"chenpp","age":21} -- 一共26个字节,而想要将其进行进一步压缩,就需要去掉一些冗余的字节
思路:1)能不能去掉定义属性(约定1=name,2=age) 约定了字段,约定了类型 去除分隔符(引号,冒号,逗号之类的)
2)压缩数字,因为日常经常使用到的都是一些比较小的数字,一共int占4个字节,但实际有效的字节数没有那么多
把英文转化成数字(ASCII),并对数字进行压缩
protobuf里使用到了两种压缩算法:varint和zigzag算法
varint算法
这是针对无符号整数,一种压缩方式
压缩方法:
1.对一个无符号整数,将其换算成二进制
2.从右往左取7位,然后在最高位补1
3.一直继续,直到取到最后一个有意义的字节(在最后一个有意义的字节上最高位补0)
4.先取到的字节排在后取到的字节前面,得到一个新的字节,转换成十进制就是压缩的结果
以:500为例:其实有意义的就是2个字节
0000 0001 1111 0100= 2^2+2^4+2^5+2^6+2^7+2^8 = 4+16+32+64+128+256 = 500
按照其压缩方式得到的新的二进制字节为:
1111 0100 | 0000 0011
1111 0100 代表的是负数,使用补码(正数取反+1),并且最高位符号位为1
转化后: 先 -1为 1111 0011 取反 0000 1100 = 12
计算出来就是 -12 3
字符如何转化成数字编码
对于英文字母,这里的name字段,使用ASCII码对照表查找对应的数字
chenpp: 对应的ASCII码
c-99 h-104 e-101 n-110 p-112
按照varint的算法 取7位补最高位为1(最后一个字节最高位补0)
对于小于127(2^7-1=127)的数字,其有效字节只有1位,压缩的时候最高位补0,故压缩之前和压缩之后的数字没有变化
protobuf的存储格式
protobuf采用T-L-V的格式进行存储
[Tag | length | value ]
l其中length为可选, 但是string必须有length(这样在反序列化的时候程序才知道该字符串从哪里开始到哪里结束), 而int是不需要length的
Tag:字段标识符,用于标识字段 其值等于
field_number(当前字段的编号,第几个字段)<<3|wire_type(int64/int32/可变长度string)
Length:Value的字节长度 (string需要有,int不需要)
Value:消息字段经过编码后的值
Age:int32 2<<3|0 = 16
Name: string 1<<3|2 = 10
在反序列化的时候根据tag mod 8 的余数判断对应的wireType,从而知道该字段对应的存储方式和编码方式
对应User(name="chenpp",age=21)按照varint进行压缩后其序列化结果为:
c-99 h-104 e-101 n-110 p-112
String:tag-length-value
Int32:tag-value
10 6 99 104 101 110 112 112 16 21
Tag – length - c - h – e - n - p - p - Tag - Value
根据上述压缩算法可知:对于int32/int64,value有且只有最后一个字节为正数,故当遇到第一个为正数的字节时就知道其value值已经获取完毕,所以对于int32类型的字段,不需要length,只需要tag和value就足够了
ZigZag算法
在计算机中,负数会被表示为很大的整数,因为负数的符号位在最高位,如果使用varint算法进行压缩的话会需要 32/7 ~ 5个字节,反
而加大了空间的开销.故在protobuf中对于有符号整数会使用sint32/sint64来表示。protobuf中负数的压缩方式是先使用ZigZag算把有符号数(无论数值是正数还是负数,都会进行一次压缩计算)转化为无符号数,再使用varint进行压缩
ZigZag算法的思路:
负数之所以不好压缩:一个原因是因为其最高位为1,另一个原因是对于绝对值比较小的负数,其正数会有很多的前导零,那么在使用补码表示负数的时候(取反+1),会导致负数会有很多的前导1,使得无法压缩
所以ZigZag采用的办法就是:先将符号位从最高位移动到最低位,其余数字均往前移动1位;然后再对所有的数字(符号位除外)进行取反,这样得到的计算结果就是一个可以压缩的数字(符号位不占据最高位,而小绝对值的数值由于取反操作其前导1都变为了前导0)
比方说-300:
其对应的正数的原码为: 0000 0000 0000 0000 0000 0001 0010 1100
取反: 1111 1111 1111 1111 1111 1110 1101 0011
再+1: 1111 1111 1111 1111 1111 1110 1101 0100 (-300)
移动符号位之后: 1111 1111 1111 1111 1111 1101 1010 1001
取反:0000 0000 0000 0000 0000 0010 0101 0111
计算后为0010 0101 0111 = 599
在ZigZag算法里也是使用这种思路对有符号整数进行压缩的,将其转化成表达式就是
Sint32: (n<<1)^ (n>>31)
Sint64: (n<<1)^(n>>63)
当n为正数时,n>>31为0000 0000 0000 0000 0000 0000 0000 0000;当n为负数时,n>>31为1111 1111 1111 1111 1111 1111 1111 1111
(n>>31)与(n<<1)进行异或之后,如果n为正数,(n>>31)^(n<<1) =n<<1;如果n为负数,其计算结果和上述所说的最高位移动到最后,然后取反效果是一样的(n<<1补的最低位为0和n>>31异或运算之后一定为1,而其他位上与1做异或运算相当于取反),这样一来就可以使用varint进行压缩计算了
对-300的ZigZag计算结果:599 使用varint算法进行压缩
得到1101 0111 0000 0100
其结果为:-41 4
到此,protobuf的压缩原理就介绍完了