ProtoBuf是一种灵活高效的独立于语言平台的结构化数据表示方法,可用于表示通信协议和数据存储等各方面,与XML相比,ProtoBuF更小更快更简单。你可以用定义自己ProtoBuf的数据结构,用ProtoBuf编译器生成特定语言的源代码,(如C++,Java,Python等,目前 ProtoBuf对主流的编程语言都提供了支持)方便的进行序列化和反序列化。
protobuf是google旗下的一款平台无关,语言无关,可扩展的序列化结构数据格式。所以很适合用做数据存储和作为不同应用,不同语言之间相互通信的数据交换格式,只要实现相同的协议格式即同一proto文件被编译成不同的语言版本,加入到各自的工程中去。这样不同语言就可以解析其他语言通过 protobuf序列化的数据。目前官网提供了C++、Python、JAVA、GO等语言的支持。
1. ProtoBuf安装
protobuf包含两部分,一个是protobuf编译器,用于解析编译.proto文件为.pb.cc和.pb.h文件,另一部分是protobuf runtime。不同编程语言有不同的runtime,主流语言基本都支持,如C++,Java,Python,Go等。
1.1 protoc安装
可直接从https://github.com/protocolbuffers/protobuf/releases安装稳定版本。
注:releases包含protoc和runtime两部分,分别属于不同安装包。C++版本源码包含protoc,不需要另外安装protoc,但其他语言都需要单独安装protoc。
1.2 protobuf runtime安装
以C++为例,参考:https://github.com/protocolbuffers/protobuf/tree/master/src。
git clone https://github.com/protocolbuffers/protobuf.git cd protobuf git submodule update --init --recursive ./autogen.sh ./configure make make check sudo make install sudo ldconfig # refresh shared library cache.
注:编译的库文件位于src目录下。
2. ProtoBuf使用
2.1应用protobuf步骤
-
在.proto文件中定义message格式。
-
用protoc编译.proto文件,生成.pb.cc和.pb.h文件。
-
运用C++ protocol buffer API读写messages。
2.2应用protobuf示例
在一个后缀名为.pro的文件中定义你所需的任何需要序列化和反序列的结构,类似于中我们C/C++语言中的结构体定义,也类似于JSON类定义,ProtoBuf文件的每一个属性都是一个键值对,引用谷歌官方的例子说明https://developers.google.cn/protocol-buffers/docs/cpptutorial,一个包含Person信息的.pro文件。
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 phone = 4; } message AddressBook{ repeated Person people = 1; }
.pro 文件的每一个属性都是一个包含类型和名称的对,protobuf的类型可以是string、int、bool,或者是其他ProtoBuf源文件中定位的类型等。还需指定该属性的一些特殊字段,比如optional表示该属性是可选的,required表示该属性是必须的,repeated表示该属性是一个list的集合(不知道用list类似形容是否合适,就差不多意思吧,包含1个或多个元素),包含了若干个相同类型的值,不过在protoBuf 3中,optional和required都不需要的了,如果配了两个,protoBuf 编译器还会报错,但是repeated的还是保留的,用来说明该字段是一个list。
As you can see, each field in the message definition has a unique number. These numbers are used to identify your fields in the message binary format, and should not be changed once your message type is in use. Note that field numbers in the range 1 through 15 take one byte to encode, including the field number and the field's type (you can find out more about this in Protocol Buffer Encoding). Field numbers in the range 16 through 2047 take two bytes. So you should reserve the field numbers 1 through 15 for very frequently occurring message elements. Remember to leave some room for frequently occurring elements that might be added in the future. https://developers.google.cn/protocol-buffers/docs/proto
.protoBuf定义完以后,就可以使用ProtoBuf编译器进行编译,编译时需要指定你需要生成的代码的语言类型。.pro文件编译成功后会生成*.pro.pb.hh 和*.pro.pb.cc两个文件,*和.pro源文件名称相同。在生成的文件中会生成一个你定义的消息类型的的类以及读取和设置该消息类型各个属性的 set和Get方法等,比如上例中会生成一个Person类,和name(),set_name()这样的成员方法,以及序列化和反序列的方法,你可以用 这些方法方便的读取和设置相应的属性,以及序列化和反序列化方法。
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
写message
#include <iostream> #include <fstream> #include <string> #include "addressbook.pb.h" using namespace std; //using namespace caffe; void PromptForAddress(tutorial::Person * person) { cout << "Enter person ID number:"; int id; cin >> id; person->set_id(id); cin.ignore(256, ' '); 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; } } } 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(!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; } google::protobuf::ShutdownProtobufLibrary(); return 0; }
编译命令:
g++ write.cpp addressbook.pb.cc -o write `pkg-config --cflags --libs protobuf` -std=c++11
读message
#include <iostream> #include <fstream> #include <string> #include "addressbook.pb.h" using namespace std; void PrintInfo(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.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; } } } 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; } PrintInfo(address_book); google::protobuf::ShutdownProtobufLibrary(); return 0; }
编译命令:
g++ read.cpp addressbook.pb.cc -o read `pkg-config --cflags --libs protobuf` -std=c++11
执行
$ ./write test_addr test_addr: File not found. Creating a new file. Enter person ID number:10000 Enter name :wang Enter email address (blank for none): 123456@163.com Enter a phone number ( or leave blank to finish):18000000000 Is this a mobile, home, or work phone?home Enter a phone number ( or leave blank to finish): $ cat test_addr * wang▒N123456@163.com" $ ./read Usage: ./read ADDRESS_BOOK_FILE wang@ubuntu-wang:~/repository/protoc/example_me$ ./read test_addr Person ID: 10000 Name: wang E-mail address: 123456@163.com Home phone #: 18000000000
3. ProtoBuf兼容性
ProtoBuf的一个亮点是提供了向前和向后的兼容性,你可以再你定义的消息格式中增加字段或者删除字段,当消息由一个增加字段后的系统向一个老 版本系统发送消息时,老版本解析该消息格式后会直接忽略新增的字段,而当消息从老版本发向新版本时,新版本解析该消息时会将新增的字段设置为默认值,所以 你不需要担心在系统中增加消息字段后的兼容问题以及版本升级配套问题。
参考: