深入 ProtoBuf - 簡介

之前在網絡通信和通用數據交換等應用場景中經常使用的技術是 JSON 或 XML,而在最近的開發中接觸到了 Google 的 ProtoBuf。

在查閱相關資料學習 ProtoBuf 以及研讀其源碼之後,發現其在效率、兼容性等方面非常出色。在以後的項目技術選型中,尤其是網絡通信、通用數據交換等場景應該會優先選擇 ProtoBuf。

自己在學習 ProtoBuf 的過程中翻譯了官方的主要文檔,一來當然是在學習 ProtoBuf,二來是培養閱讀英文文檔的能力,三來是因爲 Google 的文檔?不存在的!

看完這些文檔對 ProtoBuf 應該就有相當程度的瞭解了。

翻譯文檔見 [索引]文章索引,導航爲翻譯 - 技術 - ProtoBuf 官方文檔。

但是官方文檔更多的是作爲查閱和權威參考,並不意味着看完官方文檔就能立馬理解其原理。

本文以及接下來的幾篇文章會對 ProtoBuf 的編碼、序列化、反序列化、反射等原理做一些詳細介紹,同時也會盡量將這些原理表達的更爲通俗易懂。

何爲 ProtoBuf

我們先來看看官方文檔給出的定義和描述:

protocol buffers 是一種語言無關、平臺無關、可擴展的序列化結構數據的方法,它可用於(數據)通信協議、數據存儲等。

Protocol Buffers 是一種靈活,高效,自動化機制的結構數據序列化方法-可類比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更爲簡單。

你可以定義數據的結構,然後使用特殊生成的源代碼輕鬆的在各種數據流中使用各種語言進行編寫和讀取結構數據。你甚至可以更新數據結構,而不破壞由舊數據結構編譯的已部署程序。

簡單來講, ProtoBuf 是結構數據序列化[1] 方法,可簡單類比於 XML[2],其具有以下特點:

  • 語言無關、平臺無關。即 ProtoBuf 支持 Java、C++、Python 等多種語言,支持多個平臺
  • 高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更爲簡單
  • 擴展性、兼容性好。你可以更新數據結構,而不影響和破壞原有的舊程序

序列化[1]:將結構數據對象轉換成能夠被存儲和傳輸(例如網絡傳輸)的格式,同時應當要保證這個序列化結果在之後(可能在另一個計算環境中)能夠被重建回原來的結構數據或對象。
更爲詳盡的介紹可參閱 維基百科
類比於 XML[2]:這裏主要指在數據通信和數據存儲應用場景中序列化方面的類比,但個人認爲 XML 作爲一種擴展標記語言和 ProtoBuf 還是有着本質區別的。

使用 ProtoBuf

對 ProtoBuf 的基本概念有了一定了解之後,我們來看看具體該如何使用 ProtoBuf。
第一步,創建 .proto 文件,定義數據結構,如下例1所示:

// 例1: 在 xxx.proto 文件中定義 Example1 message
message Example1 {
    optional string stringVal = 1;
    optional bytes bytesVal = 2;
    message EmbeddedMessage {
        int32 int32Val = 1;
        string stringVal = 2;
    }
    optional EmbeddedMessage embeddedExample1 = 3;
    repeated int32 repeatedInt32Val = 4;
    repeated string repeatedStringVal = 5;
}

我們在上例中定義了一個名爲 Example1 的 消息,語法很簡單,message 關鍵字後跟上消息名稱:

message xxx {

}

之後我們在其中定義了 message 具有的字段,形式爲:

message xxx {
  // 字段規則:required -> 字段只能也必須出現 1 次
  // 字段規則:optional -> 字段可出現 0 次或1次
  // 字段規則:repeated -> 字段可出現任意多次(包括 0)
  // 類型:int32、int64、sint32、sint64、string、32-bit ....
  // 字段編號:0 ~ 536870911(除去 19000 到 19999 之間的數字)
  字段規則 類型 名稱 = 字段編號;
}

在上例中,我們定義了:

  • 類型 string,名爲 stringVal 的 optional 可選字段,字段編號爲 1,此字段可出現 0 或 1 次
  • 類型 bytes,名爲 bytesVal 的 optional 可選字段,字段編號爲 2,此字段可出現 0 或 1 次
  • 類型 EmbeddedMessage(自定義的內嵌 message 類型),名爲 embeddedExample1 的 optional 可選字段,字段編號爲 3,此字段可出現 0 或 1 次
  • 類型 int32,名爲 repeatedInt32Val 的 repeated 可重複字段,字段編號爲 4,此字段可出現 任意多次(包括 0)
  • 類型 string,名爲 repeatedStringVal 的 repeated 可重複字段,字段編號爲 5,此字段可出現 任意多次(包括 0)

關於 proto2 定義 message 消息的更多語法細節,例如具有支持哪些類型,字段編號分配、import
導入定義,reserved 保留字段等知識請參閱 [翻譯] ProtoBuf 官方文檔(二)- 語法指引(proto2)

關於定義時的一些規範請參閱 [翻譯] ProtoBuf 官方文檔(四)- 規範指引

第二步,protoc 編譯 .proto 文件生成讀寫接口

我們在 .proto 文件中定義了數據結構,這些數據結構是面向開發者和業務程序的,並不面向存儲和傳輸。

當需要把這些數據進行存儲或傳輸時,就需要將這些結構數據進行序列化、反序列化以及讀寫。那麼如何實現呢?不用擔心, ProtoBuf 將會爲我們提供相應的接口代碼。如何提供?答案就是通過 protoc 這個編譯器。

可通過如下命令生成相應的接口代碼:

// $SRC_DIR: .proto 所在的源目錄
// --cpp_out: 生成 c++ 代碼
// $DST_DIR: 生成代碼的目標目錄
// xxx.proto: 要針對哪個 proto 文件生成接口代碼

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

最終生成的代碼將提供類似如下的接口:

例子-序列化和解析接口.png
例子-protoc 生成接口.png

第三步,調用接口實現序列化、反序列化以及讀寫
針對第一步中例1定義的 message,我們可以調用第二步中生成的接口,實現測試代碼如下:

//
// Created by yue on 18-7-21.
//
#include <iostream>
#include <fstream>
#include <string>
#include "single_length_delimited_all.pb.h"

int main() {
    Example1 example1;
    example1.set_stringval("hello,world");
    example1.set_bytesval("are you ok?");

    Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();

    embeddedExample2->set_int32val(1);
    embeddedExample2->set_stringval("embeddedInfo");
    example1.set_allocated_embeddedexample1(embeddedExample2);

    example1.add_repeatedint32val(2);
    example1.add_repeatedint32val(3);
    example1.add_repeatedstringval("repeated1");
    example1.add_repeatedstringval("repeated2");

    std::string filename = "single_length_delimited_all_example1_val_result";
    std::fstream output(filename, std::ios::out | std::ios::trunc | std::ios::binary);
    if (!example1.SerializeToOstream(&output)) {
        std::cerr << "Failed to write example1." << std::endl;
        exit(-1);
    }

    return 0;
}

關於 protoc 的使用以及接口調用的更多信息可參閱 [翻譯] ProtoBuf 官方文檔(九)- (C++開發)教程

關於例1的完整代碼請參閱 源碼:protobuf 例1。其中的 single_length_delimited_all.* 爲例子相關代碼和文件。

因爲此係列文章重點在於深入 ProtoBuf 的編碼、序列化、反射等原理,關於 ProtoBuf 的語法、使用等只做簡單介紹,更爲詳見的使用教程可參閱我翻譯的系列官方文檔。

關於 ProtoBuf 的一些思考

官方文檔以及網上很多文章提到 ProtoBuf 可類比 XML 或 JSON。

那麼 ProtoBuf 是否就等同於 XML 和 JSON 呢,它們是否具有完全相同的應用場景呢?

個人認爲如果要將 ProtoBuf、XML、JSON 三者放到一起去比較,應該區分兩個維度。一個是數據結構化,一個是數據序列化。這裏的數據結構化主要面向開發或業務層面,數據序列化面向通信或存儲層面,當然數據序列化也需要“結構”和“格式”,所以這兩者之間的區別主要在於面向領域和場景不同,一般要求和側重點也會有所不同。數據結構化側重人類可讀性甚至有時會強調語義表達能力,而數據序列化側重效率和壓縮。

從這兩個維度,我們可以做出下面的一些思考。

XML 作爲一種擴展標記語言,JSON 作爲源於 JS 的數據格式,都具有數據結構化的能力。

例如 XML 可以衍生出 HTML (雖然 HTML 早於 XML,但從概念上講,HTML 只是預定義標籤的 XML),HTML 的作用是標記和表達萬維網中資源的結構,以便瀏覽器更好的展示萬維網資源,同時也要儘可能保證其人類可讀以便開發人員進行編輯,這就是面向業務或開發層面的數據結構化

再如 XML 還可衍生出 RDF/RDFS,進一步表達語義網中資源的關係和語義,同樣它強調數據結構化的能力和人類可讀。

關於 RDF/RDFS 和語義網的概念可查詢相關資料瞭解,或參閱 2-Answer 系列-本體構建模塊(一)3-Answer 系列-本體構建模塊(二) ,文中有一些簡單介紹。

JSON 也是同理,在很多場合更多的是體現了數據結構化的能力,例如作爲交互接口的數據結構的表達。在 MongoDB 中採用 JSON 作爲查詢語句,也是在發揮其數據結構化的能力。

當然,JSON、XML 同樣也可以直接被用來數據序列化,實際上很多時候它們也是這麼被使用的,例如直接採用 JSON、XML 進行網絡通信傳輸,此時 JSON、XML 就成了一種序列化格式,它發揮了數據序列化的能力。但是經常這麼被使用,不代表這麼做就是合理。實際將 JSON、XML 直接作用數據序列化通常並不是最優選擇,因爲它們在速度、效率、空間上並不是最優。換句話說它們更適合數據結構化而非數據序列化。

扯完 XML 和 JSON,我們來看看 ProtoBuf,同樣的 ProtoBuf 也具有數據結構化的能力,其實也就是上面介紹的 message 定義。我們能夠在 .proto 文件中,通過 message、import、內嵌 message 等語法來實現數據結構化,但是很容易能夠看出,ProtoBuf 在數據結構化方面和 XML、JSON 相差較大,人類可讀性較差,不適合上面提到的 XML、JSON 的一些應用場景。

但是如果從數據序列化的角度你會發現 ProtoBuf 有着明顯的優勢,效率、速度、空間幾乎全面佔優,看完後面的 ProtoBuf 編碼的文章,你更會了解 ProtoBuf 是如何極盡所能的壓榨每一寸空間和性能,而其中的編碼原理正是 ProtoBuf 的關鍵所在,message 的表達能力並不是 ProtoBuf 最關鍵的重點。所以可以看出 ProtoBuf 重點側重於數據序列化 而非 數據結構化

最終對這些個人思考做一些小小的總結:

  1. XML、JSON、ProtoBuf 都具有數據結構化數據序列化的能力
  2. XML、JSON 更注重數據結構化,關注人類可讀性和語義表達能力。ProtoBuf 更注重數據序列化,關注效率、空間、速度,人類可讀性差,語義表達能力不足(爲保證極致的效率,會捨棄一部分元信息)
  3. ProtoBuf 的應用場景更爲明確,XML、JSON 的應用場景更爲豐富。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章