1. 程式人生 > >【MINA學習筆記】—— 1.體系結構分析[z]

【MINA學習筆記】—— 1.體系結構分析[z]

前言

Apache的MINA框架是一個早年非常流行的NIO框架,它出自於Netty之父Trustin Lee大神之手。雖然目前市場份額已經逐漸被Netty取代了,但是其作為NIO初學者入門學習框架是非常合適的,因為MINA足夠的簡單,它的實現相對於Netty的難易程度,大概只有Netty的40%左右(個人在對比了MINA和Netty的底層實現得出的結論);然而其在整體架構上的設計是非常類似的,因此在學習完MINA之後再去看Netty,也會相對簡單一些。與此同時,一些老的系統在底層實現上也有很多使用了MINA來進行通訊的,如果在接手後不懂其原理,也是很難去維護的。因此個人覺得還是有必要去花些時間去好好研究一下它!

MINA巨集觀架構

  MINA巨集觀的體系結構

從巨集觀上面看,MINA作為應用程式與底層網路協議的中間橋樑,它所支援的底層協議包括TCP、UDP、in-VM、 RS-232C串列埠程式設計協議等。對於我們開發者而言,無論你是在編寫客戶端還是伺服器端程式,我們都只需在MINA之上設計你的應用本身即可,而無需關注底層網路層協議的複雜處理。

MINA中的元件

上面我們從巨集觀的角度看完了MINA的整體結構,下面讓我們MINA中的元件做一個剖析。

 
MINA中的元件

從上面的圖中,我們可以看到,從廣義的劃分方式來說,一個基於MINA的應用,無論是服務端還是客戶端,它都一共分為三個部分:

  • I/O Service:負責埠繫結、接受網路連線或者是主動發起網路連線、網路IO讀寫、生成對應的IOSession(IOSession是MINA對底層網路連線的一個封裝)等功能。
  • I/O Filter Chain:資料過濾、資料報文的編解碼、黑白名單過濾等等。
  • I/O Handler:執行真正的業務邏輯。

所以,如果我們想要建立一個基於MINA的網路應用程式,其實只需要3步:

  1. 建立I/O Service,通常直接使用MINA內建的IO Service即可。
  2. 建立一個Filter Chain,MINA也內建了大量Filter Chain的實現,在某些特殊情況下需要自定義IoFilter,比如實現自定義協議的編解碼功能。
  3. 建立I/O Handler,編寫我們真正的業務邏輯,處理不同的訊息。

MINA伺服器端架構

上面我們已經分析了MINA的整體架構,下面對其再細分一下,我們一起來看一下MINA伺服器端的架構組成。

  MINA伺服器端架構

細心的同學會發現,上面的圖與之前的整體架構圖對比起來看,其實真正變化的內容就是從I/O Service變成了I/O Acceptor

MINA對I/O Service做了一個整體的抽象,在伺服器端,因為是接收連線,因此是I/O Acceptor;而在客戶端,因為是主動發起連線,因此就是I/O Connnector。但是無論IOAcceptor還是IOConnector,它們都繼承了IOService這個介面。

  IOService的結構圖

MINA伺服器示例

好了,說了這麼多,下面搞個MINA服務端的例子來感受一下,我們使用的MINA版本是2.0.16,JDK的版本為1.8

1.新增maven的pom依賴
  <properties>
        <mina.version>2.0.16</mina.version> <logback.version>1.2.3</logback.version> <java.version>1.8</java.version> </properties> <dependencyManagement> <dependencies> <!-- https://mvnrepository.com/artifact/org.apache.mina/mina-core --> <dependency> <groupId>org.apache.mina</groupId> <artifactId>mina-core</artifactId> <version>${mina.version}</version> <!--Mina自帶會引入SLF4J包--> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency> <!--使用Logback作為日誌系統,自帶會引入SLF4J--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.apache.mina</groupId> <artifactId>mina-core</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.2</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> </configuration> </plugin> </plugins> </build> 

MINA自帶就會使用SLF4J作為其日誌框架,但是其引入的版本是1.7.21,而我們使用Logback也會自動引入SLF4J的包,其使用的版本是1.7.25。我們這裡排除掉MINA自動引入的版本,使用1.7.25的SLF4J-API包。

2.編寫Server端主程式
package com.panlingxiao.mina.quickstart;

