基于MINA構(gòu)建高性能的NIO應(yīng)用
MINA是非常好的C/S架構(gòu)的java服務(wù)器,這里轉(zhuǎn)了一篇關(guān)于它的使用感受。
前 言
MINA是Trustin Lee最新制作的Java通訊框架。通訊框架的主要作用是封裝底層IO操作,提供高級的操作API。比較出名的通訊框架有C++的ACE、Python的Twisted,而Java的通訊框架還有QuickServer、Netty2、Cindy、Grizzly等。
2004年6月,Trustin Lee發(fā)布了一個(gè)通訊框架Netty2,是Java界第一個(gè)事件模型架構(gòu)的通訊框架,Cindy也從中借鑒了不少思想。由于Netty2的架構(gòu)不是很好,Trustin Lee在2004年底加入Apache Directory組之后,重寫了整個(gè)框架,取名為MINA。MINA是一個(gè)基于Java NIO的通訊框架,Java從1.4開始引入NIO,提供了一個(gè)非阻塞、高性能的IO底層。
目前使用MINA的產(chǎn)品并不是很多,比較出名的就有Apache Directory、Openfire(Jive出品的一個(gè)XMPP產(chǎn)品)、red5(研究flash流媒體flv技術(shù)的朋友應(yīng)該很清楚這個(gè)東西,adobe fms的競爭者,國內(nèi)也有視頻網(wǎng)站在使用)等等。
筆者在07年初的時(shí)候,公司新項(xiàng)目需要用Java實(shí)現(xiàn)一個(gè)Socket Server,對比了Netty2、Cindy、QuickServer和MINA。當(dāng)時(shí)Netty2已經(jīng)停止開發(fā),也找不到官方網(wǎng)站和代碼,比較了另外三個(gè)框架之后,毅然選擇了當(dāng)時(shí)文檔比較缺乏和使用群較少的MINA,一年以來的使用經(jīng)驗(yàn)來看,感覺還是很不錯(cuò)的,MINA有著清晰的架構(gòu),很方便做自定義的擴(kuò)充。在1.0發(fā)布之后,官方網(wǎng)站充實(shí)了很多,增加了不少文檔,也聽到越來越多的朋友開始使用MINA。后來專門針對JDK 1.5發(fā)布了1.1的版本,使用JDK內(nèi)置的concurrent代替backport-util-concurrent。目前1.0和1.1同時(shí)存在,但已經(jīng)不再增加新功能,僅僅發(fā)布bug fix的版本,新功能都在2.0中實(shí)現(xiàn),2.0調(diào)整了架構(gòu),性能有更大的提升,目前還在開發(fā)中。
基本特性
通過Java NIO支持TCP和UDP協(xié)議,另外還支持RS232和VM內(nèi)通訊。由于MINA有清晰的架構(gòu),你也能很簡單地實(shí)現(xiàn)一個(gè)底層網(wǎng)絡(luò)協(xié)議。目前不支持阻塞IO,似乎還沒有計(jì)劃支持,當(dāng)然你可以在其之上實(shí)現(xiàn)一個(gè)阻塞的模型,不過按照筆者的經(jīng)驗(yàn)來說,非阻塞IO更適合Server端編程。
一個(gè)類似ServletFilter的過濾器模型。這是筆者認(rèn)為MINA的精髓所在,通過引入過濾器模型,可以將一些非業(yè)務(wù)的功能獨(dú)立開來,層次更清晰,很有AOP的思想,可以很方便地進(jìn)行日志、協(xié)議轉(zhuǎn)換、壓縮等等功能,還能在運(yùn)行中動(dòng)態(tài)增加或去掉功能。
可以直接使用底層的ByteBuffer,也可以使用用戶定義的消息Object和編碼方式。
高度可定制的線程模型,單線程、一個(gè)線程池,或者類似SEDA的多個(gè)線程池。
SSL支持,攻擊防御和流量控制,mock測試友好,JMX支持,Spring集成,你還需要更多嗎。
一個(gè)簡單的例子
MINA使用非常簡單,筆者以前做過一段時(shí)間傳統(tǒng)的Java Socket開發(fā),不過一直對Java NIO不是很理解,但是MINA很快就上手了,MINA封裝了NIO繁瑣的部分,使你可以更專注于業(yè)務(wù)功能實(shí)現(xiàn)。話不多說,讓我們來看一個(gè)簡單的例子,一個(gè)很常見的例子,時(shí)間服務(wù)器。
我們的實(shí)現(xiàn)目標(biāo)是一個(gè)能響應(yīng)多個(gè)客戶端的請求,然后返回服務(wù)器當(dāng)前的系統(tǒng)時(shí)間的功能。傳統(tǒng)的Java Socket程序,我們需要每accept一個(gè)客戶端連接,就創(chuàng)建一個(gè)新的線程來響應(yīng),這會(huì)令到系統(tǒng)整體負(fù)載能力有較大的限制,而且我們必須手工編寫連接管理等代碼。讓我們來看看MINA是怎么處理的。
首先我們從官方網(wǎng)站下載MINA 1.1,這里我們假設(shè)JDK為1.5以上的版本,如果你使用的是JDK 1.4,請下載MINA 1.0,MINA 1.0跟1.1幾乎一樣,但是強(qiáng)烈建議使用JDK 1.5以上以獲得更好的性能。
解開壓縮包之后,能看見很多jar包,這里暫不介紹每個(gè)包的具體作用,可以把所有包都導(dǎo)入項(xiàng)目。值得留意的是MINA使用了一個(gè)slf4j的日志庫,該日志庫大有取締common-logging之勢。 這里是我們的主程序,非常簡單。
首先我們需要一個(gè)IoAcceptor,這里我們選擇了一個(gè)SocketAcceptor,也就是TCP協(xié)議。
然后,我們給應(yīng)用加上日志過濾器和協(xié)議編碼過濾器。
最后,我們把a(bǔ)cceptor bind到本機(jī)的8123端口,并且使用TimeServerHandler來實(shí)現(xiàn)協(xié)議。
TimeServerHandler是我們實(shí)現(xiàn)具體業(yè)務(wù)功能的地方。 IoHandlerAdapter提供了7個(gè)事件方法,我們要做的事情僅僅是挑選我們需要做出響應(yīng)的事件進(jìn)行重載。在我這個(gè)例子了,我重載了兩個(gè)方法。sessionCreated會(huì)在客戶端連接的時(shí)候調(diào)用,通常我們會(huì)在這里進(jìn)行一些初始化操作,我這里僅僅是打印一條信息。messageReceived就是整個(gè)Handler的中心部分,每一個(gè)從客戶端發(fā)過來的消息都會(huì)轉(zhuǎn)化成對該方法的調(diào)用。由于我們加入了協(xié)議編碼過濾器,因此這里獲得的Object msg是一個(gè)String,而不是默認(rèn)的ByteBuffer(下文會(huì)詳細(xì)介紹ProtocolCodecFilter)。這里我們實(shí)現(xiàn)了一個(gè)很簡單的業(yè)務(wù)功能,如果用戶輸入的是quit,就斷開連接,否則就輸入當(dāng)前時(shí)間??梢钥闯?,IoSession封裝了對當(dāng)前連接的操作。
至此,我們就實(shí)現(xiàn)了一個(gè)時(shí)間服務(wù)器。
- public class TimeServer {
 - public static void main(String[] args) throws IOException {
 - IoAcceptor acceptor = new SocketAcceptor();
 - SocketAcceptorConfig cfg = new SocketAcceptorConfig();
 - cfg.getFilterChain().addLast( "logger", new LoggingFilter() );
 - cfg.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory()));
 - acceptor.bind( new InetSocketAddress(8123), new TimeServerHandler(), cfg);
 - System.out.println("Time server started.");
 - }
 
