摘要:ProtoBuf(Protocol Buffers)是一种跨平台、语言无关、可扩展的序列化结构数据的方法,可用于网络数据交换及数据存储。
Protocol Buffers介绍
不同于 XML
、JSON
这种文本格式数据,Protocol Buffers 是一种二进制格式数据。在Protocol Buffers 诞生之初,就被赋予两个特点:
- 向前兼容,很容易引入新字段,应对字段的频繁变更
- 数据格式具备描述性,并且支持多语言处理
- 传输效率高
基于以上这些特性,Protocol Buffers 被广泛应用于各种 RPC 框架中,并且是 Google 的数据通用语言。
Protocol Buffers协议文件
Protocol Buffers 在使用前需要先定义好协议文件,以 .proto
为后缀的文件就是Protocol Buffers 的协议文件。
Example:
// 指定protobuf的版本,proto3是最新的语法版本
syntax = "proto3";
// 定义数据结构,message 你可以想象成java的class,c语言中的struct
message Response {
string data = 1; // 定义一个string类型的字段,字段名字为data, 序号为1
int32 status = 2; // 定义一个int32类型的字段,字段名字为status, 序号为2
}
如果 A 和 B 要基于 Protocol Buffers 协议进行通信,那么在通信前,A 和 B 都需要有同一份协议文件,所以在 Protocol Buffers 数据传输过程中,不需要数据的 Schema
信息;
Protocol Buffers软件
值得一提的是,Protocol Buffers 可以安装软件,将指定的 proto
协议文件转换为多种编程语言代码,并且提供了序列化和反序列化等一系列方法库。
下载地址: https://github.com/protocolbuffers/protobuf/releases
MAC 下安装:
brew install protobuf
# 安装完成后验证
protoc --version
转换为代码:
# 在当前目录输出 response.proto 协议生成的Java代码
protoc --java_out=. response.proto
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.9.1</version>
</dependency>
ResponseOuterClass.Response.Builder builder = ResponseOuterClass.Response.newBuilder();
// 设置字段值
builder.setData("hello www.tizi365.com");
builder.setStatus(200);
ResponseOuterClass.Response response = builder.build();
// 将数据根据protobuf格式,转化为字节数组
byte[] byteArray = response.toByteArray();
// 反序列化,二进制数据
ResponseOuterClass.Response newResponse = ResponseOuterClass.Response.parseFrom(byteArray);
System.out.println(newResponse.getData());
System.out.println(newResponse.getStatus());
Protocol Buffers 编码
Protocol Buffers 编码主要借助 Varint
和 ZigZag
算法实现,这也使得 Protocol Buffers 有更优秀的传输效率。
在理解这两个算法前,首先需要回顾一下计算机二进制表示的几种方式:
-
原码,第一位表示符号位(0为非负数,1为负数),剩余表示值。 +8 = 0000 1000「原码」 -8 = 1000 1000「原码」
-
反码,第一位表示符号位(0为非负数,1为负数),剩余位,负数按位取反,非负数不变,正数的反码是原码本身。 +8 = 0000 1000「反码」 -8 = 1111 0111「反码」
-
补码,由于原码和反码无法直接参与位运算(符号位的存在),所以引入补码,补码用第一位表示符号(0为非负数,1为负数),剩下的位非负数保持不变,负数按位求反末位加1.(补码的补码是原码,正数的补码是本身) +8 = 0000 1000「补码」 -8 = 1000 1000「原码」-> 1111 0111 +1 -> 1111 1000「补码」
计算机中的数值是用补码来表示和存储的。
Varint
Varint 编码是将数字转换为字节数组的编码方式,使用前提是数字较小,这样才能更高效地压缩。
编码过程:
300 的二进制表示,int 类型(4个字节)
1 0010 1100
由于前2个字节都是空位,浪费了2个字节的空间。
使用 Varint 编码则会变为 1010 1100 0000 0010
。 变为了两个字节。
1 0010 1100
// 每7位为一个单元,从后往前编码,再加首位 msb(most significant bit) ,表示后续8位是否连续
010 1100 → 1010 1100
000 0010 → 0000 0010
→ 1010 1100 0000 0010
解码过程:
000 0010 010 1100
// 解码需去除msb
→ 000 0010 ++ 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300
ZigZag
ZigZag 编码是为了解决 Varint 对负数编码效率低的问题(最高位是符号位 1 ),将有符号整数映射为无符号整数,在实现上,映射通过移位即可实现。
在 Varint 中,负数有个什么问题呢? 符号位在第一位!导致高位一直需要编码。
// -1
10000000 00000000 00000000 00000001「原」
11111111 11111111 11111111 11111111「补」
如果对 -1 进行 Varint 编码,就显得不那么高效了,因为所有位都是1,那有没有办法把这个符号位换一下呢?ZigZag 就完成了这件事:
// -1 经过zigzag 编码变成 1
11111111 11111111 11111111 11111111「补」
00000000 00000000 00000000 00000001「zigzag」
// 1 经过zigzag 编码变成 2
00000000 00000000 00000000 00000001「补码」
00000000 00000000 00000000 00000010「zigzag」
移位运算:
<<
: 左移运算,左移几位低位就补几个0>>
: 右移运算,如果数字为正数时,移位后在高位补0;如果数字为负数时,移位后在高位补1>>>
: 无符号右移位,不管正数还是负数,高位都用0补齐(忽略符号位)
编码过程:
(n << 1) ^ (n >> 31) // 异或(相同为0,不同为1)
// 推演过程 -1
11111111 11111111 11111111 11111111「补」
→ n << 1 → 1111111 11111111 11111111 111111110「补」
→ n >> 31 → 11111111 11111111 11111111 11111111「补」
→ 异或 → 00000000 00000000 00000000 00000001「补」
// 推演过程 1
00000000 00000000 00000000 00000001「补」
→ n << 1 → 00000000 00000000 00000000 00000010「补」
→ n >> 31 → 00000000 00000000 00000000 00000000「补」
→ 异或 → 00000000 00000000 00000000 00000010「补」
解码过程:
(n >>> 1) ^ -(n & 1)
// 推演过程 -1
00000000 00000000 00000000 00000001「补」
→ n >>> 1 → 00000000 00000000 00000000 00000000「补」
→ -(n & 1) → 00000000 00000000 00000000 00000001「补」 → 11111111 11111111 11111111 11111111「补」
→ 异或 → 11111111 11111111 11111111 11111111「补」
参考:
https://colobu.com/2019/10/03/protobuf-ultimate-tutorial-in-go/