在C++中使用Protocol Buffer
例子最好看这里
https://www.cnblogs.com/yinheyi/p/6081500.html
之前我翻译过两篇关于protocol buffers的文章:《protocol buffers简单介绍》《在Python中使用protocol buffers参考指南》
后来想想,现在自己是用在C++中的,不如再翻译一下,顺便看看自己的E文有没有提高。当然,查字典是少不了的。
翻译自:https://developers.google.com/protocol-buffers/docs/cpptutorial
Protocol Buffer基础: C++
这个教程是教你怎样在c++中用protocol buffers。通过一个简单的事例应用,教会了你:
- 在.proto文件中定义消息的形式。
- 使用protocol buffer编译器。
- 使用C++ protocol buffer API去读写消息。
这不是一个教你在C++中用protocol buffers的综合指南。更多内容,请看Protocol Buffer Language Guide,C++ API Reference, ,C++ Generated Code Guide和Encoding Reference。
为什么要使用Protocol Buffers?
我们将会使用一个可以读写通讯录的"address book"的简单应用作为例子。每一个在通讯录中的人会有一个名字,一个ID,一个邮件地址,一个通讯号码。
你如何序列化和检索类似的结构化数据?这里有几个方法可以解决这个问题:
- 可以用二进制形式来传送/保存原始数据。随着时间的过去,这不是一个好方法,接收/阅读代码都必须使用编译好的相同的内存布局,字节顺序等。 同时,因为积累的数据文件在原始形式和软件拷贝有严格的形式分布,所以形式很难扩展。
- 你可以用一种ad-hot方法编码数据到单独的字符串中——像编码4个ints“12:3:-23:67”。这是一个简单灵活的方法, 但它需要编写一下编码和解码的代码,和在解码时带来些微的运行成本。这最适合编码非常简单的数据。
- 用XML序列化数据。这个方法比较诱人,因为有XML容易阅读,而且许多语言已经有相应的XML库。这可是一个好的选择,如果你想共享你的数据到其它应用/工程。但是,XML密集的书写空间十分讨人厌,而且编码和解码也会为应用带来一些性能缺失。而且,阅读XML DOM树比阅读类中的字段复杂。
Protocol buffers是一种灵活,高效,自动化的方案,用于解决这类问题。有了protocol buffers, 你可以编写.proto
文件去存储你想描述的数据结构。Protocol buffers编译器会创造一个类来将protocol buffer数据自动编码和解析成高效的二进制格式。生成的类为protocol buffer中的字段提供了getters和setters接口,而且还把读写protocal的细节封装成了一个单元。最重要的,protocol buffer支持扩展格式,随着时间的推移,这样的代码仍然可以用旧的格式读取数据编码。
哪里可以找到事例代码
事例代码包含于源码“examples”目录中,在这里可以下载Download it here.
定义你的协议形式
创造通讯录之前,你需要定一下.proto
文件。写.proto
文件是十分简单的:你需要为每个你想序列化的数据结构写一个消息,并为每一个在消息里的字段指定名称和类型。这里有一个.proto
文件已经定义好你的消息addressbook.proto
.
就像你看到的那样,语法类似于c++或Java。我们浏览一下文件,看看它每一部分都做了什么。
这个.proto
文件始于一个包声明,这有助于防止不同的项目间的命名冲突。在C++里,你生成的类将被放置在一个与包名相匹配的名称空间中。
然后,看看消息定义。一个消息只是一些类型字段的集合。许多标准简单的数据类型可以作为字段的类型,包括bool,int32,float,double和string。你还可以进一步构建你的消息通过使用其他消息类型作为字段类型——在上面的例子中,Person消息包含了PhoneNumber消息,而AddressBook消息也包含了Person消息。你甚至可以定义消息类型嵌套在其他消息中——如你所见,PhoneNumber类型在Person定义。您还可以定义枚举类型,如果你想你的一个字段有一个预定义的值列表——在这里你想指定一个电话号码可以是MOBILE,HOME,WORK。
每个元素中的“= 1”,“= 2”标记符是识别使用的二进制编码字段的独特的“标签”。标签号码1-15要求的字节会比号码高的少点,所以你想优化一下,你可以使用这些标签用于常用的或repeated元素,把标签16和更高的标签给那些不常用或optional元素。每一个repeated的元素需要重新编码标记号,所以repeated字段特别适合优化。
每个字段必须声明以下修饰符:
required
: 必须提供给字段的值,否则,消息将被视为“未初始化”。如果libprotobuf
是编译成debug模式, 序列化一个未初始化的消息会产生一个断言错误。在优化构建时,这个检查会被跳过,消息将直接写入。然而,解析未初始化消息总是会失败的(通过解析方法返回false). 除此之外,这个required字段的行为就像一个可选的字段。optional
: 这个字段可能会或可能不会被设置。如果不设置一个optional字段的值,将会使用的默认值。对于简单的类型, 您可以指定自己的默认值,就像我们做过电话号码类型的例子那样。否则,会用系统提供的值:数值类型为0,字符类型为空, 布尔类型为false。对于嵌入式消息,默认值总是“默认实例”或“原型”这些消息里没有设置的字段。调用访问器来获得一个可选的(或要求)字段的值(没有被显式地设置),总是会返回字段的默认值的。repeated
: 这个字段可能会重复任意次(包括零)。repeated值的顺序会保存在protocol buffer中。repeated字段将补当作动态数组。
Required是永久的 你应该小心对标识字段为required。
如果在某一时刻你希望停止写或发一个required字段,要将问题考虑成改变字段为一个optional字段——老读者会考虑这个消息没有这个字段是不完整的,而且可能会抛弃或无意中删除他们。你应该考虑为您替代的buffers编写特定的自定义验证应用程序。一些在谷歌的工程师们已经得出了结论,使用required弊大于利,他们宁愿用optional和repeated。然而,这种观点并不普遍。
你会发现一个完整编写.prote文件的指南——包括所有可能的字段类型——在Protocol Buffer Language Guide里。不要去找类似的类继承的设定,protocol buffers不做这些的。
编译你的Protocol Buffers
现在你已经有了.proto文件,你需要做的下一件事是生成你需要读写AddressBook(和后面的Person和PhoneNumber)消息的类。要做到这一点,您需要运行protocol buffer编译器protoc编译你的.proto :
- 如果你还没有安装编译器,下载这个包download the package,看看README里的指示。
- 现在运行编译器, 指定源码目录(你的应用源码呆的地方——如果你不提供这个值,默认为现在的目录),目标目录(你希望生成代码的目录;通常会与源码目录相同),和你的
.proto
文件的路径。在这种情况下,你...:-
因为你想用C++的类,所以你要写protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
--cpp_out
选项——类似的选项对其他受支持的语言也有提供。
这个会在你指定目标目录下生成下列文件:
addressbook.pb.h,
声明你生成的类的头文件。addressbook.pb.cc
, 包含你的类的实现文件。
-
Protocol Buffer的API
让我们来看看一些生成的代码,看看编译器生成了哪些类和函数给你。如果你看了tutorial.pb.h,你可以看到,你有一个您在tutorial.proto中指定每个消息的类。 再看看Person类,您可以看到,编译嘎哈为每个字段生成了访问器。比如,对于name,id,email,和phone字段,你会有这些方法:
如您所见,getters有完整的名称包含小写的字段,setter函数始于set_。还有has_函数为每个单独的字段(required和optional)验证是否已经被设置。最后,每个字段都有一个clear_函数使该字段重置到空的状态。
然而,数字id字段只有基本的set,name和email字段因为是字符串,所以有一对额外的函数。——一个让你可以得到一个直接指向字符串的指针的mutable_ getter,和一个额外的setter。注意,您可以调用mutable_email(),即使email还没有设置;它将被自动初始化为一个空字符串。如果你有一个单独的消息字段在这个例子中,它还将会有一个mutable_函数但不会是一个set_函数。
Repeated字段也有一些特殊的函数——如果你看了下repeated phone字段的函数,你可以发现
- 检查repeated字段的_size(换句话说,有多少电话号码与这个Person有关)。
- 得到一个使用其索引指定的电话号码。
- 在指定的索引中更新现有的电话号码。
- 添加另一个您可以编辑的电话号码的消息(repeated标量类型有一个add_可以让你使用新值)。
想知道更多关于protocol编译器为特定字段生成的定义,请看C++ generated code reference。
枚举和嵌套类
生成的代码包含一个PhoneType enum,对应于你的.proto里的enum。你可以参考这个类型Person::PhoneType
和他的值如Person::MOBILE
,Person::HOME
,和Person::WORK
(实现细节是有点复杂,但是你不需要了解他们就可以使用enum)。
编译器也生成了一个你可以调用的嵌套类Person::PhoneNumber
。如果你看了代码,您可以看到,实际上被调用的“真正的”类是Person_PhoneNumber
,但是在typedef定义里面的Person允许你把它看成是一个嵌套类。唯一的情况,使这不同的是,如果你想提前声明另一个文件中的类——你不能在c++中提前声明嵌套类型,但是你可以提前声明Person_PhoneNumber。
标准消息的函数
每个消息类还包含其他一些方法,让你检查或操纵整个消息,包括:
bool IsInitialized() const;
: 检查是否所有required字段都已经设置了。string DebugString() const;
: 返回一个可读的消息,特别是用于调试。void CopyFrom(const Person& from);
: 用给定消息的值覆盖消息。void Clear();
: 清除所有元素退回空的状态。
这些和下面一节中描述的I / O方法实现消息接口由所有c++ protocol buffer类共享。更多内容,请参阅complete API documentation for Message
。
解析和序列化
最后,每个protocol buffer类都有函数写入和读取您选择的类型使用protocol buffer二进制格式的消息。这些包括:
bool SerializeToString(string* output) const;
:序列化消息并存储给定字符串的字节。 注意这个字节是二进制的,而不是文本;我们只使用string类作为一个方便的容器。bool ParseFromString(const string& data);
:从给定字符串解析消息。bool SerializeToOstream(ostream* output) const;
:将消息写入给定的c++ ostream上。bool ParseFromIstream(istream* input);
:从给定的c++ istream上解析消息。
这些只是解析和序列化提供的几个选项。可以在这里Message
API reference看完整的列表。
Protocol Buffers and O-O 设计 Protocol buffer类基本上是哑数据持有者(如在c++中的结构);他们不会在对象模型上做一等公民。如果你想为生成的类添加更多的行为,最好的方法是将生成的protocol buffer类封装为一个应用程序的特定的类。 封装protocol buffers也是一个好主意,如果你不想控制.proto文件的设计(就是说你重用了另一个项目的.proto文件)。在这种情况下,你可以使用封装好的类制作更适合你的独特环境的应用程序的接口:隐藏一些数据和函数,暴露便利的函数,等等。你绝不应该通过继承生成的类而添加行为。这将打破内部的机制和没有良好的面向对象的实践。
写一个消息
现在,让我们试着用你的protocol buffer类。第一件事你想让你的通讯录应用程序能够做的就是写入个人信息到你的通讯录文件。要做到这一点,您需要创建和填充你的protocol buffer类的实例,然后把它们写到输出流。
这是从一个文件中读取一个AddressBook的程序,基于用户输入添加了一个新的Person,并把新的AddressBook再写回文件中。直接调用或引用protocol编译器生成的代码的部分已经高亮显示了。
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// This function fills in a Person message based on user input.
void PromptForAddress(tutorial::Person* person) {
cout << "Enter person ID number: ";
int id;
cin >> id;
person->set_id(id);
cin.ignore(256, '\n');
cout << "Enter name: ";
getline(cin, *person->mutable_name());
cout << "Enter email address (blank for none): ";
string email;
getline(cin, email);
if (!email.empty()) {
person->set_email(email);
}
while (true) {
cout << "Enter a phone number (or leave blank to finish): ";
string number;
getline(cin, number);
if (number.empty()) {
break;
}
tutorial::Person::PhoneNumber* phone_number = person->add_phone();
phone_number->set_number(number);
cout << "Is this a mobile, home, or work phone? ";
string type;
getline(cin, type);
if (type == "mobile") {
phone_number->set_type(tutorial::Person::MOBILE);
} else if (type == "home") {
phone_number->set_type(tutorial::Person::HOME);
} else if (type == "work") {
phone_number->set_type(tutorial::Person::WORK);
} else {
cout << "Unknown phone type. Using default." << endl;
}
}
}
// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
int main(int argc, char* argv[]) {
// Verify that the version of the library that we linked against is
// compatible with the version of the headers we compiled against.
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
// Read the existing address book.
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cout << argv[1] << ": File not found. Creating a new file." << endl;
} else if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
// Add an address.
PromptForAddress(address_book.add_person());
{
// Write the new address book back to disk.
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!address_book.SerializeToOstream(&output)) {
cerr << "Failed to write address book." << endl;
return -1;
}
}
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
<iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// This function fills in a Person message based on user input.
void PromptForAddress(tutorial::Person* person) {
cout << "Enter person ID number: ";
int id;
cin >> id;
person->set_id(id);
cin.ignore(256, '\n');
cout << "Enter name: ";
getline(cin, *person->mutable_name());
cout << "Enter email address (blank for none): ";
string email;
getline(cin, email);
if (!email.empty()) {
person->set_email(email);
}
while (true) {
cout << "Enter a phone number (or leave blank to finish): ";
string number;
getline(cin, number);
if (number.empty()) {
break;
}
tutorial::Person::PhoneNumber* phone_number = person->add_phone();
phone_number->set_number(number);
cout << "Is this a mobile, home, or work phone? ";
string type;
getline(cin, type);
if (type == "mobile") {
phone_number->set_type(tutorial::Person::MOBILE);
} else if (type == "home") {
phone_number->set_type(tutorial::Person::HOME);
} else if (type == "work") {
phone_number->set_type(tutorial::Person::WORK);
} else {
cout << "Unknown phone type. Using default." << endl;
}
}
}
// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
int main(int argc, char* argv[]) {
// Verify that the version of the library that we linked against is
// compatible with the version of the headers we compiled against.
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
// Read the existing address book.
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cout << argv[1] << ": File not found. Creating a new file." << endl;
} else if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
// Add an address.
PromptForAddress(address_book.add_person());
{
// Write the new address book back to disk.
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!address_book.SerializeToOstream(&output)) {
cerr << "Failed to write address book." << endl;
return -1;
}
}
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
注意这个GOOGLE_PROTOBUF_VERIFY_VERSION宏。这是一种很好的做法——尽管不是严格必需的——使用C++ Protocol Buffer库之前执行这个宏。它验证了你有没有意外地用了一个版本不兼容的库的头文件。如果检测到一个版本不匹配,则程序将终止。注意,每个.pb.cc文件在启动时会自动嵌入这个宏。
还要注意一下在程序结尾调用的ShutdownProtobufLibrary()。这都是为了删除Protocol Buffer库分配的所有的全局对象。这对大多数程序是不必要的,因为这个进程是要退出的,操作系统将会负责回收所有的内存。然而,如果你使用一个内存泄漏检查器,会要求每一个对象被释放,或者如果您正在编写一个可能多次被一个进程加载或卸载的库,那么你可能会想强迫Protocol Buffers去清理这一切。
读一个消息
当然,通讯录不会有多大用处,如果你从中不能得到任何信息!这个例子会读取由上面的例子创造的文件并打印里面的所有信息。
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// Iterates though all people in the AddressBook and prints info about them.
void ListPeople(const tutorial::AddressBook& address_book) {
for (int i = 0; i < address_book.person_size(); i++) {
const tutorial::Person& person = address_book.person(i);
cout << "Person ID: " << person.id() << endl;
cout << " Name: " << person.name() << endl;
if (person.has_email()) {
cout << " E-mail address: " << person.email() << endl;
}
for (int j = 0; j < person.phone_size(); j++) {
const tutorial::Person::PhoneNumber& phone_number = person.phone(j);
switch (phone_number.type()) {
case tutorial::Person::MOBILE:
cout << " Mobile phone #: ";
break;
case tutorial::Person::HOME:
cout << " Home phone #: ";
break;
case tutorial::Person::WORK:
cout << " Work phone #: ";
break;
}
cout << phone_number.number() << endl;
}
}
}
// Main function: Reads the entire address book from a file and prints all
// the information inside.
int main(int argc, char* argv[]) {
// Verify that the version of the library that we linked against is
// compatible with the version of the headers we compiled against.
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
// Read the existing address book.
fstream input(argv[1], ios::in | ios::binary);
if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
ListPeople(address_book);
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
<iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// Iterates though all people in the AddressBook and prints info about them.
void ListPeople(const tutorial::AddressBook& address_book) {
for (int i = 0; i < address_book.person_size(); i++) {
const tutorial::Person& person = address_book.person(i);
cout << "Person ID: " << person.id() << endl;
cout << " Name: " << person.name() << endl;
if (person.has_email()) {
cout << " E-mail address: " << person.email() << endl;
}
for (int j = 0; j < person.phone_size(); j++) {
const tutorial::Person::PhoneNumber& phone_number = person.phone(j);
switch (phone_number.type()) {
case tutorial::Person::MOBILE:
cout << " Mobile phone #: ";
break;
case tutorial::Person::HOME:
cout << " Home phone #: ";
break;
case tutorial::Person::WORK:
cout << " Work phone #: ";
break;
}
cout << phone_number.number() << endl;
}
}
}
// Main function: Reads the entire address book from a file and prints all
// the information inside.
int main(int argc, char* argv[]) {
// Verify that the version of the library that we linked against is
// compatible with the version of the headers we compiled against.
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
// Read the existing address book.
fstream input(argv[1], ios::in | ios::binary);
if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
ListPeople(address_book);
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
扩展你的Protocol Buffer
早晚,在你发布代码并使用protocol buffer后,你必定会想“改进”protocol buffer的定义。如果你想要你新的buffers向后兼容, 而你旧的buffers向后兼容——你几乎可以肯定你想要这样——那么你需要遵守一些规则。在新版本的protocol buffer里:
- 你不能改变任何现有的数据字段标签。
- 你不能添加或删除任何required字段。
- 你可以删除optional或repeated字段。
- 你可以添加新的optional或repeated字段但必须使用新的标签号码(换句话说,只能使用那些没有在这个protocol buffer用过的数字标签,即使那些删除了的字段的数字标签也不能使用)。
(这些规则有一些some exceptions,但他们很少使用。)
如果你遵循这些规则,旧代码会很乐意读新消息和简单地忽略一些新的字段。对于旧的代码,那些删除了的optional字段会有自己简单的默认值,那些删除了的repeated字段会为空。新代码还会显然地读旧的消息。然而,请记住,新的optional字段将不会出现在旧的消息中,所以你需要用has_明确检查是否已经设置了, 或者在你的.proto文件中在数字标签后通过[default = value]提供一个可读的默认值。如果没有为一个optional元素指定默认值,会使用一个特定的类型的默认值: 对于字符串,默认值为空字符串。对于布尔值,默认值是false。对于数值类型,默认值是0。还要注意,如果你添加了一个新的repeated字段,你的新代码将无法判断它是空的(通过新代码)或从不设置(通过旧代码)因为这里没有has_标记。
优化建议
c++ Protocol Buffers库做了非常大量的优化。 然而,正确地使用也可以提升一下性能。这里有一些小贴士,最大限度地提升库的速度:
- 在可能的情况下重用消息对象。消息尽量保持在任何内存内分配重用,即使他们已经被清除。因此,如果你连续处理许多有相同的类型和类似的结构的消息,这是一个好主意每次分配内存时重用相同的消息对象。然而,在你的消息在“形状”或你偶尔构造一个比平时大得多的消息时,对象会随着时间的推移变得臃肿。您应该通过调用SpaceUsed函数来监视你的消息对象的大小,一旦太大就删除它们。
- 你的系统的内存分配器可能不会为从多个线程中分配的大量的小对象优化。试试Google's tcmalloc。
高级用法
Protocol buffers已经使用了超级简单的访问器和序列化器。看看这里C++ API reference有什么你还可以浏览的。
协议消息类提供的一个关键特性是反射。 您可以遍历消息的字段和操纵他们的值,无需编写对任何特定消息类型的代码。使用反射的一个非常有用的方法是将协议消息转换成其他编码,如XML或JSON。更高级的使用反射是可能会找到相同类型的两个消息之间的区别,或者开发一种“协议消息的正则表达式”,您可以编写表达式匹配特定的消息内容。如果你使用你的想象力,你可以使用Protocol Buffers超乎你的想像!
可以在这里查查这个反射Message::Reflection
interface。