Google Protocol Buffer 的使用和原理

本文轉(zhuǎn)自劉明的分享。原文

簡介

什么是 Google Protocol Buffer? 假如您在網(wǎng)上搜索姻成,應(yīng)該會得到類似這樣的文字介紹:

Google Protocol Buffer( 簡稱 Protobuf) 是 Google 公司內(nèi)部的混合語言數(shù)據(jù)標(biāo)準(zhǔn)撑蚌,目前已經(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ù)交換格式∧璧ⅲ可用于通訊協(xié)議结耀、數(shù)據(jù)存儲等領(lǐng)域的語言無關(guān)、平臺無關(guān)匙铡、可擴展的序列化結(jié)構(gòu)數(shù)據(jù)格式图甜。目前提供了 C++、Java鳖眼、Python 三種語言的 API黑毅。

或許您和我一樣,在第一次看完這些介紹后還是不明白 Protobuf 究竟是什么钦讳,那么我想一個簡單的例子應(yīng)該比較有助于理解它矿瘦。

一個簡單的例子

安裝 Google Protocol Buffer

在網(wǎng)站 http://code.google.com/p/protobuf/downloads/list上可以下載 Protobuf 的源代碼枕面。然后解壓編譯安裝便可以使用它了。

安裝步驟如下所示:

cd protobuf-2.1.0
./configure --prefix=$INSTALL_DIR
make
make check
make install

關(guān)于簡單例子的描述

我打算使用 Protobuf 和 C++ 開發(fā)一個十分簡單的例子程序缚去。

該程序由兩部分組成潮秘。第一部分被稱為 Writer,第二部分叫做 Reader易结。

Writer 負責(zé)將一些結(jié)構(gòu)化的數(shù)據(jù)寫入一個磁盤文件枕荞,Reader 則負責(zé)從該磁盤文件中讀取結(jié)構(gòu)化數(shù)據(jù)并打印到屏幕上。

準(zhǔn)備用于演示的結(jié)構(gòu)化數(shù)據(jù)是 HelloWorld搞动,它包含兩個基本數(shù)據(jù):

  • ID躏精,為一個整數(shù)類型的數(shù)據(jù)
  • Str,這是一個字符串

書寫 .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. 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 編譯器將該文件編譯成目標(biāo)語言了峦耘。本例中我們將使用 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衩茸。

