如何使用Protobuf进行数据交换
在以不同语言编写并在不同平台上运行的应用程序之间交换数据时,Protobuf 编码可提高效率。#
诸如 XML 和 JSON 之类的协议缓冲区(Protobufs)允许可能以不同语言编写并在不同平台上运行的应用程序交换数据。例如,用 Go 编写的发送应用程序可以在 Protobuf 中对 Go 特定的销售订单进行编码,然后用 Java 编写的接收方可以对接收到的订单的 Java 特定表示进行解码。这是网络连接上的体系结构示意图:
Go sales order--->Pbuf-encode--->network--->Pbuf-decode--->Java sales order
与 XML 和 JSON 相比,Protobuf 编码是二进制而不是文本,这会使调试复杂化。但是,正如本文中的代码示例所确认的,Protobuf 编码的大小比 XML 或 JSON 编码有效得多。
Protobuf 以另一种方式有效。在实现级别,Protobuf 和其他编码系统对结构化数据进行序列化和反序列化。序列化将特定语言的数据结构转换为字节流,反序列化是将字节流转换回特定语言的数据结构的逆运算。序列化和反序列化可能会成为数据交换的瓶颈,因为这些操作占用大量 CPU。高效的序列化和反序列化是 Protobuf 的另一个设计目标。
最近的编码技术,例如 Protobuf 和 FlatBuffers,源自 1990 年代初的 DCE / RPC(分布式计算环境 / 远程过程调用)计划。与 DCE / RPC 一样,Protobuf 在数据交换中为 IDL(接口定义语言)和编码层做出了贡献。
本文将着眼于这两层,然后提供 Go 和 Java 中的代码示例,以充实 Protobuf 的细节并表明 Protobuf 易于使用。
Protobuf 作为 IDL 和编码层#
像 Protobuf 一样,DCE / RPC 被设计为与语言和平台无关。适当的库和实用程序允许任何语言和平台在 DCE / RPC 领域中播放。此外,DCE / RPC 体系结构非常优雅。IDL 文档是一侧的远程过程与另一侧的调用者之间的合同。Protobuf 也以 IDL 文档为中心。
IDL 文档是文本,在 DCE / RPC 中,使用基本 C 语法以及元数据的语法扩展(方括号)和一些新关键字(例如 interface)。这是一个例子:
[uuid(2d6ead46-05e3-11ca-7dd1-426909beabcd),版本(1.0)]
接口echo {
const long int ECHO_SIZE = 512;
void echo(
[输入] handle_t h,
[输入,字符串] idl_char from_client [],
[输出,字符串] idl_char from_service [ECHO_SIZE]
);
}
该 IDL 文档声明了一个名为 echo 的过程,该过程带有三个参数:handle_t 类型的 [in] 参数(实现指针)和 idl_char 类型(ASCII 字符数组)将传递给远程过程,而 [out] 参数(也将是字符串)从过程传回。在此示例中,echo 过程不会显式返回值(echo 左边的空白),但可以这样做。返回值,以及一个或多个 [输出] 参数,允许远程过程任意返回许多值。下一节将介绍 Protobuf IDL,它的语法不同,但是在数据交换中同样用作合同。
DCE / RPC 和 Protobuf 中的 IDL 文档是创建用于交换数据的基础结构代码的实用程序的输入:
IDL document--->DCE/PRC or Protobuf utilities--->support code for data interchange
作为相对简单的文本,IDL 同样是关于数据交换的细节的人类可读文档,尤其是交换的数据项的数量和每个项的数据类型。
Protobuf 可以在现代 RPC 系统(例如 gRPC)中使用;但是 Protobuf 本身仅提供 IDL 层和编码层,用于从发送者传递到接收者的消息。与原始 DCE / RPC 一样,Protobuf 编码是二进制的,但效率更高。
当前,XML 和 JSON 编码仍在通过 Web 服务等技术进行数据交换中占主导地位,这些技术利用 Web 服务器,传输协议(例如 TCP,HTTP)以及标准库和实用程序等就地基础结构来处理 XML 和 JSON 文档。此外,各种类型的数据库系统可以存储 XML 和 JSON 文档,甚至旧式关系系统也可以轻松生成查询结果的 XML 编码。现在,每种通用编程语言都具有支持 XML 和 JSON 的库。那么,建议什么返回到 Protobuf 之类的二进制编码系统呢?
考虑负十进制值 -128。在 2 的补码二进制表示形式(在系统和语言中占主导地位)中,此值可以存储在单个 8 位字节中:10000000。此整数值在 XML 或 JSON 中的文本编码需要多个字节。例如,UTF-8 编码需要四个字节的字符串,即 -128,即每个字符一个字节(以十六进制表示,值为 0x2d,0x31、0x32 和 0x38)。XML 和 JSON 还将标记字符(例如尖括号和大括号)添加到混合中。有关 Protobuf 编码的详细信息即将到来,但现在的关注点是一个通用的观点:文本编码的压缩性明显低于二进制编码。
使用 Protobuf 进行 Go 中的代码示例#
我的代码示例着重于 Protobuf 而不是 RPC。以下是第一个示例的概述:
- 名为 dataitem.proto 的 IDL 文件定义了一个 Protobuf 消息,其中包含六个不同类型的字段:具有不同范围的整数值,固定大小的浮点值以及两个不同长度的字符串。
- Protobuf 编译器使用 IDL 文件生成 Protobuf 消息的 Go 特定版本(以及后来的 Java 特定版本)以及支持功能。
- Go 应用程序使用随机生成的值填充本机 Go 数据结构,然后将结果序列化到本地文件。为了进行比较,还将 XML 和 JSON 编码序列化为本地文件。
- 作为测试,Go 应用程序通过反序列化 Protobuf 文件的内容来重建其本机数据结构的实例。
- 作为语言中立性测试,Java 应用程序还会反序列化 Protobuf 文件的内容以获得本机数据结构的实例。
该 IDL 文件以及两个 Go 和一个 Java 源文件在我的网站上可以作为 ZIP 文件获得。
最重要的 Protobuf IDL 文档如下所示。该文档存储在文件 dataitem.proto 中,其扩展名为.proto。
例子 1. Protobuf IDL 文档#
语法=“ proto3”;
包主
消息DataItem {
int64 oddA = 1;
int64 evenA = 2;
int32 oddB = 3;
int32 evenB = 4;
浮动小= 5;
大浮点= 6;
字符串short = 7;
字符串长= 8;
}
IDL 使用当前的 proto3 而不是较早的 proto2 语法。程序包名称(在本例中为 main)是可选的,但是惯用的;它用于避免名称冲突。结构化消息包含八个字段,每个字段都有一个 Protobuf 数据类型(例如 int64,string),一个名称(例如 oddA,short)和一个等号 = 之后的数字标记(即键)。标签(在此示例中为 1 到 8)是唯一的整数标识符,用于确定字段序列化的顺序。
Protobuf 消息可以嵌套到任意级别,而另一则消息可以是字段类型。这是一个使用 DataItem 消息作为字段类型的示例:
消息 DataItems {
重复的 DataItem 项目 = 1;
}
单个 DataItems 消息由重复的(无一个或多个)DataItem 消息组成。
为了清楚起见,Protobuf 还支持枚举类型:
枚举 PartnershipStatus {
保留 “免费”,“约束”,“其他”;
}
该保留的预选赛确保用于实施三个符号名的数值不能重复使用。
为了生成一个或多个声明的 Protobuf 消息结构的特定于语言的版本,包含这些消息结构的 IDL 文件将传递到 protoc 编译器(可在 Protobuf GitHub 存储库中找到)。对于 Go 代码,可以按常规方式安装支持的 Protobuf 库(以%作为命令行提示符):
% go get github.com/golang/protobuf/proto
将 Protobuf IDL 文件 dataitem.proto 编译为 Go 源代码的命令是:
% protoc --go_out=. dataitem.proto
标志–go_out 指示编译器生成 Go 源代码;其他语言也有类似的标志。在这种情况下,结果是一个名为 dataitem.pb.go 的文件,该文件足够小,可以将基本内容复制到 Go 应用程序中。以下是生成的代码的要点:
var _ = proto.Marshal
类型DataItem struct {
OddA int64`protobuf:“ varint,1,opt,name = oddA” json:“ oddA,omitempty”`
EvenA int64`protobuf:“ varint,2,opt,name = evenA” json:“ evenA,omitempty”`
OddB int32`protobuf:“ varint,3,opt,name = oddB” json:“ oddB,omitempty”`
EvenB int32`protobuf:“ varint,4,opt,name = evenB” json: “ evenB,omitempty”`
小float32`protobuf:“ fixed32,5,opt,name = small” json:“ small,omitempty”`
Big float32`protobuf:“ fixed32,6,opt,name = big” json:“ big ,omitempty“`
短字符串protobuf:” bytes,7,opt,name = short“ json:” short,omitempty“`
长字符串`protobuf:”bytes,8,opt,name = long“ json:” long,omitempty“`
}
func(m * DataItem)Reset(){* m = DataItem {}}
func(m * DataItem)String()字符串{return proto.CompactTextString(m)}
func(* DataItem)ProtoMessage(){}
func init() {}
编译器生成的代码具有 Go 结构 DataItem,该结构导出 Go 字段(名称现已大写),该字段与 Protobuf IDL 中声明的名称匹配。结构字段具有标准的 Go 数据类型:int32,int64,float32 和 string。在每个字段行的末尾,作为字符串,是描述 Protobuf 类型,提供 Protobuf IDL 文档中的数字标签并提供有关 JSON 信息的元数据,这将在后面讨论。
也有功能;最重要的是 proto.Marshal,用于将 DataItem 结构的实例序列化为 Protobuf 格式。辅助函数包括 Reset(清除一个 DataItem 结构)和 String(一个生成 DataItem 的单行字符串表示形式)。
描述 Protobuf 编码的元数据应在更详细地分析 Go 程序之前进行仔细研究。
Protobuf 编码#
Protobuf 消息的结构为键 / 值对的集合,其中数字标签为键,相应的字段为值。字段名称(例如 oddA 和 small)是为了人类可读性,但是协议编译器在生成特定于语言的对应名称时确实使用了字段名称。例如,Protobuf IDL 中的 oddA 和 small 名称在 Go 结构中分别成为字段 OddA 和 Small。
键和它们的值都经过编码,但是有一个重要的区别:某些数字值具有固定大小的 32 或 64 位编码,而其他数字(包括消息标签)是 varint 编码的 - 位的数量取决于整数的绝对值。例如,整数值 1 到 15 需要 8 位以 varint 编码,而值 16 到 2047 需要 16 位。的 varint 编码,在精神类似(但不详细地)为 UTF-8 编码,经路数有利于小整数值。(有关详细分析,请参阅 Protobuf 编码指南。)其结果是 Protobuf 消息 如果可能,字段中的整数值应尽可能小,并且键数应尽可能少,但每个字段不可避免地要使用一个键。
下表 1 列出了 Protobuf 编码的要点:
表 1. Protobuf 数据类型
编码方式 | 样本类型 | 长度 |
---|---|---|
瓦林特 | int32,uint32,int64 | 可变长度 |
固定 | fixed32,float,double | 固定的 32 位或 64 位长度 |
字节序列 | 字符串,字节 | 序列长度 |
未明确固定的整数类型是 varint 编码的;因此,在一个 varint 类型如 UINT32(Ú 为无符号),数字 32 说明了整数的范围(在这种情况下,0 到 2 ^32^ - 1),而不是它的比特大小,这取决于值不同。相比之下,对于固定类型(例如 fixed32 或 double),Protobuf 编码分别需要 32 位和 64 位。Protobuf 中的字符串是字节序列;因此,字段编码的大小是字节序列的长度。
另一个效率值得一提。回想一下前面的示例,其中 DataItems 消息由重复的 DataItem 实例组成:
消息 DataItems {
重复的 DataItem 项目 = 1;
}
的重复单元,所述的 DataItem 实例包装:收集具有单个标签,在这种情况下,1,一种 DataItems 与重复的消息的 DataItem 实例因此比与多个但分开的消息更有效的 DataItem 字段,其每一个将需要自己的标签。
考虑到这一背景,让我们返回 Go 程序。
dataItem 程序详细#
所述的 DataItem 程序创建一个 DataItem 的实例,并与适当的类型的随机生成的值的字段。Go 有一个 rand 包,其中包含用于生成伪随机整数和浮点值的函数,而我的 randString 函数从字符集中生成指定长度的伪随机字符串。设计目标是使 DataItem 实例具有不同类型和位大小的字段值。例如,OddA 和 EvenA 值分别是奇偶校验的 64 位非负整数值;但是 OddB 和 EvenB 变体的大小为 32 位,并包含 0 到 2047 之间的小整数值。随机浮点值的大小为 32 位,字符串的长度为 16(短)和 32(长)个字符。这是用随机值填充 DataItem 结构的代码段:
//变长整数
n1:= rand.Int63()//较大的整数
if(n1&1)== 0 {n1 ++} //确保它是奇数
...
n3:= rand.Int31()%UpperBound //较小的整数
if(n3&1)== 0 {n3 ++} //确保它是奇数
//定长浮点数
...
t1:= rand.Float32()
t2:= rand.Float32()
...
//字符串
str1:= randString(StrShort)
str2:= randString(StrLong)
//消息
dataItem:=&DataItem {
OddA:n1,
EvenA:n2,
OddB:n3,
EvenB:n4,
Big:f1,
Small:f2,
Short:str1 ,
Long:str2,
}
创建并填充值后,DataItem 实例将以 XML,JSON 和 Protobuf 进行编码,每种编码均写入本地文件:
func encodeAndserialize(dataItem * DataItem){
字节,_:= xml.MarshalIndent(dataItem,“”,“”)// Xml到dataitem.xml
ioutil.WriteFile(XmlFile,bytes,0644)// 0644是文件访问权限
字节,_ = json.MarshalIndent(dataItem,“”,“”)// Json到dataitem.json
ioutil.WriteFile(JsonFile,bytes,0644)
字节,_ = proto.Marshal(dataItem)// Protobuf到dataitem.pbuf
ioutil .WriteFile(PbufFile,bytes,0644)
}
这三个序列化函数使用术语 marshal,它与序列化大致同义。如代码所示,三个 Marshal 函数中的每个函数都返回一个字节数组,然后将其写入文件。(为简单起见,可能的错误将被忽略。)在示例运行中,文件大小为:
dataitem.xml:262 字节
dataitem.json:212 字节
dataitem.pbuf:88 字节
Protobuf 编码明显小于其他两个编码。通过消除缩进字符(在这种情况下为空白和换行符),可以稍微减小 XML 和 JSON 序列化的大小。
以下是 dataitem.json 文件,该文件最终由 json.MarshalIndent 调用生成,并添加了以##开头的注释:
{
“ oddA”:4744002665212642479,## 64位> = 0
“ evenA”:2395006495604861128,##同上
“ oddB”:57,## 32位> = 0但<2048
“ evenB”:468,##同上
“ small”:0.7562016,## 32位浮点
“ big”:0.85202795,## ditto
“ short”:“ ClH1oDaTtoX $ HBN5”,## 16个随机字符
“ long”:“ xId0rD3Cri%3Wt%^ QjcFLJgyXBu9 ^ DZI“ ## 32个随机字符
}
尽管序列化的数据进入本地文件,但将使用相同的方法将数据写入网络连接的输出流。
测试序列化 / 反序列化#
Go 程序接下来通过将先前写入 dataitem.pbuf 文件的字节反序列化为 DataItem 实例来运行基本测试。这是代码段,其中除去了错误检查部分:
filebytes,err:= ioutil.ReadFile(PbufFile)//从文件中获取字节
...
testItem.Reset()//清除DataItem结构
err = proto.Unmarshal(filebytes,testItem)//反序列化为DataItem实例
该 proto.Unmarshal 为解串 Protbuf 功能是逆 proto.Marshal 功能。将打印原始 DataItem 和反序列化的克隆以确认完全匹配:
原文:
2041519981506242154 3041486079683013705 1192 1879 年
0.572123 0.326855
boPb#T0O8Xd&Ps5EnSZqDg4Qztvo7IIs 9vH66AiGSQgCDxk 和
反序列化:
2041519981506242154 3041486079683013705 1192 1879 年
0.572123 0.326855
boPb#T0O8Xd&Ps5EnSZqDg4Qztvo7IIs 9vH66AiGSQgCDxk&
Java 中的 Protobuf 客户端#
Java 中的示例是确认 Protobuf 的语言中立性。原始 IDL 文件可用于生成 Java 支持代码,其中涉及嵌套类。但是,为了抑制警告,可以进行少量添加。这是修订,它指定一个 DataMsg 作为外部类的名称,内部类在 Protobuf 消息后自动命名为 DataItem:
语法 =“proto3”;
包主
选项 java_outer_classname =“DataMsg”;
消息 DataItem {
…
进行此更改后,协议编译与之前相同,只是所需的输出现在是 Java 而不是 Go:
% protoc --java_out=. dataitem.proto
产生的源文件(在名为 main 的子目录中)是 DataMsg.java,长度约为 1,120 行:Java 并不简洁。编译然后运行 Java 代码,需要具有 Protobuf 库支持的 JAR 文件。该文件在 Maven 存储库中可用。
放置好这些片段后,我的测试代码相对较短(并且可以在 ZIP 文件中作为 Main.java 获得):
包主
导入java.io.FileInputStream;
公共类Main {
公共静态void main(String [] args){
字符串路径=“ dataitem.pbuf”; // //从Go程序的序列化中
尝试{
DataMsg.DataItem deserial =
DataMsg.DataItem.newBuilder()。mergeFrom(new FileInputStream(path))。build();
System.out.println(deserial.getOddA()); // 64位奇数
System.out.println(deserial.getLong()); // 32个字符的字符串
}
catch(Exception e){System.err.println(e); }
}
}
当然,生产级测试将更加彻底,但是即使是此初步测试也可以证明 Protobuf 的语言中立性:dataitem.pbuf 文件是 Go 程序对 Go DataItem 进行序列化的结果,并且对该文件中的字节进行了反序列化生成 Java 中的 DataItem 实例。Java 测试的输出与 Go 测试的输出相同。
结束 numPairs 程序#
让我们以一个突出 Protobuf 效率但又强调任何编码技术所涉及的成本的示例结尾。考虑以下 Protobuf IDL 文件:
语法=“ proto3”;
包主
消息NumPairs {
重复NumPair对= 1;
}
消息NumPair {
int32奇数= 1;
int32偶数= 2;
}
甲 NumPair 消息由两个 INT32 值连同用于每个字段的整数标记。甲 NumPairs 消息嵌入的序列 NumPair 消息。
Go(下面)中的 numPairs 程序创建了 200 万个 NumPair 实例,每个实例都附加到 NumPairs 消息中。该消息可以按常规方式进行序列化和反序列化。
例子 2. numPairs 程序#
封装主
进口(
“数学/兰特”
“时间”
“编码/ xml”的
“编码/ JSON”
“IO / ioutil”
“github.com/golang/protobuf/proto”
)
// protoc生成的代码:开始
VAR _ = proto.Marshal
类型的NumPairs struct {
Pair [] * NumPair`protobuf:“ bytes,1,rep,name = pair” json:“ pair,omitempty”`
}}
func(m * NumPairs)Reset(){* m = NumPairs { }}
func(m * NumPairs)String()string {return proto.CompactTextString(m)}
func(* NumPairs)ProtoMessage(){}
func(m * NumPairs)GetPair()[] * NumPair {
如果m!= nil {返回m。对}
return nil
}
类型NumPair struct {
奇数int32`protobuf:“ varint,1,opt,name = odd” json:“ odd,omitempty”`
甚至int32`protobuf:“ varint,2,opt,name = even” json:“ even,omitempty”`
}
func (m * NumPair)Reset(){* m = NumPair {}}
func(m * NumPair)String()字符串{return proto.CompactTextString(m)}
func(* NumPair)ProtoMessage(){}
func init(){ }
//协议生成的代码:完成
var numPairsStruct NumPairs
var numPairs =&numPairsStruct
func encodeAndserialize(){
// XML编码
filename:=“ ./pairs.xml”
字节,_:= xml.MarshalIndent(numPairs,“”,“ “)
ioutil.WriteFile(文件名,字节,0644)
// JSON编码
filename =“ ./pairs.json”
字节,_ = json.MarshalIndent(numPairs,“”,“”)
ioutil.WriteFile(文件名,字节,
0644 )// ProtoBuf编码
filename =“ ./pairs.pbuf”
字节,_ = proto.Marshal(numPairs)
ioutil .WriteFile(文件名,字节, 0644 )
}
const HowMany = 200 * 100 * 100 // 200万个
func main(){
rand.Seed(time.Now()。UnixNano())
//取消对模运算的注释,以获取
i的更有效版本:= 0; 我<多少; i ++ {
n1:= rand.Int31()//%2047
if(n1&1)== 0 {n1 ++} //确保它是奇数
n2:= rand.Int31()//%2047
if(n2&1)= = 1 {n2 ++} //确保
奇数:n1,
偶数:n2,
}
numPairs.Pair = append(numPairs.Pair,下一个)
}
encodeAndserialize()
}
每个 NumPair 中随机生成的奇数和偶数值的范围从零到 20 亿,并且在变化。就原始数据而言,而不是编码数据而言,Go 程序中生成的整数加起来为 16MB:每个 NumPair 中两个整数,总计为 400 万个整数,每个值的大小为四个字节。
为了进行比较,下表在示例 NumsPairs 消息中具有 200 万个 NumPair 实例的 XML,JSON 和 Protobuf 编码的条目。原始数据也包括在内。由于 numPairs 程序生成随机值,因此样本运行的输出有所不同,但接近表中显示的大小。
表 2. 16MB 整数的编码开销
编码方式 | 文件 | 字节大小 | Pbuf / 其他比率 |
---|---|---|---|
没有 | pair.raw | 16MB | 169% |
原虫 | 对.pbuf | 27MB | - |
JSON 格式 | pair.json | 100MB | 27% |
XML 格式 | pair.xml | 126 兆字节 | 21% |
不出所料,Protobuf 紧随 XML 和 JSON 之后。Protobuf 编码约为 JSON 的四分之一,而 XML 则为五分之一。但是原始数据清楚地表明 Protobuf 会产生编码的开销:序列化的 Protobuf 消息比原始数据大 11MB。包括 Protobuf 在内的任何编码都涉及结构化数据,这不可避免地会增加字节。
序列化的 200 万个 NumPair 实例中的每个实例都包含四个整数值:Go 结构中的 Even 和 Odd 字段每个对应一个值,Protobuf 编码中的每个字段每个对应一个标签。作为原始数据而不是编码数据,每个实例将达到 16 个字节,并且样本 NumPairs 消息中有 200 万个实例。但是 Protobuf 标记(如 NumPair 字段中的 int32 值)使用 varint 编码,因此字节长度有所不同。特别是,小的整数值(在这种情况下,包括标签在内)需要少于四个字节来进行编码。
如果对 numPairs 程序进行了修改,以使两个 NumPair 字段的值小于 2048(使用一字节或两个字节的编码),则 Protobuf 编码将从 27MB 下降到 16MB(原始数据的大小)。下表总结了样本运行中的新编码大小。
表 3. 用小于等于 2048 的 16MB 整数进行编码
编码方式 | 文件 | 字节大小 | Pbuf / 其他比率 |
---|---|---|---|
没有 | pair.raw | 16MB | 100% |
原虫 | 对.pbuf | 16MB | - |
JSON 格式 | pair.json | 77 兆字节 | 21% |
XML 格式 | pair.xml | 103MB | 15% |
总而言之,修改后的 numPairs 程序的字段值小于 2048,可减少原始数据中每个整数值的四字节大小。但是 Protobuf 编码仍然需要标签,这些标签会在 Protobuf 消息中添加字节。Protobuf 编码确实会增加消息大小,但是如果正在编码相对较小的整数值(无论是字段还是键中的值),则可以通过 varint 因子来减少此开销。
对于包含混合类型的结构化数据(且整数值相对较小)的中等大小的消息,Protobuf 明显优于 XML 和 JSON 等选项。在其他情况下,数据可能不适合 Protobuf 编码。例如,如果两个应用程序需要共享大量文本记录或大整数值,则可以采用压缩而不是编码技术。
推荐文章: