<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Jack Jiang

    我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
    posts - 494, comments - 13, trackbacks - 0, articles - 1

    本文由IBM開發者社區分享,有較多修訂和改動。

    1、引言

    在當今移動網絡時代,手機流量和電量是寶貴的資源,對于移動端最常見的即時通訊IM應用,由于實時通信基于Socket長連接,它對于流量和電量的需求較一般應用來說更高(詳見《移動端IM實踐:WhatsApp、Line、微信的心跳策略分析》)。

    在IM應用中,優化數據流量消耗過多的基本方法就是使用高度壓縮的通訊協議,而數據壓縮后流量減小帶來的自然結果也就是省電:因為大數據量的傳輸必然需要更久的網絡操作、數據序列化及反序列化操作,這些都是電量消耗過快的根源。

    當前IM應用中最熱門的通訊協議無疑就是Google的Protobuf了,基于它的優秀表現,微信和手機QQ這樣的主流IM應用也早已在使用它。

    本文作為《IM通訊協議專題學習》系列文章的首篇,將從初學者的角度,用通俗簡潔的文字,從零開始為你介紹Protobuf的方方面面,特別適合新手入門。

     

    學習交流:

    (本文已同步發布于:http://www.52im.net/thread-4080-1-1.html

    2、系列文章

    本文是系列文章中的第 1 篇,本系列總目錄如下:

    • 《IM通訊協議專題學習(一):Protobuf從入門到精通,一篇就夠!》(* 本文
    • 《IM通訊協議專題學習(二):快速理解ProtoBuf的背景、原理、使用、優缺點》(稍后發布..)
    • 《IM通訊協議專題學習(三):由淺入深,從通信編解碼原理上理解Protobuf》(稍后發布..)
    • 《IM通訊協議專題學習(四):從Base64到Protobuf,詳解Protobuf的數據編碼原理》(稍后發布..)
    • 《IM通訊協議專題學習(五):Protobuf到底比JSON快幾倍?請看全方位實測!》(稍后發布..)
    • 《IM通訊協議專題學習(六):手把手教你如何在Android上從零使用Protobuf》(稍后發布..)
    • 《IM通訊協議專題學習(七):手把手教你如何在NodeJS中從零使用Protobuf》(稍后發布..)
    • 《IM通訊協議專題學習(八):金蝶隨手記團隊的Protobuf應用實踐(原理篇)  》(稍后發布..)
    • 《IM通訊協議專題學習(九):金蝶隨手記團隊的Protobuf應用實踐(實戰篇) 》(稍后發布..)

    3、什么是Protocol Buffer?

    什么是 Google Protocol Buffer?

    假如您在網上搜索,應該會得到類似于下面這樣的文字介紹:

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

    實際上:Protocol Buffers(簡稱 Protobuf)是一種輕便高效的結構化數據存儲格式,可以用于結構化數據串行化,或者說序列化。它很適合做數據存儲或 RPC 數據交換格式。可用于通訊協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。

    目前:Protobuf官方工程主頁上顯示的已支持的開發語言多達10種,分別有:C++、Java、Python、Objective-C、C#、Ruby、Go、PHP、Dart、Javascript,基本上主流的語言都已支持(具體詳見Protobuf工程主頁:https://github.com/protocolbuffers/protobuf)。

    Protobuf已支持的開發語言如下圖:

    PS:Protobuf的官網上有很多入門資料,有興趣一定要看看:https://developers.google.com/protocol-buffers(如果不能直接訪問,你懂的。。。)

    寫到這里:或許您和我一樣,在第一次看完這些介紹后還是不明白 Protobuf 究竟是什么,那么我想一個簡單的例子應該比較有助于理解它(請繼續往下閱讀)。

    4、一個簡單的例子

    4.1 安裝Protobuf

    在網站 https://developers.google.com/protocol-buffers 上可以下載 Protobuf 的源代碼。然后解壓編譯安裝便可以使用它了。

    安裝步驟如下所示:

    tar-xzf protobuf-2.1.0.tar.gz

    cdprotobuf-2.1.0

    ./configure--prefix=$INSTALL_DIR

    make

    makecheck

    makeinstall

    4.2 關于簡單例子的描述

    我打算使用 Protobuf 和 C++ 開發一個十分簡單的例子程序。該程序由兩部分組成。第一部分被稱為 Writer,第二部分叫做 Reader。Writer 負責將一些結構化的數據寫入一個磁盤文件,Reader 則負責從該磁盤文件中讀取結構化數據并打印到屏幕上。

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

    1)ID:為一個整數類型的數據;

    2)Str:這是一個字符串。

    4.3 書寫 .proto 文件

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

    清單 1. proto 文件:

    package lm;

    message helloworld

    {

       required int32     id = 1;  // ID

       required string    str = 2;  // str

       optional int32     opt = 3;  //optional field

    }

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

    packageName.MessageName.proto

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

    4.4 編譯 .proto 文件

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

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

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

    命令將生成兩個文件:

    1)lm.helloworld.pb.h:定義了 C++ 類的頭文件;

    2)lm.helloworld.pb.cc: C++ 類的實現文件。

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

    4.5 編寫 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 的主要代碼:

    #include "lm.helloworld.pb.h"

     

     intmain(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;

      }        

      return0;

     }

    Msg1 是一個 helloworld 類的對象,set_id() 用來設置 id 的值。SerializeToOstream 將對象序列化后寫入一個 fstream 流。

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

    清單 3. Reader:

    #include "lm.helloworld.pb.h"

     voidListMsg(constlm::helloworld & msg) {

      cout << msg.id() << endl;

      cout << msg.str() << endl;

     }

     

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

    4.6 運行結果

    運行 Writer 和 Reader 的結果如下:

    >writer

    >reader

    101

    Hello

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

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

    5、和其他類似技術的比較

    5.1 概述

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

    簡單說來 Protobuf 的主要優點就是:簡單,快。這有測試為證,項目 thrift-protobuf-compare 比較了這些類似的技術,下圖顯示了該項目的一項測試結果——Total Time。

    性能測試結果:

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

    5.2 Protobuf 的優點

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

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

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

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

    5.3 Protobuf 的不足

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

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

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

    6、Protobuf 的更多細節

    6.1 概述

    人們一直在強調,同 XML 相比, Protobuf 的主要優點在于性能高。它以高效的二進制方式存儲,比 XML 小 3 到 10 倍,快 20 到 100 倍。對于這些 “小 3 到 10 倍”,“快 20 到 100 倍”的說法,嚴肅的程序員需要一個解釋。因此在本文的最后,讓我們稍微深入 Protobuf 的內部實現吧。

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

    第一項:我們可以考察 Protobuf 序列化后的信息內容。您可以看到 Protocol Buffer 信息的表示非常緊湊,這意味著消息的體積減少,自然需要更少的資源。比如網絡上傳輸的字節數更少,需要的 IO 更少等,從而提高性能。

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

    6.2 Protobuf的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。

    下圖演示了Protobuf如何解析兩個 bytes。注意到最終計算前將兩個 byte 的位置相互交換過一次,這是因為Protobuf字節序采用 little-endian(即小端字節序,詳見:《面試必考,史上最通俗大小端字節序詳解》) 的方式。

    Varint 編碼:

    消息經過序列化后會成為一個二進制數據流,該流中的數據為一系列的 Key-Value 對。如下圖所示。

    Message Buffer:

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

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

    Test1.id = 10;

    Test1.str = “hello”;

    則最終的 Message Buffer 中有兩個 Key-Value 對,一個對應消息中的 id;另一個對應 str。

    Key 用來標識具體的 field,在解包的時候,Protocol Buffer 根據 Key 就可以知道相應的 Value 應該對應于消息中的哪一個 field。

    Key 的定義如下:

    (field_number << 3) | wire_type

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

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

    在我們的例子當中,field id 所采用的數據類型為 int32,因此對應的 wire type 為 0。細心的讀者或許會看到在 Type 0 所能表示的數據類型中有 int32 和 sint32 這兩個非常類似的數據類型。Google Protocol Buffer 區別它們的主要意圖也是為了減少 encoding 后的字節數。

    在計算機內,一個負數一般會被表示為一個很大的整數,因為計算機定義負數的符號位為數字的最高位。如果采用 Varint 表示一個負數,那么一定需要 5 個 byte。為此 Google Protocol Buffer 定義了 sint32 這種類型,采用 zigzag 編碼。

    Zigzag 編碼用無符號數來表示有符號數字,正數和負數交錯,這就是 zigzag 這個詞的含義了。

    ZigZag 編碼:

    使用 zigzag 編碼,絕對值小的數字,無論正負都可以采用較少的 byte 來表示,充分利用了 Varint 這種技術。

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

    通過以上對 protobuf Encoding 方法的介紹,想必您也已經發現 protobuf 消息的內容小,適于網絡傳輸。假如您對那些有關技術細節的描述缺乏耐心和興趣,那么下面這個簡單而直觀的比較應該能給您更加深刻的印象。

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

    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 個字節,這些奇怪的數字需要稍微解釋一下,其含義用 ASCII 表示如下:

    <helloworld>

       <id>101</id>

       <name>hello</name>

    </helloworld>

    6.3 封解包的速度

    首先我們來了解一下 XML 的封解包過程。XML 需要從文件中讀取出字符串,再轉換為 XML 文檔對象結構模型。之后,再從 XML 文檔對象結構模型中讀取指定節點的字符串,最后再將這個字符串轉換成指定類型的變量。這個過程非常復雜,其中將 XML 文件轉換為文檔對象結構模型的過程通常需要完成詞法文法分析等大量消耗 CPU 的復雜計算。

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

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

    以代碼清單 3 中的 Reader 為例,該程序首先調用 msg1 的 ParseFromIstream 方法,這個方法解析從文件讀入的二進制數據流,并將解析出來的數據賦予 helloworld 類的相應數據成員。

    該過程可以用下圖表示解包流程圖:

    整個解析過程需要 Protobuf 本身的框架代碼和由 Protobuf 編譯器生成的代碼共同完成。Protobuf 提供了基類 Message 以及 Message_lite 作為通用的 Framework,CodedInputStream 類,WireFormatLite 類等提供了對二進制數據的 decode 功能。

    Protobuf 的解碼可以通過幾個簡單的數學運算完成,無需復雜的詞法語法分析,因此 ReadTag() 等方法都非常快。 在這個調用路徑上的其他類和方法都非常簡單,感興趣的讀者可以自行閱讀。

    相對于 XML 的解析過程,以上的流程圖實在是非常簡單吧?這也就是 Protobuf 效率高的第二個原因了。

    7、寫在最后

    往往了解越多,人們就會越覺得自己無知。我惶恐地發現自己竟然寫了一篇關于序列化的文章,文中必然有許多想當然而自以為是的東西,還希望各位能夠去偽存真,更希望真的高手能不吝賜教。

    另外,如果您覺得理論還不夠,以下幾篇較完整的IM編碼實操都使用了Protobuf,可以一并學習:

    1. 一個基于Protocol Buffer的Java代碼演示

    2. 跟著源碼學IM(二):自已開發IM很難?手把手教你擼一個Andriod版IM

    3. 跟著源碼學IM(四):拿起鍵盤就是干,教你徒手開發一套分布式IM系統

    4. 跟著源碼學IM(十):基于Netty,搭建高性能IM集群(含技術思路+源碼)

    8、參考資料

    [1] Protobuf官方網站

    [2] Protobuf通信協議詳解:代碼演示、詳細原理介紹等

    [3] 如何選擇即時通訊應用的數據傳輸格式

    [4] 強列建議將Protobuf作為你的即時通訊應用數據傳輸格式

    [5] APP與后臺通信數據格式的演進:從文本協議到二進制協議

    [6] 面試必考,史上最通俗大小端字節序詳解

    [7] 移動端IM開發需要面對的技術問題(含通信協議選擇)

    [8] 簡述移動端IM開發的那些坑:架構設計、通信協議和客戶端

    [9] 理論聯系實際:一套典型的IM通信協議設計詳解

    [10] 58到家實時消息系統的協議設計等技術實踐分享

    (本文已同步發布于:http://www.52im.net/thread-4080-1-1.html



    作者:Jack Jiang (點擊作者姓名進入Github)
    出處:http://www.52im.net/space-uid-1.html
    交流:歡迎加入即時通訊開發交流群 215891622
    討論:http://www.52im.net/
    Jack Jiang同時是【原創Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
    本博文 歡迎轉載,轉載請注明出處(也可前往 我的52im.net 找到我)。


    只有注冊用戶登錄后才能發表評論。


    網站導航:
     
    Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
    主站蜘蛛池模板: 一级毛片免费播放视频| 久久久精品免费视频| 一区二区三区精品高清视频免费在线播放| 国产乱子伦片免费观看中字| 黄色免费在线网站| 在线综合亚洲欧洲综合网站| 亚洲一区日韩高清中文字幕亚洲| 亚洲精品9999久久久久无码| 亚洲午夜久久久久久久久久| 怡红院免费的全部视频| 亚洲一级毛片在线播放| 国产成人精品亚洲精品| 18国产精品白浆在线观看免费| 日韩在线观看免费完整版视频| 亚洲视频一区二区三区| 亚洲成a人片在线观看日本麻豆| 蜜臀AV免费一区二区三区| 一级毛片正片免费视频手机看| 亚洲国产韩国一区二区| 毛片基地免费视频a| 三级网站免费观看| 看全免费的一级毛片| 亚洲综合小说久久另类区| 亚洲精品网站在线观看不卡无广告 | 国产精品网站在线观看免费传媒| 国产午夜亚洲精品| 亚洲免费视频网站| 亚洲中文字幕在线观看| 日本一道本高清免费| 成人免费激情视频| a级毛片免费高清视频| 亚洲另类无码专区丝袜| 亚洲视频精品在线观看| 久久久久久久亚洲精品| 精品视频一区二区三区免费| 精品无码专区亚洲| 国产亚洲人成网站在线观看不卡| 黄在线观看www免费看| 中文字幕乱码一区二区免费| 国产成人精品日本亚洲专一区 | 亚洲中文字幕无码不卡电影|