當前位置:
首頁 > 最新 > 服務化基石之遠程通信系列五:序列化協議之二進位序列化

服務化基石之遠程通信系列五:序列化協議之二進位序列化

二進位Java序列化

基於JSON或XML的文本序列化的方式簡單清晰,且文本傳輸對於異構語言都天然的優勢,只要各開發語言可以JSON或XML格式即可。但文本格式由於未經壓縮,其內容所佔據的空間較大,並且解析較慢,因此,對於性能要求高的互聯網場景,二進位的序列化的方案更受青睞。對於由Java語言所搭建而成的同構系統,有很多僅針對Java語言的序列化方案。僅針對Java的二進位序列化方案可以很好的和Java語言本身結合,能夠給開發工作帶來很大的便利。

Java原生序列化

Java提供了原生的序列化方式,非常簡單易用。只要一個類實現了java.io.Serializable介面,那麼它就可以被序列化。使用Java對象序列化保存對象,會將其狀態轉化為位元組數組。當某個欄位被聲明為transient後,序列化機制會忽略該欄位。另外,序列化保存的是對象的成員變數,即對象的狀態。因此,對象序列化不會保存靜態變數,因為它們是類的屬性。

在上文的Netty介紹中,我們已經引入了序列化這個概念,使用的正是Java的原生序列化方案。

Java原生序列化使用serialVersionUID來控制兼容性。凡是實現Serializable介面的類都有一個標識序列化版本標識符的靜態變數:

如果不顯示指定,它將由Java運行時環境根據類的內部細節自動生成的。修改源碼再重新編譯的話,類文件的serialVersionUID的取值可能會發生變化。

Java的序列化機制是通過在運行時判斷類的serialVersionUID來驗證版本是否一致的。反序列化時,JVM會將位元組流中的serialVersionUID與相應類中的serialVersionUID比較,如果不同,則拋出序列化版本不一致的異常。

如果希望實現序列化介面的實體能夠兼容之前的版本,可以顯式指定serialVersionUID,以保證不同版本的類對序列化兼容。

雖然Java原生支持的序列化機制足夠簡單,但在性能方面,它簡直可以用災難來形容。由於Java原生的序列化後的位元組大小過於臃腫,導致非常不利於在網路中的傳輸性能;並且它序列化與反序列化本身的性能也並不理想。因此在互聯網這樣對性能要求很高的場景,不會採用Java原生的序列化的方案,它僅僅適合於對性能要求不高的場景。

對於Java提供的RMI、EJB等原生組件,由於採用了其原生序列化的方式,導致吞吐量無法突破瓶頸,也逐漸被棄用。

高性能序列化框架Kryo

由於Java原生的序列化方案性能無法滿足互聯網的需要,很多優秀的第三方高性能序列化框架層出不窮。它們在不同的場景性能可能略有波動起伏,但總體來說,高於Java原生的序列化方案十幾倍的性能,是很容易達成的。

Kryo是一個高效的Java序列化框架。Kryo可以選擇不將類的元信息序列化,因此,當一個類第一次被Kryo序列化時,它需要需要時間去載入該類。這雖然導致Kryo在其序列化工具的初始化時間較長,但這僅僅是一次性消耗。另外可以使用註冊序列化類的方式將這樣的開銷放在應用程序啟動時,用於避免不確定的第一次序列化時間。這樣做的好處是使得序列化位元組的容量大小明顯降低,增加了位元組信息網路傳輸的效率;並且由於類信息均已經在內存只載入,讓其序列化和反序列化的性能也有所提升。使用Kryo無需再實現Serializable介面。

下面是使用Kryo序列化的核心代碼:

下面是使用Kryo反序列化的核心代碼:

使用Kryo必須有一個無參的構造器,否則程序將無法正確運行。如果不提供無參構造器,可以通過Kryo的setInstantiatorStrategy方法設置對象初始化策略為StdInstantiatorStrategy,該策略可以直接創建一個空對象。但如果構造函數中需要一些初始化操作,使用這種策略會破殼對象的完整性。因此最佳實踐還是從一開始就考慮設計一個無參的構造器為妙。

