我们知道Protocol Buffer有一个重要的特性——跨语言,它支持C++,Python,Java等多种语言。本文围绕着C++语言实例来往下展开,主要介绍以下几点:如何在.proto文件中定义message类型,如何使用protoc编译器,如何使用Protobuf提供的C++ API去读/写数据。
1.为什么使用Protocol Buffers?
首先,假设有如下场景:一个“通讯录”应用程序,需要去从一个文件中读取或写入每个人的相关信息,这些信息主要包括每个人的ID,姓名,邮箱以及电话号码等。那么,你将采用何种方法来序列化或反序列化这些结构化的数据呢?
这里,我们先给出如下几种方案:
(1)以二进制形式去读/写加载到内存中的结构化数据。但是,该方法要求读/写双方具有相同的内存布局以及字节顺序。比如,您必须考虑到字节对齐的方式(一般将其设为1字节对齐)、字节顺序的大小端转换等问题。其次,这种方式还会给这种结构化的数据带来了难以扩展的弊端。
(2)您可以自定义一种特殊的方式来将数据编码为单个字符串。虽然这是一种简单灵活的方法,适用于编码简单的数据。但是,在编码和解析的过程会增加运行时成本。
(3)使用XML。将数据序列化为可读的XML格式,这对于那些想要将自己的数据与其他应用程序共享的场景来说是一个不错的选择。然而,XML却是相当的消耗空间的,编码/解码的过程会给应用程序带来巨大的性能损失。
针对于上面提到的种种弊端,Google的Protocol Buffer横空出世,给出了解决方案(嗯,故事总是这样发展的…)。
通过Protocol Buffer,您可以编写一个.proto文件来描述您希望存储的数据结构。在此基础上,Protocol Buffer会给您创建一个相应的类(该类会为您提供各种操作您所定义的数据结构的接口),以高效的二进制格式实现数据的自动编码和解析。更为重要的一点是,Protocol Buffer支持后期更新扩展。
2.编写.proto文件
接着上面假设的场景,我们来定义一个“通讯录”的.proto文件,以addressbook.proto命名。此场景下:
(1)首先,为通讯录中的每一个“人”定义为一个message数据结构(命名Person),相应地,每个人都应该包含字段:姓名,id,邮件以及电话号码等。
(2)其次,每个人可能有多个电话号码,可能是手机、家庭座机或办公座机。所以,我们将电话号码字段定义为嵌套在Person结构中的另一个message数据结构(命名PhoneNumber);
(3)最后,电话号码的类型定义为一个枚举型(命名PhoneType)。
于是,我们得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
syntax = "proto2"; //表明使用的是proto2语法规范 package tutorial; //类似于C++中的命名空间,防止命名冲突 /*Person结构*/ message Person{ required string name = 1; required int32 id = 2; optional string email = 3; /*电话类型,枚举型*/ enum PhoneType{ MOBILE = 0; HOME = 1; WORK = 2; } /*PhoneNumber结构*/ message PhoneNumber{ required string number = 1; optional PhoneType type = 2 [default = HOME]; //电话类型默认为HOME } repeated PhoneNumber phones = 4; //repeated修饰,一个人可以有多个电话 } /*通讯录结构体*/ message AddressBook{ repeated Person people = 1; //repeated修饰,通讯录可以有多个人 } |
其中的语法细节此处不再赘述,可参考前一篇博文(Protocol Buffer语法规范:http://dulishu.top/protocol-buffer-syntax/)。
3.编译.proto文件
现在我们已经写好了addressbook.proto文件了,离能够读/写AddressBook结构化数据还差一步:使用protoc编译.proto文件,生成我们所需要的C++类文件。
protoc命令的使用方法在前一篇博文中也有所介绍,比如,此处我们输入如下命令:
1 2 3 4 5 6 |
[leo@ubuntu 02]$ protoc -I=./ --cpp_out=./ addressbook.proto [leo@ubuntu 02]$ ll total 88 -rw-rw-r-- 1 leo leo 42958 Aug 23 01:42 addressbook.pb.cc -rw-rw-r-- 1 leo leo 30944 Aug 23 01:42 addressbook.pb.h -rw-rw-r-- 1 leo leo 803 Aug 23 01:42 addressbook.proto |
如上所示,该目录下多了两个文件:addressbook.pb.h为生成类的头文件, addressbook.pb.cc为该类的具体实现。
4.Protobuf API
如上所示,使用protoc命令生成了addressbook类,该类包含了我们序列化/反序列化需要用到的所有方法,幸运的是,这些方法已经全部由Protobuf帮我们实现了。在使用之前,我们还是有必要了解一些常用的API。
(1)字段相关API
Protobuf会为在addressbook.proto文件中声明的每一个message类型生成一个同名的类,就Person类而言,Protoc编译器为Person中的每一个字段都生成了相应的操纵接口。比如,对name, id, eamil以及phones而言有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// name /*判断name字段是否被设置,若是,则返回true*/ inline bool has_name() const; /*将name字段的内容清空*/ inline void clear_name(); /*返回name字段的内容,返回值为const类型,不可修改*/ inline const ::std::string& name() const; /*设置name字段的内容,形参为string类型的引用*/ inline void set_name(const ::std::string& value); /*设置name字段的内容,形参为C风格的字符串*/ inline void set_name(const char* value); /*返回name字段的内容,返回值为非const类型,可修改*/ inline ::std::string* mutable_name(); // id /*判断id字段是否被设置,若是,则返回true*/ inline bool has_id() const; /*将id字段的内容清空*/ inline void clear_id(); /*返回id字段的内容*/ inline int32_t id() const; /*设置id字段的内容*/ inline void set_id(int32_t value); // email /*email字段和name字段都为string类型,所以,请参考name字段的注释*/ 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(); // phones /*返回phones字段重复的个数,因为phones字段是repeated修饰的*/ inline int phones_size() const; /*将phones字段的内容清空*/ inline void clear_phones(); /*返回phones字段的内容,返回值为const类型,不可修改*/ inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phones() const; /*返回phones字段的内容,返回值为非const类型,可修改*/ inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phones(); /*返回第index个phones字段的内容,返回值const,不可修改*/ inline const ::tutorial::Person_PhoneNumber& phones(int index) const; /*返回第index个phones字段的内容,返回值为非const,可修改*/ inline ::tutorial::Person_PhoneNumber* mutable_phones(int index); /*向消息中添加一个phones字段*/ inline ::tutorial::Person_PhoneNumber* add_phones(); |
如上所示,我为每个字段的API都添加了相应的注释,此处不再赘述。需要说明的是,对不同的字段而言,protobuf生成的操纵接口会稍有差异,详情请见参考链接[2]。
(2)message相关API
除了上述提及的字段相关API,protobuf还为message类型生成了相关API,用来检查或操作整个message,比如:
1 2 3 4 5 6 7 8 |
/*判断message是否设置了所有required字段,若是,则返回true*/ bool IsInitialized() const; /*返回可读的message数据内容,可用于调试*/ string DebugString() const; /*拷贝from的内容至现在的message中(会被覆盖)*/ void CopyFrom(const Person& from); /*清空message中所有元素*/ void Clear(); |
(3)序列化/反序列化相关API
对每个Protobuf类而言,protobuf同样提供了使用二进制形式读/写这些消息的接口,主要有:
1 2 3 4 5 6 7 8 |
/*序列化到字符串流output中。注意:字符串output中存储的是二进制而非普通文本*/ bool SeralizeToString(string* output) const; /*从字符串流data中解析(反序列化)*/ bool ParseFromString(const string& data); /*序列化到输出流output中*/ bool SeralizeToOstream(ostream* output); /*从输入流input中解析(反序列化)*/ bool ParseFromIstream(istream* input); |
5.实例
下面两个实例使用Protobuf提供的C++ API,分别向“通讯录”addressbook.txt文件中写入和读取每个人的相关信息。
(1)write.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
#include <iostream> #include <fstream> #include <string> #include "addressbook.pb.h" using namespace std; /*向Person结构中添加相关的信息*/ void PromptForAddress(tutorial::Person* person) { /*设置id字段*/ cout << "Enter person ID number: "; int id; cin >> id; person->set_id(id); cin.ignore(256, '\n'); /*设置name字段*/ cout << "Enter name: "; getline(cin, *person->mutable_name()); /*设置email字段*/ cout << "Enter email address(blank for none): "; string email; getline(cin, email); if(!email.empty()){ person->set_email(email); } /*设置phones字段,可以设置多个*/ while(true){ cout << "Enter a phone number(or leave blank to finish): "; string number; getline(cin, number); if(number.empty()){ break; } /*添加一个phones字段,该字段包括两个域:number,type*/ tutorial::Person::PhoneNumber* phone_number = person->add_phones(); /*设置number*/ phone_number->set_number(number); /*设置type*/ 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 << "Unknow phone type. Using default." << endl; } } } /*主函数,读取通讯录文件,根据标准输入向通讯录中添加个人信息,并写入通讯录文件*/ int main(int argc, char* argv[]) { /*用来验证我们链接的库的版本与我们编译的头文件版本是否一致*/ GOOGLE_PROTOBUF_VERIFY_VERSION; if(argc != 2){ cerr << "Usage: " << argv[0] << " address_book_file" << endl; return -1; } /*定义一个通讯录实例address_book*/ tutorial::AddressBook 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; } /*向通讯录中添加记录*/ PromptForAddress(address_book.add_people()); /*将通讯录中添加的记录写入磁盘上的通讯录文件中*/ fstream output(argv[1], ios::out | ios::trunc | ios::binary); if(!address_book.SerializeToOstream(&output)){ cerr << "Failed to write address book." << endl; return -1; } /*删除libprotobuf分配的所有全局对象(可选的操作)*/ google::protobuf::ShutdownProtobufLibrary(); return 0; } |
编译、运行,并添加一条记录:
1 2 3 4 5 6 7 8 9 10 |
[leo@ubuntu 02]$ g++ -Wall -g write.cpp addressbook.pb.cc -o write -std=c++11 -lprotobuf -lpthread [leo@ubuntu 02]$ ./write addressbook.txt Enter person ID number: 1 Enter name: leo Enter email address(blank for none): leolee512@foxmail.com Enter a phone number(or leave blank to finish): 123456 Is this a mobile, home, or work phone? mobile Enter a phone number(or leave blank to finish): 654321 Is this a mobile, home, or work phone? work Enter a phone number(or leave blank to finish): |
(2)read.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
#include <iostream> #include <fstream> #include <string> #include "addressbook.pb.h" using namespace std; /*遍历通讯录文件中的所有人,并打印输出*/ 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 << " Email 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; } } } /*主函数,读取磁盘上的通讯录文件并打印输出*/ int main(int argc, char* argv[]) { GOOGLE_PROTOBUF_VERIFY_VERSION; if(argc != 2){ cerr << "Usage: " << argv[0] << " address_book_file" << endl; return -1; } tutorial::AddressBook 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); google::protobuf::ShutdownProtobufLibrary(); return 0; } |
编译、运行:
1 2 3 4 5 6 7 |
[leo@ubuntu 02]$ g++ -Wall -g read.cpp addressbook.pb.cc -o read -std=c++11 -lprotobuf -lpthread [leo@ubuntu 02]$ ./read addressbook.txt Person ID: 1 Name: leo Email address: leolee512@foxmail.com Mobile phone: 123456 Work phone: 654321 |
6.其他
OK, Protocol Buffer初阶内容就介绍到这里,与君共享。其他未述高阶内容等以后用到再做总结吧~
参考:
[1]. https://developers.google.com/protocol-buffers/docs/cpptutorial
[2]. https://developers.google.com/protocol-buffers/docs/reference/cpp-generated
[3]. https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.message.html#Message