- public class TimeServerHandler extends IoHandlerAdapter {
 - public void messageReceived(IoSession session, Object msg) throws Exception {
 - String str = (String) msg;
 - if( "quit".equalsIgnoreCase(str) ) {
 - session.close();
 - return;
 - }
 - Date date = new Date();
 - session.write( date.toString() );
 - System.out.println("Message written...");
 - }
 - public void sessionCreated(IoSession session) throws Exception {
 - System.out.println("Session created...");
 - }
 - }
 
MINA架構(gòu)
這里,我借用了一張Trustin Lee在Asia 2006的ppt里面的圖片來介紹MINA的架構(gòu)。
Remote Peer就是客戶端,而下方的框是MINA的主要結(jié)構(gòu),各個(gè)框之間的箭頭代表數(shù)據(jù)流向。
大家可以對比剛剛的例子來看這個(gè)架構(gòu)圖,IoService就是整個(gè)MINA的入口,負(fù)責(zé)底層的IO操作,客戶端發(fā)過來的消息就是由它處理。剛剛我們使用的IoAcceptor就是一個(gè)IoService,之所以抽象成IoService,是因?yàn)镸INA用同樣的架構(gòu)來處理服務(wù)器和客戶端編程,IoService的另一個(gè)子類就是IoConnector,用于客戶端。不過根據(jù)筆者的使用經(jīng)驗(yàn),使用非阻塞的模型進(jìn)行客戶端編程非常的不方便,你最好尋求其他的阻塞通訊框架。
IoService把數(shù)據(jù)轉(zhuǎn)化成一個(gè)一個(gè)的事件,傳遞給IoFilterChain。你可以加入一連串的IoFilter,進(jìn)行各種功能。筆者的建議是將一些功能性的,業(yè)務(wù)不相關(guān)的代碼,用IoFilter來實(shí)現(xiàn),使得整個(gè)應(yīng)用結(jié)構(gòu)更清晰,也方便代碼重用。
被IoFilter處理過的事件,發(fā)送給 IoHandler,然后我們在這里實(shí)現(xiàn)具體的業(yè)務(wù)邏輯。這個(gè)部分很簡單,如果你有Swing的使用經(jīng)驗(yàn)的話,你會(huì)發(fā)現(xiàn)它跟Swing的事件非常相像,你要做的事情,僅僅是重載你需要的方法,然后編寫具體的業(yè)務(wù)功能。在這其中,最重要的一個(gè)方法就是messageReceived了。
值得留意的是一個(gè)IoSession的類,每一個(gè)IoSession實(shí)例代表這一個(gè)連接,我們需要對連接進(jìn)行的任何操作都通過這個(gè)類來實(shí)現(xiàn)。
從IoHandler通過調(diào)用IoSession.write等方法向客戶端發(fā)送的消息,會(huì)通過跟輸入數(shù)據(jù)相反的次序依次傳遞,直至由IoService負(fù)責(zé)把數(shù)據(jù)發(fā)送給客戶端。
這就已經(jīng)是MINA的全部,是不是很簡單。
接下來,我會(huì)詳細(xì)介紹我們編寫具體代碼的時(shí)候主要涉及到的三個(gè)類,IoHandler、IoSession和IoFilter。
IoHandler
MINA的內(nèi)部實(shí)現(xiàn)了一個(gè)事件模型,而IoHanlder則是所有事件最終產(chǎn)生響應(yīng)的位置。每一個(gè)方法的名字很明確表明該事件的含義。messageReceived是接收客戶端消息的事件,我們應(yīng)該在這里實(shí)現(xiàn)業(yè)務(wù)邏輯。messageSent是服務(wù)器發(fā)送消息的事件,一般情況下我們不會(huì)使用它。sessionClosed是客戶端斷開連接的事件,可以在這里進(jìn)行一些資源回收等操作。值得留意的是,客戶端連接有兩個(gè)事件,sessionCreated和sessionOpened,兩者稍有不同,sessionCreated是由I/O processor線程觸發(fā)的,而sessionOpened在其后,由業(yè)務(wù)線程觸發(fā)的,由于MINA的I/O processor線程非常少,因此如果我們真的需要使用sessionCreated,也必須是耗時(shí)短的操作,一般情況下,我們應(yīng)該把業(yè)務(wù)初始化的功能放在sessionOpened事件中。
細(xì)心的讀者可能會(huì)發(fā)現(xiàn),我們剛剛的例子繼承的是IoHandlerAdapter,IoHandlerAdapter其實(shí)就是一個(gè)IoHanlder的空的實(shí)現(xiàn),這樣我們就可以不用重載不感興趣的事件。
IoSession
IoSession是一個(gè)接口,MINA里很多的地方都使用接口,很好地體現(xiàn)了面向接口編程的思想。它提供了對當(dāng)前連接的操作功能,還有用戶定義屬性的存儲功能,這點(diǎn)非常重要。IoSession是線程安全的,也就是我們能夠在多線程環(huán)境中隨意操作IoSession,這點(diǎn)給開發(fā)帶來很大的好處。我們來看看具體提供的方法,筆者列舉一些比較常用和重要的方法
在這里,筆者把IoSession的方法大致分成三類
第一類,連接操作功能。
最主要的方法有兩個(gè),向客戶端發(fā)送消息和斷開連接??梢钥吹某觯瑆rite接受的變量是一個(gè)Object,但是實(shí)際上應(yīng)該傳入什么類型呢?具體還得看你是否使用了ProtocolCodecFilter(下面會(huì)詳細(xì)介紹),如果使用了ProtocolCodecFilter,那這個(gè)message將可能是一個(gè)String,或者是一個(gè)用戶定義的JavaBean。默認(rèn)的情況,message是一個(gè)ByteBuffer。ByteBuffer是MINA的一個(gè)類,跟java.nio.ByteBuffer類同名,MINA 2.0將會(huì)將它改成IoBuffer,以避免討論上的誤會(huì)。
另一個(gè)值得留意的是Future類,MINA是一個(gè)非阻塞的通信框架,其中一個(gè)明顯的體現(xiàn)就是調(diào)用IoSession.write方法是不會(huì)阻塞的。用戶調(diào)用了write方法之后,消息內(nèi)容會(huì)發(fā)到底層等候發(fā)送,至于什么時(shí)候發(fā)出,就不得而知了。當(dāng)然,實(shí)際上調(diào)用了write之后,數(shù)據(jù)幾乎是立刻發(fā)出的,這得益與NIO的高性能。但是,如果我們必須確認(rèn)了消息發(fā)出,然后進(jìn)行某些處理,我們就需要使用Future類,以下是一個(gè)很常見的代碼。
通過調(diào)用future.join,程序就會(huì)阻塞,直至消息處理結(jié)束。我們還能通過future.isWritten得知消息是否成功發(fā)送。
在這里,筆者順便說一個(gè)實(shí)際使用的發(fā)現(xiàn),消息發(fā)送是會(huì)自動(dòng)合并的,簡單來說,如果在很短的時(shí)間里,對同一個(gè)IoSession進(jìn)行了兩次write操作,客戶端有可能只收到一條消息,而這條消息就是服務(wù)器發(fā)出的兩條消息前后接起來。這樣的設(shè)計(jì)可以在高并發(fā)的時(shí)候節(jié)省網(wǎng)絡(luò)開銷,而筆者的實(shí)際使用過程中,效果也相當(dāng)好。但是如果這樣行為會(huì)導(dǎo)致客戶端工作不正常,你也可以通過參數(shù)關(guān)閉它。
第二類,屬性存儲操作。
通常來說,我們的系統(tǒng)是有用戶狀態(tài)的,我們就需要在連接上存儲用戶屬性,IoSession的Attribute就是這樣一個(gè)功能。例如兩個(gè)連接同時(shí)連入服務(wù)器,一個(gè)連接是用戶A,用戶ID是13,另一個(gè)連接是用戶B,用戶ID是14,我們就可以在用戶登錄成功之后,調(diào)用IoSession.setAttribute(“login_id”,13),然后在其后的操作中,通過IoSession.getAttribute(“login_id”)獲得當(dāng)前登錄用戶ID,并進(jìn)行相應(yīng)的操作。簡單來說,就是一個(gè)類似HttpSession的功能,當(dāng)然具體的實(shí)現(xiàn)方法不一樣。
第三類,連接狀態(tài)。
這里就不多說了,從方法名上我們就能知道它具體的功能。
IoFilter
過濾器是MINA的一個(gè)很重要的功能。IoFilter也是一個(gè)接口,但是相對比較復(fù)雜,這里就不列舉它的方法了。簡單來說IoFilter就像ServletFilter,在事件被IoHandler處理之前或之后進(jìn)行一些特定的操作,但是它比ServletFilter復(fù)雜,可以處理很多種事件,除了包括IoHandler的7個(gè)事件以外,還有一些內(nèi)部的事件可以進(jìn)行操作。
MINA提供了一些常用的IoFilter實(shí)現(xiàn),例如有LoggingFilter(日志功能)、BlacklistFilter(黑名單功能)、CompressionFilter(壓縮功能)、SSLFilter(SSL支持),這些過濾器比較簡單,通過閱讀它們的源代碼,能夠更進(jìn)一步理解過濾器的實(shí)現(xiàn)。筆者在這里要重點(diǎn)介紹兩個(gè)過濾器,ProtocolCodecFilter和ExecutorFilter
ProtocolCodecFilter
網(wǎng)絡(luò)傳輸?shù)膬?nèi)容其實(shí)本質(zhì)是一個(gè)二進(jìn)制流,但是我們的業(yè)務(wù)功能不會(huì),或者說不應(yīng)該去直接操作二進(jìn)制流。MINA默認(rèn)向IoHandler傳入的message是一個(gè)ByteBuffer,如果我們直接在IoHandler操作ByteBuffer,會(huì)導(dǎo)致大量協(xié)議分析的代碼和實(shí)際的業(yè)務(wù)代碼混雜在一起。最適合的做法,就是在IoFilter把ByteBuffer轉(zhuǎn)換成String或者JavaBean,ProtocolCodecFilter正是這樣的一個(gè)功能的過濾器。
使用ProtocolCodecFilter很簡單,我們只要把ProtocolCodecFilter加入到FilterChain就可以了,但是我們需要提供一個(gè)ProtocolCodecFactory。其實(shí)ProtocolCodecFilter僅僅是實(shí)現(xiàn)了過濾器部分的功能,它會(huì)將最終的轉(zhuǎn)換工作,交給從ProtocolCodecFactory獲得的Encode和Decode。如果我們需要編寫自己的ProtocolCodec,就應(yīng)該從ProtocolCodecFactory入手。MINA內(nèi)置了幾個(gè)ProtocolCodecFactory,比較常用的就是ObjectSerializationCodecFactory和TextLineCodecFactory。
ObjectSerializationCodecFactory是Java Object序列化之后的內(nèi)容直接跟ByteBuffer互相轉(zhuǎn)化,比較適合兩端都是Java的情況使用。TextLineCodecFactory就是String跟ByteBuffer的轉(zhuǎn)化,說白了就是文本,例如你要實(shí)現(xiàn)一個(gè)SMTP服務(wù)器,或者POP服務(wù)器,就可以使用它。而筆者的實(shí)際使用,大多數(shù)情況都是使用
TextLineCodecFactory
這里提及一下IoFilter的順序問題,IoFilter是有加入順序的,例如,先加入LoggingFilter再加入ProtocolCodecFilter,和先加入ProtocolCodecFilter再加入LoggingFilter的效果是不一樣的,前者LoggingFilter寫入日志的內(nèi)容是ByteBuffer,而后者寫入日志的是轉(zhuǎn)換后具體的類,例如String。實(shí)際使用的時(shí)候,一定要處理好過濾器的順序。
ExecutorFilter
另一個(gè)重要的過濾器就是ExecutorFilter。這里,我需要先說明一下MINA的線程工作模式,MINA默認(rèn)是單線程處理所有客戶端的消息,也就是說,即使你在一臺8CPU的機(jī)器上面跑,可能也只用到一個(gè)CPU,另外,如果某次消息處理太耗時(shí),就會(huì)導(dǎo)致其他消息等待,整體的吞吐量下降。很多朋友抱怨MINA的性能差,其實(shí)是因?yàn)樗麄儧]有加入ExecutorFilter的緣故。ExecutorFilter設(shè)計(jì)的很精巧,大家可以仔細(xì)閱讀一下源代碼,它會(huì)將同一個(gè)連接的消息合并起來按順序調(diào)用,不會(huì)出現(xiàn)兩個(gè)線程同時(shí)處理同一個(gè)連接的情況。
- 1.IoAcceptor acceptor = ...;
 - 2.IoServiceConfig acceptorConfig = acceptor.getDefaultConfig();
 - 3.acceptorConfig.setThreadModel(ThreadModel.MANUAL);
 
這里再次提及IoFitler的順序問題,一般情況下,我們會(huì)將ExecutorFilter放在ProtocolCodecFilter之后,因?yàn)槲覀儾恍枰嗑€程地執(zhí)行ProtocolCodec操作,用單一線程來進(jìn)行ProtocolCodec性能會(huì)比較高,而具體的業(yè)務(wù)邏輯可能還設(shè)計(jì)數(shù)據(jù)庫操作,因此更適合放在不同的線程中運(yùn)行。
優(yōu)化指南
MINA默認(rèn)配置的性能并不是很高的,部分原因是MINA目前還保留初期版本的架構(gòu),另外一個(gè)原因是因?yàn)镴VM的發(fā)展。
1.IoAcceptor acceptor = new SocketAcceptor(Runtime.getRuntime().availableProcessors() + 1, Executors.newCachedThreadPool());
首先我們關(guān)閉默認(rèn)的ThreadModel設(shè)置 ThreadModel是一個(gè)很簡單的線程實(shí)現(xiàn),用于IoService。但是它實(shí)在太弱,以至于在并發(fā)環(huán)境產(chǎn)生大量問題。在MINA 2.0中,ThreadModel直接被取消。你應(yīng)該使用ExecutorFilter來實(shí)現(xiàn)線程。
- acceptor.getDefaultConfig().getFilterChain().addLast("threadPool", new ExecutorFilter(Executors.newCachedThreadPool());
 
然后我們增加I/O處理線程
每一個(gè)Acceptor/Connector都使用一個(gè)線程來處理連接,然后把連接發(fā)送給I/O processor進(jìn)行讀寫操作,我們只可以修改I/O processor使用的線程數(shù),用以下代碼設(shè)置 當(dāng)然是要將ExecutorFilter加入,上文已經(jīng)很詳細(xì)地描述了 筆者在開發(fā)過程中,多次遇到OutOfMemoryError,經(jīng)過研究之后才發(fā)現(xiàn)原因。MINA默認(rèn)是使用direct memory實(shí)現(xiàn)ByteBuffer池的方案(以下簡稱direct buffer),通過JNI在內(nèi)存開辟一段空間來使用,該方案在早期的MINA版本中是一個(gè)非常好的特性,那是因?yàn)镸INA開發(fā)初期,JVM并沒有現(xiàn)在的強(qiáng)大,帶有池效果的direct buffer性能比較好。但是當(dāng)我們使用-Xms -Xmx等指令增加JVM可使用的內(nèi)存,那僅僅增加了堆的內(nèi)存空間,而direct memory的空間并沒有增加,導(dǎo)致MINA實(shí)際使用的時(shí)候經(jīng)常出現(xiàn)OutOfMemoryError。如果你的確想使用direct memory,可以通過-XX:MaxDirectMemorySize選項(xiàng)來設(shè)置。不過筆者不建議這樣做,因?yàn)樽钚碌臏y試表明,在現(xiàn)代的JVM里面,direct memory比堆的表現(xiàn)更差。這里可能有讀者會(huì)覺得奇怪,為什么不用池,而要用堆呢,而且還需要gc。那是因?yàn)楝F(xiàn)在的JVM gc能力已經(jīng)很強(qiáng)了,而且在并發(fā)環(huán)境里面,pool的同步也是一個(gè)性能的問題。我們可以通過這樣的代碼進(jìn)行設(shè)置 MINA 2.0已經(jīng)默認(rèn)把直接內(nèi)存分配改成堆,為了提供最好的性能和穩(wěn)定性。
- ByteBuffer.setUseDirectBuffers(false);
 - ByteBuffer.setAllocator(new SimpleByteBufferAllocator());
 
最后一條優(yōu)化技巧就是,把你的應(yīng)用部署在Linux上,并且打開Java NIO使用Linux epoll的功能。可能你還沒聽過epoll,但是你應(yīng)該聽過Lighttpd、Nginx、Squid等,得益于epoll,它們提供很高的網(wǎng)絡(luò)性能,還占用非常少的系統(tǒng)資源。JDK6已經(jīng)默認(rèn)把epoll配置打開,因此筆者建議把你的應(yīng)用部署在JDK6上面,也同時(shí)因?yàn)镴DK6還有別的優(yōu)化特性。如果你的應(yīng)用必須部署在JDK5上,你也可以通過參數(shù)把epoll支持打開。
原文鏈接:http://blog.csdn.net/chenyi8888/article/details/5341916
【編輯推薦】















 
 
 











 
 
 
 