protobuf使用-概述-总结1

Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

简介

什么是 Google Protocol Buffer? 假如您在网上搜索,应该会得到类似这样的文字介绍:

Google Protocol Buffer(简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。

Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

  • 语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
  • 高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
  • 扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序

序列化[1]:将结构数据或对象转换成能够被存储和传输(例如网络传输)的格式,同时应当要保证这个序列化结果在之后(可能在另一个计算环境中)能够被重建回原来的结构数据或对象。
更为详尽的介绍可参阅 维基百科。
类比于 XML[2]:这里主要指在数据通信和数据存储应用场景中序列化方面的类比,但个人认为 XML 作为一种扩展标记语言和 ProtoBuf 还是有着本质区别的。

  • 一个简单的例子
    安装 Google Protocol Buffer
    http://code.google.com/p/protobuf/downloads/list
    https://github.com/protocolbuffers/protobuf/releases
    下载 Protobuf 的源代码。然后解压编译安装便可以使用它了。
    解压后,打开protobuf-3.8.0\README.md 文件,里边明确指出了c++的安装参考文件:
    安装步骤如下所示:

$ tar -xzf protobuf-2.1.0.tar.gz
$ cd protobuf-2.1.0
$ ./configure --prefix=$INSTALL_DIR
$ make
$ make check
$ make install

关于简单例子的描述
我打算使用 Protobuf 和 C++ 开发一个十分简单的例子程序。
该程序由两部分组成。第一部分被称为 Writer,第二部分叫做 Reader。
Writer 负责将一些结构化的数据写入一个磁盘文件,Reader 则负责从该磁盘文件中读取结构化数据并打印到屏幕上。
准备用于演示的结构化数据是 HelloWorld,它包含两个基本数据:

ID,为一个整数类型的数据
Str,这是一个字符串

  • 书写 .proto 文件
    首先我们需要编写一个 proto 文件,定义我们程序中需要处理的结构化数据,在 protobuf 的术语中,结构化数据被称为 Message。proto 文件非常类似 java 或者 C 语言的数据定义。代码清单 1 显示了例子应用中的 proto 文件内容。
//清单 1. proto 文件
package lm;
message helloworld
{
   required int32     id = 1;  // ID
   required string    str = 2;  // str
   optional int32     opt = 3;  //optional field
}

一个比较好的习惯是认真对待 proto 文件的文件名。比如将命名规则定于如下:

packageName.MessageName.proto

编译 .proto 文件

写好 proto 文件之后就可以用 Protobuf 编译器将该文件编译成目标语言了。本例中我们将使用 C++。
假设您的 proto 文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一个目录下,则可以使用如下命令:

protoc -I= S R C D I R − − c p p o u t = SRC_DIR --cpp_out= SRCDIRcppout=DST_DIR $SRC_DIR/addressbook.proto

命令将生成两个文件:
lm.helloworld.pb.h , 定义了 C++ 类的头文件
lm.helloworld.pb.cc , C++ 类的实现文件
在生成的头文件中,定义了一个 C++ 类 helloworld,后面的 Writer 和 Reader 将使用这个类来对消息进行操作。诸如对消息的成员进行赋值,将消息序列化等等都有相应的方法。

编写 writer 和 Reader

如前所述,Writer 将把一个结构化数据写入磁盘,以便其他人来读取。假如我们不使用 Protobuf,其实也有许多的选择。一个可能的方法是将数据转换为字符串,然后将字符串写入磁盘。转换为字符串的方法可以使用 sprintf(),这非常简单。数字 123 可以变成字符串”123”。
这样做似乎没有什么不妥,但是仔细考虑一下就会发现,这样的做法对写 Reader 的那个人的要求比较高,Reader 的作者必须了 Writer 的细节。比如”123”可以是单个数字 123,但也可以是三个数字 1,2 和 3,等等。这么说来,我们还必须让 Writer 定义一种分隔符一样的字符,以便 Reader 可以正确读取。但分隔符也许还会引起其他的什么问题。最后我们发现一个简单的 Helloworld 也需要写许多处理消息格式的代码。
如果使用 Protobuf,那么这些细节就可以不需要应用程序来考虑了。
使用 Protobuf,Writer 的工作很简单,需要处理的结构化数据由 .proto 文件描述,经过上一节中的编译过程后,该数据化结构对应了一个 C++ 的类,并定义在 lm.helloworld.pb.h 中。对于本例,类名为 lm::helloworld。
Writer 需要 include 该头文件,然后便可以使用这个类了。
现在,在 Writer 代码中,将要存入磁盘的结构化数据由一个 lm::helloworld 类的对象表示,它提供了一系列的 get/set 函数用来修改和读取结构化数据中的数据成员,或者叫 field。
当我们需要将该结构化数据保存到磁盘上时,类 lm::helloworld 已经提供相应的方法来把一个复杂的数据变成一个字节序列,我们可以将这个字节序列写入磁盘。
对于想要读取这个数据的程序来说,也只需要使用类 lm::helloworld 的相应反序列化方法来将这个字节序列重新转换会结构化数据。这同我们开始时那个“123”的想法类似,不过 Protobuf 想的远远比我们那个粗糙的字符串转换要全面,因此,我们不如放心将这类事情交给 Protobuf 吧。
程序清单 2 演示了 Writer 的主要代码,您一定会觉得很简单吧

#include "lm.helloworld.pb.h"
...
int main(void)
{
 lm::helloworld msg1;
 msg1.set_id(101);
 msg1.set_str(“hello”);
 // Write the new address book back to disk.
 fstream output("./log", ios::out | ios::trunc | ios::binary);
 if (!msg1.SerializeToOstream(&output)) {
     cerr << "Failed to write msg." << endl;
     return -1;
 }
 return 0;
}

Msg1 是一个 helloworld 类的对象,set_id() 用来设置 id 的值。SerializeToOstream 将对象序列化后写入一个 fstream 流。
代码清单 3 列出了 reader 的主要代码。

#include "lm.helloworld.pb.h"
...
void ListMsg(const lm::helloworld & msg) {
 cout << msg.id() << endl;
 cout << msg.str() << endl;
}
int main(int argc, char* argv[]) {
 lm::helloworld msg1;
 {
   fstream input("./log", ios::in | ios::binary);
   if (!msg1.ParseFromIstream(&input)) {
     cerr << "Failed to parse address book." << endl;
     return -1;
   }
 }
 ListMsg(msg1);
 ...
}

同样,Reader 声明类 helloworld 的对象 msg1,然后利用 ParseFromIstream 从一个 fstream 流中读取信息并反序列化。此后,ListMsg 中采用 get 方法读取消息的内部信息,并进行打印输出操作。

  • 运行结果
    运行 Writer 和 Reader 的结果如下:

writer
reader
101
Hello

这个例子本身并无意义,但只要您稍加修改就可以将它变成更加有用的程序。比如将磁盘替换为网络 socket,那么就可以实现基于网络的数据交换任务。而存储和交换正是 Protobuf 最有效的应用领域。


https://www.jianshu.com/u/38b24d657925
https://www.jianshu.com/p/a24c88c0526a

  • 它是如何工作的
    你可以通过在 .proto 文件中定义 protocol buffer message 类型,来指定你想如何对序列化信息进行结构化。每一个 protocol buffer message 是一个信息的小逻辑记录,包含了一系列的 name-value 对。这里有一个非常基础的 .proto 文件样例,它定义了一个包含 “person” 相关信息的 message:
message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

正如你所见,message 格式很简单 - 每种 message 类型都有一个或多个具有唯一编号的字段,每个字段都有一个名称和一个值类型,其中值类型可以是数字(整数或浮点数),布尔值,字符串,原始字节,甚至(如上例所示)其它 protocol buffer message 类型,这意味着允许你分层次地构建数据。你可以指定 optional 字段,required 字段和 repeated 字段。 你可以在 Protocol Buffer 语言指南 (https://www.jianshu.com/p/6f68fb2c7d19)中找到有关编写 .proto 文件的更多信息。

译者注:
proto3 已舍弃 required 字段,optional 字段也无法显示使用(因为缺省默认就设置为 optional)

一旦定义了 messages,就可以在 .proto 文件上运行 protocol buffer 编译器来生成指定语言的数据访问类。这些类为每个字段提供了简单的访问器(如 name()和 set_name()),以及将整个结构序列化为原始字节和解析原始字节的方法 - 例如,如果你选择的语言是 C++,则运行编译器上面的例子将生成一个名为 Person 的类。然后,你可以在应用程序中使用此类来填充,序列化和检索 Person 的 messages。于是你可以写一些这样的代码:

Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);

之后,你可以重新读取解析你的 message

fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

你可以在 message 格式中添加新字段,而不会破坏向后兼容性;旧的二进制文件在解析时只是忽略新字段。因此,如果你的通信协议使用 protocol buffers 作为其数据格式,则可以扩展协议而无需担心破坏现有代码。
你可以在 API 参考部分(https://developers.google.com/protocol-buffers/docs/reference/overview) 中找到使用生成的 protocol buffer 代码的完整参考,你可以在 协议缓冲区编码(https://www.jianshu.com/p/82ff31c6adc6) 中找到更多关于如何对 protocol buffer messages 进行编码的信息。


高级应用话题

更复杂的 Message
到这里为止,我们只给出了一个简单的没有任何用处的例子。在实际应用中,人们往往需要定义更加复杂的 Message。我们用“复杂”这个词,不仅仅是指从个数上说有更多的 fields 或者更多类型的 fields,而是指更加复杂的数据结构:
嵌套 Message
嵌套是一个神奇的概念,一旦拥有嵌套能力,消息的表达能力就会非常强大。
代码清单 4 给出一个嵌套 Message 的例子。
清单 4. 嵌套 Message 的例子

message Person {
 required string name = 1;
 required int32 id = 2;        // Unique ID number for this person.
 optional string email = 3;

 enum PhoneType {
   MOBILE = 0;
   HOME = 1;
   WORK = 2;
 }

 message PhoneNumber {
   required string number = 1;
   optional PhoneType type = 2 [default = HOME];
 }
 repeated PhoneNumber phone = 4;
}

在 Message Person 中,定义了嵌套消息 PhoneNumber,并用来定义 Person 消息中的 phone 域。这使得人们可以定义更加复杂的数据结构。
Import Message

在一个 .proto 文件中,还可以用 Import 关键字引入在其他 .proto 文件中定义的消息,这可以称做 Import Message,或者 Dependency Message。
比如下例:
清单 5. 代码

import common.header;

message youMsg{
 required common.info_header header = 1;
 required string youPrivateData = 2;
}

Import Message

在一个 .proto 文件中,还可以用 Import 关键字引入在其他 .proto 文件中定义的消息,这可以称做 Import Message,或者 Dependency Message。
比如下例:
清单 5. 代码

import common.header;

message youMsg{
 required common.info_header header = 1;
 required string youPrivateData = 2;
}

其中, common.info_header 定义在 common.header 包内。
Import Message 的用处主要在于提供了方便的代码管理机制,类似 C 语言中的头文件。您可以将一些公用的 Message 定义在一个 package 中,然后在别的 .proto 文件中引入该 package,进而使用其中的消息定义。
Google Protocol Buffer 可以很好地支持嵌套 Message 和引入 Message,从而让定义复杂的数据结构的工作变得非常轻松愉快。

动态编译

一般情况下,使用 Protobuf 的人们都会先写好 .proto 文件,再用 Protobuf 编译器生成目标语言所需要的源代码文件。将这些生成的代码和应用程序一起编译。

可是在某且情况下,人们无法预先知道 .proto 文件,他们需要动态处理一些未知的 .proto 文件。比如一个通用的消息转发中间件,它不可能预知需要处理怎样的消息。这需要动态编译 .proto 文件,并使用其中的 Message。

Protobuf 提供了 google::protobuf::compiler 包来完成动态编译的功能。主要的类叫做 importer,定义在 importer.h 中。使用 Importer 非常简单,下图展示了与 Import 和其它几个重要的类的关系。
在这里插入图片描述
Import 类对象中包含三个主要的对象,分别为处理错误的 MultiFileErrorCollector 类,定义 .proto 文件源目录的 SourceTree 类。
下面还是通过实例说明这些类的关系和使用吧。
对于给定的 proto 文件,比如 lm.helloworld.proto,在程序中动态编译它只需要很少的一些代码。如代码清单 6 所示。
清单 6. 代码

google::protobuf::compiler::MultiFileErrorCollector errorCollector;
google::protobuf::compiler::DiskSourceTree sourceTree;

google::protobuf::compiler::Importer importer(&sourceTree, &errorCollector);
sourceTree.MapPath("", protosrc);

importer.import(“lm.helloworld.proto”);

首先构造一个 importer 对象。构造函数需要两个入口参数,一个是 source Tree 对象,该对象指定了存放 .proto 文件的源目录。第二个参数是一个 error collector 对象,该对象有一个 AddError 方法,用来处理解析 .proto 文件时遇到的语法错误。
之后,需要动态编译一个 .proto 文件时,只需调用 importer 对象的 import 方法。非常简单。

那么我们如何使用动态编译后的 Message 呢?我们需要首先了解几个其他的类

Package google::protobuf::compiler 中提供了以下几个类,用来表示一个 .proto 文件中定义的 message,以及 Message 中的 field,如图所示。
在这里插入图片描述
类 FileDescriptor 表示一个编译后的 .proto 文件;类 Descriptor 对应该文件中的一个 Message;类 FieldDescriptor 描述一个 Message 中的一个具体 Field。
比如编译完 lm.helloworld.proto 之后,可以通过如下代码得到 lm.helloworld.id 的定义:
清单 7. 得到 lm.helloworld.id 的定义的代码

const protobuf::Descriptor *desc =
   importer_.pool()->FindMessageTypeByName(“lm.helloworld”);
const protobuf::FieldDescriptor* field =
   desc->pool()->FindFileByName (“id”);

通过 Descriptor,FieldDescriptor 的各种方法和属性,应用程序可以获得各种关于 Message 定义的信息。比如通过 field->name() 得到 field 的名字。这样,您就可以使用一个动态定义的消息了。

编写新的 proto 编译器

随 Google Protocol Buffer 源代码一起发布的编译器 protoc 支持 3 种编程语言:C++,java 和 Python。但使用 Google Protocol Buffer 的 Compiler 包,您可以开发出支持其他语言的新的编译器。

类 CommandLineInterface 封装了 protoc 编译器的前端,包括命令行参数的解析,proto 文件的编译等功能。您所需要做的是实现类 CodeGenerator 的派生类,实现诸如代码生成等后端工作:

程序的大体框架如图所示
在这里插入图片描述
在 main() 函数内,生成 CommandLineInterface 的对象 cli,调用其 RegisterGenerator() 方法将新语言的后端代码生成器 yourG 对象注册给 cli 对象。然后调用 cli 的 Run() 方法即可。

这样生成的编译器和 protoc 的使用方法相同,接受同样的命令行参数,cli 将对用户输入的 .proto 进行词法语法等分析工作,最终生成一个语法树。该树的结构如图所示。
在这里插入图片描述
其根节点为一个 FileDescriptor 对象(请参考“动态编译”一节),并作为输入参数被传入 yourG 的 Generator() 方法。在这个方法内,您可以遍历语法树,然后生成对应的您所需要的代码。简单说来,要想实现一个新的 compiler,您只需要写一个 main 函数,和一个实现了方法 Generator() 的派生类即可。

Google Protocol Buffer 的 Encoding
Protobuf 序列化后所生成的二进制消息非常紧凑,这得益于 Protobuf 采用的非常巧妙的 Encoding 方法。

参看:http://www.yeolar.com/note/2015/04/30/google-protocol-buffer/#id31

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值