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

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

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

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

圖4 ProtoBuf注釋示例
最后,執(zhí)行圖5中的編譯指令后,我們就可以在相應(yīng)的目錄下找到生成的對(duì)應(yīng)語(yǔ)言的代碼文件。該文件包含了對(duì)不同message進(jìn)行定義、修改、訪問(wèn)等操作的方法。圖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目標(biāo)代碼存放目錄。以C++為例,執(zhí)行該編譯指令后會(huì)在目標(biāo)目錄生成file.pb.h和file.pb.cc兩個(gè)文件,file.pb.h中聲明了相關(guān)類和方法,file.pb.cc中定義了相關(guān)類和方法。

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

ProtoBuf測(cè)試代碼如下:
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 << "--編碼耗時(shí)--" << endl;
cout << "編碼次數(shù): " << times << " 數(shù)據(jù)長(zhǎng)度: " << 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 << "用時(shí): " << chrono::duration<double,std::milli>(end - start).count() << " ms" << endl;
cout << "--解碼測(cè)試--" << endl;
cout << "解碼次數(shù): " << times << " 數(shù)據(jù)長(zhǎng)度: " << 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 << "用時(shí): " << chrono::duration<double,std::milli>(end - start).count() << " ms" << endl;
}
JSON測(cè)試代碼如下:
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 << "--編碼耗時(shí)--" << endl;
cout << "編碼次數(shù): " << times << " 數(shù)據(jù)長(zhǎng)度: " << 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 << "用時(shí): " << chrono::duration<double,std::milli>(end - start).count() << " ms" << endl;
cout << "--解碼測(cè)試--" << endl;
cout << "解碼次數(shù): " << times << " 數(shù)據(jù)長(zhǎng)度: " << 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 << "用時(shí): " << chrono::duration<double,std::milli>(end - start).count() << " ms" << endl;
}
測(cè)試結(jié)果如下:


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

研究團(tuán)隊(duì)介紹

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

實(shí)驗(yàn)室精神

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

學(xué)者網(wǎng)

評(píng)論 0