當(dāng)我們需要將該結(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 的主要代碼
#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 的主要代碼姥芥。

清單 3. Reader
#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.

圖 1. 性能測試結(jié)果
圖 1. 性能測試結(jié)果

Total Time 指一個對象操作的整個時間勺拣,包括創(chuàng)建對象,將對象序列化為內(nèi)存中的字節(jié)序列鱼填,然后再反序列化的整個過程药有。從測試結(jié)果可以看到 Protobuf 的成績很好,感興趣的讀者可以自行到網(wǎng)站 http://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking上了解更詳細的測試結(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)進行升級稽寒。這樣您的程序就可以不必擔(dān)心因為消息結(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è)標(biāo)準(zhǔn)的編寫工具褂始,Protobuf 只是 Google 公司內(nèi)部使用的工具,在通用性上還差很多描函。

由于文本并不適合用來描述數(shù)據(jù)結(jié)構(gòu)病袄,所以 Protobuf 也不適合用來對基于文本的標(biāo)記文檔(如 HTML)建模搂赋。另外,由于 XML 具有某種程度上的自解釋性益缠,它可以被人直接讀取編輯,在這一點上 Protobuf 不行基公,它以二進制的方式存儲幅慌,除非你有 .proto 定義,否則你沒法直接讀出 Protobuf 的任何內(nèi)容【 2 】轰豆。

高級應(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)。

4.1.2 Import Message

在一個 .proto 文件中磕潮,還可以用 Import 關(guān)鍵字引入在其他 .proto 文件中定義的消息翠胰,這可以稱做 Import Message,或者 Dependency Message自脯。

比如下例:

清單 5. 代碼
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 編譯器生成目標(biāo)語言所需要的源代碼文件。將這些生成的代碼和應(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)系。

圖 2. Importer 類
圖 2. Importer 類

Import 類對象中包含三個主要的對象摹芙,分別為處理錯誤的 MultiFileErrorCollector 類灼狰,定義 .proto 文件源目錄的 SourceTree 類。

下面還是通過實例說明這些類的關(guān)系和使用吧浮禾。

對于給定的 proto 文件交胚,比如 lm.helloworld.proto,在程序中動態(tài)編譯它只需要很少的一些代碼伐厌。如代碼清單 6 所示承绸。

清單 6. 代碼
google::protobuf::compiler::MultiFileErrorCollector errorCollector;
google::protobuf::compiler::DiskSourceTree sourceTree;
google::protobuf::compiler::Importer importer(&sourceTree, &errorCollector);
sourceTree.MapPath("", protosrc);
importer.import(“l(fā)m.helloworld.proto”);

首先構(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. 各個 Compiler 類之間的關(guān)系
圖 3. 各個 Compiler 類之間的關(guān)系

類 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)諸如代碼生成等后端工作:

程序的大體框架如圖所示:

圖 4. XML 編譯器框圖
圖 4. XML 編譯器框圖

在 main() 函數(shù)內(nèi)兜材,生成 CommandLineInterface 的對象 cli,調(diào)用其 RegisterGenerator() 方法將新語言的后端代碼生成器 yourG 對象注冊給 cli 對象逞力。然后調(diào)用 cli 的 Run() 方法即可曙寡。

這樣生成的編譯器和 protoc 的使用方法相同,接受同樣的命令行參數(shù)寇荧,cli 將對用戶輸入的 .proto 進行詞法語法等分析工作举庶,最終生成一個語法樹。該樹的結(jié)構(gòu)如圖所示揩抡。

圖 5. 語法樹
圖 5. 語法樹

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

在本文的下載附件中副瀑,有一個參考例子,將 .proto 文件編譯生成 XML 的 compiler曹体,可以作為參考俗扇。

Protobuf 的更多細節(jié)

人們一直在強調(diào),同 XML 相比箕别, Protobuf 的主要優(yōu)點在于性能高铜幽。它以高效的二進制方式存儲滞谢,比 XML 小 3 到 10 倍,快 20 到 100 倍除抛。

對于這些 “小 3 到 10 倍”,“快 20 到 100 倍”的說法狮杨,嚴(yán)肅的程序員需要一個解釋。因此在本文的最后到忽,讓我們稍微深入 Protobuf 的內(nèi)部實現(xiàn)吧橄教。

有兩項技術(shù)保證了采用 Protobuf 的程序能獲得相對于 XML 極大的性能提高。

第一點喘漏,我們可以考察 Protobuf 序列化后的信息內(nèi)容护蝶。您可以看到 Protocol Buffer 信息的表示非常緊湊,這意味著消息的體積減少翩迈,自然需要更少的資源持灰。比如網(wǎng)絡(luò)上傳輸?shù)淖止?jié)數(shù)更少,需要的 IO 更少等负饲,從而提高性能堤魁。

第二點我們需要理解 Protobuf 封解包的大致過程,從而理解為什么會比 XML 快很多返十。

Google Protocol Buffer 的 Encoding

Protobuf 序列化后所生成的二進制消息非常緊湊妥泉,這得益于 Protobuf 采用的非常巧妙的 Encoding 方法。

考察消息結(jié)構(gòu)之前洞坑,讓我首先要介紹一個叫做 Varint 的術(shù)語盲链。

Varint 是一種緊湊的表示數(shù)字的方法。它用一個或多個字節(jié)來表示一個數(shù)字检诗,值越小的數(shù)字使用越少的字節(jié)數(shù)。這能減少用來表示數(shù)字的字節(jié)數(shù)逢慌。

比如對于 int32 類型的數(shù)字悠轩,一般需要 4 個 byte 來表示。但是采用 Varint攻泼,對于很小的 int32 類型的數(shù)字火架,則可以用 1 個 byte 來表示。當(dāng)然凡事都有好的也有不好的一面忙菠,采用 Varint 表示法何鸡,大的數(shù)字則需要 5 個 byte 來表示。從統(tǒng)計的角度來說牛欢,一般不會所有的消息中的數(shù)字都是大數(shù)骡男,因此大多數(shù)情況下,采用 Varint 后傍睹,可以用更少的字節(jié)數(shù)來表示數(shù)字信息隔盛。下面就詳細介紹一下 Varint犹菱。

Varint 中的每個 byte 的最高位 bit 有特殊的含義,如果該位為 1吮炕,表示后續(xù)的 byte 也是該數(shù)字的一部分腊脱,如果該位為 0,則結(jié)束龙亲。其他的 7 個 bit 都用來表示數(shù)字陕凹。因此小于 128 的數(shù)字都可以用一個 byte 表示。大于 128 的數(shù)字鳄炉,比如 300杜耙,會用兩個字節(jié)來表示:1010 1100 0000 0010

