本文主要是介绍protocol buffers 基本要素:基于c++,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
为什么使用protocol buffers?
我们将使用一个非常简单的例子,做一个地址簿的应用。在这个地址簿中,我们可以读写联系人的信息,有名字,ID,还有电话号码。
那么我们如何序列化和反序列化一个这样的数据结构呢?下面列举了几种方式:
首先说的方法就是直接传递结构体的二进制序列,但是这是一种不好的方法,因为发送端和接受端都要相同的存储配置,字节序等。同时,由于文件是以原始格式存储的,它的副本也是这样的格式,这使得他非常难以扩展。
你也可以自己发明特殊的编码方式,例如将结构体的成员组成一个字符串,像这样把4个int型数据组合:“12:2:-23:67”。这个例子是个改进的结构,它不再需要一次性编码解码,而且解码的代价也很小,这对简单的数据结构十分的适用。
还可以将数据序列化为xml文件,这个很好,因为xml文件易于阅读,而且还还有很多语言的库都支持处理xml文件。如果你要把数据共享给其他的应用或者是工程的话xml是个很好的选择。但是众所周知的是xml占用过多的空间(但是xml没有太多的发挥空间),它的编码解码也会影响到应用的性能。而且通常导航一个xml dom树要比导航一个类中的简单的域要复杂得多。(而且通常一个xml要比数据结构复杂得多)。
protocol buffers 可以灵活高效的处理这种问题。使用protocol buffers在编写描述你想要存储的数据结构的.proto文件后,protocol buffers的编译器会生成一个以二进制格式自动编码解码这种数据结构的类,并且提供了set和get方法。protocol buffers读写类的时候都是作为一整个单元进行的。更重要的是,protocol buffers格式支持拓展,也就是说,能够支持读写旧的格式编码的数据。
定义protocol buffers 格式
我们需要定义一个.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 文件以包名开始,它是用来防止文件名冲突。在c++中在会被编译为命名空间。
接下来就是定义消息了,一个消息就是一个定义了一系列域的容器,很多标准的简单数据类型都可以作为域类型,包括bool,int32,float,double,和string。你可以添加更多的消息类型到你的消息中,在上面的例子中,PhoneNumber就是作为Person的一个域。你甚至可以在一个消息中定义一个嵌套的消息。就像上面的PhoneNumber定义在Person里一样。如果你想要域有预定义的值的话,还可以定义枚举,在这里你可以定义一个电话号码,是MOBILE,还是HOME,还是WORK。
后面的“=1”,“=2”是给没给元素一个独立的的“标签”,这个标签会在编码为二进制的时候用到。标签数字1-15的比更高的至少少了一个字节编码,所以可以把这些标签用于公共的或者是重复较多的域,编号16以上的标签用于使用较少的域。在每个重复的字段中,每个元素都需要重新编码便签编号。使用重复的字段是个好的优化方法。
每个域都必须被下面的修饰符修饰:
required:这个值是必须提供的,否则的话就认为这个消息 是没有被初始化的,如果protocol buf使用调试模式被编译的话,序列化一个没有被初始化的消息会导致断言失败。在优化了的构建中,检查步骤被跳过了,这些消息还是会被写入,但是在解析的时候依然还是会失败(在解析方法里返回false)。除之外,required域和optional域完全相同。
optional:这个域可以不用被设定,如果可选域没有被设置的话,那么默认值就派上用场了。对一个简单的类型(举一个简单的例子),你可以指定一个默认的值,就像我们在定义电话号码类型里做的一样。否则的话,就会调用一个系统默认的值,数字类型是0,string类型是空的字符串,布尔类型就是false。对于嵌入的消息,默认值通常是 消息的"default instance"或 "prototype",它没有任何设置。可以通过调用accessor来获取optional或required的值,如果没有被设置值的话就会返回默认的值。
repeated:这个域可以重复任意多次,重复区的值将被protocol buffers保存好,可以把repeated看成是一个动态的数组。
required Is Forever :你在把字段标记为required的时候一定要十分的小心。如果由于某些原因你想把required的域改为optional的话,会有问题。旧格式的解析者如果收到没有这个域的消息的时候,可能会丢失这个消息。这个时候需要自己编写特定的验证程序。有些人认为使用required利大于弊,他完全可以用repeated取代。
protocol buffers里面没有继承。
编译你的protocol buffers:
现在你有了.proto文件,接下来要做的事情就是生成需要读写的addressBook的类,这里需要用到protocol buffers的编译器。
编译的命令:
protoc -I=源文件路径 --cpp_out=目标文件路径 xxx/*.proto protocol文件路径。
如果是输出c++则是使用cpp_out,go就是go_out。其他语言也是类似的。
这会生成两个文件:
addressbook.pb.h ,addressbookpb.cc c文件中有具体的实现。
Protocol buffers的API:
来看一下这个生成的文件,看看编译器都为我们生成了那些东西。仔细看person类,你可以看到以下内容:
// required string name = 1;bool has_name() const;void clear_name();static const int kNameFieldNumber = 1;const ::std::string& name() const;void set_name(const ::std::string& value);#if LANG_CXX11void set_name(::std::string&& value);#endifvoid set_name(const char* value);void set_name(const char* value, size_t size);::std::string* mutable_name();::std::string* release_name();void set_allocated_name(::std::string* name);// optional string email = 3;bool has_email() const;void clear_email();static const int kEmailFieldNumber = 3;const ::std::string& email() const;void set_email(const ::std::string& value);#if LANG_CXX11void set_email(::std::string&& value);#endifvoid set_email(const char* value);void set_email(const char* value, size_t size);::std::string* mutable_email();::std::string* release_email();void set_allocated_email(::std::string* email);// required int32 id = 2;bool has_id() const;void clear_id();static const int kIdFieldNumber = 2;::google::protobuf::int32 id() const;void set_id(::google::protobuf::int32 value);
你可以看到,这里的get方法是和在域里定义的一样是小写的,set方法则是以set_开头,同样required方法和optional都有一个has_方法,如果域已经被设置了那就返回的是true,否则的话就是返回的false。最后每个域都有clear方法。
但是在上面可以看到数字类型的id却只有一个基础的set方法,name和email域却有一对二外的方法,这是因为他们是Sting类型的,mutable_的get set方法可以让你直接操作指向字符串的指针。需要注意的是你可以直接调用mutable_emailm,即使email没有被设置,因为他会被自动的初始化为空,你可以在这个例子里看到,因为他也是只有mutable_方法,而没有set_方法。
repeated自段同样有一些特殊的方法,你可以看看这里的phones域,你可以看到:
检查repeated的大小,(换一句话说,有多少电话号码关联到了这个人)
用index下标获取到电话号码
在指定的下标处更新电话号码
添加你可能编辑的电话号码到消息中(repeated有一个add方法让你传一个值进去)
枚举和嵌套类型:
在生成的类中有一个PhoneType对应类proto中的枚举类型,你可以这样去引用他:Person::PhoneType,他的值就是Person::MOBILE,Person::HOME,和Person::WORK,(枚举的实现细节有一些负杂,但是不妨碍你使用他)
编译器还生成了一个嵌套的类型,你可以使用Person::PhoneNumber去调用它,如果你去看代码,你会发现这个类的真实名字是Person_PhoneNumber,但是在Person里的一个重定向声明允许你把它当做一个嵌套的类。只有这种情况下有写不同,如果你想要在另一个文件中提前声明这个类的话(Person::PhoneNumber),在C++是不能的,但是可以声明为Person_PhonNumber;
标准的消息方法:
没个消息都有一些其他的方法,让你可以检查或操作(manipulate)这个消息实体对象,它包括:
bool IsInitialized() const 用来检查是不是所有的required字段都已经被设置了。
string DebugString() const,他会返回一个可阅读的消息(representation ),在调试的时候特别(particularly )有用.
void CopyFrom(const Person& from);这个会用传进来的消息重新给消息赋值。
void Clear() 清除所有的消息。
解压和序列化:
最后,没给protocol buffers有方法去读写不选择的消息类型,使用protocol buffers二进制格式,包括以下函数:
bool SerialToSting(string *output) const;序列化消息,并把它保存在指定的字符串中需要注意的是那是二进制格式而不是文本格式,我们只是用string类型作为一个传递的容器。
bool ParseFormString(const string& data);从指定的字符解析消息。
bool SerializeToOstream(ostream * output)const; writes the message to given C++ ostream,把消息写到传入的osrteam里面。
bool PariseFromIstream(istream *input);从给定的istream解析消息。
protocol buffers 和o-o设计 protocol buffers的类只是简单的(basically dumb)持有数据,他不是一个好的面向对象模型,如果你想在生成的类中加入更多的函数,那就只有将它生成的类关联到一个特定的类。如果你没有对proto文件的支配权,关联proto生成的类是一个好办法(就是说你只是程序接受端--如果你是重用项目中的文件)在这种情况下,使用关联类去精心制作一个适应你应用程序环境的独特接口:这个接口里有隐藏(持有)数据的方法,和使用便利的函数。千万别通过继承生成的类来添加方法,他将打破继承的机制,而且也不是一个好的面向对象的设计。
写一个消息
现在来尝试一下使用protocol buffers生成的类,你首先想到的是这个应用程序要能够保存数据到你的地址簿上。首先需要创建并且填充protocol buffers的实例,再把它写到输出流中。下面的程序是从一个文件里读取地址信息,根据用户的输入生成一个新的person类,再把这个新的地址信息写入到文件中。这些都是直接调用或者引用了protocol编译器生成的代码都被高亮了--需看原文。
#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_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(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_people());{// 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 ++协议缓冲区库之前执行此宏。 它验证您是否没有意外地链接到与使用其编译的标头版本不兼容的库版本。 如果检测到版本不匹配,程序将中止。 请注意,每个.pb.cc文件在启动时都会自动调用此宏。
最后要调用ShutdownProtobufLibrary() ,否则可能会出现内存泄露。
读取消息:
#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.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;}}
}// 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 buffers
如果想要protocol兼容的话,在新的protocol中你必须注意这些:
1.不要改变已经有的标签数字
2.不要删除任何required字段
3.可以删除optional和repeated字段
3.添加新的optional或repeated字段的时候,千万不要用已经用过的标签--就算是那些已经被删除的字段的标签也不可以。
优化建议
1.尽可能的重复使用protocol buffers对,避免重新申请。
2.使用谷歌的tcmalloc申请内存,可以更的支持多线程。
高级用法:
英文原文:
https://developers.google.com/protocol-buffers/docs/cpptutorial
词汇:
keep around :身边常备有
squeezing :挤压
forward-compatible:向上兼容
backwards-compatible:向下兼容
undoubtedly: adv. 确实地,无庸置疑地
are wired for :被联结
as be:由于,并且,也,又。
notoriously :众所周知地
nested :v,嵌入;adj,嵌套的。
navigating :导航
intensive:adj,加强的;n,加强器。
impose :v ,加强,利用,占用(贬义)。
specify:指定
aggregate:n,集合;v,聚集;adj,合计的;
further:adj,更多的。
optimization :n,最优化。
modifiers:n,修饰器,修改器。
be annotated with:被注释.。
unintentionally:adj,无意的,非故意的。
implementation:n,实现。
convenient :adj,方便的,快捷的。
mechanisms:n,机制,机理。
craft :n.手艺; 工艺; 技巧; 技能; 技艺; 诡计; 手腕; 骗术;v.(尤指用手工) 精心制作;
populate:v.居住于; 生活于; 构成…的人口; 迁移; 移居; 殖民于; (给文件) 增添数据,输入数据;
instance of :实例
populate instances of :填充实例
optional field:可选栏。
Other than this:除此之外。
Looking closer :仔细观看。
forward-declare:提前声明
control over:支配,支配权。
a couple of extra methods :一对额外的方法。
句型:xxx less zzz than yyy;xxx比yyy少了zzz。
Each element in a repeated field requires re-encoding the tag number, so repeated fields are particularly good candidates for this optimization.
Required Is Forever You should be very careful about marking fields as required.
If you had a singular message field in this example, it would also have a mutable_
method but not a set_
method.
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.
这篇关于protocol buffers 基本要素:基于c++的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!