import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoService;
import org.apache.mina.core.service.IoServiceListener;
import org.apache.mina.core.session.IdleStatus; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.codec.textline.TextLineCodecFactory; import org.apache.mina.filter.logging.LoggingFilter; import org.apache.mina.transport.socket.nio.NioSocketAcceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.charset.Charset; import java.util.concurrent.TimeUnit; /** * Created by panlingxiao on 2017/8/13. * 基於Mina的時間伺服器 */ public class MinaTimeServer { private static final int PORT = 9123; private static final Logger logger = LoggerFactory.getLogger(MinaTimeServer.class); public static void main(String[] args) throws Exception { IoAcceptor acceptor = new NioSocketAcceptor(); // 1 //新增日誌處理器 2. acceptor.getFilterChain().addLast("logger", new LoggingFilter()); //新增編解碼處理器,讀取一行資料作為一個報文 acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8")))); //3.設定IoHandler acceptor.setHandler(new TimeServerHandler()); //4.配置IoSession的屬性 acceptor.getSessionConfig().setReadBufferSize(2048); acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10); //5.在埠繫結前新增IOServiceListener,否則事件無法監聽到 acceptor.addListener(new IoServiceListener() { @Override public void serviceActivated(IoService service) throws Exception { logger.info("{} active", service); } @Override public void serviceIdle(IoService service, IdleStatus idleStatus) throws Exception { } @Override public void serviceDeactivated(IoService service) throws Exception { } @Override public void sessionCreated(IoSession session) throws Exception { logger.info("{} create session,session:{}", session); } @Override public void sessionClosed(IoSession session) throws Exception { } @Override public void sessionDestroyed(IoSession session) throws Exception { } }); //6.如果不指定埠,則由OS隨機分配一個可用埠 //acceptor.bind(); acceptor.bind(new InetSocketAddress(PORT)); } } 
  1. 首選我們建立了一個IOAcceptor,它用於監聽網路埠,等待進來的連線,以及客戶端傳送過來的報文。對於一個新的連線,一個新的Session會被建立。Session是MINA對客戶端連線的一個封裝實現,不讓使用者直接去處理JDK原生的SocketChannel。

  2. 設定了兩個IoFilter,第一個是用於日誌記錄,每當有接受到連線或者讀取到資料,都將日誌輸出。第二個是用於資料的編解碼,以一行資料作為一個報文的形式,將讀取的資料解碼成一個字串,同時也將輸出的字串轉換成對應的ByteBuffer輸出。

  3. 設定IoHandler,它的作用就是用於處理具體的業務邏輯的。這裡的業務邏輯非常簡單,就是將當前的日期向客戶端輸出。

  4. 通過IoSessionConfig設定IoSession的屬性,setReadBufferSize(2048)表示每一次讀取最大的位元組數為2048個位元組,setIdleTime(IdleStatus.BOTH_IDLE, 10)表示設定一個網路連線在10秒之內都沒有讀寫,則認為是一次閒置。後面可以統計到一個連接出現了多少次閒置,業務上可以根據閒置的次數,當其達到最大值時,將連線斷開,從而降低不必要的資源開銷。

  5. 新增IoServiceListener去監聽當前IoAcceptor所發生的事件。在Server端,如果當埠繫結成功之後,IoAcceptor就會處於active狀態,此時IoServiceListener的serviceActivated就會得到通知。

  6. 最後,我們通過指定伺服器端的埠,讓IoAcceptor監聽給定的埠。

3.編寫IoHandler
package com.panlingxiao.mina.quickstart;

import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;

import java.util.Date; /** * Created by panlingxiao on 2017/8/13. */ public class TimeServerHandler extends IoHandlerAdapter { private static final Logger logger = LoggerFactory.getLogger(TimeServerHandler.class); //1 @Override public void exceptionCaught(IoSession session, Throwable cause) throws Exception { cause.printStackTrace(); } //2 @Override public void sessionIdle(IoSession session, IdleStatus status) throws Exception { logger.info("IDLE " + session.getIdleCount(status)); } //3 @Override public void messageReceived(IoSession session, Object message) throws Exception { String str = message.toString(); if (str.trim().equalsIgnoreCase("quit")) { session.closeNow().addListener(future -> { logger.info("Session Close"); }); return; } Date date = new Date(); //通過新增IOFutureListener,回撥得到結果 session.write(date.toString()).addListener((ioFuture) -> { if (ioFuture.isDone()) { logger.info("Message written..."); } }); } } 

在這裡,我們並沒有直接去實現IoHandler介面,而是去繼承IoHandlerAdapter。這裡使用了一個典型的介面卡模式,通過在IoHandlerAdapter中去重寫需要的方法。下面我們看一下這幾個重寫的方法:

  1. exceptionCaught方法見名知意,當在執行處理過程中發生異常時會得到呼叫。這裡的異常包括由網路引起的IOExcetpion,或者是由於IoHandler處理業務邏輯而引起的異常等,都會呼叫該方法來處理。這裡我們只是簡單地將異常輸出而已,在實際的開發中,我們可以根據自己的業務,向另外一方影響一個錯誤處理的訊息。

  2. 當連線在指定時間內沒有發生網路讀寫時,IoService就將該連線的Idle次數加1。此時sessionIdle方法就會得到呼叫。該方法只針對面向連線的協議有效,如果我們使用UDP這種無連線的傳輸模式時,該方法就不會被執行。

  3. 最後我們完成當接受到訊息的處理。由於前面已經經過IoFilter對資料的解碼處理,因此我們接受到的資料就是一個String型別。如果我們接受到的資料是一個quit,那麼伺服器端則主動關閉連線;否則的話,我們將當前時間返回給客戶端。需要注意的是,MINA與Netty一樣,都是一個非同步處理的框架,因此我們在處理一個操作的時候,建議通過新增IoFutureListener的方式來獲取執行的返回結果。

4.演示結果
  Server端日誌   通過telnet連線

客戶端架構

  客戶端架構

在分析完伺服器的結構之後,我們再來看一下客戶端的結構。仔細看我們會發現,客戶端結構與伺服器端結構唯一的不同就在於,IOAcceptor被換成了IOConnector。

客戶端程式碼示例

package com.panlingxiao.mina.quickstart.client;

import org.apache.mina.core.service.IoConnector;
import org.apache.mina.core.service.IoService;
import org.apache.mina.core.service.IoServiceListener;
import org.apache.mina.core.session.IdleStatus; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.codec.textline.TextLineCodecFactory; import org.apache.mina.filter.logging.LoggingFilter; import org.apache.mina.transport.socket.nio.NioSocketConnector; import java.net.InetSocketAddress; import java.nio.charset.Charset; /** * Created by panlingxiao on 2017/8/28. */ public class MinaTimeClient { public static void main(String[] args) throws Throwable { //建立IOConnector,用於完成與伺服器建立連線 IoConnector connector = new NioSocketConnector(); //新增IoFilter connector.getFilterChain().addLast("logger", new LoggingFilter()); connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8")))); //設定IoHandler connector.setHandler(new TimeClientHandler()); //新增監聽器,獲取IOConnector的事件通知 connector.addListener(new IoServiceListener() { @Override public void serviceActivated(IoService service) throws Exception { } @Override public void serviceIdle(IoService service, IdleStatus idleStatus) throws Exception { } @Override public void serviceDeactivated(IoService service) throws Exception { } @Override public void sessionCreated(IoSession session) throws Exception { } @Override public void sessionClosed(IoSession session) throws Exception { } @Override public void sessionDestroyed(IoSession session) throws Exception { //當關閉完成session銷燬之後,將IOConnector資源釋放 connector.dispose(); } }); //連線伺服器 connector.connect(new InetSocketAddress(9123)); } } 
package com.panlingxiao.mina.quickstart.client;

import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /** * Created by panlingxiao on 2017/8/28. */ public class TimeClientHandler extends IoHandlerAdapter { private static final Logger logger = LoggerFactory.getLogger(TimeClientHandler.class); private int counter = 0; private int num = 100; @Override public void exceptionCaught(IoSession session, Throwable cause) throws Exception { super.exceptionCaught(session, cause); } @Override public void sessionOpened(IoSession session) throws Exception { //當連線建立之後,客戶端主動向伺服器傳送100條訊息 for (int i = 0; i < num; i++) { session.write("hello:" + i); } } @Override public void messageReceived(IoSession session, Object message) throws Exception { counter++; logger.info("receive message:{},counter:{}", message, counter); //當客戶端接受到100條訊息後,主動斷開連線 if(num == counter){ session.closeNow(); } } } 
  客戶端執行結果

總結

至此,我們已經學習完了MINA的整體結構以及它的組成部分。在後面會開始分析NIOSocketAcceptor的實現,分析埠的繫結過程、網路資料的讀寫過程、Session的建立過程、以及一個網路事件是如果通過IoFilter一層層地傳遞到IoHandler等內容。


連結:https://www.jianshu.com/p/5d47e56f89de