下圖演示了 Google Protocol Buffer 如何解析兩個 bytes。注意到最終計算前將兩個 byte 的位置相互交換過一次拂盯,這是因為 Google Protocol Buffer 字節(jié)序采用 little-endian 的方式泥技。

圖 6. Varint 編碼
圖 6. Varint 編碼

消息經(jīng)過序列化后會成為一個二進制數(shù)據(jù)流,該流中的數(shù)據(jù)為一系列的 Key-Value 對磕仅。如下圖所示:

圖 7. Message Buffer
圖 7. Message Buffer

采用這種 Key-Pair 結(jié)構(gòu)無需使用分隔符來分割不同的 Field。對于可選的 Field簸呈,如果消息中不存在該 field榕订,那么在最終的 Message Buffer 中就沒有該 field,這些特性都有助于節(jié)約消息本身的大小蜕便。

以代碼清單 1 中的消息為例劫恒。假設(shè)我們生成如下的一個消息 Test1:

Test1.id = 10;
Test1.str = “hello”;

則最終的 Message Buffer 中有兩個 Key-Value 對轿腺,一個對應(yīng)消息中的 id两嘴;另一個對應(yīng) str。

Key 用來標(biāo)識具體的 field族壳,在解包的時候憔辫,Protocol Buffer 根據(jù) Key 就可以知道相應(yīng)的 Value 應(yīng)該對應(yīng)于消息中的哪一個 field。

Key 的定義如下:

(field_number << 3) | wire_type

可以看到 Key 由兩部分組成仿荆。第一部分是 field_number贰您,比如消息 lm.helloworld 中 field id 的 field_number 為 1。第二部分為 wire_type拢操。表示 Value 的傳輸類型锦亦。

Wire Type 可能的類型如下表所示:

表 1. Wire Type
Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimi string, bytes, embedded messages, packed repeated fields
3 Start group Groups (deprecated)
4 End group Groups (deprecated)
5 32-bit fixed32, sfixed32, float

在我們的例子當(dāng)中,field id 所采用的數(shù)據(jù)類型為 int32令境,因此對應(yīng)的 wire type 為 0杠园。細心的讀者或許會看到在 Type 0 所能表示的數(shù)據(jù)類型中有 int32 和 sint32 這兩個非常類似的數(shù)據(jù)類型。Google Protocol Buffer 區(qū)別它們的主要意圖也是為了減少 encoding 后的字節(jié)數(shù)舔庶。

在計算機內(nèi)抛蚁,一個負數(shù)一般會被表示為一個很大的整數(shù)陈醒,因為計算機定義負數(shù)的符號位為數(shù)字的最高位。如果采用 Varint 表示一個負數(shù)篮绿,那么一定需要 5 個 byte孵延。為此 Google Protocol Buffer 定義了 sint32 這種類型,采用 zigzag 編碼亲配。

Zigzag 編碼用無符號數(shù)來表示有符號數(shù)字尘应,正數(shù)和負數(shù)交錯,這就是 zigzag 這個詞的含義了吼虎。

如圖所示:

圖 8. ZigZag 編碼
圖 8. ZigZag 編碼

使用 zigzag 編碼犬钢,絕對值小的數(shù)字,無論正負都可以采用較少的 byte 來表示思灰,充分利用了 Varint 這種技術(shù)玷犹。

其他的數(shù)據(jù)類型,比如字符串等則采用類似數(shù)據(jù)庫中的 varchar 的表示方法洒疚,即用一個 varint 表示長度歹颓,然后將其余部分緊跟在這個長度部分之后即可。

通過以上對 protobuf Encoding 方法的介紹油湖,想必您也已經(jīng)發(fā)現(xiàn) protobuf 消息的內(nèi)容小巍扛,適于網(wǎng)絡(luò)傳輸。假如您對那些有關(guān)技術(shù)細節(jié)的描述缺乏耐心和興趣乏德,那么下面這個簡單而直觀的比較應(yīng)該能給您更加深刻的印象撤奸。

對于代碼清單 1 中的消息,用 Protobuf 序列化后的字節(jié)序列為:

08 65 12 06 48 65 6C 6C 6F 77

而如果用 XML喊括,則類似這樣:

31 30 31 3C 2F 69 64 3E 3C 6E 61 6D 65 3E 68 65
6C 6C 6F 3C 2F 6E 61 6D 65 3E 3C 2F 68 65 6C 6C
6F 77 6F 72 6C 64 3E

