Protobuf 的優點
Protobuf 有如 XML,不過它更小、更快、也更簡單。你可以定義自己的數據結構,然后使用代碼生成器生成的代碼來讀寫這個數據結構。你甚至可以在無需重新部署程序的情況下更新數據結構。只需使用 Protobuf 對數據結構進行一次描述,即可利用各種不同語言或從各種不同數據流中對你的結構化數據輕松讀寫。
它有一個非常棒的特性,即“向后”兼容性好,人們不必破壞已部署的、依靠“老”數據格式的程序就可以對數據結構進行升級。這樣您的程序就可以不必擔心因為消息結構的改變而造成的大規模的代碼重構或者遷移的問題。因為添加新的消息中的 field 並不會引起已經發布的程序的任何改變。
Protobuf 語義更清晰,無需類似 XML 解析器的東西(因為 Protobuf 編譯器會將 .proto 文件編譯生成對應的數據訪問類以對 Protobuf 數據進行序列化、反序列化操作)。
使用 Protobuf 無需學習復雜的文檔對象模型,Protobuf 的編程模式比較友好,簡單易學,同時它擁有良好的文檔和示例,對於喜歡簡單事物的人們而言,Protobuf 比其他的技術更加有吸引力。
Protobuf 的不足
Protobuf 與 XML 相比也有不足之處。它功能簡單,無法用來表示復雜的概念。
XML 已經成為多種行業標准的編寫工具,Protobuf 只是 Google 公司內部使用的工具,在通用性上還差很多。
由於文本並不適合用來描述數據結構,所以 Protobuf 也不適合用來對基於文本的標記文檔(如 HTML)建模。另外,由於 XML 具有某種程度上的自解釋性,它可以被人直接讀取編輯,在這一點上 Protobuf 不行,它以二進制的方式存儲,除非你有 .proto 定義,否則你沒法直接讀出 Protobuf 的任何內容。
高級應用話題
更復雜的 Message
到這里為止,我們只給出了一個簡單的沒有任何用處的例子。在實際應用中,人們往往需要定義更加復雜的 Message。我們用“復雜”這個詞,不僅僅是指從個數上說有更多的 fields 或者更多類型的 fields,而是指更加復雜的數據結構:
嵌套 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 域。這使得人們可以定義更加復雜的數據結構。
Import Message
在一個 .proto 文件中,還可以用 Import 關鍵字引入在其他 .proto 文件中定義的消息,這可以稱做 Import Message,或者 Dependency Message。比如下例:
import common.header; message youMsg{ required common.info_header header = 1; required string youPrivateData = 2; }
其中,
common.info_header定義在
common.header包內。
Import Message 的用處主要在於提供了方便的代碼管理機制。您可以將一些公用的 Message 定義在一個 package 中,然后在別的 .proto 文件中引入該 package,進而使用其中的消息定義。Google Protocol Buffer 可以很好地支持嵌套 Message 和引入 Message,從而讓定義復雜的數據結構的工作變得非常輕松愉快。
Protobuf 的更多細節
人們一直在強調,同 XML 相比, Protobuf 的主要優點在於性能高。它以高效的二進制方式存儲,比 XML 小 3 到 10 倍,快 20 到 100 倍。對於這些 “小 3 到 10 倍”,“快 20 到 100 倍”的說法,嚴肅的程序員需要一個解釋。因此在本文的最后,讓我們稍微深入 Protobuf 的內部實現吧。
有兩項技術保證了采用 Protobuf 的程序能獲得相對於 XML 極大的性能提高。
第一點,我們可以考察 Protobuf 序列化后的信息內容。您可以看到 Protocol Buffer 信息的表示非常緊湊,這意味着消息的體積減少,自然需要更少的資源。比如網絡上傳輸的字節數更少,需要的 IO 更少等,從而提高性能。
第二點,我們需要理解 Protobuf 封解包的大致過程,從而理解為什么會比 XML 快很多。
Google Protocol Buffer 的 Encoding
Protobuf 序列化后所生成的二進制消息非常緊湊,這得益於 Protobuf 采用的非常巧妙的 Encoding 方法。考察消息結構之前,讓我首先要介紹一個叫做 Varint 的術語。Varint 是一種緊湊的表示數字的方法。它用一個或多個字節來表示一個數字,值越小的數字使用越少的字節數。這能減少用來表示數字的字節數。比如對於 int32 類型的數字,一般需要 4 個 byte 來表示。但是采用 Varint,對於很小的 int32 類型的數字,則可以用 1 個 byte 來表示。當然凡事都有好的也有不好的一面,采用 Varint 表示法,大的數字則需要 5 個 byte 來表示。從統計的角度來說,一般不會所有的消息中的數字都是大數,因此大多數情況下,采用 Varint 后,可以用更少的字節數來表示數字信息。下面就詳細介紹一下 Varint。
Varint 中的每個 byte 的最高位 bit 有特殊的含義,如果該位為 1,表示后續的 byte 也是該數字的一部分,如果該位為 0,則結束。其他的 7 個 bit 都用來表示數字。因此小於 128 的數字都可以用一個 byte 表示。大於 128 的數字,比如 300,會用兩個字節來表示:1010 1100 0000 0010
下圖演示了 Google Protocol Buffer 如何解析兩個 bytes。注意到最終計算前將兩個 byte 的位置相互交換過一次,這是因為 Google Protocol Buffer 字節序采用 little-endian 的方式。
消息經過序列化后會成為一個二進制數據流,該流中的數據為一系列的 Key-Value 對。如下圖所示:
采用這種 Key-Pair 結構無需使用分隔符來分割不同的 Field。對於可選的 Field,如果消息中不存在該 field,那么在最終的 Message Buffer 中就沒有該 field,這些特性都有助於節約消息本身的大小。
Key 用來標識具體的 field,在解包的時候,Protocol Buffer 根據 Key 就可以知道相應的 Value 應該對應於消息中的哪一個 field。Key 的定義如下:
(field_number << 3) | wire_type
可以看到 Key 由兩部分組成。第一部分是 field_number,即為標識數字id。第二部分為 wire_type。表示 Value 的傳輸類型。Wire Type 可能的類型如下表所示:
細心的讀者或許會看到在 Type 0 所能表示的數據類型中有 int32 和 sint32 這兩個非常類似的數據類型。Google Protocol Buffer 區別它們的主要意圖也是為了減少 encoding 后的字節數。在計算機內,一個負數一般會被表示為一個很大的整數,因為計算機定義負數的符號位為數字的最高位。如果采用 Varint 表示一個負數,那么一定需要 5 個 byte。為此 Google Protocol Buffer 定義了 sint32 這種類型,采用 zigzag 編碼。Zigzag 編碼用無符號數來表示有符號數字,正數和負數交錯,這就是 zigzag 這個詞的含義了。如圖所示:
使用 zigzag 編碼,絕對值小的數字,無論正負都可以采用較少的 byte 來表示,充分利用了 Varint 這種技術。其他的數據類型,比如字符串等則采用類似數據庫中的 varchar 的表示方法,即用一個 varint 表示長度,然后將其余部分緊跟在這個長度部分之后即可。