官术网_书友最值得收藏!

1.4 網絡傳輸中的對象序列化問題

僅僅懂了Socket編程還不夠,因為我們不是簡單地寫一個發送字符串的Hello World程序,需要實現復雜的對象實例傳輸,因此,如何將一個對象實例編碼成為高效的二進制數據報文傳輸到對端,并且正確地“還原”出來,就是一個專業的技術問題了。

對象序列化技術是Java本身的重要底層機制之一,因為Java一開始就是面向網絡的,遠程方法調用(RPC)是必不可少的,需要方便地將一個對象實例通過網絡傳輸到遠端。Java自身的序列化機制有兩個大問題。

● 序列化的數據比較大,傳輸效率低。

● 其他語言無法識別和對接。

在后來相當長的一段時間內,基于XML格式編碼的對象序列化機制盛行,它解決了多語言兼容的問題,同時比二進制的序列化方式更容易理解和排錯,于是基于XML的SOAP和其上的Web Service框架幾乎成為各個主流開發語言的必備擴展包,會不會熟練定義和開發Web Service接口,一度成為一個“高級技能”。后來,基于JSON的簡單文本格式編碼的HTTP REST接口又基本取代了復雜的Web Service接口,成為事實上的分布式架構中遠程通信的首要選擇。

JSON序列化存在占用空間大、性能低下等缺陷,隨著多語言協作開發的互聯網應用越來越普及,更多的移動客戶端應用需要更高效地傳輸數據,以提升用戶體驗。在這種情況下,與語言無關的高效二進制編碼協議就成為熱點技術之一。

首先,誕生了一個知名開源二進制序列化框架——MessagePack,它的出現比Google的Protocol Buffers要早,是模仿JSON設計的一個高性能二進制的通用序列化框架,它有兩大優勢:序列化后空間占用最小,而且更快。如下所示是它的序列化機制原理示意圖。

我們看到,在MessagePack中,數據類型被分為兩大類:定長數據(整數、浮點數、布爾、空值等)與變長數據(字節數組、通用數組、集合類型的數據)。對于定長數據,只要在序列化時標明數據類型與對應的值即可;對于變長數據,則多了一個“長度”屬性,用來表明數據的真實長度。下圖是其數據類型(Type)的分類詳情,我們看到,為了最大可能地節省存儲空間,MessagePack把數值型又細分了很多種,不僅如此,連正數和負數都分開了。

對照上面的數據類型定義,我們就可以理解下圖中一個有27個字節的JSON的Map是如何在MessagePack里用18個字節序列化的。

其次,除了MessagePack,Google開源的多語言支持的Protocol Buffers編碼協議也是這方面的代表作品,其官方實現了C++、Python、Java三種語言的API接口,其他語言版本也相繼由不同的作者實現。據統計,截至2010年,采用Protocol Buffers定義的報文格式就接近5萬種,這些報文格式被大量用于RPC調用與持久化數據傳輸和存儲系統中。

如果要做到語言中立及多語言支持,就不能用任何一種已有語言的語法來定義協議,只能用一種新的“中立”的第三方語言來描述協議。這種指導思想早在20世紀90年代的COBRA里就體現過了。當時,為了定義各種語言都能使用的RPC接口,COBRA設計了IDL接口定義文檔及語言相關的編譯器,將接口語言編譯成相應語言定義的接口,并且配套復雜的多語言支持的數據序列化機制,從而描繪了一個大一統的、從未真正實現的“完美IT世界”。Protocol Buffers同樣創建了一個后綴名為.proto的描述文件,用來定義一個數據對象的具體結構。下面是一個簡單的例子:

在這個例子中定義了一個被稱為helloworld的數據對象,也被稱為“消息”,它有3個屬性:一個32位整數的id、一個字符串類型的str變量,是必須賦值的屬性;一個可選的32位整數變量opt。在定義好proto文件后,就可以將其編譯成支持各種語言的接口代碼了。如果有興趣,則建議將其編譯成自己熟悉的語言,分析隱藏在其背后的復雜的編碼和解碼細節,這有助于你加深對RPC實現機制的理解,因為高性能RPC的關鍵技術點之一就在于如何設計和實現一個高效的數據序列化機制。這里提示一點,與MessagePack類似,Protocol Buffers為了減少序列化后的存儲空間,也使用了一些技巧,比如Varint是Protocol Buffers中的變長整數類型,用一個或多個字節來表示一個數字,數字的值越小,使用的字節數存儲越少,可減少用來表示數字的字節。比如對于int32類型的數字,一般需要4個字節來表示。但是采用Varint后,對于很小的int32類型的數字,則可以用1個字節來表示。

