• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            Gordon.Ma

            近山則志高,臨水而聰慧
            隨筆 - 3, 文章 - 0, 評(píng)論 - 1, 引用 - 0
            數(shù)據(jù)加載中……

            【轉(zhuǎn)載】Google Protocol Buffer 的使用和原理

            原文鏈接:

            簡(jiǎn)介

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

            Google Protocol Buffer( 簡(jiǎn)稱 Protobuf) 是 Google 公司內(nèi)部的混合語(yǔ)言數(shù)據(jù)標(biāo)準(zhǔn),目前已經(jīng)正在使用的有超過(guò) 48,162 種報(bào)文格式定義和超過(guò) 12,183 個(gè) .proto 文件。他們用于 RPC 系統(tǒng)和持續(xù)數(shù)據(jù)存儲(chǔ)系統(tǒng)。

            Protocol Buffers 是一種輕便高效的結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)格式,可以用于結(jié)構(gòu)化數(shù)據(jù)串行化,或者說(shuō)序列化。它很適合做數(shù)據(jù)存儲(chǔ)或 RPC 數(shù)據(jù)交換格式。可用于通訊協(xié)議、數(shù)據(jù)存儲(chǔ)等領(lǐng)域的語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān)、可擴(kuò)展的序列化結(jié)構(gòu)數(shù)據(jù)格式。目前提供了 C++、Java、Python 三種語(yǔ)言的 API。

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

            一個(gè)簡(jiǎn)單的例子

            安裝 Google Protocol Buffer

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

            安裝步驟如下所示:

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

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

            我打算使用 Protobuf 和 C++ 開(kāi)發(fā)一個(gè)十分簡(jiǎn)單的例子程序。

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

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

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

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

            書(shū)寫(xiě) .proto 文件

            首先我們需要編寫(xiě)一個(gè) proto 文件,定義我們程序中需要處理的結(jié)構(gòu)化數(shù)據(jù),在 protobuf 的術(shù)語(yǔ)中,結(jié)構(gòu)化數(shù)據(jù)被稱為 Message。proto 文件非常類似 java 或者 C 語(yǔ)言的數(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
            }

            一個(gè)比較好的習(xí)慣是認(rèn)真對(duì)待 proto 文件的文件名。比如將命名規(guī)則定于如下:

             packageName.MessageName.proto

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

            編譯 .proto 文件

            寫(xiě)好 proto 文件之后就可以用 Protobuf 編譯器將該文件編譯成目標(biāo)語(yǔ)言了。本例中我們將使用 C++。

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

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

            命令將生成兩個(gè)文件:

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

            lm.helloworld.pb.cc , C++ 類的實(shí)現(xiàn)文件

            在生成的頭文件中,定義了一個(gè) C++ 類 helloworld,后面的 Writer 和 Reader 將使用這個(gè)類來(lái)對(duì)消息進(jìn)行操作。諸如對(duì)消息的成員進(jìn)行賦值,將消息序列化等等都有相應(yīng)的方法。

            編寫(xiě) writer 和 Reader

            如前所述,Writer 將把一個(gè)結(jié)構(gòu)化數(shù)據(jù)寫(xiě)入磁盤(pán),以便其他人來(lái)讀取。假如我們不使用 Protobuf,其實(shí)也有許多的選擇。一個(gè)可能的方法是將數(shù)據(jù)轉(zhuǎn)換為字符串,然后將字符串寫(xiě)入磁盤(pán)。轉(zhuǎn)換為字符串的方法可以使用 sprintf(),這非常簡(jiǎn)單。數(shù)字 123 可以變成字符串”123”。

            這樣做似乎沒(méi)有什么不妥,但是仔細(xì)考慮一下就會(huì)發(fā)現(xiàn),這樣的做法對(duì)寫(xiě) Reader 的那個(gè)人的要求比較高,Reader 的作者必須了 Writer 的細(xì)節(jié)。比如”123”可以是單個(gè)數(shù)字 123,但也可以是三個(gè)數(shù)字 1,2 和 3,等等。這么說(shuō)來(lái),我們還必須讓 Writer 定義一種分隔符一樣的字符,以便 Reader 可以正確讀取。但分隔符也許還會(huì)引起其他的什么問(wèn)題。最后我們發(fā)現(xiàn)一個(gè)簡(jiǎn)單的 Helloworld 也需要寫(xiě)許多處理消息格式的代碼。

            如果使用 Protobuf,那么這些細(xì)節(jié)就可以不需要應(yīng)用程序來(lái)考慮了。

            使用 Protobuf,Writer 的工作很簡(jiǎn)單,需要處理的結(jié)構(gòu)化數(shù)據(jù)由 .proto 文件描述,經(jīng)過(guò)上一節(jié)中的編譯過(guò)程后,該數(shù)據(jù)化結(jié)構(gòu)對(duì)應(yīng)了一個(gè) C++ 的類,并定義在 lm.helloworld.pb.h 中。對(duì)于本例,類名為 lm::helloworld。

            Writer 需要 include 該頭文件,然后便可以使用這個(gè)類了。

            現(xiàn)在,在 Writer 代碼中,將要存入磁盤(pán)的結(jié)構(gòu)化數(shù)據(jù)由一個(gè) lm::helloworld 類的對(duì)象表示,它提供了一系列的 get/set 函數(shù)用來(lái)修改和讀取結(jié)構(gòu)化數(shù)據(jù)中的數(shù)據(jù)成員,或者叫 field。

            當(dāng)我們需要將該結(jié)構(gòu)化數(shù)據(jù)保存到磁盤(pán)上時(shí),類 lm::helloworld 已經(jīng)提供相應(yīng)的方法來(lái)把一個(gè)復(fù)雜的數(shù)據(jù)變成一個(gè)字節(jié)序列,我們可以將這個(gè)字節(jié)序列寫(xiě)入磁盤(pán)。

            對(duì)于想要讀取這個(gè)數(shù)據(jù)的程序來(lái)說(shuō),也只需要使用類 lm::helloworld 的相應(yīng)反序列化方法來(lái)將這個(gè)字節(jié)序列重新轉(zhuǎn)換會(huì)結(jié)構(gòu)化數(shù)據(jù)。這同我們開(kāi)始時(shí)那個(gè)“123”的想法類似,不過(guò) Protobuf 想的遠(yuǎn)遠(yuǎn)比我們那個(gè)粗糙的字符串轉(zhuǎn)換要全面,因此,我們不如放心將這類事情交給 Protobuf 吧。

            程序清單 2 演示了 Writer 的主要代碼,您一定會(huì)覺(jué)得很簡(jiǎn)單吧?

            清單 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 是一個(gè) helloworld 類的對(duì)象,set_id() 用來(lái)設(shè)置 id 的值。SerializeToOstream 將對(duì)象序列化后寫(xiě)入一個(gè) 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 的對(duì)象 msg1,然后利用 ParseFromIstream 從一個(gè) fstream 流中讀取信息并反序列化。此后,ListMsg 中采用 get 方法讀取消息的內(nèi)部信息,并進(jìn)行打印輸出操作。

            運(yùn)行結(jié)果

            運(yùn)行 Writer 和 Reader 的結(jié)果如下:

             >writer   >reader   101   Hello

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

            這個(gè)例子本身并無(wú)意義,但只要您稍加修改就可以將它變成更加有用的程序。比如將磁盤(pán)替換為網(wǎng)絡(luò) socket,那么就可以實(shí)現(xiàn)基于網(wǎng)絡(luò)的數(shù)據(jù)交換任務(wù)。而存儲(chǔ)和交換正是 Protobuf 最有效的應(yīng)用領(lǐng)域。


            和其他類似技術(shù)的比較

            看完這個(gè)簡(jiǎn)單的例子之后,希望您已經(jīng)能理解 Protobuf 能做什么了,那么您可能會(huì)說(shuō),世上還有很多其他的類似技術(shù)啊,比如 XML,JSON,Thrift 等等。和他們相比,Protobuf 有什么不同呢?

            簡(jiǎn)單說(shuō)來(lái) Protobuf 的主要優(yōu)點(diǎn)就是:簡(jiǎn)單,快。

            這有測(cè)試為證,項(xiàng)目 thrift-protobuf-compare 比較了這些類似的技術(shù),圖 1 顯示了該項(xiàng)目的一項(xiàng)測(cè)試結(jié)果,Total Time.

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

            Total Time 指一個(gè)對(duì)象操作的整個(gè)時(shí)間,包括創(chuàng)建對(duì)象,將對(duì)象序列化為內(nèi)存中的字節(jié)序列,然后再反序列化的整個(gè)過(guò)程。從測(cè)試結(jié)果可以看到 Protobuf 的成績(jī)很好,感興趣的讀者可以自行到網(wǎng)站 http://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking上了解更詳細(xì)的測(cè)試結(jié)果。

            Protobuf 的優(yōu)點(diǎn)

            Protobuf 有如 XML,不過(guò)它更小、更快、也更簡(jiǎn)單。你可以定義自己的數(shù)據(jù)結(jié)構(gòu),然后使用代碼生成器生成的代碼來(lái)讀寫(xiě)這個(gè)數(shù)據(jù)結(jié)構(gòu)。你甚至可以在無(wú)需重新部署程序的情況下更新數(shù)據(jù)結(jié)構(gòu)。只需使用 Protobuf 對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行一次描述,即可利用各種不同語(yǔ)言或從各種不同數(shù)據(jù)流中對(duì)你的結(jié)構(gòu)化數(shù)據(jù)輕松讀寫(xiě)。

            它有一個(gè)非常棒的特性,即“向后”兼容性好,人們不必破壞已部署的、依靠“老”數(shù)據(jù)格式的程序就可以對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行升級(jí)。這樣您的程序就可以不必?fù)?dān)心因?yàn)橄⒔Y(jié)構(gòu)的改變而造成的大規(guī)模的代碼重構(gòu)或者遷移的問(wèn)題。因?yàn)樘砑有碌南⒅械?field 并不會(huì)引起已經(jīng)發(fā)布的程序的任何改變。

            Protobuf 語(yǔ)義更清晰,無(wú)需類似 XML 解析器的東西(因?yàn)?Protobuf 編譯器會(huì)將 .proto 文件編譯生成對(duì)應(yīng)的數(shù)據(jù)訪問(wèn)類以對(duì) Protobuf 數(shù)據(jù)進(jìn)行序列化、反序列化操作)。

            使用 Protobuf 無(wú)需學(xué)習(xí)復(fù)雜的文檔對(duì)象模型,Protobuf 的編程模式比較友好,簡(jiǎn)單易學(xué),同時(shí)它擁有良好的文檔和示例,對(duì)于喜歡簡(jiǎn)單事物的人們而言,Protobuf 比其他的技術(shù)更加有吸引力。

            Protobuf 的不足

            Protbuf 與 XML 相比也有不足之處。它功能簡(jiǎn)單,無(wú)法用來(lái)表示復(fù)雜的概念。

            XML 已經(jīng)成為多種行業(yè)標(biāo)準(zhǔn)的編寫(xiě)工具,Protobuf 只是 Google 公司內(nèi)部使用的工具,在通用性上還差很多。

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


            高級(jí)應(yīng)用話題

            更復(fù)雜的 Message

            到這里為止,我們只給出了一個(gè)簡(jiǎn)單的沒(méi)有任何用處的例子。在實(shí)際應(yīng)用中,人們往往需要定義更加復(fù)雜的 Message。我們用“復(fù)雜”這個(gè)詞,不僅僅是指從個(gè)數(shù)上說(shuō)有更多的 fields 或者更多類型的 fields,而是指更加復(fù)雜的數(shù)據(jù)結(jié)構(gòu):

            嵌套 Message

            嵌套是一個(gè)神奇的概念,一旦擁有嵌套能力,消息的表達(dá)能力就會(huì)非常強(qiáng)大。

            代碼清單 4 給出一個(gè)嵌套 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,并用來(lái)定義 Person 消息中的 phone 域。這使得人們可以定義更加復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。

            4.1.2 Import Message

            在一個(gè) .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 的用處主要在于提供了方便的代碼管理機(jī)制,類似 C 語(yǔ)言中的頭文件。您可以將一些公用的 Message 定義在一個(gè) package 中,然后在別的 .proto 文件中引入該 package,進(jìn)而使用其中的消息定義。

            Google Protocol Buffer 可以很好地支持嵌套 Message 和引入 Message,從而讓定義復(fù)雜的數(shù)據(jù)結(jié)構(gòu)的工作變得非常輕松愉快。

            動(dòng)態(tài)編譯

            一般情況下,使用 Protobuf 的人們都會(huì)先寫(xiě)好 .proto 文件,再用 Protobuf 編譯器生成目標(biāo)語(yǔ)言所需要的源代碼文件。將這些生成的代碼和應(yīng)用程序一起編譯。

            可是在某且情況下,人們無(wú)法預(yù)先知道 .proto 文件,他們需要?jiǎng)討B(tài)處理一些未知的 .proto 文件。比如一個(gè)通用的消息轉(zhuǎn)發(fā)中間件,它不可能預(yù)知需要處理怎樣的消息。這需要?jiǎng)討B(tài)編譯 .proto 文件,并使用其中的 Message。

            Protobuf 提供了 google::protobuf::compiler 包來(lái)完成動(dòng)態(tài)編譯的功能。主要的類叫做 importer,定義在 importer.h 中。使用 Importer 非常簡(jiǎn)單,下圖展示了與 Import 和其它幾個(gè)重要的類的關(guān)系。

            圖 2. Importer 類
            圖 2. Importer 類

            Import 類對(duì)象中包含三個(gè)主要的對(duì)象,分別為處理錯(cuò)誤的 MultiFileErrorCollector 類,定義 .proto 文件源目錄的 SourceTree 類。

            下面還是通過(guò)實(shí)例說(shuō)明這些類的關(guān)系和使用吧。

            對(duì)于給定的 proto 文件,比如 lm.helloworld.proto,在程序中動(dòng)態(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(“lm.helloworld.proto”);

            首先構(gòu)造一個(gè) importer 對(duì)象。構(gòu)造函數(shù)需要兩個(gè)入口參數(shù),一個(gè)是 source Tree 對(duì)象,該對(duì)象指定了存放 .proto 文件的源目錄。第二個(gè)參數(shù)是一個(gè) error collector 對(duì)象,該對(duì)象有一個(gè) AddError 方法,用來(lái)處理解析 .proto 文件時(shí)遇到的語(yǔ)法錯(cuò)誤。

            之后,需要?jiǎng)討B(tài)編譯一個(gè) .proto 文件時(shí),只需調(diào)用 importer 對(duì)象的 import 方法。非常簡(jiǎn)單。

            那么我們?nèi)绾问褂脛?dòng)態(tài)編譯后的 Message 呢?我們需要首先了解幾個(gè)其他的類

            Package google::protobuf::compiler 中提供了以下幾個(gè)類,用來(lái)表示一個(gè) .proto 文件中定義的 message,以及 Message 中的 field,如圖所示。

            圖 3. 各個(gè) Compiler 類之間的關(guān)系
            圖 3. 各個(gè) Compiler 類之間的關(guān)系

            類 FileDescriptor 表示一個(gè)編譯后的 .proto 文件;類 Descriptor 對(duì)應(yīng)該文件中的一個(gè) Message;類 FieldDescriptor 描述一個(gè) Message 中的一個(gè)具體 Field。

            比如編譯完 lm.helloworld.proto 之后,可以通過(guò)如下代碼得到 lm.helloworld.id 的定義:

            清單 7. 得到 lm.helloworld.id 的定義的代碼
            const protobuf::Descriptor *desc =
             importer_.pool()->FindMessageTypeByName("lm.helloworld");
            const protobuf::FieldDescriptor* field =
            desc->pool()->FindFileByName("id");

            通過(guò) Descriptor,F(xiàn)ieldDescriptor 的各種方法和屬性,應(yīng)用程序可以獲得各種關(guān)于 Message 定義的信息。比如通過(guò) field->name() 得到 field 的名字。這樣,您就可以使用一個(gè)動(dòng)態(tài)定義的消息了。

            編寫(xiě)新的 proto 編譯器

            隨 Google Protocol Buffer 源代碼一起發(fā)布的編譯器 protoc 支持 3 種編程語(yǔ)言:C++,java 和 Python。但使用 Google Protocol Buffer 的 Compiler 包,您可以開(kāi)發(fā)出支持其他語(yǔ)言的新的編譯器。

            類 CommandLineInterface 封裝了 protoc 編譯器的前端,包括命令行參數(shù)的解析,proto 文件的編譯等功能。您所需要做的是實(shí)現(xiàn)類 CodeGenerator 的派生類,實(shí)現(xiàn)諸如代碼生成等后端工作:

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

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

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

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

            圖 5. 語(yǔ)法樹(shù)
            圖 5. 語(yǔ)法樹(shù)

            其根節(jié)點(diǎn)為一個(gè) FileDescriptor 對(duì)象(請(qǐng)參考“動(dòng)態(tài)編譯”一節(jié)),并作為輸入?yún)?shù)被傳入 yourG 的 Generator() 方法。在這個(gè)方法內(nèi),您可以遍歷語(yǔ)法樹(shù),然后生成對(duì)應(yīng)的您所需要的代碼。簡(jiǎn)單說(shuō)來(lái),要想實(shí)現(xiàn)一個(gè)新的 compiler,您只需要寫(xiě)一個(gè) main 函數(shù),和一個(gè)實(shí)現(xiàn)了方法 Generator() 的派生類即可。

            在本文的下載附件中,有一個(gè)參考例子,將 .proto 文件編譯生成 XML 的 compiler,可以作為參考。


            Protobuf 的更多細(xì)節(jié)

            人們一直在強(qiáng)調(diào),同 XML 相比, Protobuf 的主要優(yōu)點(diǎn)在于性能高。它以高效的二進(jìn)制方式存儲(chǔ),比 XML 小 3 到 10 倍,快 20 到 100 倍。

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

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

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

            第二點(diǎn)我們需要理解 Protobuf 封解包的大致過(guò)程,從而理解為什么會(huì)比 XML 快很多。

            Google Protocol Buffer 的 Encoding

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

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

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

            比如對(duì)于 int32 類型的數(shù)字,一般需要 4 個(gè) byte 來(lái)表示。但是采用 Varint,對(duì)于很小的 int32 類型的數(shù)字,則可以用 1 個(gè) byte 來(lái)表示。當(dāng)然凡事都有好的也有不好的一面,采用 Varint 表示法,大的數(shù)字則需要 5 個(gè) byte 來(lái)表示。從統(tǒng)計(jì)的角度來(lái)說(shuō),一般不會(huì)所有的消息中的數(shù)字都是大數(shù),因此大多數(shù)情況下,采用 Varint 后,可以用更少的字節(jié)數(shù)來(lái)表示數(shù)字信息。下面就詳細(xì)介紹一下 Varint。

            Varint 中的每個(gè) byte 的最高位 bit 有特殊的含義,如果該位為 1,表示后續(xù)的 byte 也是該數(shù)字的一部分,如果該位為 0,則結(jié)束。其他的 7 個(gè) bit 都用來(lái)表示數(shù)字。因此小于 128 的數(shù)字都可以用一個(gè) byte 表示。大于 128 的數(shù)字,比如 300,會(huì)用兩個(gè)字節(jié)來(lái)表示:1010 1100 0000 0010

            下圖演示了 Google Protocol Buffer 如何解析兩個(gè) bytes。注意到最終計(jì)算前將兩個(gè) byte 的位置相互交換過(guò)一次,這是因?yàn)?Google Protocol Buffer 字節(jié)序采用 little-endian 的方式。

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

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

            圖 7. Message Buffer
            圖 7. Message Buffer

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

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

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

            則最終的 Message Buffer 中有兩個(gè) Key-Value 對(duì),一個(gè)對(duì)應(yīng)消息中的 id;另一個(gè)對(duì)應(yīng) str。

            Key 用來(lái)標(biāo)識(shí)具體的 field,在解包的時(shí)候,Protocol Buffer 根據(jù) Key 就可以知道相應(yīng)的 Value 應(yīng)該對(duì)應(yīng)于消息中的哪一個(gè) 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
            TypeMeaningUsed For
            0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
            164-bitfixed64, sfixed64, double
            2Length-delimistring, bytes, embedded messages, packed repeated fields
            3Start groupGroups (deprecated)
            4End groupGroups (deprecated)
            532-bitfixed32, sfixed32, float

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

            在計(jì)算機(jī)內(nèi),一個(gè)負(fù)數(shù)一般會(huì)被表示為一個(gè)很大的整數(shù),因?yàn)橛?jì)算機(jī)定義負(fù)數(shù)的符號(hào)位為數(shù)字的最高位。如果采用 Varint 表示一個(gè)負(fù)數(shù),那么一定需要 5 個(gè) byte。為此 Google Protocol Buffer 定義了 sint32 這種類型,采用 zigzag 編碼。

            Zigzag 編碼用無(wú)符號(hào)數(shù)來(lái)表示有符號(hào)數(shù)字,正數(shù)和負(fù)數(shù)交錯(cuò),這就是 zigzag 這個(gè)詞的含義了。

            如圖所示:

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

            使用 zigzag 編碼,絕對(duì)值小的數(shù)字,無(wú)論正負(fù)都可以采用較少的 byte 來(lái)表示,充分利用了 Varint 這種技術(shù)。

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

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

            對(duì)于代碼清單 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 個(gè)字節(jié),這些奇怪的數(shù)字需要稍微解釋一下,其含義用 ASCII 表示如下:
            <helloworld>
            <id>101</id>
            <name>hello</name>
            </helloworld>

            封解包的速度

            首先我們來(lái)了解一下 XML 的封解包過(guò)程。XML 需要從文件中讀取出字符串,再轉(zhuǎn)換為 XML 文檔對(duì)象結(jié)構(gòu)模型。之后,再?gòu)?XML 文檔對(duì)象結(jié)構(gòu)模型中讀取指定節(jié)點(diǎn)的字符串,最后再將這個(gè)字符串轉(zhuǎn)換成指定類型的變量。這個(gè)過(guò)程非常復(fù)雜,其中將 XML 文件轉(zhuǎn)換為文檔對(duì)象結(jié)構(gòu)模型的過(guò)程通常需要完成詞法文法分析等大量消耗 CPU 的復(fù)雜計(jì)算。

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

            為了說(shuō)明這并不是我拍腦袋隨意想出來(lái)的說(shuō)法,下面讓我們簡(jiǎn)單分析一下 Protobuf 解包的代碼流程吧。

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

            該過(guò)程可以用下圖表示:

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

            整個(gè)解析過(guò)程需要 Protobuf 本身的框架代碼和由 Protobuf 編譯器生成的代碼共同完成。Protobuf 提供了基類 Message 以及 Message_lite 作為通用的 Framework,,CodedInputStream 類,WireFormatLite 類等提供了對(duì)二進(jìn)制數(shù)據(jù)的 decode 功能,從 5.1 節(jié)的分析來(lái)看,Protobuf 的解碼可以通過(guò)幾個(gè)簡(jiǎn)單的數(shù)學(xué)運(yùn)算完成,無(wú)需復(fù)雜的詞法語(yǔ)法分析,因此 ReadTag() 等方法都非常快。 在這個(gè)調(diào)用路徑上的其他類和方法都非常簡(jiǎn)單,感興趣的讀者可以自行閱讀。 相對(duì)于 XML 的解析過(guò)程,以上的流程圖實(shí)在是非常簡(jiǎn)單吧?這也就是 Protobuf 效率高的第二個(gè)原因了。


            結(jié)束語(yǔ)

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


            參考資料

            學(xué)習(xí)

            posted on 2014-06-19 18:06 Gordooooon 閱讀(245) 評(píng)論(0)  編輯 收藏 引用 所屬分類: C/C++

            97久久精品人妻人人搡人人玩| 久久久久亚洲Av无码专| 欧美亚洲另类久久综合| 久久精品国产AV一区二区三区| 久久99国产综合精品免费| 久久国产视屏| 秋霞久久国产精品电影院| 2020最新久久久视精品爱| 日韩av无码久久精品免费| 欧洲国产伦久久久久久久| 久久精品无码一区二区无码| segui久久国产精品| 粉嫩小泬无遮挡久久久久久| 久久精品国产一区| 国内精品久久久久伊人av| 国内精品久久久久久久影视麻豆| 久久er99热精品一区二区| 久久91这里精品国产2020| 色综合久久综合中文综合网| 中文字幕无码久久人妻| 婷婷久久综合九色综合九七| 久久久一本精品99久久精品88| 久久国产成人午夜aⅴ影院| 久久超乳爆乳中文字幕| 国产精品99久久久精品无码| 国内精品伊人久久久久妇| 精品国产91久久久久久久a| 国内精品九九久久久精品| 99精品久久久久久久婷婷| 久久久无码精品亚洲日韩京东传媒| 国产午夜精品久久久久九九| 国产精品久久网| 99久久国产综合精品网成人影院| 国产成年无码久久久久毛片| 久久精品国产亚洲AV无码麻豆| 精品人妻伦九区久久AAA片69| 久久精品一本到99热免费| 亚洲国产成人精品女人久久久 | 亚州日韩精品专区久久久| 久久久久国产亚洲AV麻豆| 精品久久久久久无码免费|