ProtoBuffer使用详解

Critical Analyze What You Read and Hear
批判地分析你读到的和听到的

Andrew Hunt/David Thomas程序员修炼之道

Protocol Buffer是Google 的一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。

定义一个Protocol Buffer消息

使用实例

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
# 定义一个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;
}

细节解释

1、package declaration为了阻止不同工程间的naming conflicts,这儿的tutorial相当于namespace。

2、message是一个包含若干类型字段的集合,可以使用bool、int32、float、double和string类型。可以内嵌message集合,类似于struct。

3、“=1”、“2”记号标识在二进制编码中类型字段的独特Tag,表示不同的字段在序列化后的二进制数据中的布局位置。

Tag number 1-15相对于更高的数字,少用了一个字节,所以可以使用1-15的Tag作为commonly used的repeated elements,16或者更高的Tag留给less-commonly use留给optional elements。

4、每个字段都必须使用如下标示符

  • required:字段值必须被提供,否则消息会被认为uninitialized。
  • optional:字段值可选
  • repeated:字段也许会被重复任何次数(包括0次)。可以将repeated field看做动态大小数组。

5、enum是枚举类型定义的关键字,0和1表示枚举值所对应的实际整型值,和C/C++一样,可以为枚举值指定任意整型值,而无需总是从0开始定义。

6、可以在同一个.proto文件中定义多个message,这样便可以很容易的实现嵌套消息的定义。Protocol Buffer提供了另外一个关键字import,这样我们便可以将很多通用的message定义在同一个.proto文件中,而其他消息定义文件可以通过import的方式将该文件中定义的消息包含进来,如:

1
import "myproject/CommonMessages.proto"

限定符(required/optional/repeated)的基本规则

1、在每个消息中必须至少留有一个required类型的字段。

2、每个消息中可以包含0个或多个optional类型的字段。

3、repeated表示的字段可以包含0个或多个数据。

4、如果打算在原有消息协议中添加新的字段,同时还要保证老版本的程序能够正常读取或写入,那么对于新添加的字段必须是optional或repeated。道理非常简单,老版本程序无法读取或写入新增的required限定符的字段。

Protocol Buffer消息升级原则

1、不要修改已经存在字段的标签号。

2、任何新添加的字段必须是optional和repeated限定符,否则无法保证新老程序在互相传递消息时的消息兼容性。

3、在原有的消息中,不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但是他们之前使用的标签号必须被保留,不能被新的字段重用。

4、int32、uint32、int64、uint64和bool等类型之间是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之间是兼容的,这意味着如果想修改原有字段的类型时,为了保证兼容性,只能将其修改为与其原有类型兼容的类型,否则就将打破新老消息格式的兼容性。

5、optional和repeated限定符也是相互兼容的。

编译你的Protocol Buffers

编译方法

1
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

注:$SRC_DIR为Source Directory,$DST_DIR为Destination Directory,编译后,Destination Directory将会有以下两个文件

1
2
addressbook.pb.h:头文件,声明产生的类
addressbook.pb.cc:cpp文件,实现产生的类。

ProtoBuffer API

经过编译后我们能够得到下面这些消息API函数

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 ();

标准Message方法

每个消息都会包含一系列其他方法,允许你检查或者操作整个消息:

1
2
3
4
bool IsInitialized () const ; /* 检查是否所有required field被设置 */
string DebugString () const ; /* 返回一个有关消息的可读描述,对于调试很有用 */
void CopyFrom (const Person & from ); /* 用给定的消息值来重写消息 */
void Clear (); /* 将所有元素清空到empty state */

使用你的ProtocBuffer

解析和序列化(Parsing and Serialization)

每个Protocol buffer类都有若干函数,这些函数能使用Protocol buffer binary format,来写入和读取你所选择的信息。

1
2
3
4
bool SerializeToString (string * output ) const ; /* serializes the message and stores the bytes in the given string. Note that the bytes are binary, not text; we only use the string class as a convenient container. */
bool ParseFromString (const string & data ); /* parses a message from the given string. */
bool SerializeToOstream (ostream * output ) const ; /* writes the message to the given C++ ostream. */
bool ParseFromIstream (istream * input ); /* parses a message from the given C++ istream. */

Writing A 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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#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;
}

Reading A 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
#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;
}

参考链接



本文作者:ZeroJiu
本文链接: http://www.freehacker.cn/tools/protobuffer/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!
温馨提示:开启科学上网访问本站,能获得更好的阅读体验,并启用Disqus评论功能和作者交流。