前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何使用Protobuf进行数据交换【Programming(Go)】

如何使用Protobuf进行数据交换【Programming(Go)】

作者头像
Potato
修改2019-11-12 10:45:56
1.5K0
修改2019-11-12 10:45:56
举报
文章被收录于专栏:Opensource翻译专栏

在用不同语言编写的应用程序之间以及在不同平台上运行的应用程序之间交换数据时,Protobuf 编码提高了效率。

图片来源:Opensource.com
图片来源:Opensource.com

像XML和JSON这样的协议缓冲区(Protobufs)允许以不同语言编写并在不同平台上运行的应用程序交换数据。例如,用Go编写的发送应用程序可以在Protobuf中对Go特定的销售订单进行编码,然后用Java编写的接收方可以对它进行解码,以获取所接收订单的Java特定表示。这是网络连接上的体系结构示意:

代码语言:txt
复制
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易于使用的原因。

作为 IDL 和编码层协议

正如Protobuf一样,DCE/RPC被设计为与语言和平台无关。适当的库和程序允许任何语言和平台在DCE/RPC领域中运行。此外,DCE/RPC体系结构非常优雅。IDL文档是一端的远程过程与另一端的调用方之间的协议。也是以IDL文档为中心的。

IDL文档是文本,在DCE/RPC中,使用基本C语法以及元数据的语法扩展(方括号)和一些新关键字(例如interface)。下面是例子:

代码语言:txt
复制
[uuid (2d6ead46-05e3-11ca-7dd1-426909beabcd), version(1.0)]
interface echo {
   const long int ECHO_SIZE = 512;
   void echo(
      [in]          handle_t h,
      [in, string]  idl_char from_client[ ],
      [out, string] idl_char from_service[ECHO_SIZE]
   );
}

该IDL文档声明了一个名为echo的过程,该过程带有三个参数:handle_t(实现指针)类型的in参数和idl_char(ASCII字符数组)类型传递给远程过程,而out参数(也包含一个字符串)从过程传回。在此示例中,echo过程不会显式返回值(echo左侧的空白),但可以这样做。返回值与一个或多个out参数一起允许远程过程任意返回许多值。下一节将介绍ProtobufIDL,它的语法不同,但同样用作数据交换中的协定。

在 DCE/ RPC 和 Protobuf 中,IDL文档可以创建用于交换数据的基础设施代码工具的输入:

代码语言:txt
复制
IDL document--->DCE/PRC or Protobuf utilities--->support code for data interchange

由于 IDL相对简单,同样也是关于数据交换细节(特别是交换的数据项的数量和每个项的数据类型)的可读的文档。

Protobuf可以用于现代RPC系统中,例如gRPC;但是Protobuf本身仅提供IDL层和编码层,用于从发送者传递到接收者的message。 与原始的DCE / RPC一样,Protobuf编码是二进制的,但效率更高。

目前,XML 和 JSON 编码仍然主要通过 web 服务等技术进行数据交换,这些技术利用现有的基础设施,如 web 服务器、传输协议(如 TCP、 HTTP)以及处理 XML 和 JSON 文档的标准库和实用程序。 此外,不同风格的数据库系统可以存储 XML 和 JSON 文档,甚至传统的关系系统也可以轻松地生成查询结果的 XML 编码。 现在每个通用编程语言都有支持 XML 和 JSON 的库。 那么,什么推荐返回到如 Protobuf 这样的二进制编码系统呢?

考虑负的十进制值 -128。 在补码二进制表示中,这个值可以存储在一个单独的8位字节中: 10000000。 Xml 或 JSON 格式的此整数值的文本编码需要多个字节。 例如,UTF-8编码要求字符串有4个字节,即-128,每个字符一个字节(十六进制中的值分别为0x2d、0x31、0x32和0x38)。 Xml 和 JSON 还添加了标记字符,如尖括号和大括号。下面将有关于 Protobuf 编码的细节,但现在的关注点是一个通用点:文本编码的压缩性明显低于二进制编码。

在Go中使用Protobuf

