田丹,張金杰,李翀,曲艷華,焦昊
1. 中國(guó)中鋼集團(tuán)有限公司,北京 100180
2. 中國(guó)科學(xué)院計(jì)算機(jī)網(wǎng)絡(luò)信息中心,北京 100190
3. 中國(guó)科學(xué)院大學(xué),北京 100049
4. 戰(zhàn)略支援部隊(duì)航天系統(tǒng)部,北京 100094
隨著互聯(lián)網(wǎng)的不斷發(fā)展,興起了多種數(shù)據(jù)交換格式,其中比較常用的便是JSON、Google Protocol Buffer 和XML,他們都有各自的使用場(chǎng)景。JSON 是一種輕量級(jí)的數(shù)據(jù)交換格式,它基于JavaScript 的一個(gè)子集,采用完全獨(dú)立于語(yǔ)言的文本格式,易于編寫和解析[1]。Google Protocol Buffer(簡(jiǎn)稱Protobuf或PB)是一種靈活高效的、用于序列化結(jié)構(gòu)化數(shù)據(jù)的機(jī)制,類似于XML,但比XML 更小且更簡(jiǎn)單[2]。Protobuf 序列化為二進(jìn)制數(shù)據(jù),不依賴平臺(tái)和語(yǔ)言,同時(shí)具備很好的兼容性。XML 是一種重量級(jí)數(shù)據(jù)交換格式,是可擴(kuò)展標(biāo)記語(yǔ)言。相比于前兩種數(shù)據(jù)交換格式,XML 占用帶寬比較大,現(xiàn)在主要用于描述數(shù)據(jù)和用作配置文件,在服務(wù)器與Web/客戶端中使用較少[3]。本文重點(diǎn)研究JSON 和Protobuf 的互相轉(zhuǎn)換方法。
JSON 和Protobuf 都是應(yīng)用非常廣泛的數(shù)據(jù)交換格式,兩者在使用場(chǎng)景上有所不同:JSON 主要用于Web 場(chǎng)景、數(shù)據(jù)對(duì)人可讀、服務(wù)端應(yīng)用程序向Web瀏覽器發(fā)送數(shù)據(jù)和跨組織的API 的交互場(chǎng)合更適合;Protobuf 自帶加密功能,主要用于客戶端到服務(wù)器端高效安全數(shù)據(jù)傳輸。Protobuf 編解碼速度和數(shù)據(jù)大小上有更多優(yōu)勢(shì),可以得到最快的序列化速度和最小的結(jié)果[4]。當(dāng)Protobuf 數(shù)據(jù)需要發(fā)送給Web 瀏覽器以供人們?yōu)g覽時(shí),或者Web 瀏覽器產(chǎn)生的JSON 數(shù)據(jù)需要大批量進(jìn)行高效傳輸時(shí),需要兩種異構(gòu)數(shù)據(jù)進(jìn)行相互轉(zhuǎn)換。
目前開源的JSON 與Protobuf 的轉(zhuǎn)換工具有Flask-Pbj、pbf2JSON、Node-proto2JSON、pb2doc等。Flask-Pbj 是基于python 實(shí)現(xiàn)的,它提供了對(duì)Protobuf 和JSON 格式的請(qǐng)求和響應(yīng)數(shù)據(jù)的支持,且API 修飾器可以實(shí)現(xiàn)JSON 或Protobuf 格式的消息與Python 字典之間的序列化和反序列化。pbf2JSON 是基于Go 實(shí)現(xiàn)的,它是一個(gè)輸出JSON的OpenStreetMap pbf 解析器,允許用戶挑選標(biāo)簽并處理反規(guī)范化的方式和關(guān)系,可作為獨(dú)立的二進(jìn)制文件提供,并帶有方便的npm 包裝器。Nodeproto2JSON 是基于JavaScript 的 .proto 文件到JSON 轉(zhuǎn)換器,沒有額外的依賴關(guān)系。pb2doc 是用于將Protobuf 消息轉(zhuǎn)換為JSON 或html 的解析器,具有遞歸解析消息、支持grpc 服務(wù)、可配置的文檔類型、基于注釋的服務(wù)描述等功能??梢?,上述轉(zhuǎn)換工具開發(fā)語(yǔ)言各異且較小眾,多數(shù)工具使用步驟繁瑣復(fù)雜,只能實(shí)現(xiàn)單向轉(zhuǎn)換,或者無(wú)法實(shí)現(xiàn)批量轉(zhuǎn)換。
本文基于C 語(yǔ)言實(shí)現(xiàn)了Protobuf 與JSON 數(shù)據(jù)轉(zhuǎn)換,可以方便快捷地將Protobuf 格式的二進(jìn)制數(shù)據(jù)批量轉(zhuǎn)換為JSON 格式的文本數(shù)據(jù),該方法可靠穩(wěn)定且兼容性好,同理也很容易將若干JSON 數(shù)據(jù)批量轉(zhuǎn)換為Protobuf 格式二進(jìn)制數(shù)據(jù)。
Protobuf 是Google 公司內(nèi)部的混合語(yǔ)言數(shù)據(jù)標(biāo)準(zhǔn),最新版本為3.X,最初用于實(shí)現(xiàn)客戶端與服務(wù)器之間的通信協(xié)議,是一種靈活高效的結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)格式[5]。Google 提供了一套軟件庫(kù),可以把Protobuf 的記錄序列化和反序列化,序列化后的數(shù)據(jù)小以及數(shù)據(jù)解析速度快,方便網(wǎng)絡(luò)傳輸和存儲(chǔ)。由于Protobuf 是二進(jìn)制數(shù)據(jù)格式,編碼和解碼雙方必須有共同的.proto 文件才能獲取到相應(yīng)的信息,數(shù)據(jù)本身不具有可讀性,因此只能反序列化之后得到真正可讀的數(shù)據(jù),一定程度上保證了其安全性;另一方面,二進(jìn)制數(shù)據(jù)比使用XML 等其他類型進(jìn)行數(shù)據(jù)交換要快得多,因此可以把它用于分布式應(yīng)用之間的數(shù)據(jù)通信或者異構(gòu)環(huán)境下的數(shù)據(jù)交換[6-7]。作為一種效率和兼容性都很優(yōu)秀的二進(jìn)制數(shù)據(jù)傳輸格式,Protobuf 可以用于諸如網(wǎng)絡(luò)傳輸、配置文件、數(shù)據(jù)存儲(chǔ)等諸多領(lǐng)域,目前提供了 C++、Java、Python、JS、Ruby 等多種語(yǔ)言的API。
要使用Protobuf,首先我們需要編寫一個(gè).proto文件來(lái)定義結(jié)構(gòu)化數(shù)據(jù)Message。如圖1 所示,Protobuf 還允許對(duì)消息的嵌套,使得其能夠表達(dá)更為復(fù)雜的數(shù)據(jù)結(jié)構(gòu),功能更加強(qiáng)大。
通過Protobuf 編譯器將.proto 文件轉(zhuǎn)換成對(duì)應(yīng)平臺(tái)(Python、C++、Java)的代碼文件。在C/C++中通過包含該代碼的頭文件就能使用定義好的數(shù)據(jù)類型,諸如對(duì)消息的成員進(jìn)行賦值,將消息序列化等都有相應(yīng)的方法。
圖1 消息的嵌套Fig.1 Demo for nested message types
Protobuf 的核心技術(shù)之一是序列化與反序列化。序列化是指將數(shù)據(jù)結(jié)構(gòu)或?qū)ο筠D(zhuǎn)換成二進(jìn)制串的過程,而反序列化則是上述過程的逆操作,即將序列化過程中所生成的二進(jìn)制串轉(zhuǎn)換成數(shù)據(jù)結(jié)構(gòu)或者對(duì)象。
Protobuf 將消息里的每個(gè)字段進(jìn)行編碼后,再利用T-L-V 存儲(chǔ)方式進(jìn)行數(shù)據(jù)的存儲(chǔ),最終得到的是一個(gè)二進(jìn)制字節(jié)流[8]。其中T 即Tag,字段的標(biāo)識(shí)號(hào),也叫Key;L 是Value 的字節(jié)長(zhǎng)度;V 是該字段對(duì)應(yīng)的值Value,即消息字段經(jīng)過編碼后的值。序列化后的Value 是按原樣保存到字符串或者文件中,Key 按照一定的轉(zhuǎn)換條件保存起來(lái),序列化后的結(jié)果就是如圖2 所示的形式。
圖2 序列化結(jié)果T-L-V 示意Fig.2 Serialization result T-L-V diagram
Key 的序列化格式是按照Message 中字段后面的域號(hào)與字段類型來(lái)轉(zhuǎn)換。序列化過程不需要分隔符就能分隔字段,各個(gè)字段存儲(chǔ)得非常緊湊,存儲(chǔ)空間利用率非常高;如果一個(gè)字段沒有被設(shè)置字段值,那么該字段在序列化時(shí)對(duì)應(yīng)的數(shù)據(jù)中是完全不存在的,即不需要編碼。同時(shí)序列化涉及到的運(yùn)算也僅是一些簡(jiǎn)單的數(shù)學(xué)操作,只用到Protocol Buffer 自身的框架代碼和編譯器,無(wú)需其他的工具,這些特點(diǎn)共同保證了運(yùn)算的高效。
Protobuf 反序列化過程如下:(1)調(diào)用消息類的parseFrom(input) 解析從輸入流讀入的二進(jìn)制字節(jié)數(shù)據(jù)流;(2)將解析出來(lái)的數(shù)據(jù)按照指定的格式讀取到Java、C++、Python 對(duì)應(yīng)的結(jié)構(gòu)類型中。由于反序列化是序列化的逆過程,因此同樣無(wú)需復(fù)雜的詞法語(yǔ)法分析,解析過程只需要通過簡(jiǎn)單的解碼方式即可完成[9]。
Jansson[10]是一個(gè)用于解碼、編碼和操作JSON的C 語(yǔ)言庫(kù),其提供了簡(jiǎn)單直觀的API 和數(shù)據(jù)模型,無(wú)需其他依賴項(xiàng),并完整地支持Unicode 編碼。JSON 規(guī)范定義了以下數(shù)據(jù)類型:對(duì)象、數(shù)組、字符串、數(shù)字、布爾值和Null。Jansson 庫(kù)中分別對(duì)應(yīng)JSON_OBJECT、JSON_ARRAY、JSON_STRING、JSON_INTEGER、JSON_REAL、JSON_TRUE、JSON_FALSE、JSON_NULL 類型。由于JSON 數(shù)據(jù)結(jié)構(gòu)是動(dòng)態(tài)變化的,使用數(shù)據(jù)結(jié)構(gòu)JSON_t 來(lái)表示所有JSON值。JSON_typeof函數(shù)可以獲得JSON值的類型。
Jansson 庫(kù)中所有以JSON 值作為參數(shù)的函數(shù)都將管理引用,即根據(jù)需要增加和減少引用計(jì)數(shù)。創(chuàng)建新JSON 值的函數(shù)將引用計(jì)數(shù)設(shè)置為1。如果函數(shù)調(diào)用增加了引用計(jì)數(shù),一旦不再需要該值,應(yīng)調(diào)用JSON_decref 釋放引用,即該值將被銷毀并且無(wú)法再使用。
在本部分,本文以Protobuf 轉(zhuǎn)JSON 為例,詳細(xì)地介紹設(shè)計(jì)思路與具體的實(shí)現(xiàn)方法。而JSON 轉(zhuǎn)Protobuf 為其的逆過程,兩者使用的思路與方法基本一致,且從JSON 結(jié)構(gòu)化到二進(jìn)制轉(zhuǎn)換相對(duì)更簡(jiǎn)單,本文不再贅述。
將Protobuf 轉(zhuǎn)換為JSON,需要以下四個(gè)步驟:
(1)讀入二進(jìn)制文件和.proto 描述文件;
(2)根據(jù).proto 文件解析出消息類型結(jié)構(gòu);
(3)動(dòng)態(tài)解析出二進(jìn)制文件中每條數(shù)據(jù)所屬的消息類型及其各個(gè)字段的含義,并組成一個(gè)或多個(gè)消息對(duì)象;
(4)根據(jù)消息對(duì)象中的每個(gè)field 的類型和屬性,找到其對(duì)應(yīng)的JSON 格式,構(gòu)造為JSON 數(shù)據(jù)。
綜合以上四個(gè)步驟,本文將實(shí)現(xiàn)的主要內(nèi)容分為兩個(gè)模塊:二進(jìn)制文件的動(dòng)態(tài)解析模塊和Protobuf轉(zhuǎn)JSON 模塊。
2.2.1 問題分析
通常輸入僅有Protobuf 的Schema 定義以及相應(yīng)的二進(jìn)制Protobuf 數(shù)據(jù),即Message 的定義事先是未知的,因此我們需要根據(jù).proto 文件解析出每條Message 的結(jié)構(gòu),根據(jù)Protobuf 二進(jìn)制數(shù)據(jù)解析出不同的數(shù)據(jù)記錄,并確定每條數(shù)據(jù)記錄所對(duì)應(yīng)的消息類型。
由于Protobuf 打包的數(shù)據(jù)沒有自帶長(zhǎng)度信息或終結(jié)符,當(dāng)有多條序列化二進(jìn)制數(shù)據(jù)時(shí),我們無(wú)法判斷哪一部分對(duì)應(yīng)的是一條完整的數(shù)據(jù),無(wú)法直接進(jìn)行反序列化。因此需要解決如下問題:第一:長(zhǎng)度。由應(yīng)用程序自己在發(fā)送和接收的時(shí)候做正確的切分;要求生成的分隔符不能與消息內(nèi)容重復(fù),否則可能出現(xiàn)無(wú)法區(qū)分的情況,從而無(wú)法獲取正確的Protobuf格式的數(shù)據(jù)。第二:類型。Protobuf 打包的數(shù)據(jù)沒有自帶類型信息,需要由發(fā)送方把類型信息發(fā)送給接收方,接收方創(chuàng)建具體的 Protobuf Message 對(duì)象,再做對(duì)應(yīng)的反序列化。
因此我們規(guī)定分隔符形式為:^@UCAS@Message_type^,其中Message_type 表示每條完整的二進(jìn)制數(shù)據(jù)對(duì)應(yīng)的類型。將其添加分隔符后如圖3所示。
圖3 加入分隔符后的序列化二進(jìn)制數(shù)據(jù)Fig.3 Serialized binary data with delimiters
根據(jù)對(duì)應(yīng)的分隔符,可以明確得出圖中含有三條序列化二進(jìn)制數(shù)據(jù),第一條、第三條對(duì)應(yīng)的消息類型為Person,第二條對(duì)應(yīng)的消息類型為Test。
在確定了每條數(shù)據(jù)的內(nèi)容以及其所對(duì)應(yīng)的Message 類型后,接下來(lái)需要研究如何自動(dòng)創(chuàng)建具體的 Protobuf Message 對(duì)象,再對(duì)其做相應(yīng)的反序列化。Google Protobuf 本身實(shí)現(xiàn)了根據(jù) type name 創(chuàng)建具體類型的 Message 對(duì)象這一功能。Protobuf Message class 采用了 prototype pattern,Message class 定義了 New() 虛函數(shù),用以返回本對(duì)象的一份新實(shí)例,類型與本對(duì)象的真實(shí)類型相同。也就是說,只需要Message* 指針,而不用知道它的具體類型,就能創(chuàng)建和它類型一樣的具體 Message Type 的對(duì)象。具體的實(shí)現(xiàn)步驟如下:
(1)調(diào)用 DescriptorPool::generated_pool() 找到一個(gè) DescriptorPool 對(duì)象,它包含了程序編譯的時(shí)候所鏈接的全部 Protobuf Message types。
(2)調(diào)用DescriptorPool::FindMessageTypeByNa me() 根據(jù) type name 查找相應(yīng) Descriptor。
(3)調(diào)用 MessageFactory::generated_factory()找到 MessageFactory 對(duì)象,創(chuàng)建程序編譯的時(shí)候所鏈接的全部 Protobuf Message types。
(4)調(diào)用 MessageFactory::GetPrototype() 找到具體 Message Type 的default instance。
(5)調(diào)用 prototype->New() 創(chuàng)建對(duì)象。
2.2.2 主要代碼實(shí)現(xiàn)
(1)string analyPackage(string protoPath)
函數(shù)參數(shù):.proto 文件的路徑。
函數(shù)功能:根據(jù).proto 文件解析出對(duì)應(yīng)的包名,若沒有則返回空。如果.proto 文件中使用了package語(yǔ)句,則需要使用對(duì)應(yīng)的package_name 才能訪問其內(nèi)部的Message 類型。
(2)string analyMessage(string protoPath);
函數(shù)參數(shù):.proto 文件的路徑。
函數(shù)功能:保證程序的健壯性。我們要求序列化的文件一定要有分隔符,若沒有檢測(cè)到分隔符,則默認(rèn)該條數(shù)據(jù)對(duì)應(yīng).proto 文件的第一個(gè)Message類型,此時(shí)需要從.proto 文件解析其第一條Message類型。
(3)string regexClassName(string p_str)
函數(shù)參數(shù):給定的字符串(含有自定義的分隔符)。
函數(shù)功能:從分隔符中獲取二進(jìn)制數(shù)據(jù)所對(duì)應(yīng)的類型。采取正則表達(dá)式的方式來(lái)實(shí)現(xiàn):
regex reg(“\^@UCAS@(.*)\^”)
smatch m;
auto ret = regex_search(p_str, m, reg);
(4)int dynamicParseFromProtoFile(const string &filePath, const string &MessageName, function<void(::google::protobuf::Message *msg)> callBack)
函數(shù)參數(shù):.proto 文件的路徑;.proto 文件中第一個(gè)Message 類型;回調(diào)函數(shù)。
函數(shù)功能及說明:對(duì)給定的一條二進(jìn)制數(shù)據(jù)msg 進(jìn)行動(dòng)態(tài)解析,若某條二進(jìn)制數(shù)據(jù)不含有分隔符,則默認(rèn)其對(duì)應(yīng).proto 文件的第一個(gè)Message 類型?;卣{(diào)函數(shù)就是一個(gè)通過函數(shù)指針調(diào)用的函數(shù)。本函數(shù)的第三個(gè)參數(shù)為一個(gè)回調(diào)函數(shù),該函數(shù)的參數(shù)為一個(gè)::google::protobuf::Message 類型的指針,用于承接解析出來(lái)的Protobuf 數(shù)據(jù)。
動(dòng)態(tài)解析部分的核心代碼如下:
2.3.1 問題分析
得到Protobuf 格式的消息對(duì)象后,需要對(duì)消息對(duì)象中的數(shù)據(jù)進(jìn)行解析,并按照J(rèn)SON 格式構(gòu)建數(shù)據(jù),轉(zhuǎn)換為字符串類型,寫入JSON 文件。
需要用到Protobuf 提供的Descriptor、Field- Descriptor 和Reflection 類的API。
Descriptor 類:描述一種Message 類型(不是一個(gè)單獨(dú)的Message 對(duì)象)的meta 信息??梢酝ㄟ^Descriptor 獲取任意Message 或service 的屬性和方法,包括Message 的名字、所有字段的描述、原始的proto 文件內(nèi)容。
FieldDescriptor 類:Message 中的每項(xiàng)field 類型的描述類,利用該類就能獲得每個(gè)field 的名稱、類型、屬性等信息。
Reflection 類:提供方法來(lái)動(dòng)態(tài)訪問/ 修改Message 中的field 的接口類,遍歷解析其中的每個(gè)field 獲取對(duì)應(yīng)的值。
解析每個(gè)filed 的過程是:首先需要判斷field 的屬性是否為重復(fù)的(repeated),如果是重復(fù)的,則需要獲取其重復(fù)數(shù)量和數(shù)據(jù)類型,依次讀取每一項(xiàng)進(jìn)行轉(zhuǎn)換。重復(fù)的field 對(duì)應(yīng)于JSON 中的數(shù)組類型,其每一項(xiàng)都轉(zhuǎn)換為數(shù)組的一個(gè)元素。如果不是重復(fù)的,則需要判斷是否為可選的(optional)。如果是可選的,則判斷這個(gè)field 是否設(shè)值,有值則進(jìn)行以下判斷:
(1) 如果是布爾、字符串和數(shù)值類型,則轉(zhuǎn)換為一個(gè)JSON 對(duì)象的鍵值對(duì),key 為field 的名字,由field->name()得到,value 為該field 的值。其中Double、Float 類型對(duì)應(yīng)JSON 中實(shí)數(shù)類型,Int32、Int64、UInt32、UInt64 類型對(duì)應(yīng)JSON 中的整數(shù)類型。
(2) 如果是枚舉類型,同樣轉(zhuǎn)換為一個(gè)JSON 對(duì)象的鍵值對(duì)。可以通過getEnum->name()獲得字符串,或者getEnum->number()獲取其索引值,可以選取任意一項(xiàng)作為value。
(3) 如果是Message 類型,同樣對(duì)應(yīng)為一個(gè)JSON 對(duì)象,其內(nèi)部的子消息,對(duì)應(yīng)嵌套的JSON 對(duì)象。key 是field 的名字,value 則是子JSON 對(duì)象,通過遞歸調(diào)用解析函數(shù)進(jìn)行嵌套填充。
需要注意的是,由于Jansson 庫(kù)不支持二進(jìn)制字節(jié)的字符,當(dāng)字符串是二進(jìn)制字節(jié)表示時(shí),即類型為FieldDescriptor::TYPE_BYTES,需要轉(zhuǎn)換為十六進(jìn)制序列。需要遍歷所有字節(jié),進(jìn)行與運(yùn)算,轉(zhuǎn)為十六進(jìn)制字符,然后拼接成字符串。使用JSON_dumps 函數(shù)將構(gòu)造好的JSON 對(duì)象轉(zhuǎn)換為字符串,并通過ofstream 流將字符串輸出到文件中。
2.3.2 主要代碼實(shí)現(xiàn)
(1)char *pb2JSON(Message *msg, char *buffer)
函數(shù)參數(shù):空消息對(duì)象msg,二進(jìn)制數(shù)據(jù)緩沖區(qū)buffer
函數(shù)功能:適用于實(shí)參是二進(jìn)制數(shù)據(jù)buffer,在函數(shù)里先進(jìn)行反序列化成Protobuf 數(shù)據(jù),保存在提供的空Message 對(duì)象中,然后再進(jìn)行轉(zhuǎn)換。返回值是JSON 的字符串形式。
(2)char *pb2JSON(Message *msg)
函數(shù)參數(shù):消息對(duì)象msg
函數(shù)功能:適用于實(shí)參是解析好的Protobuf 數(shù)據(jù),直接對(duì)msg 的內(nèi)容進(jìn)行轉(zhuǎn)換;其內(nèi)部功能由以下函數(shù)實(shí)現(xiàn):
①JSON_t *parseFromMsg(const Message *msg)
函數(shù)參數(shù):消息對(duì)象msg
函數(shù)功能:對(duì)msg 的每個(gè)field 進(jìn)行判斷和處理,生成對(duì)應(yīng)的JSON 對(duì)象作為返回值。
②JSON_t *parseFromRepeatField(const Message *msg, const FieldDescriptor *field)
函數(shù)參數(shù):消息對(duì)象msg,field 的描述類對(duì)象
函數(shù)功能:對(duì)判斷為重復(fù)屬性的field 進(jìn)行解析,利用for 循環(huán)對(duì)所有項(xiàng)進(jìn)行處理,返回一個(gè)JSON 對(duì)象。
③string hexEncode(string binInput)
函數(shù)參數(shù):二進(jìn)制的字符串binInput
函數(shù)功能:將每個(gè)二進(jìn)制字符轉(zhuǎn)為十六進(jìn)制字符,然后拼接成字符串,返回十六進(jìn)制字符串。
實(shí)驗(yàn)環(huán)境為Ubuntu 20.04 LTS,Protobuf 3.11.4,Jansson 2.12,GCC 9.3.0。
首先需要編譯安裝Protobuf 與Jansson 庫(kù),因?yàn)楸疚氖腔贑 平臺(tái)的轉(zhuǎn)換工具,因此需要GCC 環(huán)境,安裝配置過程本文不再贅述。
然后通過Protobuf 編譯器編譯.proto 文件,生成.h 文件和.cc 文件。通過在C/C++代碼中包含.h頭文件,可以使用其中的一系列獲取、設(shè)置字段值等方法為Message 中的字段賦值,編寫測(cè)試用例。
為了驗(yàn)證轉(zhuǎn)換的正確性,本文針對(duì)三種比較極端的場(chǎng)景進(jìn)行測(cè)試:
(1)定義一條Message,Message 含有很多成員;
(2)定義多條Message,每條Message 不含或僅含較少成員;
(3)多重嵌套測(cè)試。
進(jìn)行正確性測(cè)試的目的是測(cè)試程序能否正常執(zhí)行,以及當(dāng)傳遞以上3 種情況的信息時(shí),程序能否對(duì)每個(gè)Message 以及Message 內(nèi)部的成員進(jìn)行正確的解析并返回正確結(jié)果。
多重嵌套用例如圖4 所示,實(shí)驗(yàn)表明設(shè)置大量Message Type 對(duì)象并不影響程序的正確性,解析程序可以正確轉(zhuǎn)換多重嵌套的Message 情況。且由于每個(gè)Message 中的成員較少,Message 在處理能力范圍內(nèi)對(duì)性能的影響不大,可以保證較好的運(yùn)行效率。測(cè)試結(jié)果如圖5 所示。
圖4 多層嵌套用例Fig.4 Multilevel nested message use case
圖5 多層嵌套測(cè)試結(jié)果Fig.5 Conversion result of multilevel nested message
綜合分析正確性測(cè)試,可以得出結(jié)論,我們的解析程序可以對(duì)每個(gè)Message 以及Message 內(nèi)部的成員進(jìn)行正確的解析,且具有較好的穩(wěn)定性,能夠適用于絕大多數(shù)的信息轉(zhuǎn)換情形。而且通過測(cè)試可以得知,當(dāng)不同Message 定義有幾乎相同的數(shù)據(jù)結(jié)構(gòu)時(shí),解析時(shí)可以相互套用不影響正確性。但是針對(duì)多重嵌套,要注意其正確的嵌套格式。
測(cè)試解析程序在一次性解析大量數(shù)據(jù)時(shí)的承受能力以及Protobuf 轉(zhuǎn)JSON 的性能。
經(jīng)過查閱官方文檔得知,Protobuf 對(duì)于定義的信息文件有默認(rèn)大小的限制,要解析的數(shù)據(jù)不能超過默認(rèn)的64MB,否則會(huì)解析失敗。
首先測(cè)試單次Protobuf 可以轉(zhuǎn)換多大量的JSON數(shù)據(jù)。該部分測(cè)試,我們采用fixed32 類型作為多次重復(fù)傳遞的消息,因?yàn)閒ixed32 類型在Protobuf 的定義中為固定的4bytes大小,方便我們計(jì)算一次傳輸?shù)臄?shù)據(jù)量。
3.3.1 對(duì)單條Message 解析測(cè)試
利用循環(huán)添加1000、10 000、100 000 個(gè)fixed32類型數(shù)據(jù),每個(gè)為4bytes 大小,都由repeated 來(lái)修飾,定義的字段可出現(xiàn)0 到多次。數(shù)據(jù)均成功解析。100 000 條數(shù)據(jù)均成功解析成JSON 格式如圖6 所示。
圖6 100 000 個(gè)fixed32 類型數(shù)據(jù)執(zhí)行結(jié)果Fig.6 Conversion results for single message with 100 000 fixed32 data
3.3.2 對(duì)多條Message 解析測(cè)試
同理,利用循環(huán)對(duì)單次轉(zhuǎn)換多條Message 進(jìn)行了測(cè)試,Message 中包含由optional 修飾的字段數(shù)據(jù)“123”。 用同樣方式測(cè)試1000、10 000、100 000 條該定義下的Message,編譯執(zhí)行后,均能成功解析。100 000 條Message 的執(zhí)行結(jié)果如圖7 所示,可以看到所有Message 均成功轉(zhuǎn)換為對(duì)應(yīng)的JSON格式。
圖7 100000 條Message 執(zhí)行結(jié)果Fig.7 Conversion Results for 100 000 Messages
3.3.3 轉(zhuǎn)換性能測(cè)試
我們?cè)谏蓽y(cè)試數(shù)據(jù)的C++文件中定義了一條擁有N 個(gè)成員信息的Message 和N 個(gè)空的Message,通過控制N 的大小來(lái)不斷增大數(shù)據(jù)量。通過在多次測(cè)試,得出在Mac 電腦(測(cè)試機(jī)型配置:i5-8279U,16GB LPDDR3,M.2 固態(tài)硬盤)上的平均運(yùn)行性能,如表1 所示。
表1 有空Message 時(shí)測(cè)試結(jié)果Table 1 Testing results with null message
我們認(rèn)為解析N 個(gè)空的Message 可能會(huì)影響解析速度,因?yàn)榇罅康腗essage 就意味著需要解析大量的分隔符,而分隔符的定義對(duì)于解析批量的Protobuf 數(shù)據(jù)是不可避免的,所以我們剔除對(duì)N 個(gè)空的Message 的解析,單純測(cè)試解析一條擁有N 個(gè)成員信息的Message 的性能,如表2 所示。
表2 排除空Message 后的測(cè)試結(jié)果Table 2 Testing results without null message
可以看出,解析一條擁有N 個(gè)成員信息的Message 的性能已基本保持線性,可以達(dá)到20 MB/s左右,這證明我們解析程序有著很好的純轉(zhuǎn)換性能。綜上可以認(rèn)為該P(yáng)rotobuf 轉(zhuǎn)換JSON 程序有良好的解析性能,但是信息傳輸應(yīng)該盡量使用少次大數(shù)據(jù)量的Message 來(lái)代替使用多次小數(shù)據(jù)量的Message,從而提升轉(zhuǎn)化效率。
上述所有測(cè)試都是在Protobuf 2 版本下進(jìn)行的,本部分測(cè)試內(nèi)容將Protobuf 替換為3.X 版本進(jìn)行測(cè)試。Protobuf 3 移除了“required”類型的字段,因?yàn)?required 字段通常被認(rèn)為是有害的且違反了 Protobuf 的兼容性語(yǔ)義,并用“singular”來(lái)替代原本的“optional”。字段的默認(rèn)值只能根據(jù)字段類型由系統(tǒng)決定,而不能使用 default 選項(xiàng)為某一字段指定默認(rèn)值,對(duì)于枚舉類型,默認(rèn)值必須為0。
針對(duì)proto3 的特性重新進(jìn)行測(cè)試,程序能夠正確地完成Protobuf 到JSON 的轉(zhuǎn)換。所以,本文提出的轉(zhuǎn)換方法同樣適用于Protobuf 3 版本,支持轉(zhuǎn)換Protobuf 3 所提供的所有數(shù)據(jù)類型,具有很好的兼容性。
本文基于動(dòng)態(tài)解析技術(shù)與類型反射技術(shù),實(shí)現(xiàn)了一種便捷的Protobuf 與JSON 轉(zhuǎn)換的方法。經(jīng)過測(cè)試,該方法穩(wěn)定性好、兼容性佳、性能穩(wěn)定,可以勝任實(shí)際生產(chǎn)需要。需要指出得是,本文的各項(xiàng)測(cè)試僅在單線程下,若開啟多個(gè)線程同時(shí)進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換效率將會(huì)進(jìn)一步提高。
本文的實(shí)現(xiàn)存在一定的局限性, 如Protobuf 間隔符有極小概率會(huì)與消息內(nèi)容產(chǎn)生沖突且對(duì)轉(zhuǎn)換效率有一定影響,在接下來(lái)的工作中可以設(shè)計(jì)使用具有固定字長(zhǎng)的分隔符,能在一定程度上解決上述問題。
利益沖突聲明
所有作者聲明不存在利益沖突關(guān)系。