2025-02-09

前言
MINA是Trustin Lee最新制作的Java通訊框架。通訊框架的主要作用是封裝底層IO操作,提供高級的操作API。比較出名的通訊框架有C++的ACE、Python的Twisted,而Java的通訊框架還有QuickServer、Netty2、Cindy、Grizzly等。
2004年6月,Trustin Lee發佈瞭一個通訊框架Netty2,是Java界第一個事件模型架構的通訊框架,Cindy也從中借鑒瞭不少思想。由於Netty2的架構不是很好,Trustin Lee在2004年底加入Apache Directory組之後,重寫瞭整個框架,取名為MINA。MINA是一個基於Java NIO的通訊框架,Java從1.4開始引入NIO,提供瞭一個非阻塞、高性能的IO底層。
目前使用MINA的產品並不是很多,比較出名的就有Apache Directory、Openfire(Jive出品的一個XMPP產品)、red5(研究flash流媒體flv技術的朋友應該很清楚這個東西,adobe fms的競爭者,國內也有視頻網站在使用)等等。
筆者在07年初的時候,公司新項目需要用Java實現一個Socket Server,對比瞭Netty2、Cindy、QuickServer和MINA。當時Netty2已經停止開發,也找不到官方網站和代碼,比較瞭另外三個框架之後,毅然選擇瞭當時文檔比較缺乏和使用群較少的MINA,一年以來的使用經驗來看,感覺還是很不錯的,MINA有著清晰的架構,很方便做自定義的擴充。在1.0發佈之後,官方網站充實瞭很多,增加瞭不少文檔,也聽到越來越多的朋友開始使用MINA。後來專門針對JDK 1.5發佈瞭1.1的版本,使用JDK內置的concurrent代替backport-util-concurrent。目前1.0和1.1同時存在,但已經不再增加新功能,僅僅發佈bug fix的版本,新功能都在2.0中實現,2.0調整瞭架構,性能有更大的提升,目前還在開發中。
基本特性
通過Java NIO支持TCP和UDP協議,另外還支持RS232和VM內通訊。由於MINA有清晰的架構,你也能很簡單地實現一個底層網絡協議。目前不支持阻塞IO,似乎還沒有計劃支持,當然你可以在其之上實現一個阻塞的模型,不過按照筆者的經驗來說,非阻塞IO更適合Server端編程。
一個類似ServletFilter的過濾器模型。這是筆者認為MINA的精髓所在,通過引入過濾器模型,可以將一些非業務的功能獨立開來,層次更清晰,很有AOP的思想,可以很方便地進行日志、協議轉換、壓縮等等功能,還能在運行中動態增加或去掉功能。
可以直接使用底層的ByteBuffer,也可以使用用戶定義的消息Object和編碼方式。
高度可定制的線程模型,單線程、一個線程池,或者類似SEDA的多個線程池。
SSL支持,攻擊防禦和流量控制,mock測試友好,JMX支持,Spring集成,你還需要更多嗎。
一個簡單的例子
MINA使用非常簡單,筆者以前做過一段時間傳統的Java Socket開發,不過一直對Java NIO不是很理解,但是MINA很快就上手瞭,MINA封裝瞭NIO繁瑣的部分,使你可以更專註於業務功能實現。話不多說,讓我們來看一個簡單的例子,一個很常見的例子,時間服務器。
我們的實現目標是一個能響應多個客戶端的請求,然後返回服務器當前的系統時間的功能。傳統的Java Socket程序,我們需要每accept一個客戶端連接,就創建一個新的線程來響應,這會令到系統整體負載能力有較大的限制,而且我們必須手工編寫連接管理等代碼。讓我們來看看MINA是怎麼處理的。
首先我們從官方網站下載MINA 1.1,這裡我們假設JDK為1.5以上的版本,如果你使用的是JDK 1.4,請下載MINA 1.0,MINA 1.0跟1.1幾乎一樣,但是強烈建議使用JDK 1.5以上以獲得更好的性能。
解開壓縮包之後,能看見很多jar包,這裡暫不介紹每個包的具體作用,可以把所有包都導入項目。值得留意的是MINA使用瞭一個slf4j的日志庫,該日志庫大有取締common-logging之勢。 這裡是我們的主程序,非常簡單。
首先我們需要一個IoAcceptor,這裡我們選擇瞭一個SocketAcceptor,也就是TCP協議。
然後,我們給應用加上日志過濾器和協議編碼過濾器。
最後,我們把acceptor bind到本機的8123端口,並且使用TimeServerHandler來實現協議。
TimeServerHandler是我們實現具體業務功能的地方。 IoHandlerAdapter提供瞭7個事件方法,我們要做的事情僅僅是挑選我們需要做出響應的事件進行重載。在我這個例子瞭,我重載瞭兩個方法。sessionCreated會在客戶端連接的時候調用,通常我們會在這裡進行一些初始化操作,我這裡僅僅是打印一條信息。messageReceived就是整個Handler的中心部分,每一個從客戶端發過來的消息都會轉化成對該方法的調用。由於我們加入瞭協議編碼過濾器,因此這裡獲得的Object msg是一個String,而不是默認的ByteBuffer(下文會詳細介紹ProtocolCodecFilter)。這裡我們實現瞭一個很簡單的業務功能,如果用戶輸入的是quit,就斷開連接,否則就輸入當前時間。可以看出,IoSession封裝瞭對當前連接的操作。
至此,我們就實現瞭一個時間服務器。
01.public class TimeServer {
02.  public static void main(String[] args) throws IOException {
03.    IoAcceptor acceptor = new SocketAcceptor();
04.  
05.    SocketAcceptorConfig cfg = new SocketAcceptorConfig();
06.    cfg.getFilterChain().addLast( "logger", new LoggingFilter() );
07.    cfg.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory()));
08.  
09.    acceptor.bind( new InetSocketAddress(8123), new TimeServerHandler(), cfg);
10.    System.out.println("Time server started.");
11.  }
12.}
01.public class TimeServerHandler extends IoHandlerAdapter {
02.  public void messageReceived(IoSession session, Object msg) throws Exception {
03.    String str = (String) msg;
04.    if( "quit".equalsIgnoreCase(str) ) {
05.    session.close();
06.    return;
07.  }
08.  
09.  Date date = new Date();
10.  session.write( date.toString() );
11.  System.out.println("Message written…");
12.  }
13.  
14.  public void sessionCreated(IoSession session) throws Exception {
15.  System.out.println("Session created…");
16.  }
17.}
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來實現線程。
1.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已經默認把直接內存分配改成堆,為瞭提供最好的性能和穩定性。
1.ByteBuffer.setUseDirectBuffers(false);
2.ByteBuffer.setAllocator(new SimpleByteBufferAllocator());
最後一條優化技巧就是,把你的應用部署在Linux上,並且打開Java NIO使用Linux epoll的功能。可能你還沒聽過epoll,但是你應該聽過Lighttpd、Nginx、Squid等,得益於epoll,它們提供很高的網絡性能,還占用非常少的系統資源。JDK6已經默認把epoll配置打開,因此筆者建議把你的應用部署在JDK6上面,也同時因為JDK6還有別的優化特性。如果你的應用必須部署在JDK5上,你也可以通過參數把epoll支持打開。
本文來自CSDN博客

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *