在大數(shù)據時代,人們每天都要面對海量數(shù)據,如何存儲和傳輸這些數(shù)據成為了一大難題。Protocol Buffers(簡稱ProtoBuf)是Google公司開發(fā)的一種與語言和平臺無關的、可擴展的、序列化結構數(shù)據的方法,可用于(數(shù)據)通信協(xié)議、數(shù)據存儲等。用戶可以利用ProtoBuf定義數(shù)據的結構,然后使用特殊生成的源代碼輕松地在各種數(shù)據流中使用各種語言來編寫和讀取結構數(shù)據,甚至還可以在不破壞由舊數(shù)據結構編譯的已部署程序的基礎上更新數(shù)據結構。ProtoBuf目前有兩個版本,分別是proto2和proto3,其中最新版本的proto3提供了對C++、C#、Dart、Go、Java、Python、Rust等多種語言的支持。ProtoBuf性能優(yōu)異,目前已經被廣泛應用于QQ、微信等主流通訊工具。

ProtoBuf的使用主要分為兩步,首先需要使用者在.proto文件中定義消息類型,然后使用protoc編譯器根據.proto文件生成相應語言的代碼。
圖1展示了一個簡單的示例。我們定義了一個搜索請求消息,每一個搜索請求都包含了查詢字符query、搜索請求返回的頁面數(shù)量page_number和每頁中的結果數(shù)量result_per_page。該示例的第一行聲明了我們正在使用proto3。如果沒有該行,ProtoBuf編譯器將默認使用proto2。需注意,該行一定要位于文件的第一非空且非注釋行。

圖1 ProtoBuf搜索請求消息示例
該請求消息示例中的SearchRequest消息定義了三個字段,每一個字段都有一個定義類型、一個字段名稱以及字段編號。ProtoBuf提供了大量標準數(shù)據類型,其中常用的有:double、float、int32、int64、bool、string、bytes等。此外,message中每個字段都可以指定一個修飾符,proto3默認使用singular修飾符,表示可以有0個或者1個該字段,但不能超過一個。除此之外,還有一種repeated修飾符,表示對應的字段在message中可以有任意數(shù)量個,包括0個。

圖2 ProtoBuf嵌套類型字段示例
message中包含的字段類型除了默認支持類型外,還支持嵌套類型,即字段類型為所定義的其他message類型。如圖2所示,我們在SearchResponse消息中包含了一個repeated修飾的Result類型字段。
圖3 ProtoBuf多消息定義示例
在同一個.proto文件中可以定義多個message類型,如圖3所示,我們在該.proto文件中定義了SearchRequest和SearchResponse兩個消息類型。與此同時,ProtoBuf也可以在不同的.proto文件中定義message,然后通過import語法進行引入。為了防止出現(xiàn)命名沖突的問題,.proto文件將通過引入package語法解決命名沖突的問題。
在解析消息時,如果使用singular修飾符的字段不包含數(shù)據,那么ProtoBuf會給對應字段設定默認值。對于string類型的字段,其默認值為空字符串;對于bytes類型的字段,其默認值為空字節(jié);對于bool類型的字段,其默認值為false;對于數(shù)字類型的字段,其默認值為0;對于enums類型的字段,其默認值為第一個定義的枚舉類型。此外,對于使用repeated修飾符的字段,其默認值為對應語言的空列表。需要特別注意的是,proto2支持指定字段默認值,但proto3已經取消了該語法。
在代碼注釋上,ProtoBuf采用與C/C++相同的 // 和 /**/ 注釋格式,如圖4所示。

圖4 ProtoBuf注釋示例
最后,執(zhí)行圖5中的編譯指令后,我們就可以在相應的目錄下找到生成的對應語言的代碼文件。該文件包含了對不同message進行定義、修改、訪問等操作的方法。圖5中的IMPORT_PATH表示import文件的搜索目錄,--cpp_out、--java_out、--python_out、--go_out、--ruby_out、--objc_out、--csharp-out、--php_out分別表示生成的C++、Java、Python、GO、Ruby、Objective-C、C#、PHP目標代碼存放目錄。以C++為例,執(zhí)行該編譯指令后會在目標目錄生成file.pb.h和file.pb.cc兩個文件,file.pb.h中聲明了相關類和方法,file.pb.cc中定義了相關類和方法。

