如何使用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编码。例如,如果两个应用程序需要共享大量文本记录或大整数值,则可以采用压缩而不是编码技术。
推荐文章: