本文共 10317 字,大约阅读时间需要 34 分钟。
1 定义 protocol 格式
创建地址簿应用程序,需从 .proto 文件开始。.proto 文件中的定义很简单:为要序列化的每个数据结构添加 message 定义,然后为 message 中的每个字段指定名称和类型。定义相关 message 的 .proto 文件如下:addressbook.proto .
syntax = "proto2";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 phones = 4;}message AddressBook { repeated Person people = 1;}
语法类似于 C++ 或 Java。浏览文件的每个部分,看看它们的作用。
.proto 文件以 package 声明开头,这在这里插入代码片有助于防止不同项目之间的命名冲突。在 C++ 中,生成的类将放在与包名匹配的 namespace (命名空间)中。
接下来,将看到相关的 message 定义。message 只是包含一组类型字段的集合。许多标准的简单数据类型都可用作字段类型,包括 bool、int32、float、double 和 string。还可以使用其他 message 类型作为字段类型在消息中添加更多结构 - 在上面的示例中,Person 包含 PhoneNumber message ,而 AddressBook 包含 Person message。甚至可以定义嵌套在其他 message 中的 message 类型 - 如上所示PhoneNumber 类型在 Person 中定义。如果希望其中一个字段具有预定义的值列表中的值,还可以定义枚举类型 - 此处指定(枚举)电话号码,它的值可以是 MOBILE,HOME 或 WORK 之一。
每个元素上的 “=1”,"=2" 标记表示该字段在二进制编码中使用的唯一 “标记”。标签号 1-15 比起更大数字需要少一个字节进行编码,因此以此进行优化,你可以决定将这些标签用于常用或重复的元素,将标记 16 和更高的标记留给不太常用的可选元素。repeated 字段中的每个元素都需要重新编码 Tag,因此 repeated 字段特别适合使用此优化。
必须使用以下修饰符之一注释每个字段:
required: 必须提供该字段的值,否则该消息将被视为“未初始化”。如果是在调试模式下编译libprotobuf,则序列化一个未初始化的 message 将导致断言失败。在优化的构建中,将跳过检查并始终写入消息。但是,解析未初始化的消息将始终失败(通过从解析方法返回 false)。除此之外,required 字段的行为与 optional 字段完全相同。optional: 可以设置也可以不设置该字段。如果未设置可选字段值,则使用默认值。对于简单类型,可以指定自己的默认值,就像在示例中为电话号码类型所做的那样。否则,使用系统默认值:数字类型为 0,字符串为空字符串,bools 为 false。对于嵌入 message,默认值始终是消息的 “默认实例” 或 “原型”,其中没有设置任何字段。调用访问器以获取尚未显式设置的 optional(或 required)字段的值始终返回该字段的默认值。
repeated: 该字段可以重复任意次数(包括零次)。重复值的顺序将保留在 protocol buffer 中。可以将 repeated 字段视为动态大小的数组。
2编译 Protocol Buffers
既然已经有了一个 .proto 文件,那么需要做的下一件事就是生成你需要读写AddressBook(以及 Person 和 PhoneNumber ) message 所需的类。为此,需要在 .proto 上运行 protocol buffer 编译器 protoc:protoc --cpp_out=. addressbook.proto
这将在指定的目标目录中生成以下文件:
addressbook.pb.h: 类声明的头文件 addressbook.pb.cc:类实现 The Protocol Buffer API下面是一些生成的代码,看看编译器创建了哪些类和函数。如果查看 addressbook.pb.h,会发现在 addressbook.proto 中指定的每条 message 都有一个对应的类。仔细观察 Person 类,可以看到编译器已为每个字段生成了访问器。例如,对于 name ,id,email 和 phone 字段,可以使用以下方法:
// required 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(); // required id inline bool has_id() const; inline void clear_id(); inline int32_t id() const; inline void set_id(int32_t value); // optional 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(); // repeated phones inline int phones_size() const; inline void clear_phones(); inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phones() const; inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phones(); inline const ::tutorial::Person_PhoneNumber& phones(int index) const; inline ::tutorial::Person_PhoneNumber* mutable_phones(int index); inline ::tutorial::Person_PhoneNumber* add_phones();
getter 的名称与小写字段完全相同,setter 方法以 set_ 开头。每个单数(required 或 optional)字段也有 has_ 方法,如果设置了该字段,则返回 true。最后,每个字段都有一个 clear_ 方法,可以将字段重新设置回 empty 状态。
虽然数字 id 字段只有上面描述的基本访问器集,但是 name 和 email 字段因为是字符串所以有几个额外的方法:一个 mutable_ 的 getter,它允许获得一个指向字符串的直接指针,以及一个额外的 setter。请注意,即使尚未设置 email ,也可以调用 mutable_email();它将自动初始化为空字符串。如果在这个例子中有一个单数的 message 字段,它也会有一个 mutable_ 方法而不是 set_ 方法。
repeated 字段也有一些特殊的方法 - 如果你看一下 repeated phones 字段的相关方法,会发现可以:
1.检查 repeated 变量长度(换句话说,与此人关联的电话号码数)
2. 使用索引获取指定的电话号码 3.更新指定索引处的现有电话号码 4.在 message 中添加另一个电话号码同时之后也可进行再修改(repeated 的标量类型有一个 add_,而且只允许你传入新值)枚举和嵌套类
生成的代码包含与 .proto 枚举对应的 PhoneType 枚举。可以将此类型称为 Person::PhoneType,其值为 Person::MOBILE,Person::HOME 和 Person::WORK(实现细节稍微复杂一些,如果仅仅只是使用不需要理解里面的实现原理)。
编译器还生成了一个名为 Person::PhoneNumber 的嵌套类。如果查看代码,可以看到 “真实” 类实际上称为 Person_PhoneNumber,但在 Person 中定义的 typedef 允许将其视为嵌套类。唯一会造成一点差异的情况是,如果想在另一个文件中前向声明该类 - 不能在 C ++ 中前向声明嵌套类型,但可以前向声明 Person_PhoneNumber。
标准 Message 方法
每个 message 类还包含许多其他方法,可用于检查或操作整个 message,包括:
1.bool IsInitialized() const;: 检查是否已设置所有必填 required 字段
2. string DebugString() const;: 返回 message 的人类可读表达,对调试特别有用 3. void CopyFrom(const Person& from);: 用给定的 message 的值覆盖 message 4.void Clear();: 将所有元素清除回 empty 状态解析和序列化
最后,每个 protocol buffer 类都有使用 protocol buffer 二进制格式 读写所选类型 message 的方法。包括:
1. bool SerializeToString(string* output) const;:序列化消息并将字节存储在给定的字符串中。请注意, 字节是二进制的,而不是文本;只使用 string 类作为方便的容器。 2.bool ParseFromString(const string& data);: 解析给定字符串到 message 3. bool SerializeToOstream(ostream* output) const;: 将 message 写入给定的 C++ 的 ostream 4. bool ParseFromIstream(istream* input);: 解析给定 C++ istream 到 message3写入一个 Message
现在尝试使用 Protocol Buffer 类。希望地址簿应用程序能够做的第一件事可能是将个人详细信息写入的地址簿文件。为此,需要创建并填充 Protocol Buffer 类的实例,然后将它们写入输出流。
这是一个从文件中读取 AddressBook 的程序,根据用户输入向其添加一个新 Person,并将新的 AddressBook 重新写回文件。
#include#include #include #include "src/addressbook.pb.h"using namespace std;// This function fills in a Person message based on user input./*getline()的原型是istream& getline ( istream &is , string &str , char delim );其中 istream &is 表示一个输入流,譬如cin;string&str表示把从输入流读入的字符串存放在这个字符串中(可以自己随便命名,str什么的都可以);char delim表示遇到这个字符停止读入,在不设置的情况下系统默认该字符为'\n',也就是回车换行符(遇到回车停止读入)。ps: 对于while(getline(cin,line)) 语句,这里默认回车符停止读入,按Ctrl+Z或键入EOF回车即可退出循环。*/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: "; string name; //getline(cin, *person->mutable_name());//自动到"\n"停止读入,自动忽略 cin>>name; //person->set_name(name); *person->mutable_name()=name;//mutable_name 表示name这个成员 cout << "Enter email address (blank for none): "; string email; // getline(cin, email); cin >> email; if (!email.empty()) { person->set_email(email); } cin.ignore(256, '\n');//cin输入会包含\n相当于enter,会导致在下文直接blank leave,因此需要忽略最后的回车 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_phones(); 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() { // 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;// } // string argv[1] = "xiaohaipeng.prototxt"; string argv= "/home/xiaohaipeng/ubuntu/" "Code/c++_code/protobuf_address_test_q/prototxt/phonebook.prototxt"; tutorial::AddressBook address_book; { // Read the existing address book. fstream input(argv, ios::in | ios::binary); if (!input) { cout << argv << ": 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_people()); { // Write the new address book back to disk. fstream output(argv, 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 来清理所有内容。
4读取一个 Message
当然,如果无法从中获取任何信息,那么地址簿就不会有多大用处!此示例读取上面示例创建的文件并打印其中的所有信息。
#include#include #include #include "src/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.people_size(); i++) { // const tutorial::Person &person = address_book.people(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.phones_size(); j++) { // const tutorial::Person::PhoneNumber &phone_number = person.phones(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;// }// } for (const auto people:address_book.people()){ cout<<"people ID:"< <
转载地址:http://hskzi.baihongyu.com/