Kryo有3種序列化方法。

1. 調用Kryo的writeObject方法。它只會序列化對象的實例,而不會記錄對象所屬類的元信息。它的優勢是進一步的節省空間,劣勢是需要提供該類作為反序列化的模板。上文的程序示例即採用此種方案。

2. 調用Kryo的writeClassAndObject方法。它將一併序列化對象數據信息和類的元信息。它的優勢是整個程序的聲明周期都無需再提供該類信息,劣勢是空間佔用大,網路間傳輸帶寬消耗多。

3. 先調用Kryo的register方法註冊需要序列化的類,再通過調用Kryo的writeClassAndObject方法序列化。Kryo通過對類的註冊而綁定一個唯一的數字作為id,在writeClassAndObject時僅需要序列化id即可,無需序列化類的全部元信息。優勢是在節省空間的同時也無需在反序列化時提供原始類的信息。劣勢是對於通過Kyro寫序列化通用框架的開發者並不友好,需要提供額外的介面提供使用方程序員註冊相關類。

使用Kryo基本可以替代Java原生序列化的場景,並且性能提升很大。因此,在Java同構語言的序列化框架選擇上,Kryo是一個理想的解決方案。

二進位Java序列化

之前講述的序列化框架都是Java語言的,而完全由單一語言組成的現代系統已不多見。由於每種開發語言都有各自的優勢和適用的場景,因此,一個複雜系統由異構語言組成是很常見的。

高性能異構語言序列化框架Protobuf

Protobuf的全稱是Protocol Buffers,是google開源的跨平台、跨語言的輕便高效的序列化協議。它是Google內部廣泛使用的異構語言數據標準。它支持反序列化後的對象支持向前兼容。與同構語言的序列化方式不同,Protobuf使用預先定義完成的協議格式生成代碼的方式。

使用Protobuf首先需要在系統上安裝它的命令用於編譯proto協議文件。

截止至本書寫作時,最新的穩定版本是3.4.0,因此本書將以這個版本舉例說明。我們介紹一下在Mac系統上如何安裝protobuf,其他操作系統請自行查閱相關資料。請確保Mac系統安裝了Homebrew,然後在命令行直接中輸入「brew install protobuf」命令等待安裝完成即可。

校驗protobuf是否正確安裝,只需在命令行中輸入「protoc --version」,即可返回當前安裝的protobuf版本號,brew命令會非常聰明的將Protobuf的環境變數自動設置完成。

Protobuf通過proto協議文件來定義程序中需要處理的結構化數據,結構化數據在Protobuf中的術語被稱為消息(Message)。proto 協議文件以.proto結尾,它類似於 Java語言中數據對象的定義。一個消息類型由一個或多個欄位組成,每個欄位至少應該包括類型、名稱和標識符。

標識符是一個正整數,每個標識符在該消息體中必須是唯一的。標識符是用於在轉化為二進位的消息中識別各個欄位,一旦開始使用則不允許更改。有一個壓縮生成二進位消息大小的竅門,1-15的數字,在16進位中是0x1-0xF,僅佔用一個位元組;以此類推,16-2047會佔用2個位元組。因此,應盡量將頻繁出現的消息欄位保留在1-15標識符之內。另外,可以為將來可能出現的欄位預留標識符。標識符的只增不刪特性,是Protobuf的消息能夠保持向後兼容的關鍵。

我們以一個簡單的例子來開始:

這是一個標準的proto協議文件,我們來逐行說明一下:

1. 指明正在使用proto3語法。預設使用proto2。Syntax語句必須是proto文件的空行和注釋行之外的第一行。Protobuf 2.x與Protobuf 3.x的語法不完全兼容,相比之下,3.x的語法更加簡明清晰。

