前言
Protobuf是一种平台无关、语言无关、可扩展且轻便高效的序列化数据结构。从一定的角度来讲,它和json,xml是一样的。现在流行的grpc在用,连《MySQL Connector/J 8.0》也在使用probuf这种格式和Mysql通信。
为什么要使用Protobuf
如何使用Protobuf
第一步:编写proto文件
syntax="proto3";
package cc.protobuf;
option java_package = "cc.protobuf.model";
option java_multiple_files=true;
message Person
{
int32 age = 1;
int32 sex = 2;
}
第二步:从官网下载安装protoc命令
第三步:用protoc把proto转化成Java类文件或者其它c文件(具体根据官方支持)
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
第四步:使用生成好的源文件进行编程
ProtoBuf序列化原理
ProtoBuf是通过同一配置文件,生成不同语言的源文件来实现对序列化的封装,方便开发者的。那他是如何序列化的呢?以上文Person类为例子。
ProtoBuf采用的是类似TLV格式。即 Tag - Length - Value。Tag 作为该字段的唯一标识,Length 代表 Value 数据域的长度,最后的 Value 便是数据本身。但是实际上又有较大区别,其结构可见下图
Tag 由 field_number 和 wire_type 两个部分组成:
- field_number: message定义字段时指定的字段编号
- wire_type: ProtoBuf 编码类型,根据这个类型选择不同的 Value 编码方案。3 bit 的 wire_type 最多可以表达 8 种编码类型,目前 ProtoBuf 已经定义了 6 种
Length 是可选的,含义是针对不同wire_type可能会变成 Tag - Value 的形式
Value 数据本身.根据字段的编码方式不同,选择不同的编码方式。
Varints 编码
Varints 编码方式被运用在TLV格式的各个环节。Varints 编码的规则主要为以下三点:
- 在每个字节开头的 bit 设置了 msb(most significant bit ),标识是否需要继续读取下一个字节
- 存储数字对应的二进制补码
- 补码的低位排在前面(高低为0)
##用例1如下
int32 val = 1; // 设置一个 int32 的字段的值 val = 1; 这时编码的结果如下
原码:0000 ... 0000 0001 // 1 的原码表示
补码:0000 ... 0000 0001 // 1 的补码表示
Varints 编码:0#000 0001(0x01) // 1 的 Varints 编码,其中第一个字节的 msb = 0
##编码过程
1. 数字 1 对应补码 0000 ... 0000 0001(规则 2)
2. 从末端开始取每 7 位一组并且反转排序(规则 3),因为 0000 ... 0000 0001 除了第一个取出的 7 位组(即原数列的后 7 位),剩下的均为 0。所以只需取第一个 7 位组,无需再取下一个 7 bit
3. 那么第一个 7 位组的 msb = 0。最终得到 0 | 000 0001(0x01) (规则 1)
##解码过程
1. 每个字节的第一个 bit 为 msb 位,msb = 1 表示需要再读一个字节(还未结束),msb = 0 表示无需再读字节(读取到此为止)。
2. 0#000 0001(0x01)中 msb = 0,所以只需要读完第一个字节无需再读。去掉 msb 之后,剩下的 000 0001 就是补码的逆序,但是这里只有一个字节,所以无需反转,直接解释补码 000 0001,还原即为数字 1。
##用例2如下
int32 val = 666; // 设置一个 int32 的字段的值 val = 666; 这时编码的结果如下
原码:000 ... 101 0011010 // 666 的源码
补码:000 ... 101 0011010 // 666 的补码
Varints 编码:1#0011010 0#000 0101 (9a 05) // 666 的 Varints 编码
##编码过程
1. 数字 666 对应补码 000 ... 101 0011010(规则 2)
2. 从末端开始取每 7 位一组并且反转排序(规则 3),得到 0011010 | 0000101
3. 加上 msb。最终得到 1 0011010 | 0 0000101 (0x9a 0x05)(规则 1)
##解码过程
1. 第一个字节 msb = 1,所以需要再读一个字节,第二个字节的 msb = 0,则读取两个字节后停止
2. 1 0011010 | 0 0000101 (0x9a 0x05)去掉两个 msb,将这两个 7-bit 组反转得到补码000 0101 0011010
3. 然后还原其原码为 666。
仔细品味上述的 Varints 编码,我们可以发现 Varints 的本质实际上是每个字节都牺牲一个 bit 位(msb),来表示是否已经结束(是否还需要读取下一个字节),msb 实际上就起到了 Length 的作用,正因为有了 msb(Length),所以我们可以摆脱原来那种无论数字大小都必须分配四个字节的窘境。通过 Varints 我们可以让小的数字用更少的字节表示。从而提高了空间利用和效率。
但负数的 Varints 编码存在着明显缺陷.因为负数必须在最高位(符号位)置 1,这一点意味着无论如何,负数都必须占用所有字节,所以它的补码总是占满 8 个字节。你没法像正数那样去掉多余的高位(都是 0)。再加上 msb,最终 Varints 编码的结果将固定在 10 个字节。为解决这个问题,ProtoBuf 为我们提供了 sint32、sint64 两种类型,当你在使用这两种类型定义字段时,ProtoBuf 将使用 ZigZag 编码,而 ZigZag 编码将解决负数编码效率低的问题。
ZigZag 编码:有符号整数映射到无符号整数,然后再使用 Varints 编码
例如我们设置 int32 val = -2。
1. 映射-2 得到 3
2. 那么对数字 3 进行 Varints 编码,将结果存储或发送出去。
3. 接收方接到数据后进行 Varints 解码,得到数字 3,
4. 再将 3 映射回 -2。
ProtoBuf用例
对上面Person类进行操作得
public class ProtoBufTest {
public static void main(String[] args) {
Person person = Person.newBuilder()
.setAge(15)
.setSex(2)
.build();
byte[] bytes = person.toByteArray();
System.out.println(bytesToHex(bytes));
}
}
输出16进制:080f1002
- 对输出结果进行2进制化得 0000 1000 0000 1111 0001 0000 0000 0010
- 读取第1字节,msg为0,表示不用继续读得 0000 1000 , 并去掉 msg位得 000 1000
- 获取wtrite_type = 000(varint), field_number = 0001 (对应proto中的age)
- 根据wtrite_type,field_number(age)和proto文件定位到采用的是TV格式,V的编码格式为int32
- 读取第2字节,msg为0,表示不用继续读,并去掉 msg位得 000 1111 -> 15 (age)
- 读取第3字节,msg为0,表示不用继续读,并去掉 msg位得 001 0000
- 获取wtrite_type = 000(varint), field_number = 0010 (对应proto中的sex)
- 根据wtrite_type,field_number(sex)和proto文件定位到采用的是TV格式,V的编码格式为int32
- 读取第4字节,msg为0,表示不用继续读,并去掉 msg位得 000 0010 -> 2 (sex)
protostuff序列化
protostuff是protobuf的改良版本。可以直接将一个java object进行序列化而不用编写“proto”文件。
// 对比2种方式序列化方式
public static void main(String[] args) throws InvalidProtocolBufferException {
//protobuff
Person person = Person.newBuilder()
.setAge(15)
.setSex(2)
.build();
byte[] bytes = person.toByteArray();
// 输出16进制 “080f1002”
System.out.println(bytesToHex(bytes));
// Protostuff方式
PP pp = new PP();
pp.setAge(15);
pp.setSex(2);
// 相当于 “proto” 文件的识别
RuntimeSchema<PP> schema = RuntimeSchema.createFrom(PP.class);
// 通过 schema 和 ProtostuffIOUtil = toByteArray
byte[] bytes2 = ProtostuffIOUtil.toByteArray(pp, schema,
LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
// 输出16进制 “080f1002”
System.out.println(bytesToHex(bytes2));
// 证明反序列化相通
Person p2 = Person.parseFrom(bytes2);
// 输出 age: 15 sex: 2
System.out.println(p2);
}