google proto buffer 的原理(轉)

原文鏈接:https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/index.html

首先聲明,這是我轉的,原作者是 劉明,原鏈接是:https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/index.html

因爲我覺得,對我我這個小白來說,我都看懂了,應該讓更多的人看到這麼優秀的博客,太精彩了!


簡介

什麼是 Google Protocol Buffer? 假如您在網上搜索,應該會得到類似這樣的文字介紹:

Google Protocol Buffer( 簡稱 Protobuf) 是 Google 公司內部的混合語言數據標準,目前已經正在使用的有超過 48,162 種報文格式定義和超過 12,183 個 .proto 文件。他們用於 RPC 系統和持續數據存儲系統。

Protocol Buffers 是一種輕便高效的結構化數據存儲格式,可以用於結構化數據串行化,或者說序列化。它很適合做數據存儲或 RPC 數據交換格式。可用於通訊協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。目前提供了 C++、Java、Python 三種語言的 API。

或許您和我一樣,在第一次看完這些介紹後還是不明白 Protobuf 究竟是什麼,那麼我想一個簡單的例子應該比較有助於理解它。

一個簡單的例子

安裝 Google Protocol Buffer

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

安裝步驟如下所示:

1

2

3

4

5

6

tar -xzf protobuf-2.1.0.tar.gz

cd protobuf-2.1.0

./configure --prefix=$INSTALL_DIR

make

make check

make install

關於簡單例子的描述

我打算使用 Protobuf 和 C++ 開發一個十分簡單的例子程序。

該程序由兩部分組成。第一部分被稱爲 Writer,第二部分叫做 Reader。

Writer 負責將一些結構化的數據寫入一個磁盤文件,Reader 則負責從該磁盤文件中讀取結構化數據並打印到屏幕上。

準備用於演示的結構化數據是 HelloWorld,它包含兩個基本數據:

  • ID,爲一個整數類型的數據
  • Str,這是一個字符串

書寫 .proto 文件

首先我們需要編寫一個 proto 文件,定義我們程序中需要處理的結構化數據,在 protobuf 的術語中,結構化數據被稱爲 Message。proto 文件非常類似 java 或者 C 語言的數據定義。代碼清單 1 顯示了例子應用中的 proto 文件內容。

清單 1. proto 文件

1

2

3

4

5

6

7

package lm;

message helloworld

{

   required int32     id = 1;  // ID

   required string    str = 2;  // str

   optional int32     opt = 3;  //optional field

}

一個比較好的習慣是認真對待 proto 文件的文件名。比如將命名規則定於如下:

1

packageName.MessageName.proto

在上例中,package 名字叫做 lm,定義了一個消息 helloworld,該消息有三個成員,類型爲 int32 的 id,另一個爲類型爲 string 的成員 str。opt 是一個可選的成員,即消息中可以不包含該成員。

編譯 .proto 文件

寫好 proto 文件之後就可以用 Protobuf 編譯器將該文件編譯成目標語言了。本例中我們將使用 C++。

假設您的 proto 文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一個目錄下,則可以使用如下命令:

1

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

命令將生成兩個文件:

lm.helloworld.pb.h , 定義了 C++ 類的頭文件

lm.helloworld.pb.cc , C++ 類的實現文件

在生成的頭文件中,定義了一個 C++ 類 helloworld,後面的 Writer 和 Reader 將使用這個類來對消息進行操作。諸如對消息的成員進行賦值,將消息序列化等等都有相應的方法。

編寫 writer 和 Reader

如前所述,Writer 將把一個結構化數據寫入磁盤,以便其他人來讀取。假如我們不使用 Protobuf,其實也有許多的選擇。一個可能的方法是將數據轉換爲字符串,然後將字符串寫入磁盤。轉換爲字符串的方法可以使用 sprintf(),這非常簡單。數字 123 可以變成字符串”123”。

這樣做似乎沒有什麼不妥,但是仔細考慮一下就會發現,這樣的做法對寫 Reader 的那個人的要求比較高,Reader 的作者必須了 Writer 的細節。比如”123”可以是單個數字 123,但也可以是三個數字 1,2 和 3,等等。這麼說來,我們還必須讓 Writer 定義一種分隔符一樣的字符,以便 Reader 可以正確讀取。但分隔符也許還會引起其他的什麼問題。最後我們發現一個簡單的 Helloworld 也需要寫許多處理消息格式的代碼。

如果使用 Protobuf,那麼這些細節就可以不需要應用程序來考慮了。

使用 Protobuf,Writer 的工作很簡單,需要處理的結構化數據由 .proto 文件描述,經過上一節中的編譯過程後,該數據化結構對應了一個 C++ 的類,並定義在 lm.helloworld.pb.h 中。對於本例,類名爲 lm::helloworld。

