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

從宏觀上面看,MINA作為應用程序與底層網(wǎng)絡協(xié)議的中間橋梁,它所支持的底層協(xié)議包括TCP、UDP、in-VM、 RS-232C串口編程協(xié)議等。對于我們開發(fā)者而言,無論你是在編寫客戶端還是服務器端程序,我們都只需在MINA之上設(shè)計你的應用本身即可,而無需關(guān)注底層網(wǎng)絡層協(xié)議的復雜處理。
MINA中的組件
上面我們從宏觀的角度看完了MINA的整體結(jié)構(gòu),下面讓我們MINA中的組件做一個剖析。

從上面的圖中,我們可以看到,從廣義的劃分方式來說,一個基于MINA的應用,無論是服務端還是客戶端,它都一共分為三個部分:
- I/O Service:負責端口綁定、接受網(wǎng)絡連接或者是主動發(fā)起網(wǎng)絡連接、網(wǎng)絡IO讀寫、生成對應的IOSession(IOSession是MINA對底層網(wǎng)絡連接的一個封裝)等功能。
- I/O Filter Chain:數(shù)據(jù)過濾、數(shù)據(jù)報文的編解碼、黑白名單過濾等等。
- I/O Handler:執(zhí)行真正的業(yè)務邏輯。
所以,如果我們想要創(chuàng)建一個基于MINA的網(wǎng)絡應用程序,其實只需要3步:
- 創(chuàng)建I/O Service,通常直接使用MINA內(nèi)置的IO Service即可。
- 創(chuàng)建一個Filter Chain,MINA也內(nèi)置了大量Filter Chain的實現(xiàn),在某些特殊情況下需要自定義IoFilter,比如實現(xiàn)自定義協(xié)議的編解碼功能。
- 創(chuàng)建I/O Handler,編寫我們真正的業(yè)務邏輯,處理不同的消息。
MINA服務器端架構(gòu)
上面我們已經(jīng)分析了MINA的整體架構(gòu),下面對其再細分一下,我們一起來看一下MINA服務器端的架構(gòu)組成。

