Mina學習總結


最近用兩周左右的時間閱讀了一下apache mina的源碼,有一些體會,現總結如下: 

一、什么是MINA

  MINA是Multipurpose Infrastructure Networked Applications的首字符縮寫,直譯過來是“多目的基礎設施網絡應用程序”,它是一個Apache的頂級開源項目,目的是為了幫助開發人員簡化網絡程序的開發,把注意力集中在業務邏輯上。它的整體架構如下:

  

   從架構圖上,我們可以看到各個組件的功能如下:

    IoService: 負責處理和其它節點的通信,並且把一個socket抽象成一個IoSession.

   IoFilterChain:  一組過濾器組成的過濾器鏈,比如codec filter ,負責數據的編碼和解碼,編碼對應寫操作,解碼對應讀操作。

  IoHandler:負責業務邏輯處理。

  我們一般只要關心過濾器的編寫和IoHandler的編寫。  

  

二、MINA在設計上有哪些特點:

  1、IO操作的異步化:  

    所謂IO操作的異步化,其實底層是通過java的wait/notifyAll機制來實現的。示意圖如下:

    

   源代碼可以參考DefaultIoFuture及其子類DefaultReadFuture和DefaultWriteFuture.

 

  2、基於事件的處理模型:

    主要是使用了java NIO 的 selector來進行就緒socket的輪詢,包括OP_ACCEPT

    OP_READ和OP_WRITE三種事件的監聽,OP_ACCEPT監聽的是新的socket 連接請求

    OP_READ和OP_WRITE監聽的是socket的讀寫事件,后面第三節會有詳細的描述。

 

  3、組件模塊化:

    從前面的mina架構來看,mina對整個網絡處理模型做了很好的抽象,方便用戶根據實際需要進行靈活的擴展。比如服務器組件IoAcceptor就有SocketAcceptor和DatagramAcceptor兩個子接口,分別對應tcp和udp協議。 IoFilter接口方便用戶方便的實現應用層協議的編解碼器的實現。 IoHandler方便應用擴展業務處理邏輯。

 

  