在Protocol Buffers之后,Google又開源了一個新項目——Google FlatBuffers,在性能、序列化過程中內存占用的大小、第三方依賴庫的數量、編譯后生成的中間代碼數量等方面都做了大幅改進。隨后Cap'n公司發布聲明,稱Google這個FlatBuffers的設計實現很像該公司的Cap'n Proto,并且給出Cap'n Proto和Protocol Buffers的性能對比圖,來證明Google FlatBuffers的確做了很多改變。

接下來說說Apache Avro(后簡稱Avro),它是一個開源項目,主要使用Java實現,支持多語言。它完全針對Protocol Buffers而來,是一種新的設計思路,其作者說:“這個世界上的每個問題都有幾種解決思路”。

Avro原本是Hadoop中的一個子項目,用于實現RPC調用,Hadoop的其他項目中例如HBase和Hive的Client端與服務端的數據傳輸也采用了這個項目。Avro與Protocol Buffers的最大區別在于,它采用了預先定義的Schema(模式)來描述對象的序列化結構,從而無須編譯。在使用Avro時必須先確定Schema,而Schema類似于表結構的定義,正是模式的引入,使得數據具有了自描述的功能,同時能夠實現動態加載。另外,與其他數據序列化系統如Protocol Buffers相比,在數據之間不存在其他任何標識,有利于提高數據處理效率。

Avro的模式采用JSON來描述,下面的代碼定義了一個名為User的對象及其屬性:

Avro獨有的Schema模式的設計,以及無須編譯生成中間代碼的做法,大大簡化和加速了各種格式數據傳輸的開發聯調工作。2014年,微軟也發布了自己對Avro通信協議的實現,即.NET版本的語言實現,截至2020年2月,Avro已經有了C、C++、C#、Java、PHP、Python與Ruby等幾個主流編程語言的實現版本。

本節最后討論一下RPC中的數據序列化可能帶來的風險問題,這個問題在Java RMI中比較明顯,因為Java RMI采用了Java對象序列化機制在網絡中傳輸數據。Java序列化就是把Java對象轉換成字節流,以便將其保存在內存、文件、數據庫中;反序列化是Java序列化對應的逆過程,即將字節流還原成對象本身。這看起來沒什么問題,但是如果某個應用可以讓用戶輸入一些數據,并且將這些輸入的數據作為某個Java對象的屬性通過Java序列化機制傳輸到服務器端,則攻擊者可以通過構造“惡意輸入”,讓服務器端對應的反序列化程序產生“非預期的對象”,而這些非預期的對象有可能導致惡意代碼的執行,與經典的SQL注入這樣的安全漏洞在本質上是一樣的。

對象序列化的安全漏洞問題并非Java所特有的,在PHP和Python中也有類似的問題。Java序列化安全問題的根源在于,ObjectInputStream在反序列化時沒有對生成的對象的類型做限制!直到JDK 9才增加了一個filter機制來解決這個問題,后面才打補丁到JDK 6、7、8的特定版本上!反觀ZeroC Ice、Thrift、ProtoBuf等傳統RPC,它們通過IDL生成代碼,并在IDL中嚴格控制數據類型,因而是安全的。這又印證了一個道理:代碼實現越復雜,Bug越多!

主站蜘蛛池模板: 怀远县| 合作市| 德保县| 孙吴县| 六枝特区| 永清县| 铅山县| 祁连县| 诏安县| 宝鸡市| 鸡西市| 漳浦县| 都昌县| 抚州市| 石首市| 罗源县| 株洲县| 英山县| 钟山县| 竹山县| 稻城县| 聂拉木县| 嵩明县| 万盛区| 万源市| 黄梅县| 怀集县| 菏泽市| 巴里| 仙桃市| 大庆市| 青河县| 东兴市| 古交市| 鱼台县| 华阴市| 陈巴尔虎旗| 苍南县| 遵义县| 黄浦区| 凌云县|