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