我的代码示例着重于Protobuf而不是RPC。 以下是第一个示例的概述:

  • 名为dataitem.proto的IDL文件定义了一个Protobufmessage,其中包含六个不同类型的字段:具有不同范围的整数值,固定大小的浮点值以及两个不同长度的字符串。
  • Protobuf编译器使用IDL文件生成Protobuf message的Go特定版本(以及后来的Java特定版本)以及支持功能。
  • Go应用程序使用随机生成的值填充本地Go数据结构,然后将结果序列化到本地文件。 为了进行比较,XML和JSON编码也被序列化为本地文件。
  • 作为测试,Go应用程序通过反序列化Protobuf文件的内容来重建其本机数据结构的实例。
  • 作为语言中立性测试,Java应用程序还会反序列化Protobuf文件的内容以获得本机数据结构的实例。

这个 IDL 文件和两个 Go 和一个 Java 源文件在我的网站上以 ZIP 文件的形式提供。

最重要的Protobuf IDL文档如下所示。 该文档存储在文件dataitem.proto中 ,扩展名为.proto 。

代码语言:txt
复制
syntax = "proto3";

package main;

message DataItem {
  int64  oddA  = 1;
  int64  evenA = 2;
  int32  oddB  = 3;
  int32  evenB = 4;
  float  small = 5;
  float  big   = 6;
  string short = 7;
  string long  = 8;
}

Protobuf message可以嵌套到任意级别,而另一则message可以是字段类型。 这是一个使用DataItem message作为字段类型的示例:

代码语言:txt
复制
message DataItems {
  repeated DataItem item = 1;
}

单个DataItems message由重复的(零或多个) DataItem message组成。

为了清楚起见,Protobuf还支持枚举类型:

代码语言:txt
复制
enum PartnershipStatus {
  reserved "FREE", "CONSTRAINED", "OTHER";
}

reserved限定符确保用于实现这三个符号名的数值不能重复使用。

为了生成一个或多个声明的Protobuf message结构的特定于语言的版本,包含这些message结构的IDL文件将传递到protoc编译器(可在Protobuf GitHub中找到)。 对于Go代码,可以按常规方式安装支持的Protobuf库(以%作为命令行提示符):

代码语言:txt
复制
% go get github.com/golang/protobuf/proto

将Protobuf IDL文件dataitem.proto编译为Go源代码的命令是:

代码语言:txt
复制
% protoc --go_out=. dataitem.proto

标志 -- Go out 指示编译器生成 Go 源代码;其他语言也有类似的标志。 在本例中,结果是一个名为 dataitem.pb.Go 的文件,这个文件非常小,可以将要点复制到 Go 应用程序中。

代码语言:txt
复制
var _ = proto.Marshal

type 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"`
   Small float32 `protobuf:"fixed32,5,opt,name=small" json:"small,omitempty"`
   Big   float32 `protobuf:"fixed32,6,opt,name=big" json:"big,omitempty"`
   Short string  `protobuf:"bytes,7,opt,name=short" json:"short,omitempty"`
   Long  string  `protobuf:"bytes,8,opt,name=long" json:"long,omitempty"`
}

func (m *DataItem) Reset()         { *m = DataItem{} }
func (m *DataItem) String() 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的单行字符串表示形式)。

在更详细地分析 Go 程序之前,描述 Protobuf 编码的元数据值得仔细研究。

Protobuf编码

Protobuf message的结构是键 / 值对的集合,数字标记作为键,相应的字段作为值。 字段名,比如 OddA 和 Small,是为了可读性,但是 protoc 编译器在生成特定于语言的对应项时使用字段名。 例如,Protobuf IDL 中的 oddA 和小名分别成为 Go 结构中的 oddA 和 Small 字段。

键和它们的值都可以被编码,但是有一个重要的区别: 一些数值的编码固定在32或64位,而另一些(包括message标签)是变容编码的——位的数量取决于整数的绝对值。 例如,整数值1到15需要8位进行变容编码,而值16到2047需要16位。 Varint 编码与 UTF-8编码相似(但不是很详细) ,它更喜欢小整数值而不是大整数值。 (有关详细分析,请参阅 Protobuf 编码指南。) 其结果是,如果可能的话,Protobuf message的字段中应该有小的整数值,并且尽可能少的键,但是每个字段一个键是不可避免的。

下面的表1给出了 Protobuf 编码的要点:

编码方式

样本类型

长度

varint

int32, uint32, int64

可变长度

fixed

fixed32, float, double

固定的32位或64位长度

byte sequence

string, bytes

序列长度

