在以不同语言编写并在不同平台上运行的应用程序之间交换数据时,Protobuf 编码可提高效率。
协议缓冲区 (Protobufs)像 XML 和 JSON 一样,可以让用不同语言编写并在不同平台上运行的应用程序交换数据。例如,用 Go 编写的发送程序可以在 Protobuf 中对以 Go 表示的销售订单数据进行编码,然后用 Java 编写的接收方可以对它进行解码,以获取所接收订单数据的 Java 表示方式。这是在网络连接上的结构示意图:
Go 销售订单 —> Pbuf 编码 —> 网络 —> Pbuf 界面 —> Java 销售订单
与 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), 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
(实现指针)和 idl_char
(ASCII 字符数组)的 [in]
参数被传递给远程过程,而 [out]
参数(也是一个字符串)从该过程中传回。在此示例中,echo
过程不会显式返回值(echo
左侧的 void
),但也可以返回值。返回值,以及一个或多个 [out]
参数,允许远程过程任意返回许多值。下一节将介绍 Protobuf IDL,它的语法不同,但同样用作数据交换中的协定。
DCE/RPC 和 Protobuf 中的 IDL 文档是创建用于交换数据的基础结构代码的实用程序的输入:
IDL 文档 —> DCE/PRC 或 Protobuf 实用程序 —> 数据交换的支持代码
作为相对简单的文本,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 编码的详细信息下面就会介绍,但现在的关注点是一个通用点:文本编码的压缩性明显低于二进制编码。
在 Go 中使用 Protobuf 的示例
我的代码示例着重于 Protobuf 而不是 RPC。以下是第一个示例的概述:
- 名为
dataitem.proto
的 IDL 文件定义了一个 Protobuf 消息,它具有六个不同类型的字段:具有不同范围的整数值、固定大小的浮点值以及两个不同长度的字符串。 - Protobuf 编译器使用 IDL 文件生成 Go 版本(以及后面的 Java 版本)的 Protobuf 消息及支持函数。
- Go 应用程序使用随机生成的值填充原生的 Go 数据结构,然后将结果序列化为本地文件。为了进行比较, XML 和 JSON 编码也被序列化为本地文件。
- 作为测试,Go 应用程序通过反序列化 Protobuf 文件的内容来重建其原生数据结构的实例。
- 作为语言中立性测试,Java 应用程序还会对 Protobuf 文件的内容进行反序列化以获取原生数据结构的实例。
我的网站上提供了该 IDL 文件以及两个 Go 和一个 Java 源文件,打包为 ZIP 文件。
最重要的 Protobuf IDL 文档如下所示。该文档存储在文件 dataitem.proto
中,并具有常规的.proto
扩展名。
示例 1、Protobuf IDL 文档
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;
}
该 IDL 使用当前的 proto3 而不是较早的 proto2 语法。软件包名称(在本例中为 main
)是可选的,但是惯例使用它以避免名称冲突。这个结构化的消息包含八个字段,每个字段都有一个 Protobuf 数据类型(例如,int64
、string
)、名称(例如,oddA
、short
)和一个等号 =
之后的数字标签(即键)。标签(在此示例中为 1 到 8)是唯一的整数标识符,用于确定字段序列化的顺序。
Protobuf 消息可以嵌套到任意级别,而一个消息可以是另外一个消息的字段类型。这是一个使用 DataItem
消息作为字段类型的示例:
message DataItems {
repeated DataItem item = 1;
}
单个 DataItems
消息由重复的(零个或多个)DataItem
消息组成。
为了清晰起见,Protobuf 还支持枚举类型:
enum PartnershipStatus {
reserved "FREE", "CONSTRAINED", "OTHER";
}
reserved
限定符确保用于实现这三个符号名的数值不能重复使用。
为了生成一个或多个声明 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
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 格式。辅助函数包括:清除 DataItem
结构的 Reset
,生成 DataItem
的单行字符串表示的 String
。
描述 Protobuf 编码的元数据应在更详细地分析 Go 程序之前进行仔细研究。
Protobuf 编码
Protobuf 消息的结构为键/值对的集合,其中数字标签为键,相应的字段为值。字段名称(例如,oddA
和 small
)是供人类阅读的,但是 protoc
编译器的确使用了字段名称来生成特定于语言的对应名称。例如,Protobuf IDL 中的 oddA
和 small
名称在 Go 结构中分别成为字段 OddA
和 Small
。
键和它们的值都被编码,但是有一个重要的区别:一些数字值具有固定大小的 32 或 64 位的编码,而其他数字(包括消息标签)则是 varint
编码的,位数取决于整数的绝对值。例如,整数值 1 到 15 需要 8 位 varint
编码,而值 16 到 2047 需要 16 位。varint
编码在本质上与 UTF-8 编码类似(但细节不同),它偏爱较小的整数值而不是较大的整数值。(有关详细分析,请参见 Protobuf 编码指南)结果是,Protobuf 消息应该在字段中具有较小的整数值(如果可能),并且键数应尽可能少,但每个字段至少得有一个键。
下表 1 列出了 Protobuf 编码的要点:
编码 | 示例类型 | 长度 |
---|
via: https://opensource.com/article/19/10/protobuf-data-interchange
作者:Marty Kalin 选题:lujun9972 译者:wxy 校对:wxy
发表回复