@anboqing
2016-01-10T09:44:31.000000Z
字数 11311
阅读 4265
caffe
.proto
Protocol buffers 是一种灵活、高效的序列化结构数据的自动机制--想想XML,但是它更小,更快,更简单。你只需要把你需要怎样结构化你的数据定义一次,你就能使用特殊生成的代码来方便的用多种语言从一系列数据流中读写你的结构化数据。你甚至不需要中断你用"老"结构编译好的已经部署的程序来更新你的数据结构。
你在一个名为.proto
的文件里用protocol buffer message 定义你需要序列化数据的结构。每个protocol buffer message 是一个小的信息逻辑记录,包含了一系列的name-value
对。这里有一个简单的.proto
例子,它定义了一个person
的信息。
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 type 都有一个或多个独特的属性,每个属性都有一个name
和一个value
类型,value
类型可以是numbers
( 整数或浮点数),booleans
,strings
,raw bytes
或者其他protocol buffer message types
,允许你以嵌套结构组织你的结构。你可以指定optional
,required
、和repeated
属性。你可以从 Protocol Buffer Language Guide找到更多的关于如何写.proto
文件的信息。
一旦你定义了你的信息,你就可以运行protocol buffer 编译器来编译你的.proto文件来生成特定语言的数据访问类。这些类提供了简单的对属性的访问函数(例如 name()
和set_name()
)和用来序列化整个结构到raw bytes和从raw bytes 解析出结构的函数。例如,假如你使用的是c++语言,用编译器编译上面那个person
的.proto
文件会生成一个Person
类。你可以在你的应用里用这个类来操纵Person
类的对象。比如,你可能会写一些这样的代码:
Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);
然后,你可以通过这样的代码来把你的message读进来:
fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
你可以给你的message添加新的属性而不打破向后兼容性(backwards-compatibility);旧的二进制文件仅仅在编译的时候忽略那些新的属性。这样一来,如果你有一个通信协议使用了protocol buffers当做它传输的数据格式,你可以扩展你的通信协议而不用担心破坏现有的代码。
你可以在API Reference section找到完整的文档,并且你可以在Protocol buffer encoding找出关于protocol buffer 编码的更多信息.
Protocol buffers相对XML在序列化数据的时候有很多优势。protocol buffers :
例如,假若你要给person
建模,它有name
和email
属性。在XML里,你需要:
<person>
<name>John Doe</name>
<email>jdoe@example.com</email>
</person>
用protocol buffer message(在protocol buffer 的text format)是这样的
# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
name: "John Doe"
email: "jdoe@example.com"
}
当上面这段代码被编译成binary format
(上面那段text format只是为了方便人类读写编辑的)的时候,它可能只占28字节长,仅仅需要100~200纳秒就能编译。那个XML版本即使移除所有空白也至少需要69字节,并且需要5000~10000纳秒来编译。
同样,操作protocol buffer 更容易:
cout << "Name: " << person.name() << endl;
cout << "E-mail:" << person.email() << endl;
然而如果使用XML你需要这样做:
cout << "Name: "
<< person.getElementsByTagName("name")->item(0)->innerText()
<< endl;
cout << "E-mail: "
<< person.getElementsByTagName("email")->item(0)->innerText()
<< endl;
下载地址--这个包包含了完整的c++,python和java语言的编译器源代码,和I/O和测试的类。安装请参阅README。
一旦你安装好了,就可以跟着入门教程来学习了。
这个教程会带你走一遍使用protocol buffer的流程,创建一个简单的实例程序,学会基本的使用方法:
.proto
文件里定义信息格式在这个教程里我们要创建一个简单的“地址簿”程序来在文件里读写人们的联系人信息。每个人都有一个name,id,email address和一个联系电话。
你怎样序列化和读取这样一个结构数据呢?这里有三种方法:
protocol buffers 灵活高效,可以解决上述问题。你只需要编写一个.proto
文件来描述你要使用的数据结构。protocol buffer 编译器可以把.proto
文件编译成一个类似于ORM(object relation mapping)实现类的数据访问类,这个类可以把高效的用二进制文件方式存储的数据读写出来。更多的是,它提供了一种向后兼容的扩展机制,使你可以不用担心兼容性问题来扩展你的数据格式。
为了创建地址簿程序,你需要首先定义一个.proto
文件。定义.proto
文件十分简单: 你添加一个 message
给你想序列化的每个数据结构 ,然后指定一个 name
和一个type
给message
的每个属性。下面是一个.proto
文件,定义了地址簿数据结构,addressbook.proto
:
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 person = 1;
}
protobuffer支持的内建数据类型包括bool
,int32
,float
,double
,string
.
注意:message可以嵌套,比如 PhoneNumber 就定义在Person里。
“=1”,“=2”标记了每个元素的唯一“tag”,这是用在二进制编码里的。使用1-15可以在1个字节里表示这些tag,节省空间,一般把常用的需要大量重复的元素使用1-15来编码,把16以上的tag留给不常用的元素。
每个属性必须标记为下列修饰符之一:
required
: 故名思议就是必须提供值的属性,当你把属性设置为required的时候要小心,因为如果以后想修改为其他类型,老的读取类就不兼容新的数据了。optional
: 就是可以不提供值的属性,如果没有提供值,会设置为默认值。默认值可以自己提供,如果没有自己提供默认值,会设置为系统默认值:numeric类型会置为0,字符串置为空串,bool置为false;对于内嵌类型,默认值永远是空实例。repeated
:就是可能重复任意次(包含0次).重复值的顺序会在二进制文件保存下来,可以把重复的属性看做动态大小的数组。注意,由于历史原因,repeated
数值属性不能有效的被编码成二进制,新的代码可以使用[packed=true]
来获得更好的编码效率 例如:
repeated int32 samples = 4 [packed=true];
protoc -I=\$SRC_DIR --cpp_out=\$DST_DIR \$SRC_DIR/addressbook.proto
addressbook.pb.h
addressbook.pb.cc
我们现在来看一些生成的code是什么样的,编译器为我们生成了什么类和函数呢?
如果我们打开tutorial.pb.h
,我们会看到编译器给我们在.proto
文件里定义的每一个message
都生成了一个class,我们再看Person
类,会发现编译器给message的每个属性都生成了getters和setters,例如,对于name
,id
,email
,和phone
属性,我们可以找到这些函数:
// 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();
// id
inline bool has_id() const;
inline void clear_id();
inline int32_t id() const;
inline void set_id(int32_t value);
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();
// phone
inline int phone_size() const;
inline void clear_phone();
inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const;
inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone();
inline const ::tutorial::Person_PhoneNumber& phone(int index) const;
inline ::tutorial::Person_PhoneNumber* mutable_phone(int index);
inline ::tutorial::Person_PhoneNumber* add_phone();
正如你所能看到的那样,getters和小写属性名一样,setters以set_
开头。还有has_
开头的判断是否设置了值的函数。还有clear_
开头的函数用于清空设置的值。
不同类型的属性方法不尽相同,例如 id
只有基本的getter,setter方法,而name
,email
等字符串类型的属性多了一个mutable_
开头的getter,和一个多出来的setter。即使还没有设置email
仍然可以调用mutable_email
。它可以自动初始化为一个空字符串。
repeated
属性同样有些特别的方法,例如phone
属性:
_size
(这个人有多少个电话号码)index
访问一个特定的值add_
方法)更多关于编译器生成函数的信息请参看C++ generated code reference
生成的代码包含了一个PhoneType
枚举对应你的.proto
文件里的enum.你可以通过Person::PhoneType
来使用这个枚举,和它的值Person::MOBILE
,Person::HOME
,Person::WORK
(具体实现很复杂,但我们不需要了解它)
编译器同样生成了一个嵌套类Person::PhoneNumber
。如果查看代码,会发现实际的类是叫做Person_PhoneNumber
,但是使用了一个typedef
来重命名了它,唯一的区别是当你想在另一个文件里前向声明这个类的时候,必须使用Person_PhoneNumber
来前向声明它。
每个message类还包含了一些其他方法来使你能检查或者操作整个message,包括:
bool IsInitialized() const
;: checks if all the required fields have been set.string DebugString() const
;: returns a human-readable representation of the message, particularly useful for debugging.void CopyFrom(const Person& from)
;: overwrites the message with the given message's values.void Clear()
;: clears all the elements back to the empty state. 最终,每个protocol buffer class使用读写方法来解析和序列化message到二进制文件里,这些方法包括:
bool SerializeToString(string* output) const
;: 序列化一个message并且把字节文件存储到string里,这里使用string仅仅是为了把它当做一个方便的容器.bool ParseFromString(const string& data)
;: 从指定的string里解析messagebool SerializeToOstream(ostream* output) const
;: 把message写到指定的c++`ostream`里。bool ParseFromIstream(istream* input)
;: 从指定的c++istream
读取message查看Message API获取更详细内容.
现在,让我们试着使用编译器为我们生成的类。我们让我们的地址簿程序做的第一件事情是把一个人的个人信息写到地址簿文件里。我们需要生成一个该类的实例然后把它写入到输出流里。
这里有一个实例程序:
#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_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;
}
}
}
// 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_person());
{
// 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++ Protocol Buffer 之前执行这个宏是一个好的习惯(尽管不是强制要求的)。它会验证你是否链接了正确的库,防止你链接版本不匹配的库。
注意代码中的ShutdownProtobufLibrary()
,它会清楚所有protocol buffer libarary分配的全局对象。通常这是不需要的,因为这个进程总是会退出,系统会接管剩下的内存。但是,如果你使用了一个内存泄露检查工具,比如valgrand
之类的,这类工具会要求你把所有分配的内存释放掉,或者你在写一个库文件,这个库文件会被同一个进程加载和卸载多次,这两种情况你就需要清理所有东西。
这是一个从二进制文件读取地址簿的例子:
#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.person_size(); i++) {
const tutorial::Person& person = address_book.person(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;
}
}
}
// 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 buffer后改进你的protocol buffer定义。如果你希望你的新buffer能够向前兼容,而你的老buffer能向后兼容,那么你就需要遵守下面这几个规则:
tag
数字required
属性repeated
或者optional
属性repeated
或者optional
属性,但是必须使用新tag number