簡介
什么是 Google Protocol Buffer? 假如您在網(wǎng)上搜索,應(yīng)該會得到類似這樣的文字介紹:
Google Protocol Buffer( 簡稱 Protobuf) 是 Google 公司內(nèi)部的混合語言數(shù)據(jù)標準,目前已經(jīng)正在使用的有超過 48,162 種報文格式定義和超過 12,183 個 .proto 文件。他們用于 RPC 系統(tǒng)和持續(xù)數(shù)據(jù)存儲系統(tǒng)。
Protocol Buffers 是一種輕便高效的結(jié)構(gòu)化數(shù)據(jù)存儲格式,可以用于結(jié)構(gòu)化數(shù)據(jù)串行化,或者說序列化。它很適合做數(shù)據(jù)存儲或 RPC 數(shù)據(jù)交換格式??捎糜谕ㄓ崊f(xié)議、數(shù)據(jù)存儲等領(lǐng)域的語言無關(guān)、平臺無關(guān)、可擴展的序列化結(jié)構(gòu)數(shù)據(jù)格式。目前提供了 C++、Java、Python 三種語言的 API。
或許您和我一樣,在第一次看完這些介紹后還是不明白 Protobuf 究竟是什么,那么我想一個簡單的例子應(yīng)該比較有助于理解它。
Google Protocol Buffer Demo
安裝Google Protocol Buffer
在網(wǎng)站 http://code.google.com/p/protobuf/downloads/list上可以下載 Protobuf 的源代碼。然后解壓編譯安裝便可以使用它了。
安裝步驟如下所示:
tar -xzf protobuf-2.1.0.tar.gz
cd protobuf-2.1.0
./configure --prefix=$INSTALL_DIR
make
make check
make install
Demo描述
我打算使用 Protobuf 和 C++ 開發(fā)一個十分簡單的例子程序。
該程序由兩部分組成。第一部分被稱為 Writer,第二部分叫做 Reader。
Writer 負責將一些結(jié)構(gòu)化的數(shù)據(jù)寫入一個磁盤文件,Reader 則負責從該磁盤文件中讀取結(jié)構(gòu)化數(shù)據(jù)并打印到屏幕上。
準備用于演示的結(jié)構(gòu)化數(shù)據(jù)是 HelloWorld,它包含兩個基本數(shù)據(jù):
- ID,為一個整數(shù)類型的數(shù)據(jù)
- Str,這是一個字符串
創(chuàng)建編寫.proto 文件
首先我們需要編寫一個 proto 文件,定義我們程序中需要處理的結(jié)構(gòu)化數(shù)據(jù),在 protobuf 的術(shù)語中,結(jié)構(gòu)化數(shù)據(jù)被稱為 Message。proto 文件非常類似 java 或者 C 語言的數(shù)據(jù)定義。代碼清單 1 顯示了例子應(yīng)用中的 proto 文件內(nèi)容。
清單1. lm.helloworld.proto
// lm.helloworld.proto
package lm;
message helloworld
{
required int32 id = 1; // ID
required string str = 2; // str
optional int32 opt = 3; //optional field
}
一個比較好的習(xí)慣是認真對待 proto 文件的文件名。比如將命名規(guī)則定于如下:
packageName.MessageName.proto
在上例中,package 名字叫做 lm,定義了一個消息 helloworld,該消息有三個成員,類型為 int32 的 id,另一個為類型為 string 的成員 str。opt 是一個可選的成員,即消息中可以不包含該成員。
編譯 .proto 文件
寫好 proto 文件之后就可以用 Protobuf 編譯器將該文件編譯成目標語言了。本例中我們將使用 C++。
假設(shè)您的 proto 文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一個目錄下,則可以使用如下命令:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
命令將生成兩個文件:
- lm.helloworld.pb.h , 定義了 C++ 類的頭文件
- lm.helloworld.pb.cc , C++ 類的實現(xiàn)文件
在生成的頭文件中,定義了一個 C++ 類 helloworld,后面的 Writer 和 Reader 將使用這個類來對消息進行操作。諸如對消息的成員進行賦值,將消息序列化等等都有相應(yīng)的方法。
編寫 writer 和 Reader
如前所述,Writer 將把一個結(jié)構(gòu)化數(shù)據(jù)寫入磁盤,以便其他人來讀取。假如我們不使用 Protobuf,其實也有許多的選擇。一個可能的方法是將數(shù)據(jù)轉(zhuǎn)換為字符串,然后將字符串寫入磁盤。轉(zhuǎn)換為字符串的方法可以使用 sprintf(),這非常簡單。數(shù)字 123 可以變成字符串”123”。
這樣做似乎沒有什么不妥,但是仔細考慮一下就會發(fā)現(xiàn),這樣的做法對寫 Reader 的那個人的要求比較高,Reader 的作者必須了 Writer 的細節(jié)。比如”123”可以是單個數(shù)字 123,但也可以是三個數(shù)字 1,2 和 3,等等。這么說來,我們還必須讓 Writer 定義一種分隔符一樣的字符,以便 Reader 可以正確讀取。但分隔符也許還會引起其他的什么問題。最后我們發(fā)現(xiàn)一個簡單的 Helloworld 也需要寫許多處理消息格式的代碼。
如果使用 Protobuf,那么這些細節(jié)就可以不需要應(yīng)用程序來考慮了。
使用 Protobuf,Writer 的工作很簡單,需要處理的結(jié)構(gòu)化數(shù)據(jù)由 .proto 文件描述,經(jīng)過上一節(jié)中的編譯過程后,該數(shù)據(jù)化結(jié)構(gòu)對應(yīng)了一個 C++ 的類,并定義在 lm.helloworld.pb.h 中。對于本例,類名為 lm::helloworld。
Writer 需要 include 該頭文件,然后便可以使用這個類了。
現(xiàn)在,在 Writer 代碼中,將要存入磁盤的結(jié)構(gòu)化數(shù)據(jù)由一個 lm::helloworld 類的對象表示,它提供了一系列的 get/set 函數(shù)用來修改和讀取結(jié)構(gòu)化數(shù)據(jù)中的數(shù)據(jù)成員,或者叫 field。
當我們需要將該結(jié)構(gòu)化數(shù)據(jù)保存到磁盤上時,類 lm::helloworld 已經(jīng)提供相應(yīng)的方法來把一個復(fù)雜的數(shù)據(jù)變成一個字節(jié)序列,我們可以將這個字節(jié)序列寫入磁盤。
對于想要讀取這個數(shù)據(jù)的程序來說,也只需要使用類 lm::helloworld 的相應(yīng)反序列化方法來將這個字節(jié)序列重新轉(zhuǎn)換會結(jié)構(gòu)化數(shù)據(jù)。這同我們開始時那個“123”的想法類似,不過 Protobuf 想的遠遠比我們那個粗糙的字符串轉(zhuǎn)換要全面,因此,我們不如放心將這類事情交給 Protobuf 吧。代碼清單2列出了Writer的核心代碼。
清單2. Writer.cpp代碼
#include "lm.helloworld.pb.h"
…
int main(void)
{
lm::helloworld msg1;
msg1.set_id(101);
msg1.set_str(“hello”);
// Write the new address book back to disk.
fstream output("./log", ios::out | ios::trunc | ios::binary);
if (!msg1.SerializeToOstream(&output)) {
cerr << "Failed to write msg." << endl;
return -1;
}
return 0;
}
其中,Msg1 是一個 helloworld 類的對象,set_id() 用來設(shè)置 id 的值。SerializeToOstream 將對象序列化后寫入一個 fstream 流。
代碼清單 3 列出了 reader 的主要代碼。
清單2. Reader.cpp代碼
#include "lm.helloworld.pb.h"
…
void ListMsg(const lm::helloworld & msg) {
cout << msg.id() << endl;
cout << msg.str() << endl;
}
int main(int argc, char* argv[])
{
lm::helloworld msg1;
{
fstream input("./log", ios::in | ios::binary);
if (!msg1.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
ListMsg(msg1);
…
}
同樣,Reader 聲明類 helloworld 的對象 msg1,然后利用 ParseFromIstream 從一個 fstream 流中讀取信息并反序列化。此后,ListMsg 中采用 get 方法讀取消息的內(nèi)部信息,并進行打印輸出操作。
運行結(jié)果
運行 Writer 和 Reader 的結(jié)果如下:
>writer
>reader
101
Hello
Reader 讀取文件 log 中的序列化信息并打印到屏幕上。
這個例子本身并無意義,但只要您稍加修改就可以將它變成更加有用的程序。比如將磁盤替換為網(wǎng)絡(luò) socket,那么就可以實現(xiàn)基于網(wǎng)絡(luò)的數(shù)據(jù)交換任務(wù)。而存儲和交換正是 Protobuf 最有效的應(yīng)用領(lǐng)域。
和其他類似技術(shù)的比較
看完這個簡單的例子之后,希望您已經(jīng)能理解 Protobuf 能做什么了,那么您可能會說,世上還有很多其他的類似技術(shù)啊,比如 XML,JSON,Thrift 等等。和他們相比,Protobuf 有什么不同呢?
簡單說來 Protobuf 的主要優(yōu)點就是:簡單,快。
這有測試為證,項目 thrift-protobuf-compare 比較了這些類似的技術(shù),圖 1 顯示了該項目的一項測試結(jié)果,Total Time.

Total Time 指一個對象操作的整個時間,包括創(chuàng)建對象,將對象序列化為內(nèi)存中的字節(jié)序列,然后再反序列化的整個過程。從測試結(jié)果可以看到 Protobuf 的成績很好,感興趣的讀者可以自行到網(wǎng)站 [https://github.com/eishay/jvm-serializers/wiki)上了解更詳細的測試結(jié)果。
Protobuf 的優(yōu)點
Protobuf 有如 XML,不過它更小、更快、也更簡單。你可以定義自己的數(shù)據(jù)結(jié)構(gòu),然后使用代碼生成器生成的代碼來讀寫這個數(shù)據(jù)結(jié)構(gòu)。你甚至可以在無需重新部署程序的情況下更新數(shù)據(jù)結(jié)構(gòu)。只需使用 Protobuf 對數(shù)據(jù)結(jié)構(gòu)進行一次描述,即可利用各種不同語言或從各種不同數(shù)據(jù)流中對你的結(jié)構(gòu)化數(shù)據(jù)輕松讀寫。
它有一個非常棒的特性,即“向后”兼容性好,人們不必破壞已部署的、依靠“老”數(shù)據(jù)格式的程序就可以對數(shù)據(jù)結(jié)構(gòu)進行升級。這樣您的程序就可以不必擔心因為消息結(jié)構(gòu)的改變而造成的大規(guī)模的代碼重構(gòu)或者遷移的問題。因為添加新的消息中的 field 并不會引起已經(jīng)發(fā)布的程序的任何改變。
Protobuf 語義更清晰,無需類似 XML 解析器的東西(因為 Protobuf 編譯器會將 .proto 文件編譯生成對應(yīng)的數(shù)據(jù)訪問類以對 Protobuf 數(shù)據(jù)進行序列化、反序列化操作)。
使用 Protobuf 無需學(xué)習(xí)復(fù)雜的文檔對象模型,Protobuf 的編程模式比較友好,簡單易學(xué),同時它擁有良好的文檔和示例,對于喜歡簡單事物的人們而言,Protobuf 比其他的技術(shù)更加有吸引力。
Protobuf 的不足
Protbuf 與 XML 相比也有不足之處。它功能簡單,無法用來表示復(fù)雜的概念。
XML 已經(jīng)成為多種行業(yè)標準的編寫工具,Protobuf 只是 Google 公司內(nèi)部使用的工具,在通用性上還差很多。
由于文本并不適合用來描述數(shù)據(jù)結(jié)構(gòu),所以 Protobuf 也不適合用來對基于文本的標記文檔(如 HTML)建模。另外,由于 XML 具有某種程度上的自解釋性,它可以被人直接讀取編輯,在這一點上 Protobuf 不行,它以二進制的方式存儲,除非你有 .proto 定義,否則你沒法直接讀出 Protobuf 的任何內(nèi)容
高級應(yīng)用話題
更復(fù)雜的 Message
到這里為止,我們只給出了一個簡單的沒有任何用處的例子。在實際應(yīng)用中,人們往往需要定義更加復(fù)雜的 Message。我們用“復(fù)雜”這個詞,不僅僅是指從個數(shù)上說有更多的 fields 或者更多類型的 fields,而是指更加復(fù)雜的數(shù)據(jù)結(jié)構(gòu):
嵌套 Message
代碼清單 4 給出一個嵌套 Message 的例子。
清單 4. 嵌套 Message 的例子
message Person {
required string name = 1;
required int32 id = 2; // Unique ID number for this person.
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 Person 中,定義了嵌套消息 PhoneNumber,并用來定義 Person 消息中的 phone 域。這使得人們可以定義更加復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。
Import Message
在一個 .proto 文件中,還可以用 Import 關(guān)鍵字引入在其他 .proto 文件中定義的消息,這可以稱做 Import Message,或者 Dependency Message。例子如下所示:
清單 5. Import Message
import common.header;
message youMsg{
required common.info_header header = 1;
required string youPrivateData = 2;
}
其中 ,common.info_header定義在common.header包內(nèi)。
Import Message 的用處主要在于提供了方便的代碼管理機制,類似 C 語言中的頭文件。您可以將一些公用的 Message 定義在一個 package 中,然后在別的 .proto 文件中引入該 package,進而使用其中的消息定義。
Google Protocol Buffer 可以很好地支持嵌套 Message 和引入 Message,從而讓定義復(fù)雜的數(shù)據(jù)結(jié)構(gòu)的工作變得非常輕松愉快。
動態(tài)編譯
一般情況下,使用 Protobuf 的人們都會先寫好 .proto 文件,再用 Protobuf 編譯器生成目標語言所需要的源代碼文件。將這些生成的代碼和應(yīng)用程序一起編譯。
可是在某且情況下,人們無法預(yù)先知道 .proto 文件,他們需要動態(tài)處理一些未知的 .proto 文件。比如一個通用的消息轉(zhuǎn)發(fā)中間件,它不可能預(yù)知需要處理怎樣的消息。這需要動態(tài)編譯 .proto 文件,并使用其中的 Message。
Protobuf 提供了 google::protobuf::compiler 包來完成動態(tài)編譯的功能。主要的類叫做 importer,定義在 importer.h 中。使用 Importer 非常簡單,下圖展示了與 Import 和其它幾個重要的類的關(guān)系。

首先構(gòu)造一個 importer 對象。構(gòu)造函數(shù)需要兩個入口參數(shù),一個是 source Tree 對象,該對象指定了存放 .proto 文件的源目錄。第二個參數(shù)是一個 error collector 對象,該對象有一個 AddError 方法,用來處理解析 .proto 文件時遇到的語法錯誤。
之后,需要動態(tài)編譯一個 .proto 文件時,只需調(diào)用 importer 對象的 import 方法。非常簡單。
那么我們?nèi)绾问褂脛討B(tài)編譯后的 Message 呢?我們需要首先了解幾個其他的類。
Package google::protobuf::compiler 中提供了以下幾個類,用來表示一個 .proto 文件中定義的 message,以及 Message 中的 field,如圖3所示。

類 FileDescriptor 表示一個編譯后的 .proto 文件;類 Descriptor 對應(yīng)該文件中的一個 Message;類 FieldDescriptor 描述一個 Message 中的一個具體 Field。
比如編譯完 lm.helloworld.proto 之后,可以通過如下代碼得到 lm.helloworld.id 的定義:
清單 7. 得到 lm.helloworld.id 的定義的代碼
const protobuf::Descriptor *desc =
importer_.pool()->FindMessageTypeByName(“l(fā)m.helloworld”);
const protobuf::FieldDescriptor* field =
desc->pool()->FindFileByName (“id”);
通過 Descriptor,F(xiàn)ieldDescriptor 的各種方法和屬性,應(yīng)用程序可以獲得各種關(guān)于 Message 定義的信息。比如通過 field->name() 得到 field 的名字。這樣,您就可以使用一個動態(tài)定義的消息了。
編寫新的 proto 編譯器
隨 Google Protocol Buffer 源代碼一起發(fā)布的編譯器 protoc 支持 3 種編程語言:C++,java 和 Python。但使用 Google Protocol Buffer 的 Compiler 包,您可以開發(fā)出支持其他語言的新的編譯器。
類 CommandLineInterface 封裝了 protoc 編譯器的前端,包括命令行參數(shù)的解析,proto 文件的編譯等功能。您所需要做的是實現(xiàn)類 CodeGenerator 的派生類,實現(xiàn)諸如代碼生成等后端工作:
程序的大體框架如圖所示:

在 main() 函數(shù)內(nèi),生成 CommandLineInterface 的對象 cli,調(diào)用其 RegisterGenerator() 方法將新語言的后端代碼生成器 yourG 對象注冊給 cli 對象。然后調(diào)用 cli 的 Run() 方法即可。
這樣生成的編譯器和 protoc 的使用方法相同,接受同樣的命令行參數(shù),cli 將對用戶輸入的 .proto 進行詞法語法等分析工作,最終生成一個語法樹。該樹的結(jié)構(gòu)如圖所示。

其根節(jié)點為一個 FileDescriptor 對象(請參考“動態(tài)編譯”一節(jié)),并作為輸入?yún)?shù)被傳入 yourG 的 Generator() 方法。在這個方法內(nèi),您可以遍歷語法樹,然后生成對應(yīng)的您所需要的代碼。簡單說來,要想實現(xiàn)一個新的 compiler,您只需要寫一個 main 函數(shù),和一個實現(xiàn)了方法 Generator() 的派生類即可。