1. 程式人生 > >【神經網路與深度學習】Google Protocol Buffer介紹

【神經網路與深度學習】Google Protocol Buffer介紹

簡介

什麼是 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

安裝步驟如下所示:

 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 檔案
 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 是一個可選的成員,即訊息中可以不包含該成員。

編譯 .proto 檔案

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

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

 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 的主要程式碼
 #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
 #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 的結果如下:

 >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 】。

高階應用話題

更復雜的 Message

到這裡為止,我們只給出了一個簡單的沒有任何用處的例子。在實際應用中,人們往往需要定義更加複雜的 Message。我們用“複雜”這個詞,不僅僅是指從個數上說有更多的 fields 或者更多型別的 fields,而是指更加複雜的資料結構:

巢狀 Message

巢狀是一個神奇的概念,一旦擁有巢狀能力,訊息的表達能力就會非常強大。

程式碼清單 4 給出一個巢狀 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,並用來定義 Person 訊息中的 phone 域。這使得人們可以定義更加複雜的資料結構。

4.1.2 Import Message

在一個 .proto 檔案中,還可以用 Import 關鍵字引入在其他 .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包內。

Import Message 的用處主要在於提供了方便的程式碼管理機制,類似 C 語言中的標頭檔案。您可以將一些公用的 Message 定義在一個 package 中,然後在別的 .proto 檔案中引入該 package,進而使用其中的訊息定義。

Google Protocol Buffer 可以很好地支援巢狀 Message 和引入 Message,從而讓定義複雜的資料結構的工作變得非常輕鬆愉快。

動態編譯

一般情況下,使用 Protobuf 的人們都會先寫好 .proto 檔案,再用 Protobuf 編譯器生成目標語言所需要的原始碼檔案。將這些生成的程式碼和應用程式一起編譯。

可是在某且情況下,人們無法預先知道 .proto 檔案,他們需要動態處理一些未知的 .proto 檔案。比如一個通用的訊息轉發中介軟體,它不可能預知需要處理怎樣的訊息。這需要動態編譯 .proto 檔案,並使用其中的 Message。

Protobuf 提供了 google::protobuf::compiler 包來完成動態編譯的功能。主要的類叫做 importer,定義在 importer.h 中。使用 Importer 非常簡單,下圖展示了與 Import 和其它幾個重要的類的關係。

圖 2. Importer 類
圖 2. Importer 類

Import 類物件中包含三個主要的物件,分別為處理錯誤的 MultiFileErrorCollector 類,定義 .proto 檔案源目錄的 SourceTree 類。

下面還是通過例項說明這些類的關係和使用吧。

對於給定的 proto 檔案,比如 lm.helloworld.proto,在程式中動態編譯它只需要很少的一些程式碼。如程式碼清單 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”);

首先構造一個 importer 物件。建構函式需要兩個入口引數,一個是 source Tree 物件,該物件指定了存放 .proto 檔案的源目錄。第二個引數是一個 error collector 物件,該物件有一個 AddError 方法,用來處理解析 .proto 檔案時遇到的語法錯誤。

之後,需要動態編譯一個 .proto 檔案時,只需呼叫 importer 物件的 import 方法。非常簡單。

那麼我們如何使用動態編譯後的 Message 呢?我們需要首先了解幾個其他的類

Package google::protobuf::compiler 中提供了以下幾個類,用來表示一個 .proto 檔案中定義的 message,以及 Message 中的 field,如圖所示。

圖 3. 各個 Compiler 類之間的關係
圖 3. 各個 Compiler 類之間的關係

類 FileDescriptor 表示一個編譯後的 .proto 檔案;類 Descriptor 對應該檔案中的一個 Message;類 FieldDescriptor 描述一個 Message 中的一個具體 Field。

比如編譯完 lm.helloworld.proto 之後,可以通過如下程式碼得到 lm.helloworld.id 的定義:

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

通過 Descriptor,FieldDescriptor 的各種方法和屬性,應用程式可以獲得各種關於 Message 定義的資訊。比如通過 field->name() 得到 field 的名字。這樣,您就可以使用一個動態定義的訊息了。

編寫新的 proto 編譯器

隨 Google Protocol Buffer 原始碼一起釋出的編譯器 protoc 支援 3 種程式語言:C++,java 和 Python。但使用 Google Protocol Buffer 的 Compiler 包,您可以開發出支援其他語言的新的編譯器。

類 CommandLineInterface 封裝了 protoc 編譯器的前端,包括命令列引數的解析,proto 檔案的編譯等功能。您所需要做的是實現類 CodeGenerator 的派生類,實現諸如程式碼生成等後端工作:

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

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

在 main() 函式內,生成 CommandLineInterface 的物件 cli,呼叫其 RegisterGenerator() 方法將新語言的後端程式碼生成器 yourG 物件註冊給 cli 物件。然後呼叫 cli 的 Run() 方法即可。

這樣生成的編譯器和 protoc 的使用方法相同,接受同樣的命令列引數,cli 將對使用者輸入的 .proto 進行詞法語法等分析工作,最終生成一個語法樹。該樹的結構如圖所示。

圖 5. 語法樹
圖 5. 語法樹

其根節點為一個 FileDescriptor 物件(請參考“動態編譯”一節),並作為輸入引數被傳入 yourG 的 Generator() 方法。在這個方法內,您可以遍歷語法樹,然後生成對應的您所需要的程式碼。簡單說來,要想實現一個新的 compiler,您只需要寫一個 main 函式,和一個實現了方法 Generator() 的派生類即可。

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

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 的方式。

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

訊息經過序列化後會成為一個二進位制資料流,該流中的資料為一系列的 Key-Value 對。如下圖所示:

圖 7. Message Buffer
圖 7. 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 可能的型別如下表所示:

表 1. Wire Type
Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimi string, bytes, embedded messages, packed repeated fields
3 Start group Groups (deprecated)
4 End group Groups (deprecated)
5 32-bit fixed32, sfixed32, float

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

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

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

如圖所示:

圖 8. ZigZag 編碼
圖 8. 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>

封解包的速度

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

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

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

以程式碼清單 3 中的 Reader 為例,該程式首先呼叫 msg1 的 ParseFromIstream 方法,這個方法解析從檔案讀入的二進位制資料流,並將解析出來的資料賦予 helloworld 類的相應資料成員。

該過程可以用下圖表示:

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

整個解析過程需要 Protobuf 本身的框架程式碼和由 Protobuf 編譯器生成的程式碼共同完成。Protobuf 提供了基類 Message 以及 Message_lite 作為通用的 Framework,,CodedInputStream 類,WireFormatLite 類等提供了對二進位制資料的 decode 功能,從 5.1 節的分析來看,Protobuf 的解碼可以通過幾個簡單的數學運算完成,無需複雜的詞法語法分析,因此 ReadTag() 等方法都非常快。 在這個呼叫路徑上的其他類和方法都非常簡單,感興趣的讀者可以自行閱讀。 相對於 XML 的解析過程,以上的流程圖實在是非常簡單吧?這也就是 Protobuf 效率高的第二個原因了。

結束語

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