Writer 需要 include 該頭文件,然後便可以使用這個類了。

現在,在 Writer 代碼中,將要存入磁盤的結構化數據由一個 lm::helloworld 類的對象表示,它提供了一系列的 get/set 函數用來修改和讀取結構化數據中的數據成員,或者叫 field。

當我們需要將該結構化數據保存到磁盤上時,類 lm::helloworld 已經提供相應的方法來把一個複雜的數據變成一個字節序列,我們可以將這個字節序列寫入磁盤。

對於想要讀取這個數據的程序來說,也只需要使用類 lm::helloworld 的相應反序列化方法來將這個字節序列重新轉換會結構化數據。這同我們開始時那個“123”的想法類似,不過 Protobuf 想的遠遠比我們那個粗糙的字符串轉換要全面,因此,我們不如放心將這類事情交給 Protobuf 吧。

程序清單 2 演示了 Writer 的主要代碼,您一定會覺得很簡單吧?

清單 2. Writer 的主要代碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

#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() 用來設置 id 的值。SerializeToOstream 將對象序列化後寫入一個 fstream 流。

代碼清單 3 列出了 reader 的主要代碼。

清單 3. Reader

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

#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 方法讀取消息的內部信息,並進行打印輸出操作。

運行結果

運行 Writer 和 Reader 的結果如下:

1

2

3

4

>writer

>reader

101

Hello

Reader 讀取文件 log 中的序列化信息並打印到屏幕上。本文中所有的例子代碼都可以在附件中下載。您可以親身體驗一下。

這個例子本身並無意義,但只要您稍加修改就可以將它變成更加有用的程序。比如將磁盤替換爲網絡 socket,那麼就可以實現基於網絡的數據交換任務。而存儲和交換正是 Protobuf 最有效的應用領域。

和其他類似技術的比較

看完這個簡單的例子之後,希望您已經能理解 Protobuf 能做什麼了,那麼您可能會說,世上還有很多其他的類似技術啊,比如 XML,JSON,Thrift 等等。和他們相比,Protobuf 有什麼不同呢?

簡單說來 Protobuf 的主要優點就是:簡單,快。

這有測試爲證,項目 thrift-protobuf-compare 比較了這些類似的技術,圖 1 顯示了該項目的一項測試結果,Total Time.

圖 1. 性能測試結果

圖 1. 性能測試結果

Total Time 指一個對象操作的整個時間,包括創建對象,將對象序列化爲內存中的字節序列,然後再反序列化的整個過程。從測試結果可以看到 Protobuf 的成績很好,感興趣的讀者可以自行到網站 http://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking上了解更詳細的測試結果。

Protobuf 的優點

Protobuf 有如 XML,不過它更小、更快、也更簡單。你可以定義自己的數據結構,然後使用代碼生成器生成的代碼來讀寫這個數據結構。你甚至可以在無需重新部署程序的情況下更新數據結構。只需使用 Protobuf 對數據結構進行一次描述,即可利用各種不同語言或從各種不同數據流中對你的結構化數據輕鬆讀寫。

它有一個非常棒的特性,即“向後”兼容性好,人們不必破壞已部署的、依靠“老”數據格式的程序就可以對數據結構進行升級。這樣您的程序就可以不必擔心因爲消息結構的改變而造成的大規模的代碼重構或者遷移的問題。因爲添加新的消息中的 field 並不會引起已經發布的程序的任何改變。

Protobuf 語義更清晰,無需類似 XML 解析器的東西(因爲 Protobuf 編譯器會將 .proto 文件編譯生成對應的數據訪問類以對 Protobuf 數據進行序列化、反序列化操作)。

使用 Protobuf 無需學習複雜的文檔對象模型,Protobuf 的編程模式比較友好,簡單易學,同時它擁有良好的文檔和示例,對於喜歡簡單事物的人們而言,Protobuf 比其他的技術更加有吸引力。

Protobuf 的不足

Protbuf 與 XML 相比也有不足之處。它功能簡單,無法用來表示複雜的概念。

XML 已經成爲多種行業標準的編寫工具,Protobuf 只是 Google 公司內部使用的工具,在通用性上還差很多。

由於文本並不適合用來描述數據結構,所以 Protobuf 也不適合用來對基於文本的標記文檔(如 HTML)建模。另外,由於 XML 具有某種程度上的自解釋性,它可以被人直接讀取編輯,在這一點上 Protobuf 不行,它以二進制的方式存儲,除非你有 .proto 定義,否則你沒法直接讀出 Protobuf 的任何內容【 2 】。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章