2. 指明該文件編譯為類之後的包名稱是protobuf.pojo。

3. 定義消息類型,對應於Java即為類名稱。該消息名稱為ProtoPojo,消息體包含3個欄位。

4. 定義名為id的屬性,類型是32位的整數,標識符是1。

5. 定義名為name的屬性,類型是字元串,標識符是2。

6. 定義名為messages的屬性,類型是可重複的字元串,對應Java是一個List集合類型,標識符是3。

對於Protobuf的協議有了直觀的了解之後,我們再系統的了解一下proto3所支持的消息類型。下表摘自Protobuf官方網站,展示了它所支持的所有消息類型。為了簡單起見,我們僅將C++、Java、Python和Go這幾種語言的相關類型展示出來,Protobuf支持的其他語言還包括Ruby、C#和PHP。

Protobuf還可以使用枚舉類型和嵌套使用其他消息類型,還可以使用import命令將其他文件中定義的消息類型導入至當前文件中使用。

Protobuf是一個向後兼容的協議,更新消息的結構而不破壞已有代碼是非常簡單的。在更新時需要滿足以下規則:

1. 不能更改已有欄位的數字標識符。

2. 使用舊代碼產生的消息被新代碼解析時,新增欄位將被賦為默認值;使用新代碼產生的消息被舊代碼解析時,新增欄位將被忽略。需要注意的是,未識別的欄位會在反序列化時將被丟棄。

3. 非必填的欄位可以刪除,但必須保證它們的數字標識符在新的消息中不再被使用。

4. int32, uint32, int64, uint64,和bool是全部兼容的,它們之間可以任意轉換,而不會破壞其兼容性。需要注意的是,如果解析出來的數字與對應的類型不相符,將進行強制類型轉換,這可能會導致精度的丟失。例如,將一個int64的數字當作int32來讀取,那麼它將會被截斷為32位的數字。

5. sint32與sint64相互兼容,但是與其他整數類型不兼容;string與有效的UTF-8編碼的bytes相互兼容;fixed32與sfixed32相互兼容;fixed64與sfixed64相互兼容;枚舉類型與int32,uint32,int64和uint64相兼容。

關於Protobuf協議的格式定義還有很多細節,更加詳細的信息請閱覽它的官方網址:https://developers.google.com/protocol-buffers/docs/proto3

在完成消息的定義之後,即可以通過Protobuf提供的命令行生成相關開發語言的代碼。這裡仍然以Java語言為例,在命令行中輸入:「protoc --java_out=. ./Pojo.proto」,即可在當前路徑生成相關的Java代碼。命令行中的protoc即為Protobuf編譯器的命令,它應該已隨著Mac系統的brewhome配置至系統的環境變數;--java_out=.則是指定生成Java語言編譯的類,位置是當前路徑;./Pojo.proto則是目標的協議文件路徑。命令執行之後即在生成的目標路徑按照配置的包名生成好了相應的.java文件。更多的protoc命令的使用細節可以在命令行中輸入:「protoc --help」來查看。

為了使生成的代碼通過編譯,需要在Maven的pom.xml文件中引用Protobuf的相應版本,在這裡我們使用的是3.4.0版本,Maven坐標如下:

下面我們看一下從.proto文件生成了什麼。

對Java語言來說,編譯器為每個.proto文件對應生成一個.java文件。這個Java文件的主類名稱與.proto的文件名保持一致,並且為每一個消息類型定義一個消息對象的內部類以及一個用來創建消息的構建內部介面。每個消息類型的內部類中會再包含一個名為Builder的內部類用於實現消息構建介面。

值得注意的是,一個Java類中可以包含多個定義的消息類型。我們之前的例子為了簡單起見,在協議中僅定義了一個名為ProtoPojo的消息,如果在同一個協議文件中定義了多個消息,那麼每個消息類型將會被生成為一對消息內部類和消息構建內部介面。

下面是ProtoPojo構建介面的生成代碼展示:

