Protocol Buffers tutorials(C++)
Table of Contents
翻译自 Protocol Buffer Basics: C++
1 概述
这篇教程为 C++程序员提供如何使用 protocol buffers 的基础指南。通过创建一个简单的示例应用,展示以下内容:
- 在.proto 文件中定义消息格式。
- 使用 protocol buffer 编译器。
- 使用 C++ protocol buffer API 读写消息。
这不是如何在 C++中 使用 protocol buffers 的完整指南。更详细的参考信息,参见 the Protocol Buffer Language Guide, the C++ API Reference, the C++ Generated Code Guide, and the Encoding Reference.
2 为何使用 Protocol Buffers?
将要使用的示例是很简单的“地址簿”应用程序,它可以读写文件中人们的联系方式。地址簿中的每个人有名字,ID,邮件地址和联系电话号码。
如何序列化和检索类似的结构化数据呢?有一些解决办法:
- 可使用二进制形式进行发送/保存内存中的原始数据结构。随着时间的推移,这变成脆弱的方法,因为接收/读取的代码必须使用相同的内存布局、字节顺序等进行编译。同时,随着文件以原始格式积累数据和专门针对这种格式软件的,扩展格式变得非常难。
可以发明一种特殊格式将数据项编码为单个字符串,比如将四个整数编码为"12:3:-23:67"。这种方法简单灵活,尽管它要求编写一次性的编码和解码程序,解码还会增加一点运行成本。这么做最合适编码非常简单的数据。
- 将数据序列化为 XML。这种方法非常有吸引力,因为 XML 是一种人类可读的语言,并且很多语言都有对应的库。如果想和其他程序/项目共享数据,这可能是非常不错的选择。然而,XML 是占用空间出了名的大,并且编解码都会使应用程序性能损失巨大。同时,操作 XML DOM 树通常也比操作类的简单字段更加复杂。
protocol bffers 可以灵活、高效、自动化的解决这个问题。使用 protocol buffers,编写.proto 文件描述想要存储的数据结构。protocol buffer 编译器通过该文件创建一个类,该类使用高效的二进制格式对 protocol buffer 数据自动编码和解码。生成的类针对组成 protocol buffer 的字段提供 getters 和 setters,并负责将 protocol buffer 的读写细节作为一个单元处理。重要的是,随着时间推移,protocol buffer 格式支持扩展,代码仍然可以读取使用旧格式编码的数据。
3 示例代码
源码包中的“example”目录包含有示例代码。这里下载 。
4 定义协议格式
创建地址簿应用程序从.proto 文件开始。.proto 文件中的定义很简单:为每个想要序列化的数据结构体添加消息,然后为消息中的每个字段指定名字和类型。此处的 addressbook.proto 文件定义了消息:
<script src="https://gist.github.com/phenix3443/744f21deec94065d7363d346f8f2a427.js"></script>
如你所见,语法类似 C++或者 Java。让我们浏览文件的每个部分,看看它的作用。
.proto 以 package 声明开始,这有助于在不同项目间防止命名冲突。C++中,生成的类将放在 package 同名的命名空间中。
接下来是消息定义。消息只是包含一组类型化字段的集合。许多标准的数据类型可以作为字段类型,包括 bool,int32,float,double,和 string。也可以通过将其他消息类型作为字段类型向消息添加进一步的结构—上面的例子中,Persion 消息包含 PhoneNumber 消息,AddressBook 消息又包含 Person 消息。甚至可以在其他消息中嵌套定义消息类型—如你所见,PhoneNumber 类型定义 Person 内。如果某个字段是预定义的值列表之一,还可以定义枚举类型—这里电话号码想指定位 MOBILIE,HOME 或者 WORK 中的一个。
每个元素上标记的"= 1", "= 2"作为字段在二进制编码中的唯一标签。编码时,标签数字 1-15 比之后的数字要少一个字节。所以作为优化,可以决定将这些标签标记用于常用或重复使用的元素,调钱 16 以及更高用于较少使用的可选元素。重复字段的元素需要重新编码标记,所以重复字段特被适合这种优化。
每个字段必须使用下面修饰符中的进行标注:
- required:必须提供字段的值,否则消息将被认为是“未初始化”的。如果 libprotobuf 是在调试模式下编译的,序列化未初始化的信息将会导致一个断言失败。优化构建,将会跳过检查,可以任意写消息。然而,解析为初始化的消息将总是失败(从解析方法返回 false)。除此之外,required 的字段行为类似 optional 字段。
optional:字段设置与否都可。如果没有设置就使用默认值。对于简单类型,可以指定自己的默认值,就像例子中电话号码类型那样做的。否则,使用系统默认值:数字类型是 0,字符串是空串。布尔是 false。嵌入消息的默认值总是没有设置字段的“默认实例”或“原型”。调用访问器时,没有显式设置的可选或必选字段总是返回字段的默认值。
- repeated:字段可能被重复任意次数(包括 0)。重复值的顺序将会保存在 protocol buffer 中。把重复字段想成动态大小的数组。
将字段标记为 required 时应该永远小心。如果在什么时候想不再写或者发送给一个 required 字段,将它变为可选字段可能会出问题–旧的 reader 将会认为消息没有该字段是不完整的,可能会无意中拒绝或删除它们。相反,应该考虑为 buffers 编写应用程序特定的定制验证程序。一些 google 的工程师认为 required 弊大于利;他们更喜欢只使用 optional 和 repeated。然而,这种观点并不普遍。
在Protocol Buffer Language Guide 中将会有编写.proto 文件的完整指南—包括所有可用的字段类型。不要查找类似类继承的功能,虽然 protocol buffers 不这么做。
5 Compiling Your Protocol Buffers
现在已经有一个.proto,接下来要做的事情是生成可以用来读写 AddressBook(也是 Person 和 PhoneNumber)消息的类。这需要运行 protocol buffer 编译器编译.proto。
- 如果还没有安装编译器,下载并按照 README 中的说明进行安装。
现在运行编译器,指定源码目录(程序源代码所放目录–如果不指定默认当前目录),目标目录(存放生成代码的目录,经常和$SRC_DIR 一样),.proto 文件路径。此处是:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
想要 c++类,使用 –cpp_out 选项,其他语言也有类似的选项。
指定目标目录下会生成以下文件:
addressbook.pb.h, the header which declares your generated classes.
addressbook.pb.h,声明生成类的头文件。
addressbook.pb.cc, which contains the implementation of your classes.
addressbook.pb.cc,类文件的实现。
5.1 The Protocol Buffer API
Let's look at some of the generated code and see what classes and functions the compiler has created for you. If you look in tutorial.pb.h, you can see that you have a class for each message you specified in tutorial.proto. Looking closer at the Person class, you can see that the complier has generated accessors for each field. For example, for the name, id, email, and phone fields, you have these methods:
看一些生成的代码,了解编译器生成哪些类和函数。如果查看 tutorial.pb.h,会发现 tutorial.proto 中定义的每个消息都有一个对应的类。
// 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();
As you can see, the getters have exactly the name as the field in lowercase, and the setter methods begin with set_. There are also has_ methods for each singular (required or optional) field which return true if that field has been set. Finally, each field has a clear_ method that un-sets the field back to its empty state.
如你所见,getters 的名字就是小写的字段名,setter 方法以 set_ 开始。每个单数(required 或 optional)字段有 has_ 方法,如果该字段被设置则返回 true。最后每个字段都有 clear_ 方法重置到空状态。
While the numeric id field just has the basic accessor set described above, the name and email fields have a couple of extra methods because they're strings – a mutable_ getter that lets you get a direct pointer to the string, and an extra setter. Note that you can call mutable_email() even if email is not already set; it will be initialized to an empty string automatically. If you had a singular message field in this example, it would also have a mutable_ method but not a set_ method.
虽然数字 id 字段只有上面描述的基本访问组,name 和 email 字段由于它们是字符串,有几个额外的方法– mutable_ getter 可以获得直接指向字符串的指针,还有一个额外的 setter。注意,即使 email 还没有设置也可以直接调用 mutable_email() ;它将会自动初始化为空字符串。如果这个例子中有一个单数消息字段,也将会有一个 mutable_ 方法,但没有 set_ 方法。
Repeated fields also have some special methods – if you look at the methods for the repeated phone field, you'll see that you can
重复字段也有一些特殊方法—查看重复的电话字段的方法,可以看到:
check the repeated field's _size (in other words, how many phone numbers are associated with this Person).
检查重复字段的 _size(换句话说,此人关联多少电话号码)
get a specified phone number using its index.
通过索引获得指定的电话号码。
update an existing phone number at the specified index.
使用指定索引更新存在的电话号码。
add another phone number to the message which you can then edit (repeated scalar types have an add_ that just lets you pass in the new value).
向可以编辑的消息添加另一个电话号码(重复的标量类型有 add_ 可以传递新值)。
For more information on exactly what members the protocol compiler generates for any particular field definition, see the C++ generated code reference.
protocol bufffers 编译器为任何特定字段定义生成的确切成员信息查看C++ generated code reference。
5.2 Enums and Nested Classes
The generated code includes a PhoneType enum that corresponds to your .proto enum. You can refer to this type as Person::PhoneType and its values as Person::MOBILE, Person::HOME, and Person::WORK (the implementation details are a little more complicated, but you don't need to understand them to use the enum).
生成代码中包含与.proto 中枚举对应的 PhoneType 枚举类型。可以用 Person::PhoneType 引用该类型,Person::MOBILE、Person::HOME、Person::WORK 引用值(实现细节稍微复杂一些,但是使用枚举不要了解这些)。
The compiler has also generated a nested class for you called Person::PhoneNumber. If you look at the code, you can see that the "real" class is actually called Person_PhoneNumber, but a typedef defined inside Person allows you to treat it as if it were a nested class. The only case where this makes a difference is if you want to forward-declare the class in another file – you cannot forward-declare nested types in C++, but you can forward-declare Person_PhoneNumber.
编译器还生成了嵌套类 Person::PhoneNumber。如果查看代码,可以发现真正的类名实际上是叫 Person_PhoneNumber,但是 Person 类中通过 typedef 定义使得可以把它看做事一个嵌套类。唯一不同的情况是如果想在另一个文件中提前声明该类—C++中不能提前声明内置类型,但是可以提前声明 Person_PhoneNumber。
5.3 Standard Message Methods
Each message class also contains a number of other methods that let you check or manipulate the entire message, including:
每个消息类都包含其他一些方法可用来检查或操纵整个消息,包括:
bool IsInitialized() const;: checks if all the required fields have been set.
bool IsInitialized() const;检查所有 required 字段是否被设置了。
string DebugString() const;: returns a human-readable representation of the message, particularly useful for debugging.
string DebugString() const;:返回可读的消息表示,这对调试特别有用。
void CopyFrom(const Person& from);: overwrites the message with the given message's values.
void CopyFrom(const Person& from);:使用给定消息的值重写消息。
void Clear();: clears all the elements back to the empty state.
清除所有元素回到空状态。
These and the I/O methods described in the following section implement the Message interface shared by all C++ protocol buffer classes. For more info, see the complete API documentation for Message.
这些以及下面的章节中描述的 I/O 方法实现来了所有 C++ protocol buffer 类共享的消息接口。更多信息参阅complete API documentation for Message。
6 Parsing and Serialization
Finally, each protocol buffer class has methods for writing and reading messages of your chosen type using the protocol buffer binary format. These include:
最后,每个 protocol buffer 类都有方法以 protocol buffer 二进制格式读写选定类型的消息。包括:
bool SerializeToString(string* output) const;: serializes the message and stores the bytes in the given string. Note that the bytes are binary, not text; we only use the string class as a convenient container.
bool SerializeToString(string* output) const;:序列化消息,并将字节存储在指定字符串中。注意字节是二进制的,不是文本格式;string 类只是作为方便的容器。
bool ParseFromString(const string& data);: parses a message from the given string.
bool ParseFromString(const string& data);: 从指定字符串中解析消息。
bool SerializeToOstream(ostream* output) const;: writes the message to the given C++ ostream.
bool SerializeToOstream(ostream* output) const;:向指定 C++ ostream 写消息。
bool ParseFromIstream(istream* input);: parses a message from the given C++ istream.
bool ParseFromIstream(istream* input);:从指定 C++ istream 中解析消息。
These are just a couple of the options provided for parsing and serialization. Again, see the Message API reference for a complete list.
针对消息解析和序列化只有几个选项。查看Message API reference for a complete list。
Protocol Buffers and O-O Design Protocol buffer classes are basically dumb data holders (like structs in C++); they don't make good first class citizens in an object model. If you want to add richer behaviour to a generated class, the best way to do this is to wrap the generated protocol buffer class in an application-specific class. Wrapping protocol buffers is also a good idea if you don't have control over the design of the .proto file (if, say, you're reusing one from another project). In that case, you can use the wrapper class to craft an interface better suited to the unique environment of your application: hiding some data and methods, exposing convenience functions, etc. You should never add behaviour to the generated classes by inheriting from them. This will break internal mechanisms and is not good object-oriented practice anyway.
Protocol Buffers 和面向对象设计的 Protocol buffer 类基本上是哑数据持有者(类似 C++中的结构体);对象模型中它们并不是一等公民。如果需要向生成的类中添加更丰富的行为,最好的方法是在应用程序特定类中封装 protocol buffer 类。如果没有设计.proto 文件(比如说从其他项目重用该文件)权限,封装 protocol buffer 仍是个好主意。这种情况下,可以使用封装类生成一个更适合应用程序特定环境的接口:隐藏一些数据和方法,暴露更方便函数等等。绝对不要通过继承来向生成类添加行为。这将会打破内部机制,并且无论如何这都不是好的面向对象实践。
7 Writing A Message
现在试着使用 protocol buffer 类。首先希望地址簿应用程序能够将个人信息写到地址簿文件。要做到这一点,需要创建和填充 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; }
Notice the GOOGLE_PROTOBUF_VERIFY_VERSION macro. It is good practice – though not strictly necessary – to execute this macro before using the C++ Protocol Buffer library. It verifies that you have not accidentally linked against a version of the library which is incompatible with the version of the headers you compiled with. If a version mismatch is detected, the program will abort. Note that every .pb.cc file automatically invokes this macro on startup.
注意 GOOGLE_PROTOBUF_VERIFY_VERSION 宏。尽管不是严格必须的,但使用 C++ protocol buffer 库前执行该宏是很好的实践。它确保没有意外的链接到和编译时的头文件版本不兼容的库。如果检测到版本不匹配,程序将会终止。注意每个.pb.cce 文件启动时自动调用该宏。
Also notice the call to ShutdownProtobufLibrary() at the end of the program. All this does is delete any global objects that were allocated by the Protocol Buffer library. This is unnecessary for most programs, since the process is just going to exit anyway and the OS will take care of reclaiming all of its memory. However, if you use a memory leak checker that requires that every last object be freed, or if you are writing a library which may be loaded and unloaded multiple times by a single process, then you may want to force Protocol Buffers to clean up everything.
还要注意程序结束时调用 ShutdownProtobufLibrary()。它做的工作就是删除所有 protocol buffer 库分配的全局对象。这对于大多数程序来说不是必须的,因为进程结束,系统将会负责回收它的所有内存。然而,如果使用要求每个对象释放的内存泄漏检查工具,或者编写一个单进程多次加载卸载的库,可能想要强制 protocol buffers 清理一切。
7.1 Reading A Message
Of course, an address book wouldn't be much use if you couldn't get any information out of it! This example reads the file created by the above example and prints all the information in it.
当然,不能读取任何信息的地址簿是没有多大用处的。下面的示例会读取之前生成的文件,并打印其中的所有信息。
#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; }
8 Extending a Protocol Buffer
发布使用 protocol buffer 的代码之后,迟早都要改善 protocol buffer 的定义。如果想要新的 buffers 向后兼容,旧的 buffers 向前兼容—几乎肯定要做这样的事情—必须遵守一些规则。在新版本的 protocol buffer 中:
- 绝对不要改变已有字段的 tag 编号。
- 绝对不要增删任何 required 字段。
- 可以删除 optional 或 repeated 字段。
- 可以添加新的 optional 或 repeated 字段,但是必须使用新的 tag 编号(例如,该 protobuf 中从来没有使用过的 tag 编号,包括已删除的字段)。
(这些规则也有一些例外,但是很少用到。)
如果遵守这些规则,旧代码将能很顺利的读取新消息,新字段将被简单忽略掉。对于旧代码,删除的 optional 字段将会简单的使用它们的默认值,删除的 repeated 字段将为空。新代码将透明的读取旧消息。然而,请记住,新的 optional 字段不会出现在新消息中,所以要么通过 has_显式检查它们是否被设置,要么在.proto 文件中通过 tag 号之后设置[default = value]来提供合理的默认值。如果 optional 元素没有指定默认值,将使用类型相关的默认值:string 将使用空,bool 将使用 false,数字将使用 0.同样注意,如果添加一个新的 repeated 字段,因为它没有 has_ 标志,新代码无法判断新代码是否将它设置为空,或者旧代码根本没有进行设置。
9 Optimization Tips
C++ Protocol Buffers 库进行了大量优化。然而,正确的使用还可以提高性能。这里有一些如何榨干该库的小贴士:
- 尽可能重用消息对象。即使消息被清除了,它们也会为了重用尽量保持它们分配的所有内存。因此,如果处理许多继承上有相同类型和类似结构的消息,每次重用相同的消息对象是个好主意,这可以减轻内存分配器的负载。然而,经过多次对象可能会变臃肿,特别是消息外形不同或经常构造比平时大的多的消息。应该通过调用 SpaceUsed 方法来监视消息对象的大小,一旦太大了就删掉它们。
- 系统内存分配器可能没有针对多线程分配大量小对象的情况进行优化。可以尝试使用 Google 的 tcmalloc 代替。
10 Advanced Usage
Protocol buffers 的用途不仅是简单访问和序列化消息。一定要看看C++ API reference 了解还可以用它做什么。
protocol 消息类型提供的一个关键特性是反射。不用针对消息类型编写代码就可以遍历消息所有字段并操作它们的值。反射的一个大用处就是可以从其他编码(比如 XML 或 JSON)中转化协议消息。更高级的用处可能是找出两个同类消息的差异,或开发一种“protocol 消息的正则表达式”,可以编写表达式类匹配特定消息内容。如果使用想象力,Protocol Buffers 适用问题的范围可能会超出最初的期望!
反射由 Message::Reflection 接口提供。