MINA架構
這里,我借用了一張Trustin Lee在Asia 2006的ppt里面的圖片來介紹MINA的架構。
Remote Peer就是客戶端,而下方的框是MINA的主要結構,各個框之間的箭頭代表數據流向。
大家可以對比剛剛的例子來看這個架構圖,IoService就是整個MINA的入口,負責底層的IO操作,客戶端發過來的消息就是由它處理。剛剛我們使用的IoAcceptor就是一個IoService,之所以抽象成IoService,是因為MINA用同樣的架構來處理服務器和客戶端編程,IoService的另一個子類就是IoConnector,用於客戶端。不過根據筆者的使用經驗,使用非阻塞的模型進行客戶端編程非常的不方便,你最好尋求其他的阻塞通訊框架。
IoService把數據轉化成一個一個的事件,傳遞給IoFilterChain。你可以加入一連串的IoFilter,進行各種功能。筆者的建議是將一些功能性的,業務不相關的代碼,用IoFilter來實現,使得整個應用結構更清晰,也方便代碼重用。
被IoFilter處理過的事件,發送給 IoHandler,然后我們在這里實現具體的業務邏輯。這個部分很簡單,如果你有Swing的使用經驗的話,你會發現它跟Swing的事件非常相像,你要做的事情,僅僅是重載你需要的方法,然后編寫具體的業務功能。在這其中,最重要的一個方法就是messageReceived了。
值得留意的是一個IoSession的類,每一個IoSession實例代表這一個連接,我們需要對連接進行的任何操作都通過這個類來實現。
從IoHandler通過調用IoSession.write等方法向客戶端發送的消息,會通過跟輸入數據相反的次序依次傳遞,直至由IoService負責把數據發送給客戶端。
這就已經是MINA的全部,是不是很簡單。
接下來,我會詳細介紹我們編寫具體代碼的時候主要涉及到的三個類,IoHandler、IoSession和IoFilter。
IoHandler
MINA的內部實現了一個事件模型,而IoHanlder則是所有事件最終產生響應的位置。每一個方法的名字很明確表明該事件的含義。messageReceived是接收客戶端消息的事件,我們應該在這里實現業務邏輯。messageSent是服務器發送消息的事件,一般情況下我們不會使用它。sessionClosed是客戶端斷開連接的事件,可以在這里進行一些資源回收等操作。值得留意的是,客戶端連接有兩個事件,sessionCreated和sessionOpened,兩者稍有不同,sessionCreated是由I/O processor線程觸發的,而sessionOpened在其后,由業務線程觸發的,由於MINA的I/O processor線程非常少,因此如果我們真的需要使用sessionCreated,也必須是耗時短的操作,一般情況下,我們應該把業務初始化的功能放在sessionOpened事件中。
細心的讀者可能會發現,我們剛剛的例子繼承的是IoHandlerAdapter,IoHandlerAdapter其實就是一個IoHanlder的空的實現,這樣我們就可以不用重載不感興趣的事件。
IoSession
IoSession是一個接口,MINA里很多的地方都使用接口,很好地體現了面向接口編程的思想。它提供了對當前連接的操作功能,還有用戶定義屬性的存儲功能,這點非常重要。IoSession是線程安全的,也就是我們能夠在多線程環境中隨意操作IoSession,這點給開發帶來很大的好處。我們來看看具體提供的方法,筆者列舉一些比較常用和重要的方法
在這里,筆者把IoSession的方法大致分成三類
第一類,連接操作功能。
最主要的方法有兩個,向客戶端發送消息和斷開連接。可以看的出,write接受的變量是一個Object,但是實際上應該傳入什么類型呢?具體還得看你是否使用了ProtocolCodecFilter(下面會詳細介紹),如果使用了ProtocolCodecFilter,那這個message將可能是一個String,或者是一個用戶定義的JavaBean。默認的情況,message是一個ByteBuffer。ByteBuffer是MINA的一個類,跟java.nio.ByteBuffer類同名,MINA 2.0將會將它改成IoBuffer,以避免討論上的誤會。
另一個值得留意的是Future類,MINA是一個非阻塞的通信框架,其中一個明顯的體現就是調用IoSession.write方法是不會阻塞的。用戶調用了write方法之后,消息內容會發到底層等候發送,至於什么時候發出,就不得而知了。當然,實際上調用了write之后,數據幾乎是立刻發出的,這得益與NIO的高性能。但是,如果我們必須確認了消息發出,然后進行某些處理,我們就需要使用Future類,以下是一個很常見的代碼。
通過調用future.join,程序就會阻塞,直至消息處理結束。我們還能通過future.isWritten得知消息是否成功發送。
在這里,筆者順便說一個實際使用的發現,消息發送是會自動合並的,簡單來說,如果在很短的時間里,對同一個IoSession進行了兩次write操作,客戶端有可能只收到一條消息,而這條消息就是服務器發出的兩條消息前后接起來。這樣的設計可以在高並發的時候節省網絡開銷,而筆者的實際使用過程中,效果也相當好。但是如果這樣行為會導致客戶端工作不正常,你也可以通過參數關閉它。
第二類,屬性存儲操作。
通常來說,我們的系統是有用戶狀態的,我們就需要在連接上存儲用戶屬性,IoSession的Attribute就是這樣一個功能。例如兩個連接同時連入服務器,一個連接是用戶A,用戶ID是13,另一個連接是用戶B,用戶ID是14,我們就可以在用戶登錄成功之后,調用IoSession.setAttribute(“login_id”,13),然后在其后的操作中,通過IoSession.getAttribute(“login_id”)獲得當前登錄用戶ID,並進行相應的操作。簡單來說,就是一個類似HttpSession的功能,當然具體的實現方法不一樣。
第三類,連接狀態。
這里就不多說了,從方法名上我們就能知道它具體的功能。
IoFilter
過濾器是MINA的一個很重要的功能。IoFilter也是一個接口,但是相對比較復雜,這里就不列舉它的方法了。簡單來說IoFilter就像ServletFilter,在事件被IoHandler處理之前或之后進行一些特定的操作,但是它比ServletFilter復雜,可以處理很多種事件,除了包括IoHandler的7個事件以外,還有一些內部的事件可以進行操作。
MINA提供了一些常用的IoFilter實現,例如有LoggingFilter(日志功能)、BlacklistFilter(黑名單功能)、CompressionFilter(壓縮功能)、SSLFilter(SSL支持),這些過濾器比較簡單,通過閱讀它們的源代碼,能夠更進一步理解過濾器的實現。筆者在這里要重點介紹兩個過濾器,ProtocolCodecFilter和ExecutorFilter
ProtocolCodecFilter
網絡傳輸的內容其實本質是一個二進制流,但是我們的業務功能不會,或者說不應該去直接操作二進制流。MINA默認向IoHandler傳入的message是一個ByteBuffer,如果我們直接在IoHandler操作ByteBuffer,會導致大量協議分析的代碼和實際的業務代碼混雜在一起。最適合的做法,就是在IoFilter把ByteBuffer轉換成String或者JavaBean,ProtocolCodecFilter正是這樣的一個功能的過濾器。
使用ProtocolCodecFilter很簡單,我們只要把ProtocolCodecFilter加入到FilterChain就可以了,但是我們需要提供一個ProtocolCodecFactory。其實ProtocolCodecFilter僅僅是實現了過濾器部分的功能,它會將最終的轉換工作,交給從ProtocolCodecFactory獲得的Encode和Decode。如果我們需要編寫自己的ProtocolCodec,就應該從ProtocolCodecFactory入手。MINA內置了幾個ProtocolCodecFactory,比較常用的就是ObjectSerializationCodecFactory和TextLineCodecFactory。
ObjectSerializationCodecFactory是Java Object序列化之后的內容直接跟ByteBuffer互相轉化,比較適合兩端都是Java的情況使用。TextLineCodecFactory就是String跟ByteBuffer的轉化,說白了就是文本,例如你要實現一個SMTP服務器,或者POP服務器,就可以使用它。而筆者的實際使用,大多數情況都是使用
TextLineCodecFactory
這里提及一下IoFilter的順序問題,IoFilter是有加入順序的,例如,先加入LoggingFilter再加入ProtocolCodecFilter,和先加入ProtocolCodecFilter再加入LoggingFilter的效果是不一樣的,前者LoggingFilter寫入日志的內容是ByteBuffer,而后者寫入日志的是轉換后具體的類,例如String。實際使用的時候,一定要處理好過濾器的順序。
ExecutorFilter
另一個重要的過濾器就是ExecutorFilter。這里,我需要先說明一下MINA的線程工作模式,MINA默認是單線程處理所有客戶端的消息,也就是說,即使你在一台8CPU的機器上面跑,可能也只用到一個CPU,另外,如果某次消息處理太耗時,就會導致其他消息等待,整體的吞吐量下降。很多朋友抱怨MINA的性能差,其實是因為他們沒有加入ExecutorFilter的緣故。ExecutorFilter設計的很精巧,大家可以仔細閱讀一下源代碼,它會將同一個連接的消息合並起來按順序調用,不會出現兩個線程同時處理同一個連接的情況。
- 1.IoAcceptor acceptor = ...;
- 2.IoServiceConfig acceptorConfig = acceptor.getDefaultConfig();
- 3.acceptorConfig.setThreadModel(ThreadModel.MANUAL);
這里再次提及IoFitler的順序問題,一般情況下,我們會將ExecutorFilter放在ProtocolCodecFilter之后,因為我們不需要多線程地執行ProtocolCodec操作,用單一線程來進行ProtocolCodec性能會比較高,而具體的業務邏輯可能還設計數據庫操作,因此更適合放在不同的線程中運行。
優化指南
MINA默認配置的性能並不是很高的,部分原因是MINA目前還保留初期版本的架構,另外一個原因是因為JVM的發展。
1.IoAcceptor acceptor = new SocketAcceptor(Runtime.getRuntime().availableProcessors() + 1, Executors.newCachedThreadPool());
首先我們關閉默認的ThreadModel設置 ThreadModel是一個很簡單的線程實現,用於IoService。但是它實在太弱,以至於在並發環境產生大量問題。在MINA 2.0中,ThreadModel直接被取消。你應該使用ExecutorFilter來實現線程。
- acceptor.getDefaultConfig().getFilterChain().addLast("threadPool", new ExecutorFilter(Executors.newCachedThreadPool());
然后我們增加I/O處理線程
每一個Acceptor/Connector都使用一個線程來處理連接,然后把連接發送給I/O processor進行讀寫操作,我們只可以修改I/O processor使用的線程數,用以下代碼設置 當然是要將ExecutorFilter加入,上文已經很詳細地描述了 筆者在開發過程中,多次遇到OutOfMemoryError,經過研究之后才發現原因。MINA默認是使用direct memory實現ByteBuffer池的方案(以下簡稱direct buffer),通過JNI在內存開辟一段空間來使用,該方案在早期的MINA版本中是一個非常好的特性,那是因為MINA開發初期,JVM並沒有現在的強大,帶有池效果的direct buffer性能比較好。但是當我們使用-Xms -Xmx等指令增加JVM可使用的內存,那僅僅增加了堆的內存空間,而direct memory的空間並沒有增加,導致MINA實際使用的時候經常出現OutOfMemoryError。如果你的確想使用direct memory,可以通過-XX:MaxDirectMemorySize選項來設置。不過筆者不建議這樣做,因為最新的測試表明,在現代的JVM里面,direct memory比堆的表現更差。這里可能有讀者會覺得奇怪,為什么不用池,而要用堆呢,而且還需要gc。那是因為現在的JVM gc能力已經很強了,而且在並發環境里面,pool的同步也是一個性能的問題。我們可以通過這樣的代碼進行設置 MINA 2.0已經默認把直接內存分配改成堆,為了提供最好的性能和穩定性。
- ByteBuffer.setUseDirectBuffers(false);
- ByteBuffer.setAllocator(new SimpleByteBufferAllocator());
最后一條優化技巧就是,把你的應用部署在Linux上,並且打開Java NIO使用Linux epoll的功能。可能你還沒聽過epoll,但是你應該聽過Lighttpd、Nginx、Squid等,得益於epoll,它們提供很高的網絡性能,還占用非常少的系統資源。JDK6已經默認把epoll配置打開,因此筆者建議把你的應用部署在JDK6上面,也同時因為JDK6還有別的優化特性。如果你的應用必須部署在JDK5上,你也可以通過參數把epoll支持打開。
