之前在網(wǎng)絡(luò)通信和通用數(shù)據(jù)交換等應(yīng)用場(chǎng)景中經(jīng)常使用的技術(shù)是 JSON 或 XML,而在最近的開(kāi)發(fā)中接觸到了 Google 的 ProtoBuf。
在查閱相關(guān)資料學(xué)習(xí) ProtoBuf 以及研讀其源碼之后,發(fā)現(xiàn)其在效率、兼容性等方面非常出色。在以后的項(xiàng)目技術(shù)選型中,尤其是網(wǎng)絡(luò)通信、通用數(shù)據(jù)交換等場(chǎng)景應(yīng)該會(huì)優(yōu)先選擇 ProtoBuf。
自己在學(xué)習(xí) ProtoBuf 的過(guò)程中翻譯了官方的主要文檔,一來(lái)當(dāng)然是在學(xué)習(xí) ProtoBuf,二來(lái)是培養(yǎng)閱讀英文文檔的能力,三來(lái)是因?yàn)?Google 的文檔?不存在的!
看完這些文檔對(duì) ProtoBuf 應(yīng)該就有相當(dāng)程度的了解了。
翻譯文檔見(jiàn) [索引]文章索引,導(dǎo)航為翻譯 - 技術(shù) - ProtoBuf 官方文檔。
但是官方文檔更多的是作為查閱和權(quán)威參考,并不意味著看完官方文檔就能立馬理解其原理。
本文以及接下來(lái)的幾篇文章會(huì)對(duì) ProtoBuf 的編碼、序列化、反序列化、反射等原理做一些詳細(xì)介紹,同時(shí)也會(huì)盡量將這些原理表達(dá)的更為通俗易懂。
何為 ProtoBuf
我們先來(lái)看看官方文檔給出的定義和描述:
protocol buffers 是一種語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān)、可擴(kuò)展的序列化結(jié)構(gòu)數(shù)據(jù)的方法,它可用于(數(shù)據(jù))通信協(xié)議、數(shù)據(jù)存儲(chǔ)等。
Protocol Buffers 是一種靈活,高效,自動(dòng)化機(jī)制的結(jié)構(gòu)數(shù)據(jù)序列化方法-可類比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更為簡(jiǎn)單。
你可以定義數(shù)據(jù)的結(jié)構(gòu),然后使用特殊生成的源代碼輕松的在各種數(shù)據(jù)流中使用各種語(yǔ)言進(jìn)行編寫和讀取結(jié)構(gòu)數(shù)據(jù)。你甚至可以更新數(shù)據(jù)結(jié)構(gòu),而不破壞由舊數(shù)據(jù)結(jié)構(gòu)編譯的已部署程序。
簡(jiǎn)單來(lái)講, ProtoBuf 是結(jié)構(gòu)數(shù)據(jù)序列化[1] 方法,可簡(jiǎn)單類比于 XML[2],其具有以下特點(diǎn):
- 語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān)。即 ProtoBuf 支持 Java、C++、Python 等多種語(yǔ)言,支持多個(gè)平臺(tái)
- 高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更為簡(jiǎn)單
- 擴(kuò)展性、兼容性好。你可以更新數(shù)據(jù)結(jié)構(gòu),而不影響和破壞原有的舊程序
序列化[1]:將結(jié)構(gòu)數(shù)據(jù)或對(duì)象轉(zhuǎn)換成能夠被存儲(chǔ)和傳輸(例如網(wǎng)絡(luò)傳輸)的格式,同時(shí)應(yīng)當(dāng)要保證這個(gè)序列化結(jié)果在之后(可能在另一個(gè)計(jì)算環(huán)境中)能夠被重建回原來(lái)的結(jié)構(gòu)數(shù)據(jù)或?qū)ο蟆?br> 更為詳盡的介紹可參閱 維基百科。
類比于 XML[2]:這里主要指在數(shù)據(jù)通信和數(shù)據(jù)存儲(chǔ)應(yīng)用場(chǎng)景中序列化方面的類比,但個(gè)人認(rèn)為 XML 作為一種擴(kuò)展標(biāo)記語(yǔ)言和 ProtoBuf 還是有著本質(zhì)區(qū)別的。
使用 ProtoBuf
對(duì) ProtoBuf 的基本概念有了一定了解之后,我們來(lái)看看具體該如何使用 ProtoBuf。
第一步,創(chuàng)建 .proto 文件,定義數(shù)據(jù)結(jié)構(gòu),如下例1所示:
// 例1: 在 xxx.proto 文件中定義 Example1 message
message Example1 {
optional string stringVal = 1;
optional bytes bytesVal = 2;
message EmbeddedMessage {
int32 int32Val = 1;
string stringVal = 2;
}
optional EmbeddedMessage embeddedExample1 = 3;
repeated int32 repeatedInt32Val = 4;
repeated string repeatedStringVal = 5;
}
我們?cè)谏侠卸x了一個(gè)名為 Example1 的 消息,語(yǔ)法很簡(jiǎn)單,message 關(guān)鍵字后跟上消息名稱:
message xxx {
}
之后我們?cè)谄渲卸x了 message 具有的字段,形式為:
message xxx {
// 字段規(guī)則:required -> 字段只能也必須出現(xiàn) 1 次
// 字段規(guī)則:optional -> 字段可出現(xiàn) 0 次或1次
// 字段規(guī)則:repeated -> 字段可出現(xiàn)任意多次(包括 0)
// 類型:int32、int64、sint32、sint64、string、32-bit ....
// 字段編號(hào):0 ~ 536870911(除去 19000 到 19999 之間的數(shù)字)
字段規(guī)則 類型 名稱 = 字段編號(hào);
}
在上例中,我們定義了:
- 類型 string,名為 stringVal 的 optional 可選字段,字段編號(hào)為 1,此字段可出現(xiàn) 0 或 1 次
- 類型 bytes,名為 bytesVal 的 optional 可選字段,字段編號(hào)為 2,此字段可出現(xiàn) 0 或 1 次
- 類型 EmbeddedMessage(自定義的內(nèi)嵌 message 類型),名為 embeddedExample1 的 optional 可選字段,字段編號(hào)為 3,此字段可出現(xiàn) 0 或 1 次
- 類型 int32,名為 repeatedInt32Val 的 repeated 可重復(fù)字段,字段編號(hào)為 4,此字段可出現(xiàn) 任意多次(包括 0)
- 類型 string,名為 repeatedStringVal 的 repeated 可重復(fù)字段,字段編號(hào)為 5,此字段可出現(xiàn) 任意多次(包括 0)
關(guān)于 proto2 定義 message 消息的更多語(yǔ)法細(xì)節(jié),例如具有支持哪些類型,字段編號(hào)分配、import
導(dǎo)入定義,reserved 保留字段等知識(shí)請(qǐng)參閱 [翻譯] ProtoBuf 官方文檔(二)- 語(yǔ)法指引(proto2)。
關(guān)于定義時(shí)的一些規(guī)范請(qǐng)參閱 [翻譯] ProtoBuf 官方文檔(四)- 規(guī)范指引
第二步,protoc 編譯 .proto 文件生成讀寫接口
我們?cè)?.proto 文件中定義了數(shù)據(jù)結(jié)構(gòu),這些數(shù)據(jù)結(jié)構(gòu)是面向開(kāi)發(fā)者和業(yè)務(wù)程序的,并不面向存儲(chǔ)和傳輸。
當(dāng)需要把這些數(shù)據(jù)進(jìn)行存儲(chǔ)或傳輸時(shí),就需要將這些結(jié)構(gòu)數(shù)據(jù)進(jìn)行序列化、反序列化以及讀寫。那么如何實(shí)現(xiàn)呢?不用擔(dān)心, ProtoBuf 將會(huì)為我們提供相應(yīng)的接口代碼。如何提供?答案就是通過(guò) protoc 這個(gè)編譯器。
可通過(guò)如下命令生成相應(yīng)的接口代碼:
// $SRC_DIR: .proto 所在的源目錄
// --cpp_out: 生成 c++ 代碼
// $DST_DIR: 生成代碼的目標(biāo)目錄
// xxx.proto: 要針對(duì)哪個(gè) proto 文件生成接口代碼
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto
最終生成的代碼將提供類似如下的接口:
第三步,調(diào)用接口實(shí)現(xiàn)序列化、反序列化以及讀寫
針對(duì)第一步中例1定義的 message,我們可以調(diào)用第二步中生成的接口,實(shí)現(xiàn)測(cè)試代碼如下:
//
// Created by yue on 18-7-21.
//
#include <iostream>
#include <fstream>
#include <string>
#include "single_length_delimited_all.pb.h"
int main() {
Example1 example1;
example1.set_stringval("hello,world");
example1.set_bytesval("are you ok?");
Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();
embeddedExample2->set_int32val(1);
embeddedExample2->set_stringval("embeddedInfo");
example1.set_allocated_embeddedexample1(embeddedExample2);
example1.add_repeatedint32val(2);
example1.add_repeatedint32val(3);
example1.add_repeatedstringval("repeated1");
example1.add_repeatedstringval("repeated2");
std::string filename = "single_length_delimited_all_example1_val_result";
std::fstream output(filename, std::ios::out | std::ios::trunc | std::ios::binary);
if (!example1.SerializeToOstream(&output)) {
std::cerr << "Failed to write example1." << std::endl;
exit(-1);
}
return 0;
}
關(guān)于 protoc 的使用以及接口調(diào)用的更多信息可參閱 [翻譯] ProtoBuf 官方文檔(九)- (C++開(kāi)發(fā))教程
關(guān)于例1的完整代碼請(qǐng)參閱 源碼:protobuf 例1。其中的 single_length_delimited_all.* 為例子相關(guān)代碼和文件。
因?yàn)榇讼盗形恼轮攸c(diǎn)在于深入 ProtoBuf 的編碼、序列化、反射等原理,關(guān)于 ProtoBuf 的語(yǔ)法、使用等只做簡(jiǎn)單介紹,更為詳見(jiàn)的使用教程可參閱我翻譯的系列官方文檔。
關(guān)于 ProtoBuf 的一些思考
官方文檔以及網(wǎng)上很多文章提到 ProtoBuf 可類比 XML 或 JSON。
那么 ProtoBuf 是否就等同于 XML 和 JSON 呢,它們是否具有完全相同的應(yīng)用場(chǎng)景呢?
個(gè)人認(rèn)為如果要將 ProtoBuf、XML、JSON 三者放到一起去比較,應(yīng)該區(qū)分兩個(gè)維度。一個(gè)是數(shù)據(jù)結(jié)構(gòu)化,一個(gè)是數(shù)據(jù)序列化。這里的數(shù)據(jù)結(jié)構(gòu)化主要面向開(kāi)發(fā)或業(yè)務(wù)層面,數(shù)據(jù)序列化面向通信或存儲(chǔ)層面,當(dāng)然數(shù)據(jù)序列化也需要“結(jié)構(gòu)”和“格式”,所以這兩者之間的區(qū)別主要在于面向領(lǐng)域和場(chǎng)景不同,一般要求和側(cè)重點(diǎn)也會(huì)有所不同。數(shù)據(jù)結(jié)構(gòu)化側(cè)重人類可讀性甚至有時(shí)會(huì)強(qiáng)調(diào)語(yǔ)義表達(dá)能力,而數(shù)據(jù)序列化側(cè)重效率和壓縮。
從這兩個(gè)維度,我們可以做出下面的一些思考。
XML 作為一種擴(kuò)展標(biāo)記語(yǔ)言,JSON 作為源于 JS 的數(shù)據(jù)格式,都具有數(shù)據(jù)結(jié)構(gòu)化的能力。
例如 XML 可以衍生出 HTML (雖然 HTML 早于 XML,但從概念上講,HTML 只是預(yù)定義標(biāo)簽的 XML),HTML 的作用是標(biāo)記和表達(dá)萬(wàn)維網(wǎng)中資源的結(jié)構(gòu),以便瀏覽器更好的展示萬(wàn)維網(wǎng)資源,同時(shí)也要盡可能保證其人類可讀以便開(kāi)發(fā)人員進(jìn)行編輯,這就是面向業(yè)務(wù)或開(kāi)發(fā)層面的數(shù)據(jù)結(jié)構(gòu)化。
再如 XML 還可衍生出 RDF/RDFS,進(jìn)一步表達(dá)語(yǔ)義網(wǎng)中資源的關(guān)系和語(yǔ)義,同樣它強(qiáng)調(diào)數(shù)據(jù)結(jié)構(gòu)化的能力和人類可讀。
關(guān)于 RDF/RDFS 和語(yǔ)義網(wǎng)的概念可查詢相關(guān)資料了解,或參閱 2-Answer 系列-本體構(gòu)建模塊(一) 和 3-Answer 系列-本體構(gòu)建模塊(二) ,文中有一些簡(jiǎn)單介紹。
JSON 也是同理,在很多場(chǎng)合更多的是體現(xiàn)了數(shù)據(jù)結(jié)構(gòu)化的能力,例如作為交互接口的數(shù)據(jù)結(jié)構(gòu)的表達(dá)。在 MongoDB 中采用 JSON 作為查詢語(yǔ)句,也是在發(fā)揮其數(shù)據(jù)結(jié)構(gòu)化的能力。
當(dāng)然,JSON、XML 同樣也可以直接被用來(lái)數(shù)據(jù)序列化,實(shí)際上很多時(shí)候它們也是這么被使用的,例如直接采用 JSON、XML 進(jìn)行網(wǎng)絡(luò)通信傳輸,此時(shí) JSON、XML 就成了一種序列化格式,它發(fā)揮了數(shù)據(jù)序列化的能力。但是經(jīng)常這么被使用,不代表這么做就是合理。實(shí)際將 JSON、XML 直接作用數(shù)據(jù)序列化通常并不是最優(yōu)選擇,因?yàn)樗鼈冊(cè)谒俣取⑿省⒖臻g上并不是最優(yōu)。換句話說(shuō)它們更適合數(shù)據(jù)結(jié)構(gòu)化而非數(shù)據(jù)序列化。
扯完 XML 和 JSON,我們來(lái)看看 ProtoBuf,同樣的 ProtoBuf 也具有數(shù)據(jù)結(jié)構(gòu)化的能力,其實(shí)也就是上面介紹的 message 定義。我們能夠在 .proto 文件中,通過(guò) message、import、內(nèi)嵌 message 等語(yǔ)法來(lái)實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)化,但是很容易能夠看出,ProtoBuf 在數(shù)據(jù)結(jié)構(gòu)化方面和 XML、JSON 相差較大,人類可讀性較差,不適合上面提到的 XML、JSON 的一些應(yīng)用場(chǎng)景。
但是如果從數(shù)據(jù)序列化的角度你會(huì)發(fā)現(xiàn) ProtoBuf 有著明顯的優(yōu)勢(shì),效率、速度、空間幾乎全面占優(yōu),看完后面的 ProtoBuf 編碼的文章,你更會(huì)了解 ProtoBuf 是如何極盡所能的壓榨每一寸空間和性能,而其中的編碼原理正是 ProtoBuf 的關(guān)鍵所在,message 的表達(dá)能力并不是 ProtoBuf 最關(guān)鍵的重點(diǎn)。所以可以看出 ProtoBuf 重點(diǎn)側(cè)重于數(shù)據(jù)序列化 而非 數(shù)據(jù)結(jié)構(gòu)化。
最終對(duì)這些個(gè)人思考做一些小小的總結(jié):
- XML、JSON、ProtoBuf 都具有數(shù)據(jù)結(jié)構(gòu)化和數(shù)據(jù)序列化的能力
- XML、JSON 更注重數(shù)據(jù)結(jié)構(gòu)化,關(guān)注人類可讀性和語(yǔ)義表達(dá)能力。ProtoBuf 更注重數(shù)據(jù)序列化,關(guān)注效率、空間、速度,人類可讀性差,語(yǔ)義表達(dá)能力不足(為保證極致的效率,會(huì)舍棄一部分元信息)
- ProtoBuf 的應(yīng)用場(chǎng)景更為明確,XML、JSON 的應(yīng)用場(chǎng)景更為豐富。
下一篇
汪
汪