没有显式固定的整数类型是 varint 编码的; 因此,在类似 uint32(u 表示无符号)这样的 varint 类型中,数字32描述的是整数的范围(在本例中是0到232-1) ,而不是它的位大小,后者根据值的不同而不同。 相比之下,对于像 fixed32或 double 这样的固定类型,Protobuf 编码分别需要32位和64位。 在 Protobuf,字符串是字节序列,因此字段编码的大小是字节序列的长度。

另一个效率值得一提。 回想一下前面的例子,在这个例子中,DataItems 消息由重复的 DataItem 实例组成:

代码语言:txt
复制
message DataItems {
  repeated DataItem item = 1;
}

repeated意味着 Datadem 实例被打包:集合具有单个标记,在本例中为1。 因此,具有repeated DataItem 实例的 DataItems 消息比具有多个但独立 DataItem 字段的消息更有效,每个字段都需要自己的标记。

有了这些背景知识,让我们回到 Go 程序。

DataItem程序详细介绍

Dataitem程序创建一个dataItem实例,并用随机生成的适当类型的值填充字段。Go有一个带有函数的rand包,用于生成伪随机整数和浮点值,我的randString函数从字符集生成指定长度的伪随机字符串。设计目标是拥有一个DataItem实例,其字段值具有不同的类型和位大小。例如,OddA和EvenA值分别是奇偶奇偶的64位非负整数值;但是OddB和EvenB变量的大小为32位,并且保存0到2047之间的小整数值。随机浮点值的大小为32位,字符串的长度为16(Short)和32(Long)字符。下面是用随机值填充DataItem结构的代码段:

代码语言:txt
复制
// variable-length integers
n1 := rand.Int63()        // bigger integer
if (n1 & 1) == 0 { n1++ } // ensure it's odd
...
n3 := rand.Int31() % UpperBound // smaller integer
if (n3 & 1) == 0 { n3++ }       // ensure it's odd

// fixed-length floats
...
t1 := rand.Float32()
t2 := rand.Float32()
...
// strings
str1 := randString(StrShort)
str2 := randString(StrLong)

// the message
dataItem := &DataItem {
   OddA:  n1,
   EvenA: n2,
   OddB:  n3,
   EvenB: n4,
   Big:   f1,
   Small: f2,
   Short: str1,
   Long:  str2,
}

一旦创建并填充了值,DataItem 实例就被编码为 XML、 JSON 和 Protobuf,每种编码都被写入一个本地文件:

代码语言:txt
复制
func encodeAndserialize(dataItem *DataItem) {
   bytes, _ := xml.MarshalIndent(dataItem, "", " ")  // Xml to dataitem.xml
   ioutil.WriteFile(XmlFile, bytes, 0644)            // 0644 is file access permissions

   bytes, _ = json.MarshalIndent(dataItem, "", " ")  // Json to dataitem.json
   ioutil.WriteFile(JsonFile, bytes, 0644)

   bytes, _ = proto.Marshal(dataItem)                // Protobuf to dataitem.pbuf
   ioutil.WriteFile(PbufFile, bytes, 0644)
}

这三个序列化函数使用术语marshal ,它与序列化大致相同。 如代码所示,三个Marshal函数中的每个函数都返回一个字节数组,然后将其写入文件。 (为简单起见,错误将被忽略。)在示例运行中,文件大小为:

代码语言:txt
复制
dataitem.xml:  262 bytes
dataitem.json: 212 bytes
dataitem.pbuf:  88 bytes

Protobuf 编码比另外两个要小得多。通过消除缩进字符(在这种情况下为空白和换行符),可以稍微减小XML和JSON序列化的大小。

下面是dataitem.json文件,该文件最终由json.MarshalIndent调用生成,并添加了以##开头的注释:

代码语言:txt
复制
{
 "oddA":  4744002665212642479,                ## 64-bit >= 0
 "evenA": 2395006495604861128,                ## ditto
 "oddB":  57,                                 ## 32-bit >= 0 but < 2048
 "evenB": 468,                                ## ditto
 "small": 0.7562016,                          ## 32-bit floating-point
 "big":   0.85202795,                         ## ditto
 "short": "ClH1oDaTtoX$HBN5",                 ## 16 random chars
 "long":  "xId0rD3Cri%3Wt%^QjcFLJgyXBu9^DZI"  ## 32 random chars
}

虽然序列化的数据进入本地文件,但是可以使用相同的方法将数据写入网络连接的输出流。

测试序列化 / 反序列化

Go程序接下来通过将先前写入dataitem.pbuf文件的字节反序列化为DataItem实例来运行基本测试。 这是代码段,其中除去了错误部分:

