目录
- Protocol Buffer Basics: C++
- 定义你的 protocol 格式
- 使用修饰符注释每个字段
- 编译你的 Protocol Buffers
- The Protocol Buffer API(功能)
- 枚举和嵌套类
- 标准 Message API(功能)
- 解析和序列化API(功能)
- 写入一个 Message
Protocol Buffer Basics: C++
本教程为 C++ 程序员如何使用 protocol buffers 做一个基本介绍。通过创建一个简单的示例应用程序,它向你展示:
- 如何在一个
.proto
文件中定义 message- 如何使用 protocol buffer 编译器
- 如何使用 C++ protocol buffer 的 API 读写 message
定义你的 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。让我们浏览文件的每个部分,看看它们的作用。
(1).proto 文件以 package 声明开头,这有助于防止不同项目之间的命名冲突。在 C++ 中,生成的类将放在与包名匹配的 namespace (命名空间)中。
(2)接下来将看到相关的 message 定义。message 只是包含一组类型字段的集合。许多标准的简单数据类型都可用作字段类型,包括 bool、int32、float、double 和 string。
此外还可以使用其他 message 类型作为字段类型在消息中添加更多结构 - 在上面的示例中,Person 包含 PhoneNumber message ,而 AddressBook 包含 Person message。甚至可以定义嵌套在其他 message 中的 message 类型 - 如你所见,PhoneNumber 类型在 Person 中定义。
如果你希望其中一个字段具有预定义的值列表中的值,你还可以定义枚举类型 - 此处你指定(枚举)电话号码,它的值可以是 MOBILE,HOME 或 WORK 之一。
(3) 每个元素上的 "=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 字段视为动态大小的数组。
编译你的 Protocol Buffers
既然你已经有了一个 .proto
文件,那么你需要做的下一件事就是生成你需要读写 AddressBook
(以及 Person
和 PhoneNumber
) message 所需的类。为此,你需要在 .proto
上运行 protocol buffer 编译器 protoc
:
- 如果尚未安装编译器,请 下载软件包 并按照 README 文件中的说明进行操作。
- 现在运行编译器,指定源目录(应用程序的源代码所在的位置 - 如果不提供值,则使用当前目录),目标目录(你希望生成代码的目标目录;通常与源目录
$SRC_DIR
相同),以及.proto
的路径。在这种情况下,你可以执行如下命令:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
因为你需要 C ++ 类,所以使用 --cpp_out
选项 - 当然,为其他支持的语言也提供了类似的选项。这将在指定的目标目录中生成以下文件:
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();
1. getter 的名称与小写字段完全相同如 name(),返回值为 std::string&。
2. setter方法为 set_ *()。
3. 每个单数(required 或 optional)字段也有 has_ *() 方法,如果设置了该字段,则返回 true。
4. 每个字段都有一个 clear_ *() 方法,可以将字段重新设置回 empty 状态。
5. 虽然数字 id 字段只有上面描述的基本访问器集,但是 name 和 email 字段因为是字符串所以有几个额外的方法:一个 mutable_ *() 的 getter,它允许你获得一个指向字符串的直接指针,以及一个额外的 set_*()。
6. 单数的 message 字段,它也会有一个mutable_ *() 方法而不是 set_ 方法。
7. 即使尚未设置 email ,也可以调用 mutable_ email();它将自动初始化为空字符串。
repeated 字段也有一些特殊的方法 - 如果你看一下 repeated phones 字段的相关方法,你会发现你可以:
- 检查 repeated 字段长度(换句话说,与此人关联的电话号码数),*_size() 方法。
- 使用索引获取指定的电话号码。
- 更新指定索引处的现有电话号码。
- 在 message 中添加另一个电话号码同时之后也可进行再修改(repeated 的标量类型有一个add_*(),而且只允许你传入新值)。
枚举和嵌套类
生成的代码包含与你的 .proto 枚举对应的 PhoneType 枚举。你可以将此类型称为 Person::PhoneType,其值为 Person::MOBILE,Person::HOME 和 Person::WORK(实现细节稍微复杂一些,但你如果仅仅只是使用不需要理解里面的实现原理)。
编译器还为你生成了一个名为 Person::PhoneNumber 的嵌套类。如果查看代码,可以看到 “真实” 类实际上称为 Person_PhoneNumber,但在 Person 中定义的 typedef 允许你将其视为嵌套类。唯一会造成一点差异的情况是,如果你想在另一个文件中前向声明该类 - 你不能在 C ++ 中前向声明嵌套类型,但你可以前向声明 Person_PhoneNumber。
标准 Message API(功能)
每个 message 类还包含许多其他方法,可用于检查或操作整个 message,包括:
bool IsInitialized() const;
: 检查是否已设置所有必填 required 字段string DebugString() const;
: 返回 message 的人类可读表达,对调试特别有用
void CopyFrom(const Person& from);
: 用给定的 message 的值覆盖 messagevoid Clear();
: 将所有元素清除回 empty 状态
这些和下一节中描述的 I/O 方法实现了所有 C++ protocol buffer 类共享的 Message
接口
解析和序列化API(功能)
最后,每个 protocol buffer 类都有使用 protocol buffer 二进制格式 读写所选类型 message 的方法。包括:
bool SerializeToString(string* output) const;
:序列化消息并将字节存储在给定的字符串中。请注意,字节是二进制的,而不是文本;我们只使用string
类作为方便的容器。bool ParseFromString(const string& data);
: 解析给定字符串到 messagebool SerializeToOstream(ostream* output) const;
: 将 message 写入给定的 C++ 的 ostreambool ParseFromIstream(istream* input);
: 解析给定 C++ istream 到 message
写入一个 Message
现在让我们尝试使用你的 Protocol Buffer 类。你希望地址簿应用程序能够做的第一件事可能是将个人详细信息写入的地址簿文件。为此,你需要创建并填充 Protocol Buffer 类的实例,然后将它们写入输出流。
这是一个从文件中读取 AddressBook 的程序,根据用户输入向其添加一个新 Person,并将新的 AddressBook 重新写回文件。
#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;
}
// @@@ Person::PhoneNumber
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") {
// @@@ Person
phone_number->set_type(tutorial::Person::MOBILE);
} else if (type == "home") {
// @@@ Person
phone_number->set_type(tutorial::Person::HOME);
} else if (type == "work") {
// @@@ Person
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;
}
// @@@ AddressBook
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;
// @@@ ParseFromIstream
} 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[1], ios::out | ios::trunc | ios::binary);
// @@@ SerializeToOstream
if (!address_book.SerializeToOstream(&output)) {
cerr << "Failed to write address book." << endl;
return -1;
}
}
// Optional: Delete all global objects allocated by libprotobuf.
// @@@ ShutdownProtobufLibrary
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
请注意 GOOGLE_PROTOBUF_VERIFY_VERSION 宏。在使用 C++ Protocol Buffer 库之前执行此宏是一种很好的做法 - 尽管不是绝对必要的。它验证你没有意外链接到与你编译的头文件不兼容的库版本。如果检测到版本不匹配,程序将中止。请注意,每个 .pb.cc 文件在启动时都会自动调用此宏。
另请注意在程序结束时调用 ShutdownProtobufLibrary()。所有这一切都是删除 Protocol Buffer 库分配的所有全局对象。对于大多数程序来说这是不必要的,因为该过程无论如何都要退出,并且操作系统将负责回收其所有内存。但是,如果你使用了内存泄漏检查程序,该程序需要释放每个最后对象,或者你正在编写可以由单个进程多次加载和卸载的库,那么你可能希望强制使用 Protocol Buffers 来清理所有内容。
参考:https://developers.google.cn/protocol-buffers/