一共 55 個字節(jié)胧瓜,這些奇怪的數(shù)字需要稍微解釋一下,其含義用 ASCII 表示如下:

<``helloworld``>
<``id``>101</``id``>
<``name``>hello</``name``>
</``helloworld``>

封解包的速度

首先我們來了解一下 XML 的封解包過程郑什。XML 需要從文件中讀取出字符串府喳,再轉(zhuǎn)換為 XML 文檔對象結(jié)構(gòu)模型。之后蹦误,再從 XML 文檔對象結(jié)構(gòu)模型中讀取指定節(jié)點的字符串劫拢,最后再將這個字符串轉(zhuǎn)換成指定類型的變量。這個過程非常復(fù)雜强胰,其中將 XML 文件轉(zhuǎn)換為文檔對象結(jié)構(gòu)模型的過程通常需要完成詞法文法分析等大量消耗 CPU 的復(fù)雜計算舱沧。

反觀 Protobuf,它只需要簡單地將一個二進制序列偶洋,按照指定的格式讀取到 C++ 對應(yīng)的結(jié)構(gòu)類型中就可以了熟吏。從上一節(jié)的描述可以看到消息的 decoding 過程也可以通過幾個位移操作組成的表達式計算即可完成。速度非常快牵寺。

為了說明這并不是我拍腦袋隨意想出來的說法悍引,下面讓我們簡單分析一下 Protobuf 解包的代碼流程吧。

以代碼清單 3 中的 Reader 為例帽氓,該程序首先調(diào)用 msg1 的 ParseFromIstream 方法趣斤,這個方法解析從文件讀入的二進制數(shù)據(jù)流,并將解析出來的數(shù)據(jù)賦予 helloworld 類的相應(yīng)數(shù)據(jù)成員黎休。

該過程可以用下圖表示:

圖 9. 解包流程圖
圖 9. 解包流程圖

整個解析過程需要 Protobuf 本身的框架代碼和由 Protobuf 編譯器生成的代碼共同完成浓领。Protobuf 提供了基類 Message 以及 Message_lite 作為通用的 Framework,势腮,CodedInputStream 類联贩,WireFormatLite 類等提供了對二進制數(shù)據(jù)的 decode 功能,從 5.1 節(jié)的分析來看捎拯,Protobuf 的解碼可以通過幾個簡單的數(shù)學(xué)運算完成泪幌,無需復(fù)雜的詞法語法分析,因此 ReadTag() 等方法都非呈鹫眨快祸泪。 在這個調(diào)用路徑上的其他類和方法都非常簡單,感興趣的讀者可以自行閱讀建芙。 相對于 XML 的解析過程浴滴,以上的流程圖實在是非常簡單吧?這也就是 Protobuf 效率高的第二個原因了岁钓。

結(jié)束語

往往了解越多,人們就會越覺得自己無知微王。我惶恐地發(fā)現(xiàn)自己竟然寫了一篇關(guān)于序列化的文章屡限,文中必然有許多想當(dāng)然而自以為是的東西,還希望各位能夠去偽存真炕倘,更希望真的高手能不吝賜教钧大,給我來信。謝謝罩旋。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末啊央,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子涨醋,更是在濱河造成了極大的恐慌瓜饥,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浴骂,死亡現(xiàn)場離奇詭異乓土,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門趣苏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狡相,“玉大人,你說我怎么就攤上這事食磕【∽兀” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵彬伦,是天一觀的道長滔悉。 經(jīng)常有香客問我,道長媚朦,這世上最難降的妖魔是什么氧敢? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮询张,結(jié)果婚禮上孙乖,老公的妹妹穿的比我還像新娘。我一直安慰自己份氧,他們只是感情好唯袄,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蜗帜,像睡著了一般恋拷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上厅缺,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天蔬顾,我揣著相機與錄音,去河邊找鬼湘捎。 笑死诀豁,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的窥妇。 我是一名探鬼主播舷胜,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼活翩!你這毒婦竟也來了烹骨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤材泄,失蹤者是張志新(化名)和其女友劉穎沮焕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拉宗,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡遇汞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片空入。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡络它,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出歪赢,到底是詐尸還是另有隱情化戳,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布埋凯,位于F島的核電站点楼,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏白对。R本人自食惡果不足惜掠廓,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望甩恼。 院中可真熱鬧蟀瞧,春花似錦、人聲如沸条摸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钉蒲。三九已至切端,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間顷啼,已是汗流浹背踏枣。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留钙蒙,地道東北人椰于。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像仪搔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蜻牢,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容