三、基於JavaNIO進行讀寫的詳細分析

  下面是一個時間服務器的demo,啟動服務器以后, telnet到本地服務器的9123端口,隨意輸入一行,telnet會回顯當前的系統時間。

   // Create the acceptor
        IoAcceptor acceptor = new NioSocketAcceptor();
       
        // Add two filters : a logger and a codec
        acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
        acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory( Charset.forName( "UTF-8" ))));

        // Attach the business logic to the server
        acceptor.setHandler( new TimeServerHandler() );

        // Configurate the buffer size and the iddle time
        acceptor.getSessionConfig().setReadBufferSize( 2048 );
        acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );
       
        // And bind  ;
        acceptor.bind( new InetSocketAddress(9123) );

  

        下面我們用一組圖來說明上面這個Time server的運作機制。

    1、建立服務器套接字,等待客戶端的連接請求並響應客戶端的連接請求:

    

    

 

       對照上面的示意圖,我們對建立服務器套接字並且處理客戶端連接請求的過程進行簡單描述: 

    1)創建一個異步的AcceptorOperationFuture,寫入一個隊列。

    2)Acceptor線程完成兩件事情:套接字綁定和響應客戶端連接請求,這兩件事情是在一輪循環中處理的 。 

    先說套接字綁定的處理: 

    step1: 創建一組ServerSocketChannel,一個ServerSocketChannel對應一個服務器套接字 。

    step2: 把ServerSocketChannel注冊到Selector,並且關心OP_ACCEPT事件(也就是關心客戶端的連接請求) 。

    響應客戶端連接請求: 

    step1: selector進行select操作,拿到一組OP_ACCEPT事件就緒的SelectionKey,通過SelectionKey獲取到ServerSocketChannel; 

    step2: 每一個ServerSocketChannel  接收一個SocketChannel ,把SocketChannel, IoHandler,IoFilterChain,IoProcessor包裝在一起成為一個NioSession. 

    step3: 把NioSession交給一個IoProcessor的池,進行處理。IoProcessor池會分配一個IoProcessor(分配算法:ioSession的id % IoProcessor池的大小  = IoProcessor的數組下標) 處理這個NioSession ,這里IoProcessor池是設計模式中復合模式(composition pattern)的使用。所謂復合模式,就是實現接口的同時包含了一組接口。 

    從IoProcessor池這種處理方式我們可以看到,一個IoProcessor處理了若干個套接字連接,這些套接字連接對應不同的服務器端口。

  

    2、接下來是NioProcessor對NioSocketSession的處理, 整個處理過程如下:

       1) NioProcessor把NioSocketSession加入一個自己的一個隊列NewSessionsQueue。

       2)NioProcessor啟動的一個Processor線程完成兩件事情: 為新到來的NioSocketSession准備讀; 處理socketchannel的讀寫。這兩件事情是在一輪循環中處理的 。 

       先說為NioSocketSession准備讀,步驟如下:

       step1:先從隊列NewSessionsQueue取NioSocketSession;

       step2: 把NioSocketSession的socketChannel注冊到NioProcessor的selector上,並且關心的是OP_READ,注冊完以后,selectionKey被寫入NioSocketSession屬性,然后為NioSocketSession准備一個完成的過濾器鏈,這里需要注意的是在前面連接請求被Accept的時候就已經創建了一個過濾器鏈,包含了HeadFilter和TailFilter,這里再把業務自己定義的Filter加到HeadFilter和TailFilter中間。

      下面是示意圖:

      

    

       socketChannel准備讀寫以后,接下來就是讀寫操作了,示意圖如下:

       

      step1: 利用selector對OP_READ和OP_WRITE也就是讀寫就緒的Niosocketsession進行輪詢,得到一組就緒的SelectionKey,利用這組SelectionKey得到NioSession的迭代器 。

      step2: 對NioSession的迭代器進行遍歷,寫就緒的NioSession進入一個隊列flushingSessions,一次性批量處理,提高寫的效率。

              讀就緒的NioSession則立即處理。

      讀的處理 :把IoBuffer(是對java.nio.ByteBuffer的一個封裝,支持內存容量的自動擴充和減少)交給一個過濾器鏈進行過濾,最后由一個TailFilter把經過過濾器過濾以后得到message交給IoHandler進行業務處理。

         寫的處理: 把一個WriteRequest(里面包含了一個異步的寫請求WriteFuture) ,交給過濾器鏈進行反向過濾,最后由HeadFilter把WriteRequest放到NioSession的WriteRequestQueue ,然后把NioSession加入到一個flushing sessions隊列,由processor thread批量處理這個flushing sessions隊列。每次取一個niosession,對里面的WriteRequest隊列進行遍歷,每個WriteRequest對應一次ByteBuffer的寫, 一直寫到WriteRequest隊列為空或者超過niosession的最大寫字節數。值得注意的是,這里每個niosession之所以有最大寫字節數,是為了保證所謂的讀寫公平,即不要在單個niosession的寫操作上耗費過多的時間,而影響了其它niosession的寫和讀(作者根據經驗,在兼顧效率和公平性的基礎上,把這個單個niosession的最大寫字節數設為了最大讀緩沖區的1.5倍)。

 

 

四、應用層協議的編碼器的設計:

  下面是Mina自帶的一個sumup的例子,這個例子完成的是客戶端發送一組整型變量給服務器,服務器求和以后,返回給客戶端,客戶端最后輸出這些整型變量的和。 示意圖如下:

  

client先后發送1,2,3給服務器,服務器返回1,3,6 三個求和結果給client. 

  在這個例子中,消息的格式如下:

  

從上面的示意圖可以看到,這個應用的消息是固定字節數的,而一般來說,消息的包頭中存放了消息體的字節長度等描述信息, 解碼器需要根據消息包頭的描述信息進行解碼。(后面結合HSF進行進一步的說明)。那么客戶端和服務器是如何進行編碼和解碼的呢?

   消息的編碼比較簡單,根據前面介紹的消息的格式,把消息寫入一個iobuffer(它是對java.nio.ByteBuffer的一個增強,支持內存的自動擴展和減少) 即可。

   消息的解碼相對復雜,示意圖如下。 因為TCP是流式傳輸協議,沒有信息邊界,所以解碼器的一個工作機制可以簡要描述如下:

  1、讀消息頭。 

  2、根據消息頭里面對消息體的定義,一個一個讀消息體的元素。

  3、在讀消息體元素的時候,如果nio bytebuffer中的字節數不夠,則需要等待bytebuffer的新一輪讀取(粘包)

  4、而bytebuffer一次讀到的字節數組,可能包含了兩個消息的內容,那么第一個消息體只需要讀到下一個消息的頭(不包括)就可以截止(半包) 。

 

  

  

  

 

 

            

      

      

 

      接下來要寫的東西:

    

       3、對javaNIO的使用做一個梳理。  

      4、事件處理框架:epoll, kequeue等。

      5、從mina源碼中學習到了什么。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM