目录html
这篇教程提供了一个面向 C++ 程序员、关于 protocol buffers
的基础介绍。经过建立一个简单的示例应用程序,它将向咱们展现:ios
.proto
文件中定义消息格式protocol buffer
编译器C++ protocol buffer API
读写消息这不是一个关于使用 C++ protocol buffers 的全面指南。要获取更详细的信息,请参考 Protocol Buffer Language Guide 和 Encoding Reference。程序员
咱们接下来要使用的例子是一个很是简单的"地址簿"应用程序,它能从文件中读取联系人详细信息。地址簿中的每个人都有一个名字,ID,邮件地址和联系电话。正则表达式
如何序列化和获取结构化的数据?这里有几种解决方案:api
以二进制形式发送/接收原生的内存数据结构。一般,这是一种脆弱的方法,由于接收/读取代码的编译必须基于彻底相同的内存布局、大小端等等。同时,当文件增长时,原始格式数据会随着与该格式相连的软件拷贝而迅速扩散,这将很难扩展文件格式。数组
你能够创造一种 ad-hoc
方法,将数据项编码为一个字符串——好比将 4 个整数编码为 "12:3:-23:67"。虽然它须要编写一次性的编码和解码代码且解码须要耗费小的运行时成本,但这是一种简单灵活的方法。这最适合编码很是简单的数据。数据结构
序列化数据为 XML
。这种方法是很是吸引人的,由于 XML
是一种适合人阅读的格式,而且有为许多语言开发的库。若是你想与其余程序和项目共享数据,这多是一种不错的选择。然而,众所周知,XML
是空间密集型的,且在编码和解码时,它对程序会形成巨大的性能损失。同时,使用 XML DOM 树被认为比操做一个类的简单字段更加复杂。多线程
Protocol buffers
是针对这个问题的一种灵活、高效、自动化的解决方案。使用 Protocol buffers
,你须要写一个 .proto
说明,用于描述你所但愿存储的数据结构。利用 .proto
文件,protocol buffer 编译器能够建立一个类,用于实现自动化编码和解码高效的二进制格式的 protocol buffer 数据。产生的类提供了构造 protocol buffer
的字段的 getters 和 setters,而且做为一个单元,关注读写 protocol buffer
的细节。重要的是,protocol buffer
格式支持扩展格式,代码仍然能够读取以旧格式编码的数据。ide
示例代码被包含于源代码包,位于 "examples" 文件夹。在这下载代码。函数
为了建立本身的地址簿应用程序,你须要从 .proto
开始。.proto
文件中的定义很简单:为你所须要序列化的数据结构添加一个消息(message),而后为消息中的每个字段指定一个名字和类型。这里是定义你消息的 .proto
文件,addressbook.proto
。
package tutorial; 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 AddressBook { repeated Person person = 1; }
如你所见,其语法相似于 C++ 或 Java。咱们开始看看文件的每一部份内容作了什么。
.proto
文件以一个 package 声明开始,这能够避免不一样项目的命名冲突。在 C++,你生成的类会被置于与 package 名字同样的命名空间。
下一步,你须要定义消息(message)。消息只是一个包含一系列类型字段的集合。大多标准简单数据类型是能够做为字段类型的,包括 bool
、int32
、float
、double
和 string
。你也能够经过使用其余消息类型做为字段类型,将更多的数据结构添加到你的消息中——在以上的示例,Person
消息包含了 PhoneNumber
消息,同时 AddressBook
消息包含 Person
消息。你甚至能够定义嵌套在其余消息内的消息类型——如你所见,PhoneNumber
类型定义于 Person
内部。若是你想要其中某一个字段拥有预约义值列表中的某个值,你也能够定义 enum
类型——这儿你想指定一个电话号码能够是 MOBILE
、HOME
或 WORK
中的某一个。
每个元素上的 “=1”、"=2" 标记肯定了用于二进制编码的惟一"标签"(tag)。标签数字 1-15 的编码比更大的数字少须要一个字节,所以做为一种优化,你能够将这些标签用于常用或 repeated 元素,剩下 16 以及更高的标签用于非常用或 optional 元素。每个 repeated 字段的元素须要从新编码标签数字,所以 repeated 字段对于这优化是一个特别好的候选者。
每个字段必须使用下面的修饰符加以标注:
required:必须提供字段的值,不然消息会被认为是 "未初始化的"(uninitialized)。若是 libprotobuf
以 debug 模式编译,序列化未初始化的消息将引发一个断言失败。以优化形式构建,将会跳过检查,而且不管如何都会写入消息。然而,解析未初始化的消息老是会失败(经过 parse 方法返回 false
)。除此以外,一个 required 字段的表现与 optional 字段彻底同样。
optional:字段可能会被设置,也可能不会。若是一个 optional 字段没被设置,它将使用默认值。对于简单类型,你能够指定你本身的默认值,正如例子中咱们对电话号码的 type
同样,不然使用系统默认值:数字类型为 0、字符串为空字符串、布尔值为 false。对于嵌套消息,默认值总为消息的"默认实例"或"原型",它的全部字段都没被设置。调用 accessor 来获取一个没有显式设置的 optional(或 required) 字段的值老是返回字段的默认值。
repeated:字段能够重复任意次数(包括 0)。repeated 值的顺序会被保存于 protocol buffer。能够将 repeated 字段想象为动态大小的数组。
你能够查找关于编写 .proto
文件的完整指导——包括全部可能的字段类型——在 Protocol Buffer Language Guide。不要在这里面查找与类继承类似的特性,由于 protocol buffers 不会作这些。
required 是永久性的,在把一个字段标识为 required 的时候,你应该特别当心。若是在某些状况下你不想写入或者发送一个 required 的字段,那么将该字段更改成 optional 可能会遇到问题——旧版本的读者(译者注:即读取、解析旧版本 Protocol Buffer 消息的一方)会认为不含该字段的消息是不完整的,从而有可能会拒绝解析。在这种状况下,你应该考虑编写特别针对于应用程序的、自定义的消息校验函数。Google 的一些工程师得出了一个结论:使用 required 弊多于利;他们更愿意使用 optional 和 repeated 而不是 required。固然,这个观点并不具备广泛性。
既然你有了一个 .proto
,那你须要作的下一件事就是生成一个将用于读写 AddressBook
消息的类(从而包括 Person
和 PhoneNumber
)。为了作到这样,你须要在你的 .proto
上运行 protocol buffer 编译器 protoc
:
$SRC_DIR
相同),而且你的 .proto
路径。在此示例,你...:protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
由于你想要 C++ 的类,因此你使用了 --cpp_out
选项——也为其余支持的语言提供了相似选项。
在你指定的目标文件夹,将生成如下的文件:
addressbook.pb.h
,声明你生成类的头文件。addressbook.pb.cc
,包含你的类的实现。让咱们看看生成的一些代码,了解一下编译器为你建立了什么类和函数。若是你查看 tutorial.pb.h
,你能够看到有一个在 tutorial.proto
中指定全部消息的类。关注 Person
类,能够看到编译器为每一个字段生成了读写函数(accessors)。例如,对于 name
、id
、email
和 phone
字段,有下面这些方法:
// name inline bool has_name() const; inline void clear_name(); inline const ::std::string& name() const; inline void set_name(const ::std::string& value); inline void set_name(const char* value); inline ::std::string* mutable_name(); // id inline bool has_id() const; inline void clear_id(); inline int32_t id() const; inline void set_id(int32_t value); // email inline bool has_email() const; inline void clear_email(); inline const ::std::string& email() const; inline void set_email(const ::std::string& value); inline void set_email(const char* value); inline ::std::string* mutable_email(); // phone inline int phone_size() const; inline void clear_phone(); inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const; inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone(); inline const ::tutorial::Person_PhoneNumber& phone(int index) const; inline ::tutorial::Person_PhoneNumber* mutable_phone(int index); inline ::tutorial::Person_PhoneNumber* add_phone();
正如你所见到,getters 的名字与字段的小写名字彻底同样,而且 setter 方法以 set_ 开头。同时每一个单一(singular)(required 或 optional)字段都有 has_
方法,该方法在字段被设置了值的状况下返回 true。最后,全部字段都有一个 clear_
方法,用以清除字段到空(empty)状态。
数字 id
字段仅有上述的基本读写函数集合(accessors),而 name
和 email
字段有两个额外的方法,由于它们是字符串——一个是能够得到字符串直接指针的mutable_
getter ,另外一个为额外的 setter。注意,尽管 email
还没被设置(set),你也能够调用 mutable_email
;由于 email
会被自动地初始化为空字符串。在本例中,若是你有一个单一的(required 或 optional)消息字段,它会有一个 mutable_
方法,而没有 set_
方法。
repeated 字段也有一些特殊的方法——若是你看看 repeated phone
字段的方法,你能够看到:
_size
(也就是说,与 Person
相关的电话号码的个数)add_
方法,用于传入新的值)为了获取 protocol 编译器为全部字段定义生成的方法的信息,能够查看 C++ generated code reference。
与 .proto
的枚举相对应,生成的代码包含了一个 PhoneType
枚举。你能够经过 Person::PhoneType
引用这个类型,经过 Person::MOBILE
、Person::HOME
和 Person::WORK
引用它的值。(实现细节有点复杂,可是你无须了解它们而能够直接使用)
编译器也生成了一个 Person::PhoneNumber
的嵌套类。若是你查看代码,你能够发现真正的类型为 Person_PhoneNumber
,但它经过在 Person
内部使用 typedef 定义,使你能够把 Person_PhoneNumber
当成嵌套类。惟一产生影响的一个例子是,若是你想要在其余文件前置声明该类——在 C++ 中你不能前置声明嵌套类,可是你能够前置声明 Person_PhoneNumber
。
全部的消息方法都包含了许多别的方法,用于检查和操做整个消息,包括:
bool IsInitialized() const;
:检查是否全部 required
字段已经被设置。string DebugString() const;
:返回人类可读的消息表示,对 debug 特别有用。void CopyFrom(const Person& from);
:使用给定的值重写消息。void Clear();
:清除全部元素为空(empty)的状态。上面这些方法以及下一节要讲的 I/O 方法实现了被全部 C++ protocol buffer 类共享的消息(Message)接口。为了获取更多信息,请查看 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 和 面向对象设计的 Protocol buffer 类一般只是纯粹的数据存储器(像 C++ 中的结构体);它们在对象模型中并非一等公民。若是你想向生成的 protocol buffer 类中添加更丰富的行为,最好的方法就是在应用程序中对它进行封装。若是你无权控制 .proto 文件的设计的话,封装 protocol buffers 也是一个好主意(例如,你从另外一个项目中重用一个 .proto 文件)。在那种状况下,你能够用封装类来设计接口,以更好地适应你的应用程序的特定环境:隐藏一些数据和方法,暴露一些便于使用的函数,等等。可是你绝对不要经过继承生成的类来添加行为。这样作的话,会破坏其内部机制,而且不是一个好的面向对象的实践。
如今咱们尝试使用 protocol buffer 类。你的地址簿程序想要作的第一件事是将我的详细信息写入到地址簿文件。为了作到这一点,你须要建立、填充 protocol buffer 类实例,而且将它们写入到一个输出流(output stream)。
这里的程序能够从文件读取 AddressBook
,根据用户输入,将新 Person
添加到 AddressBook
,而且再次将新的 AddressBook
写回文件。这部分直接调用或引用 protocol buffer 类的代码会高亮显示。
#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; }
注意 GOOGLE_PROTOBUF_VERIFY_VERSION
宏。它是一种好的实践——虽然不是严格必须的——在使用 C++ Protocol Buffer 库以前执行该宏。它能够保证避免不当心连接到一个与编译的头文件版本不兼容的库版本。若是被检查出来版本不匹配,程序将会终止。注意,每一个 .pb.cc
文件在初始化时会自动调用这个宏。
同时注意在程序最后调用 ShutdownProtobufLibrary()
。它用于释放 Protocol Buffer 库申请的全部全局对象。对大部分程序,这不是必须的,由于虽然程序只是简单退出,可是 OS 会处理释放程序的全部内存。然而,若是你使用了内存泄漏检测工具,工具要求所有对象都要释放,或者你正在写一个库,该库可能会被一个进程屡次加载和卸载,那么你可能须要强制 Protocol Buffer 清除全部东西。
固然,若是你没法从它获取任何信息,那么这个地址簿没多大用处!这个示例读取上面例子建立的文件,并打印文件里的全部内容。
#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; }
迟早在你发布了使用 protocol buffer 的代码以后,毫无疑问,你会想要 "改善"
protocol buffer 的定义。若是你想要新的 buffers 向后兼容,而且老的 buffers 向前兼容——几乎能够确定你很渴望这个——这里有一些规则,你须要遵照。在新的 protocol buffer 版本:
(这是对于上面规则的一些异常状况,但它们不多用到。)
若是你能遵照这些规则,旧代码则能够欢快地读取新的消息,而且简单地忽略全部新的字段。对于旧代码来讲,被删除的 optional 字段将会简单地赋予默认值,被删除的 repeated
字段会为空。新代码显然能够读取旧消息。然而,请记住新的 optional 字段不会呈如今旧消息中,所以你须要显式地使用 has_
检查它们是否被设置或者在 .proto
文件在标签数字后使用 [default = value]
提供一个合理的默认值。若是一个 optional 元素没有指定默认值,它将会使用类型特定的默认值:对于字符串,默认值为空字符串;对于布尔值,默认值为 false;对于数字类型,默认类型为 0。注意,若是你添加一个新的 repeated 字段,新代码将没法辨别它被留空(left empty)(被新代码)或者从没被设置(被旧代码),由于 repeated 字段没有 has_
标志。
C++ Protocol Buffer 库已极度优化过了。可是,恰当的用法可以更多地提升性能。这里是一些技巧,能够帮你从库中挤压出最后一点速度:
尽量复用消息对象。即便它们被清除掉,消息也会尽可能保存全部被分配来重用的内存。所以,若是咱们正在处理许多相同类型或一系列类似结构的消息,一个好的办法是重用相同的消息对象,从而减小内存分配的负担。可是,随着时间的流逝,对象可能会膨胀变大,尤为是当你的消息尺寸(译者注:各消息内容不一样,有些消息内容多一些,有些消息内容少一些)不一样的时候,或者你偶尔建立了一个比日常大不少的消息的时候。你应该本身经过调用 SpaceUsed 方法监测消息对象的大小,并在它太大的时候删除它。
对于在多线程中分配大量小对象的状况,你的操做系统内存分配器可能优化得不够好。你能够尝试使用 google 的 tcmalloc。
Protocol Buffers 毫不仅用于简单的数据存取以及序列化。请阅读 C++ API reference 来看看你还能用它来作什么。
protocol 消息类所提供的一个关键特性就是反射。你不须要编写针对一个特殊的消息类型的代码,就能够遍历一个消息的字段并操做它们的值。一个使用反射的有用方法是 protocol 消息与其余编码互相转换,好比 XML 或 JSON。反射的一个更高级的用法可能就是能够找出两个相同类型的消息之间的区别,或者开发某种 "协议消息的正则表达式",利用正则表达式,你能够对某种消息内容进行匹配。只要你发挥你的想像力,就有可能将 Protocol Buffers 应用到一个更普遍的、你可能一开始就指望解决的问题范围上。
反射是由 Message::Reflection interface 提供的。
via: https://developers.google.com/protocol-buffers/docs/cpptutorial