代码语言:txt
复制
filebytes, err := ioutil.ReadFile(PbufFile) // get the bytes from the file
...
testItem.Reset()                            // clear the DataItem structure
err = proto.Unmarshal(filebytes, testItem)  // deserialize into a DataItem instance

用于反序列化Protbuf的proto.Unmarshal函数是proto.Marshal函数的逆函数。将打印原始的DataItem和反序列化的克隆以确认完全匹配:

代码语言:txt
复制
Original:
2041519981506242154 3041486079683013705 1192 1879
0.572123 0.326855
boPb#T0O8Xd&Ps5EnSZqDg4Qztvo7IIs 9vH66AiGSQgCDxk&

Deserialized:
2041519981506242154 3041486079683013705 1192 1879
0.572123 0.326855
boPb#T0O8Xd&Ps5EnSZqDg4Qztvo7IIs 9vH66AiGSQgCDxk&

Java 中的 Protobuf 客户端

Java 的例子可以确认 Protobuf 的语言中立性。 原始的 IDL 文件可以用来生成 Java 支持代码,这涉及到嵌套的类。 但是,为了抑制警告,可以稍微增加一个内容。 下面是修订版本,它指定了一个 DataMsg 作为外部类的名称,内部类在 Protobuf 消息之后自动命名为 DataItem:

代码语言:txt
复制
yntax = "proto3";

package main;

option java_outer_classname = "DataMsg";