細心的同學會發(fā)現(xiàn),上面的圖與之前的整體架構(gòu)圖對比起來看,其實真正變化的內(nèi)容就是從I/O Service變成了I/O Acceptor。
MINA對I/O Service做了一個整體的抽象,在服務器端,因為是接收連接,因此是I/O Acceptor;而在客戶端,因為是主動發(fā)起連接,因此就是I/O Connnector。但是無論IOAcceptor還是IOConnector,它們都繼承了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作為日志系統(tǒng),自帶會引入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());
//添加編解碼處理器,讀取一行數(shù)據(jù)作為一個報文
acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"))));
//3.設(shè)置IoHandler
acceptor.setHandler(new TimeServerHandler());
//4.配置IoSession的屬性
acceptor.getSessionConfig().setReadBufferSize(2048);
acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
//5.在端口綁定前添加IOServiceListener,否則事件無法監(jiān)聽到
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));
}
}
首選我們創(chuàng)建了一個IOAcceptor,它用于監(jiān)聽網(wǎng)絡端口,等待進來的連接,以及客戶端發(fā)送過來的報文。對于一個新的連接,一個新的Session會被創(chuàng)建。Session是MINA對客戶端連接的一個封裝實現(xiàn),不讓用戶直接去處理JDK原生的SocketChannel。
設(shè)置了兩個IoFilter,第一個是用于日志記錄,每當有接受到連接或者讀取到數(shù)據(jù),都將日志輸出。第二個是用于數(shù)據(jù)的編解碼,以一行數(shù)據(jù)作為一個報文的形式,將讀取的數(shù)據(jù)解碼成一個字符串,同時也將輸出的字符串轉(zhuǎn)換成對應的ByteBuffer輸出。
設(shè)置IoHandler,它的作用就是用于處理具體的業(yè)務邏輯的。這里的業(yè)務邏輯非常簡單,就是將當前的日期向客戶端輸出。
通過IoSessionConfig設(shè)置IoSession的屬性,setReadBufferSize(2048)表示每一次讀取最大的字節(jié)數(shù)為2048個字節(jié),setIdleTime(IdleStatus.BOTH_IDLE, 10)表示設(shè)置一個網(wǎng)絡連接在10秒之內(nèi)都沒有讀寫,則認為是一次閑置。后面可以統(tǒng)計到一個連接出現(xiàn)了多少次閑置,業(yè)務上可以根據(jù)閑置的次數(shù),當其達到最大值時,將連接斷開,從而降低不必要的資源開銷。
添加IoServiceListener去監(jiān)聽當前IoAcceptor所發(fā)生的事件。在Server端,如果當端口綁定成功之后,IoAcceptor就會處于active狀態(tài),此時IoServiceListener的serviceActivated就會得到通知。
最后,我們通過指定服務器端的端口,讓IoAcceptor監(jiān)聽給定的端口。
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,回調(diào)得到結(jié)果
session.write(date.toString()).addListener((ioFuture) -> {
if (ioFuture.isDone()) {
logger.info("Message written...");
}
});
}
}
在這里,我們并沒有直接去實現(xiàn)IoHandler接口,而是去繼承IoHandlerAdapter。這里使用了一個典型的適配器模式,通過在IoHandlerAdapter中去重寫需要的方法。下面我們看一下這幾個重寫的方法:
exceptionCaught方法見名知意,當在執(zhí)行處理過程中發(fā)生異常時會得到調(diào)用。這里的異常包括由網(wǎng)絡引起的IOExcetpion,或者是由于IoHandler處理業(yè)務邏輯而引起的異常等,都會調(diào)用該方法來處理。這里我們只是簡單地將異常輸出而已,在實際的開發(fā)中,我們可以根據(jù)自己的業(yè)務,向另外一方影響一個錯誤處理的消息。
當連接在指定時間內(nèi)沒有發(fā)生網(wǎng)絡讀寫時,IoService就將該連接的Idle次數(shù)加1。此時sessionIdle方法就會得到調(diào)用。該方法只針對面向連接的協(xié)議有效,如果我們使用UDP這種無連接的傳輸模式時,該方法就不會被執(zhí)行。
最后我們完成當接受到消息的處理。由于前面已經(jīng)經(jīng)過IoFilter對數(shù)據(jù)的解碼處理,因此我們接受到的數(shù)據(jù)就是一個String類型。如果我們接受到的數(shù)據(jù)是一個quit,那么服務器端則主動關(guān)閉連接;否則的話,我們將當前時間返回給客戶端。需要注意的是,MINA與Netty一樣,都是一個異步處理的框架,因此我們在處理一個操作的時候,建議通過添加IoFutureListener的方式來獲取執(zhí)行的返回結(jié)果。
4.演示結(jié)果


客戶端架構(gòu)

在分析完服務器的結(jié)構(gòu)之后,我們再來看一下客戶端的結(jié)構(gòu)。仔細看我們會發(fā)現(xiàn),客戶端結(jié)構(gòu)與服務器端結(jié)構(gòu)唯一的不同就在于,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 {
//創(chuàng)建IOConnector,用于完成與服務器建立連接
IoConnector connector = new NioSocketConnector();
//添加IoFilter
connector.getFilterChain().addLast("logger", new LoggingFilter());
connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"))));
//設(shè)置IoHandler
connector.setHandler(new TimeClientHandler());
//添加監(jiān)聽器,獲取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 {
//當關(guān)閉完成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 {
//當連接建立之后,客戶端主動向服務器發(fā)送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();
}
}
}

總結(jié)
至此,我們已經(jīng)學習完了MINA的整體結(jié)構(gòu)以及它的組成部分。在后面會開始分析NIOSocketAcceptor的實現(xiàn),分析端口的綁定過程、網(wǎng)絡數(shù)據(jù)的讀寫過程、Session的創(chuàng)建過程、以及一個網(wǎng)絡事件是如果通過IoFilter一層層地傳遞到IoHandler等內(nèi)容。
參考
http://mina.apache.org/mina-project/userguide/ch2-basics/ch2-basics.html