韓星+劉姣+周淑君
摘要:近年來,面向服務(wù)的體系架構(gòu)(SOA)逐漸成為構(gòu)建大中型分布式系統(tǒng)的主流方式,遠(yuǎn)程過程調(diào)用(RPC)在其中起著舉足輕重的作用。Netty作為一個(gè)基于事件驅(qū)動(dòng)的、異步的網(wǎng)絡(luò)應(yīng)用框架,能夠快捷高效的實(shí)現(xiàn)分布式系統(tǒng)間的遠(yuǎn)程服務(wù)調(diào)用。該文對(duì)Netty編解碼器進(jìn)行分析和研究,并結(jié)合消息序列化,提出了一種性能和可靠性更高的編解碼方法。
關(guān)鍵詞:netty;編解碼;序列化;遠(yuǎn)程過程調(diào)用;消息協(xié)議
中圖分類號(hào):TP311.5 文獻(xiàn)標(biāo)識(shí)碼:A 文章編號(hào):1009-3044(2017)26-0104-02
Abstract: In recent years, service oriented architecture (SOA) has gradually become the mainstream way to build large and medium-sized distributed systems. Remote procedure call (RPC) plays an important role in it. Netty, as an event driven and asynchronous network application framework, can quickly and efficiently realize remote service invocation among distributed systems. In this paper, the Netty codec is analyzed and studied, and a method of encoding and decoding with higher performance and reliability is proposed combining with message serialization.
Key words: netty;serialization; codec; remote procedure call; message protocol
SUN公司在2002年推出了JDK1.4,基于Java的Socket通信開始支持非阻塞I/O,系統(tǒng)性能和可靠性均得到了很大的提高。但早期的API和類庫依然存在一些不完善的地方,特別是對(duì)文件系統(tǒng)的處理能力非常薄弱。直到2007年JDK1.7發(fā)布,升級(jí)后的NIO2.0提供了異步文件通道和異步套接字通道的實(shí)現(xiàn),文件處理能力有了進(jìn)一步的提升[1-2]。盡管NIO的吞吐量和可靠性相對(duì)于傳統(tǒng)的BIO(同步阻塞式IO)有了質(zhì)的飛躍,但其類庫和API十分繁雜,使用起來非常困難[3]。再加上粘包拆包、斷線重連等可靠性處理的工作量和復(fù)雜度都非常大,因此不建議使用NIO原生API進(jìn)行通信系統(tǒng)的開發(fā)。為了簡化NIO網(wǎng)絡(luò)編程,一些開源組織發(fā)布了諸如Netty、Mina、Grizzly和xSocket等通信框架。其中,Netty的功能、性能、健壯性、可定制和可擴(kuò)展行在同類框架中都是首屈一指的,并且已經(jīng)得到了大量商業(yè)項(xiàng)目的成功驗(yàn)證,如阿里巴巴的分布式服務(wù)框架Dubbo,Hadoop的RPC框架Avro等[4]。本文基于Netty框架,定義了一種通用的消息結(jié)構(gòu)Message,繼承Netty的半包解碼器LengthFieldBaseFrameDecoder解碼消息以解決TCP粘包拆包問題,使用protobuf對(duì)消息體進(jìn)行序列化,使通信系統(tǒng)的性能和可靠性均得到了極大的提高。
1 編解碼方法
1.1 粘包拆包問題
由于應(yīng)用層發(fā)送消息時(shí)寫入的字節(jié)大小不固定以及IP分片等原因,TCP底層會(huì)根據(jù)緩沖區(qū)的實(shí)際情況將單個(gè)業(yè)務(wù)消息拆分成多個(gè)包,或者將多個(gè)小包封裝成一個(gè)大包進(jìn)行發(fā)送。接收方有可能一次接收不完整個(gè)業(yè)務(wù)消息或者一次收到幾個(gè)消息,此時(shí)消息解碼就會(huì)出現(xiàn)異常,不能進(jìn)行接下來的業(yè)務(wù)處理和消息回應(yīng)。TCP粘包拆包無法在底層進(jìn)行規(guī)避,只能通過合理的上層應(yīng)用協(xié)議設(shè)計(jì)進(jìn)行處理[5]。常用的解決方案有三種:一是消息定長;二是使用特殊字符對(duì)消息進(jìn)行分割;三是將消息分為消息頭和消息體,在消息頭中存儲(chǔ)消息長度。第一種方案在消息封裝上不夠靈活,固定創(chuàng)建的緩沖區(qū)長度必須大于最長的消息長度,因此在寫入較短的消息時(shí)會(huì)造成資源浪費(fèi)。第二種方案中使用特殊字符分割消息,如果消息本身就包含了該字符,則不能正確進(jìn)行解碼,存在一定的局限性。本文采用第三種方案,使用消息頭描述消息長度。接收方先讀取固定長度的消息頭,獲取其中包含的消息長度,根據(jù)消息長度再次(或多次)讀取相應(yīng)長度的字節(jié)即讀完整個(gè)消息,將包中余下的字節(jié)緩存起來作為下一個(gè)消息的前一部分。
1.2 消息結(jié)構(gòu)定義
消息分為消息頭和消息體兩個(gè)部分。消息頭固定長度,用來描述消息的類型、長度和優(yōu)先級(jí)等信息。消息體可變長度,承載消息實(shí)體。具體定義如表1和表2。
1.3 繼承半包解碼器
根據(jù)上文對(duì)消息結(jié)構(gòu)的定義,本文將業(yè)務(wù)整包消息定義為4個(gè)部分。如圖1所示,HDR1中包含標(biāo)識(shí)符和版本號(hào),HDR2中包含會(huì)話ID、消息類型和消息優(yōu)先級(jí),Length和ActualContent分別表示數(shù)據(jù)幀長度和數(shù)據(jù)內(nèi)容。定義MessageDecoder繼承半包解碼器LengthFieldBasedFrameDecoder實(shí)現(xiàn)粘包拆包處理。在其構(gòu)造方法中設(shè)置lengthFieldOffset=8(長度字段偏移的字節(jié)數(shù))、lengthFieldLength=4(數(shù)據(jù)幀長度)、lengthAdjustment=10(長度字段調(diào)整長度)和initialBytesToStrip=12(數(shù)據(jù)幀跳過字節(jié)數(shù))。實(shí)際的長度字段偏移位置等于in.readerIndex()加上lengthFieldOffset,讀取消息長度字段所占的4個(gè)字節(jié)表示的數(shù)值即為消息長度。通常情況下再次讀取Length長度的字節(jié)就能獲取完整的消息,通過lengthAdjustment和initialBytesToStrip對(duì)消息長度進(jìn)行調(diào)整。endprint
2 消息序列化
在網(wǎng)絡(luò)傳輸上,Java序列化的碼流大小和性能一直以來都為人詬病,再加上無法跨語言進(jìn)行服務(wù)調(diào)用,幾乎很少有通信系統(tǒng)使用Java序列化[6]。XML和JSON因其平臺(tái)無關(guān)性和較小的內(nèi)存占用成為了大多數(shù)通信系統(tǒng)的首選協(xié)議,但其為了良好的可讀性增大了空間開銷[7-8]。本文采用Google的Protobuf框架進(jìn)行POJO對(duì)象的序列化。Protobuf是一個(gè)平臺(tái)無關(guān)、語言無關(guān)的結(jié)構(gòu)化數(shù)據(jù)的序列化工具,相對(duì)于XML和JSON,其序列化與反序列化處理時(shí)間更短,系列化后的碼流更小,更有利于網(wǎng)絡(luò)傳輸和持久化[9,10]。使用Protobuf序列化,首先要根據(jù)持久化對(duì)象的系列屬性編寫數(shù)據(jù)描述文件proto,其中包含了對(duì)包名、類名和屬性的描述。然后將編寫的proto文件與protoc.exe文件放在同一目錄下,進(jìn)入dos執(zhí)行編譯命令,在指定目錄生成相應(yīng)的FileDescriptorProto類,F(xiàn)ileDescriptorProto類中的FileDescriptor對(duì)象通過toByteArray()和parseFrom(byte[] array)方法實(shí)現(xiàn)與二進(jìn)制數(shù)組之間的互相轉(zhuǎn)換。
3 測(cè)試驗(yàn)證
本文在PC上對(duì)上述系統(tǒng)進(jìn)行測(cè)試,電腦配置為:CPU主頻2.10GHz,內(nèi)存4.00G,硬盤容量500G、轉(zhuǎn)速7200轉(zhuǎn)。為了簡單方便地配置和加載服務(wù)接口對(duì)象和服務(wù)接口實(shí)現(xiàn)對(duì)象,本文通過Spring容器進(jìn)行統(tǒng)一的對(duì)象管理。測(cè)試場(chǎng)景為:客戶端同時(shí)開啟10000個(gè)線程,同一時(shí)刻向服務(wù)器發(fā)起并發(fā)計(jì)算請(qǐng)求,服務(wù)器在異步線程中進(jìn)行兩數(shù)加法計(jì)算并返回結(jié)果值給客戶端,從控制臺(tái)打印出請(qǐng)求消息、響應(yīng)消息和處理耗時(shí)。重復(fù)進(jìn)行10次測(cè)試的處理耗時(shí)結(jié)果如圖3.1所示,Netty RPC 對(duì)10000起并發(fā)計(jì)算請(qǐng)求的處理耗時(shí)平均為11280毫秒,遠(yuǎn)低于傳統(tǒng)RPC系統(tǒng)的處理耗時(shí)。使用JConsole監(jiān)視服務(wù)器程序在Java虛擬機(jī)中的運(yùn)行狀態(tài),其堆內(nèi)存使用量最高為81.5Mb,相對(duì)于傳統(tǒng)RPC,其資源占用率也比較低。
4 結(jié)束語
使用原生的Java NIO進(jìn)行消息系統(tǒng)的開發(fā)十分困難,主要體現(xiàn)在線程的并發(fā)控制和TCP粘包拆包的處理上。本文基于Netty搭建了一個(gè)高性能RPC框架,其異步的線程模型能夠勝任高并發(fā),高吞吐量的消息處理。自描述的消息協(xié)議配合半包解碼器有效解決了TCP粘包和拆包的問題,持久化對(duì)象傳輸采用Protobuf進(jìn)行序列化使得傳輸碼流更小,解析速度更快。十次萬級(jí)并發(fā)計(jì)算的測(cè)試結(jié)果表明該系統(tǒng)無論是可靠性還是性能都十分出色。在實(shí)際的消息通信應(yīng)用中,本文還存在一些可以改進(jìn)和完善的地方,如Reactor主從線程模型的優(yōu)化,另外還可以引入Zookeeper對(duì)RPC服務(wù)器集群進(jìn)行統(tǒng)一協(xié)調(diào)管理和服務(wù)調(diào)度。
參考文獻(xiàn):
[1] Norman Maurer,Marvin Allen Wolfthal. Netty in Action[M]. Manning, 2015.
[2] Netty[EB/OL]. (2016-6-29)[2017-5-17]. http://netty.io/.
[3] Pugh W, Spacco J. MPJava: High-Performance Message Passing in Java Using Java.nio[J]. Lecture Notes in Computer Science, 2003, 2958: 323-339.
[4] 李林峰. Netty權(quán)威指南[M]. 北京: 電子工業(yè)出版社, 2015.
[5] 曹建, 劉瓊, 王遠(yuǎn). 基于數(shù)據(jù)流轉(zhuǎn)發(fā)的實(shí)時(shí)數(shù)據(jù)交換系統(tǒng)設(shè)計(jì)[J]. 計(jì)算機(jī)應(yīng)用, 2016, 36(3):596-600.
[6] 崔曉旻. 基于Netty 的高可服務(wù)消息中間件的研究與實(shí)現(xiàn)[D]. 成都: 電子科技大學(xué), 2015.
[7] Breg F. CD Polychronopoulos[J]. Concurrency & Computation Practice & Experience, 2003, 15(35):173-180.
[8] 何成萬, 余秋惠. JosML—一個(gè)用于實(shí)現(xiàn)Java對(duì)象序列化的XML模型[J]. 計(jì)算機(jī)工程, 2002, 28(1):283-284.
[9] Ayham Mhd Hailiam, Andrey Borisovich Nikolaev. Data Transmission over the Network Using PROTOBUF Protocol[J]. Automation and Control in Technical Systems, 2015, 2: 3-12.
[10] 查駿. 基于NIO的遠(yuǎn)程調(diào)用框架的設(shè)計(jì)與實(shí)現(xiàn)[D]. 上海: 復(fù)旦大學(xué), 2012.endprint