ProtoBuf数据协议

2022/01/11

Tags: Algorithm ProtoBuf

摘要:ProtoBuf(Protocol Buffers)是一种跨平台、语言无关、可扩展的序列化结构数据的方法,可用于网络数据交换及数据存储。


Protocol Buffers介绍

不同于 XMLJSON 这种文本格式数据,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 编码主要借助 VarintZigZag 算法实现,这也使得 Protocol Buffers 有更优秀的传输效率。

在理解这两个算法前,首先需要回顾一下计算机二进制表示的几种方式:

计算机中的数值是用补码来表示和存储的。

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」

移位运算:

编码过程:

(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/

https://github.com/halfrost/Halfrost-Field/blob/master/contents/Protocol/Protocol-buffers-encode.md#%E5%85%AD-protocol-buffer-%E7%BC%96%E7%A0%81%E5%8E%9F%E7%90%86

https://developers.google.com/protocol-buffers

https://www.tizi365.com/archives/367.html