什么是 protocol buffers ?

Protocol buffers 是一种灵活、高效的序列化结构数据的自动机制--想想XML,但是它更小,更快,更简单。你只需要把你需要怎样结构化你的数据定义一次,你就能使用特殊生成的代码来方便的用多种语言从一系列数据流中读写你的结构化数据。你甚至不需要中断你用”老”结构编译好的已经部署的程序来更新你的数据结构。


它是怎样工作的?

你在一个名为.proto的文件里用protocol buffer message 定义你需要序列化数据的结构。每个protocol buffer message 是一个小的信息逻辑记录,包含了一系列的name-value对。这里有一个简单的.proto例子,它定义了一个person的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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类的对象。比如,你可能会写一些这样的代码:

1
2
3
4
5
6
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读进来:

1
2
3
4
5
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 编码的更多信息.


为什么不用XML等其他技术?

Protocol buffers相对XML在序列化数据的时候有很多优势。protocol buffers :

  • 更简单
  • 比XML小3到10倍
  • 比XML快20到100倍
  • 更少歧义
  • 可以生成方便编程的数据访问类

例如,假若你要给person建模,它有nameemail属性。在XML里,你需要:

1
2
3
4
<person>
<name>John Doe</name>
<email>jdoe@example.com</email>
</person>

用protocol buffer message(在protocol buffer 的text format)是这样的

1
2
3
4
5
6
# 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 更容易:

1
2
cout << "Name: " << person.name() << endl;
cout << "E-mail:" << person.email() << endl;

然而如果使用XML你需要这样做:

1
2
3
4
5
6
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。

一旦你安装好了,就可以跟着入门教程来学习了。


入门教程c++版

这个教程会带你走一遍使用protocol buffer的流程,创建一个简单的实例程序,学会基本的使用方法:

  • .proto文件里定义信息格式
  • 使用protocol buffer编译器
  • 使用c++ protocol buffer API来读写信息

为什么使用protocol buffers?

在这个教程里我们要创建一个简单的“地址簿”程序来在文件里读写人们的联系人信息。每个人都有一个name,id,email address和一个联系电话。

你怎样序列化和读取这样一个结构数据呢?这里有三种方法:

  • 内存中原始的字节数据结构可以存储为2进制形式。这种方法很脆弱,因为读取代码必须用同样的内存布局编译,还要考虑使用相同的内存大小端等等。当文件积累了很多数据之后,拷贝到处都是,扩展结构就很困难了。
  • 你可以发明一个点对点的方式来把数据编码为一个简单的字符串—例如编码4个整数为”12:3:-23:67”.这是一个简单且灵活的方法,尽管它需要你编写一次性的读写代码,读取需要一些运行时间。这种方法适用于编码十分简单的数据。
  • 序列化数据到XML文件。如果你需要和其他程序共享数据,那么这将是个好方法。然而,XML占内存已经臭名昭著了,解析编码它会造成程序性能大幅下降。在XML DOM tree里巡弋也远比在类里查找属性复杂的多。

protocol buffers 灵活高效,可以解决上述问题。你只需要编写一个.proto文件来描述你要使用的数据结构。protocol buffer 编译器可以把.proto文件编译成一个类似于ORM(object relation mapping)实现类的数据访问类,这个类可以把高效的用二进制文件方式存储的数据读写出来。更多的是,它提供了一种向后兼容的扩展机制,使你可以不用担心兼容性问题来扩展你的数据格式。


定义你的protocol Format

为了创建地址簿程序,你需要首先定义一个.proto文件。定义.proto文件十分简单: 你添加一个 message 给你想序列化的每个数据结构 ,然后指定一个 name和一个typemessage的每个属性。下面是一个.proto文件,定义了地址簿数据结构,addressbook.proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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]来获得更好的编码效率

    例如: 

    int32 samples = 4 [packed=true];```
    1
    2
    3
    4
    5
    6
    ---
    ### 编译你的protocol buffers文件
    现在你拥有一个`.proto`文件,接下来你需要生成一个读写你的`AddressBook`类的访问类。你需要用protocol buffer编译器`protoc`来编译你的`.proto`文件:
    > ```protoc -I=\$SRC_DIR --cpp_out=\$DST_DIR \$SRC_DIR/addressbook.proto

cpp_out可以换成python_out或者java_out
编译完成后,就会在DST_DIR下面生成2个文件:

  • addressbook.pb.h
  • addressbook.pb.cc

Protocol Buffer API

我们现在来看一些生成的code是什么样的,编译器为我们生成了什么类和函数呢?
如果我们打开tutorial.pb.h,我们会看到编译器给我们在.proto文件里定义的每一个message都生成了一个class,我们再看Person类,会发现编译器给message的每个属性都生成了getters和setters,例如,对于name,id,email,和phone属性,我们可以找到这些函数:

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
// 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);
// 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();
// 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类还包含了一些其他方法来使你能检查或者操作整个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里解析message
  • bool SerializeToOstream(ostream* output) const;: 把message写到指定的c++`ostream`里。
  • bool ParseFromIstream(istream* input);: 从指定的c++istream读取message

查看Message API获取更详细内容.


写一个Message

现在,让我们试着使用编译器为我们生成的类。我们让我们的地址簿程序做的第一件事情是把一个人的个人信息写到地址簿文件里。我们需要生成一个该类的实例然后把它写入到输出流里。

这里有一个实例程序:

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
#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之类的,这类工具会要求你把所有分配的内存释放掉,或者你在写一个库文件,这个库文件会被同一个进程加载和卸载多次,这两种情况你就需要清理所有东西。


读取一个Message

这是一个从二进制文件读取地址簿的例子:

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
#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后改进你的protocol buffer定义。如果你希望你的新buffer能够向前兼容,而你的老buffer能向后兼容,那么你就需要遵守下面这几个规则:

  • 不要修改tag数字
  • 不要增删任何required属性
  • 可以删除repeated或者optional属性
  • 可以添加 repeated或者optional属性,但是必须使用新tag number

Comments

2016-01-10

⬆︎TOP