message DataItem {
...

这个改变之后,protoc 编译和之前一样,只是希望输出的是 Java 而不是 Go:

代码语言:txt
复制
% protoc --java_out=. dataitem.proto

产生的源文件(在名为main的子目录中)是DataMsg.java ,长度约为1,120行:Java并不简洁。 编译然后运行Java代码,需要具有Protobuf库支持的JAR文件。 该文件在Maven仓库中可用。

有了这些部分,我的测试代码相对较短(并且在 ZIP 文件中可以使用 Main.java):

代码语言:txt
复制
package main;
import java.io.FileInputStream;

public class Main {
   public static void main(String[] args) {
      String path = "dataitem.pbuf";  // from the Go program's serialization
      try {
         DataMsg.DataItem deserial =
           DataMsg.DataItem.newBuilder().mergeFrom(new FileInputStream(path)).build();

         System.out.println(deserial.getOddA()); // 64-bit odd
         System.out.println(deserial.getLong()); // 32-character string
      }
      catch(Exception e) { System.err.println(e); }
    }
}

当然,生产级测试将更加彻底,但是即使是此初步测试也可以证明Protobuf的语言中立性: dataitem.pbuf文件是Go程序对Go DataItem进行序列化的结果,并且对该文件中的字节进行了反序列化在Java中生成一个DataItem实例。 Java测试的输出与Go测试的输出相同。

最后是numPairs 程序

让我们以一个例子来结束,这个例子强调了 Protobuf 的效率,但也强调了任何编码技术所涉及的成本。 看看这个 Protobuf IDL 文件:

代码语言:txt
复制
syntax = "proto3";
package main;

message NumPairs {
  repeated NumPair pair = 1;
}

message NumPair {
  int32 odd = 1;
  int32 even = 2;
}

NumPair消息由两个int32值以及每个字段的整数标记组成。 NumPairs消息是嵌入的NumPair消息的序列。

Go中的 numPairs 程序创建了200万个 NumPair 实例,每个实例都附加到 numPairs 消息中。 此消息可以按照通常的方式进行序列化和反序列化。

代码语言:txt
复制
package main

import (
   "math/rand"
   "time"
   "encoding/xml"
   "encoding/json"
   "io/ioutil"
   "github.com/golang/protobuf/proto"
)

// protoc-generated code: start
var _ = proto.Marshal
type 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 {
   if m != nil { return m.Pair }
   return nil
}

type NumPair struct {
   Odd  int32 `protobuf:"varint,1,opt,name=odd" json:"odd,omitempty"`
   Even int32 `protobuf:"varint,2,opt,name=even" json:"even,omitempty"`
}

func (m *NumPair) Reset()         { *m = NumPair{} }
func (m *NumPair) String() string { return proto.CompactTextString(m) }
func (*NumPair) ProtoMessage()    {}
func init() {}
// protoc-generated code: finish

var numPairsStruct NumPairs
var numPairs = &numPairsStruct

func encodeAndserialize() {
   // XML encoding
   filename := "./pairs.xml"
   bytes, _ := xml.MarshalIndent(numPairs, "", " ")
   ioutil.WriteFile(filename, bytes, 0644)

   // JSON encoding
   filename = "./pairs.json"
   bytes, _ = json.MarshalIndent(numPairs, "", " ")
   ioutil.WriteFile(filename, bytes, 0644)

   // ProtoBuf encoding
   filename = "./pairs.pbuf"
   bytes, _ = proto.Marshal(numPairs)
   ioutil.WriteFile(filename, bytes, 0644)
}

const HowMany = 200 * 100  * 100 // two million

func main() {
   rand.Seed(time.Now().UnixNano())

   // uncomment the modulus operations to get the more efficient version
   for i := 0; i < HowMany; i++ {
      n1 := rand.Int31() // % 2047
      if (n1 & 1) == 0 { n1++ } // ensure it's odd
      n2 := rand.Int31() // % 2047
      if (n2 & 1) == 1 { n2++ } // ensure it's even

      next := &NumPair {
                 Odd:  n1,
                 Even: n2,
              }
      numPairs.Pair = append(numPairs.Pair, next)
   }
   encodeAndserialize()
}

在每个 NumPair 中随机生成的奇数和偶数值范围从0到20亿,并且会发生变化。 根据原始数据而不是编码数据,Go 程序中生成的整数加起来达到16 MB: 每 NumPair 两个整数,总共400万个整数,每个值的大小为4个字节。

为了进行比较,下面的表中包含示例 NumsPairs 消息中200万个 NumPair 实例的 XML、 JSON 和 Protobuf 编码条目。 原始数据也包括在内。 因为 numPairs 程序生成随机值,所以每次样本运行的输出都不同,但是接近表中显示的大小。

编码方式

文件名

文件大小

Pbuf/其它

None

pair.raw

16MB

169%

Protobuf

pairs.pbuf

27MB

-

JSON

pair.json

100MB

27%

XML

pair.xml

126MB

21%

正如预期的那样,Protobuf 在 XML 和 JSON 之后表现突出。 Protobuf 编码大约是 JSON 的四分之一,XML 的五分之一。 但是原始数据表明,Protobuf 会产生编码开销: 序列化的 Protobuf 消息比原始数据大11 MB。 任何编码,包括 Protobuf,都涉及到数据的结构化,这不可避免地增加了字节。

序列化的200万 NumPair 实例中的每个实例都包含四个整数值: Go 结构中的 Even 和 Odd 字段各一个,Protobuf 编码中的每个字段各一个标记。 作为原始数据,而不是编码的数据,每个实例将有16个字节,并且示例 NumPairs 消息中有200万个实例。 但是,Protobuf 标记,如 NumPair 字段中的 int32值,使用 varint 编码,因此字节长度不同; 特别是,小整数值(包括标记,在本例中)需要少于4个字节来进行编码。

如果对 numPairs 程序进行修改,使得两个 NumPair 字段的值小于2048,而这两个字段的编码是一个或两个字节,那么 Protobuf 编码将从27 MB 下降到16mb,这正是原始数据的大小。 下表总结了示例运行中的新编码大小。

编码方式

文件名

文件大小

Pbuf/其它

None

pair.raw

16MB

100%

Protobuf

pairs.pbuf

16MB

-

JSON

pair.json

77MB

21%

XML

pair.xml

103MB

15%

总而言之,修改后的numPairs程序的字段值小于2048,可减少原始数据中每个整数值的四字节大小。 但是Protobuf编码仍然需要标签,这些标签会在Protobuf消息中添加字节。 Protobuf编码确实会增加消息大小,但是如果正在编码相对较小的整数值(无论是在字段还是在键中),则可以通过varint因子来减少此开销。

对于包含混合类型的结构化数据(且整数值相对较小)的中等大小的消息,Protobuf明显优于XML和JSON等选项。 在其他情况下,数据可能不适合Protobuf编码。 例如,如果两个应用程序需要共享大量文本记录或大整数值,则可以采用压缩而不是编码技术。

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 作为 IDL 和编码层协议
  • 在Go中使用Protobuf
  • Protobuf编码
  • DataItem程序详细介绍
  • 测试序列化 / 反序列化
  • Java 中的 Protobuf 客户端
  • 最后是numPairs 程序
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档