圖5 ProtoBuf編譯指令示例
俗話說得好:“光說不練假把式。”接下來,我們就拿ProtoBuf與目前最常見的同類型工具JSON進行對比,看看它到底強在哪里。JSON作為一種輕量級的基于文本的編碼方法,也可以用來存儲結構化數(shù)據,經常被應用于Client/Server端的通訊中。在對比實驗中,我們選擇由騰訊公司發(fā)布的、使用性能較好的RapidJSON,基于C++編程語言進行測試。
.proto文件如下:

ProtoBuf測試代碼如下:
void Protobuf(int times)
{
Person person;
person.set_id(1000000);
person.set_name("XIAOMING");
person.add_phone_num(1008611);
person.add_phone_num(1001011);
string person_string;
cout << "[Protobuf]" << endl << "--編碼耗時--" << endl;
cout << "編碼次數(shù): " << times << " 數(shù)據長度: " << person.SerializeAsString().size() << endl;
auto start = chrono::steady_clock::now();
for (size_t index = 0; index < times; ++index)
{
person_string = person.SerializeAsString();
}
auto end = chrono::steady_clock::now();
cout << "用時: " << chrono::duration<double,std::milli>(end - start).count() << " ms" << endl;
cout << "--解碼測試--" << endl;
cout << "解碼次數(shù): " << times << " 數(shù)據長度: " << person.SerializeAsString().size() << endl;
start = chrono::steady_clock::now();
for (size_t index = 0; index < times; ++index)
{
person.ParseFromString(person_string);
}
end = chrono::steady_clock::now();
cout << "用時: " << chrono::duration<double,std::milli>(end - start).count() << " ms" << endl;
}
JSON測試代碼如下:
void Json(int times)
{
Document doc;
doc.Parse("{}");
doc.AddMember("id", 1000000, doc.GetAllocator());
doc.AddMember("name", "XIAOMING", doc.GetAllocator());
Value phone_number(kArrayType);
phone_number.PushBack(1008611, doc.GetAllocator());
phone_number.PushBack(1001011, doc.GetAllocator());
const char *person_string;
StringBuffer buffer;
Writer<StringBuffer> writer(buffer);
doc.Accept(writer);
person_string = buffer.GetString();
cout << "[JSON]" << endl << "--編碼耗時--" << endl;
cout << "編碼次數(shù): " << times << " 數(shù)據長度: " << buffer.GetSize() << endl;
auto start = chrono::steady_clock::now();
for (size_t index = 0; index < times; ++index)
{
buffer.Clear();
writer.Reset(buffer);
doc.Accept(writer);
}
auto end = chrono::steady_clock::now();
cout << "用時: " << chrono::duration<double,std::milli>(end - start).count() << " ms" << endl;
cout << "--解碼測試--" << endl;
cout << "解碼次數(shù): " << times << " 數(shù)據長度: " << buffer.GetSize() << endl;
start = chrono::steady_clock::now();
for (size_t index = 0; index < times; ++index)
{
doc.Parse(person_string);
}
end = chrono::steady_clock::now();
cout << "用時: " << chrono::duration<double,std::milli>(end - start).count() << " ms" << endl;
}
測試結果如下:


從上述測試結果可以看出,整體上ProtoBuf的編碼效率為RapidJSON的2.99倍,其解碼效率為RapidJSON的3.29倍,而且存儲空間僅為RapidJSON的68.75%。當編碼和解碼頻率較低時,二者耗時差異不明顯;但當編碼和解碼頻率較高時,ProtoBuf可以節(jié)省大量的時間。當數(shù)據量較大時,使用ProtoBuf可以有效降低空間需求,在網絡傳輸場景下,可以降低對網絡的要求,提高數(shù)據傳輸效率。
總體來說,ProtoBuf序列化和反序列的性能都比較高,編碼后的數(shù)據大小也不錯,編程模式簡單易學,同時擁有較為完備的文檔和示例,有需要的小伙伴放心用起來吧!

研究團隊介紹

智能算法研究中心(原智能算法實驗室,2018年與2020年更名)主要承擔國內外重要智能算法類的研究課題,以算法與軟件工具包的形式,根據國內外企業(yè)、科研與教育機構等單位在智能信息處理方面的需求,解決相關技術難點問題,并從中培養(yǎng)國際化算法研究型人才與算法工程化人才。
實驗室必修課

實驗室精神

-END-
![]()
總編:黃翰
責任編輯:袁中錦
文字:劉一鳴
圖片:劉一鳴
校稿:何莉怡
時間:2021年12月30日

學者網

評論 0