可以看到生成的ProtoPojoOrBuilder介面中包含了協議文件中定義的3個屬性的getter方法。 相關屬性的方法上保留著協議文件中原始定義的字元串以及相關注釋。下面我們一一對應下協議文件中聲明的屬性和Java文件中生成的屬性,為了清晰起見,我們將生成文件中的包名都去掉。

1. 協議文件中的int32 id = 1,對應的代碼中僅生成了一個int getId()方法。因為int32類型的數據無需做複雜的序列化。

3. 協議中的repeated string messages = 3,對應的代碼生成了四個方法,分別是List< String> getMessagesList()、int getMessagesCount()、String getMessages(int index)和ByteString getMessagesBytes(int index)。由於是repeated類型,因此將messages映射為一個集合,並且提供了集合長度以及通過索引獲取集合中元素的方法。

使用Protobuf API進行序列化和反序列化比較簡單,序列化的方式主要是兩個:

1. byte[] toByteArray():這個方法可以將Java對象序列化為二進位位元組數組,以便進行網路傳遞。

2. void writeTo(OutputStream output):這個方法用於將Java對象直接序列化並寫入一個輸出流。

兩個序列化的方法分別對應的兩個反序列化方法,與序列化方法不同,反序列化方法都是類的靜態方法:

1. static T parseFrom(byte[] data):將二進位的位元組數組反序列化為Java對象。其中返回值T借用了Java的泛型概念,用於表示其返回類型與調用它的類的類型一致。該方法是對應於byte[] toByteArray()的反序列化方式。

2. static T parseFrom(InputStream input):通過一個輸入流讀取二進位位元組數組並反序列化為Java對象。該方法是對應於void writeTo(OutputStream output) 的反序列化方式。

下面是使用Protobuf將Java對象序列化的核心代碼:

下面是使用Protobuf將Java對象反序列化的核心代碼:

小結

面對種類如此之多的序列化方案,如何選擇合適的序列化框架呢?從調試的便利性以及協議的清晰度來說,基於文本的JSON協議是不錯的選擇;從性能方面考慮,文本協議比二進位協議差一些。二進位協議中,無論是Protobuf還是Kryo都是高效的,而Java原生的序列化方案則並不理想;在異構語言方面,本文協議全方位支持,二進位的協議中則只有類似於Protobuf這種靜態代碼生成方式可以支持,但在日常開發中卻略顯麻煩,因為它們即使在同構語言的交互中,也仍然需要根據協議文件靜態生成代碼。因此,使用何種序列化框架是需要綜合考量的。我們通過下表的各類序列化框架的直觀對比來結束本節的話題。

以上內容節選自

《 Java雲原生新一代分散式中間件架構》

內容簡介

【互聯網架構不斷演化,經歷了從集中式架構到分散式架構,再到雲原生架構的過程。雲原生因能解決傳統應用升級緩慢、架構臃腫、不能快速迭代等問題而成為未來雲端應用的目標。本書首先介紹了架構演化及雲原生的概念,讓讀者對基礎概念有一個準確的了解。接著闡述容器調度、服務化、分散式等體系的原理,講解分散式中間件設計方法。最後輔以實戰,以中心化和平台化角度切入,深度揭秘兩大開源項目Elastic-Job和Sharding-JDBC的實現】

GIF

盡請期待

GIF

《Java雲原生 新一代分散式中間件架構》

2018與您見面

書名尚未完全確定,歡迎您寶貴建議。

感謝大家關注「點亮架構」,歡迎對公眾號文章的內容批評指正,如果有其他想要了解的技術問題,也可以留言提出。

『點亮架構』的火炬,燃燒雲原生『

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 全球大搜羅 的精彩文章:

一塊賭色的手鐲料 3000給你要不要 反正切開後貨主是笑了
IF=20.7分的ONFH病例報道from BMJ《英國醫學雜誌》

TAG:全球大搜羅 |