Apache MINA 2 是一個(gè)開(kāi)發(fā)高性能和高可伸縮性網(wǎng)絡(luò)應(yīng)用程序的網(wǎng)絡(luò)應(yīng)用框架。它提供了一個(gè)抽象的事件驅(qū)動(dòng)的異步 API,可以使用
TCP/IP、UDP/IP、串口和虛擬機(jī)內(nèi)部的管道等傳輸方式。Apache MINA 2
可以作為開(kāi)發(fā)網(wǎng)絡(luò)應(yīng)用程序的一個(gè)良好基礎(chǔ)。下面將首先簡(jiǎn)單介紹一下 Apache MINA 2。
Apache MINA 2 介紹
Apache MINA 是 Apache 基金會(huì)的一個(gè)開(kāi)源項(xiàng)目,目前最新的版本是 2.0.0-RC1。本文中使用的版本是 2.0.0-M6。從 參考資料 中可以找到相關(guān)的下載信息。下面首先介紹基于 Apache MINA 的網(wǎng)絡(luò)應(yīng)用的一般架構(gòu)。
基于 Apache MINA 的網(wǎng)絡(luò)應(yīng)用的架構(gòu)
基于 Apache MINA 開(kāi)發(fā)的網(wǎng)絡(luò)應(yīng)用,有著相似的架構(gòu)。圖 1 中給出了架構(gòu)的示意圖。
圖 1. 基于 Apache MINA 的網(wǎng)絡(luò)應(yīng)用的架構(gòu)
如 圖 1 所示,基于 Apache MINA 的網(wǎng)絡(luò)應(yīng)用有三個(gè)層次,分別是 I/O 服務(wù)、I/O 過(guò)濾器和 I/O 處理器:
- I/O 服務(wù):I/O 服務(wù)用來(lái)執(zhí)行實(shí)際的 I/O 操作。Apache MINA 已經(jīng)提供了一系列支持不同協(xié)議的 I/O 服務(wù),如 TCP/IP、UDP/IP、串口和虛擬機(jī)內(nèi)部的管道等。開(kāi)發(fā)人員也可以實(shí)現(xiàn)自己的 I/O 服務(wù)。
- I/O 過(guò)濾器:I/O 服務(wù)能夠傳輸?shù)氖亲止?jié)流,而上層應(yīng)用需要的是特定的對(duì)象與數(shù)據(jù)結(jié)構(gòu)。I/O
過(guò)濾器用來(lái)完成這兩者之間的轉(zhuǎn)換。I/O 過(guò)濾器的另外一個(gè)重要作用是對(duì)輸入輸出的數(shù)據(jù)進(jìn)行處理,滿(mǎn)足橫切的需求。多個(gè) I/O 過(guò)濾器串聯(lián)起來(lái),形成
I/O 過(guò)濾器鏈。
- I/O 處理器:I/O 處理器用來(lái)執(zhí)行具體的業(yè)務(wù)邏輯。對(duì)接收到的消息執(zhí)行特定的處理。
創(chuàng)建一個(gè)完整的基于 Apache MINA 的網(wǎng)絡(luò)應(yīng)用,需要分別構(gòu)建這三個(gè)層次。Apache MINA 已經(jīng)為 I/O 服務(wù)和 I/O
過(guò)濾器提供了不少的實(shí)現(xiàn),因此這兩個(gè)層次在大多數(shù)情況下可以使用已有的實(shí)現(xiàn)。I/O 處理器由于是與具體的業(yè)務(wù)相關(guān)的,一般來(lái)說(shuō)都是需要自己來(lái)實(shí)現(xiàn)的。
事件驅(qū)動(dòng)的 API
Apache MINA 提供的是事件驅(qū)動(dòng)的
API。它把與網(wǎng)絡(luò)相關(guān)的各種活動(dòng)抽象成事件。網(wǎng)絡(luò)應(yīng)用只需要對(duì)其感興趣的事件進(jìn)行處理即可。事件驅(qū)動(dòng)的 API 使得基于 Apache MINA
開(kāi)發(fā)網(wǎng)絡(luò)應(yīng)用變得比較簡(jiǎn)單。應(yīng)用不需要考慮與底層傳輸相關(guān)的具體細(xì)節(jié),而只需要處理抽象的 I/O
事件。比如在實(shí)現(xiàn)一個(gè)服務(wù)端應(yīng)用的時(shí)候,如果有新的連接進(jìn)來(lái),I/O 服務(wù)會(huì)產(chǎn)生 sessionOpened
這樣一個(gè)事件。如果該應(yīng)用需要在有連接打開(kāi)的時(shí)候,執(zhí)行某些特定的操作,只需要在 I/O 處理器中此事件處理方法 sessionOpened
中添加相應(yīng)的代碼即可。
在介紹 Apache MINA 中的基本概念的細(xì)節(jié)之前,首先通過(guò)一個(gè)簡(jiǎn)單的應(yīng)用來(lái)熟悉上面提到的三個(gè)層次的具體職責(zé)。
回頁(yè)首
從簡(jiǎn)單應(yīng)用開(kāi)始
在使用 Apache MINA 開(kāi)發(fā)復(fù)雜的應(yīng)用之前,首先將介紹一個(gè)簡(jiǎn)單的應(yīng)用。通過(guò)此應(yīng)用可以熟悉上面提到的三個(gè)層次,即 I/O
服務(wù)、I/O 過(guò)濾器和 I/O 處理器。該應(yīng)用是一個(gè)簡(jiǎn)單的計(jì)算器服務(wù),客戶(hù)端發(fā)送要計(jì)算的表達(dá)式給服務(wù)器,服務(wù)器返回計(jì)算結(jié)果。比如客戶(hù)端發(fā)送 2+2
,服務(wù)器返回 4.0
作為結(jié)果。
在實(shí)現(xiàn)此計(jì)算器的時(shí)候,首先需要考慮的是 I/O 服務(wù)。該計(jì)算器使用 TCP/IP
協(xié)議,需要在指定端口監(jiān)聽(tīng),接受客戶(hù)端的連接。Apache MINA 提供了基于 Java NIO 的套接字實(shí)現(xiàn),可以直接使用。其次要考慮的是
I/O 過(guò)濾器。I/O 過(guò)濾器過(guò)濾所有的 I/O 事件和請(qǐng)求,可以用來(lái)處理橫切的需求,如記錄日志、壓縮等。最后就是 I/O 處理器。I/O
處理器用來(lái)處理業(yè)務(wù)邏輯。具體到該應(yīng)用來(lái)說(shuō),就是在接收到消息之后,把該消息作為一個(gè)表達(dá)式來(lái)執(zhí)行,并把結(jié)果發(fā)送回去。I/O 處理器需要實(shí)現(xiàn) org.apache.mina.core.service.IoHandler
接口或者繼承自 org.apache.mina.core.service.IoHandlerAdapter
。該應(yīng)用的 I/O 處理器的實(shí)現(xiàn)如 清單 1 所示。
清單 1. 計(jì)算器服務(wù)的 I/O 處理器 CalculatorHandler
public class CalculatorHandler extends IoHandlerAdapter {
private static final Logger LOGGER = LoggerFactory
.getLogger(CalculatorHandler.class);
private ScriptEngine jsEngine = null;
public CalculatorHandler() {
ScriptEngineManager sfm = new ScriptEngineManager();
jsEngine = sfm.getEngineByName("JavaScript");
if (jsEngine == null) {
throw new RuntimeException("找不到 JavaScript 引擎。");
}
}
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
LOGGER.warn(cause.getMessage(), cause);
}
public void messageReceived(IoSession session, Object message)
throws Exception {
String expression = message.toString();
if ("quit".equalsIgnoreCase(expression.trim())) {
session.close(true);
return;
}
try {
Object result = jsEngine.eval(expression);
session.write(result.toString());
} catch (ScriptException e) {
LOGGER.warn(e.getMessage(), e);
session.write("Wrong expression, try again.");
}
}
}
|
在 清單 1 中,messageReceived
由 IoHandler
接口聲明。當(dāng)接收到新的消息的時(shí)候,該方法就會(huì)被調(diào)用。此處的邏輯是如果傳入了“quit”,則通過(guò) session.close
關(guān)閉當(dāng)前連接;如果不是的話(huà),就執(zhí)行該表達(dá)式并把結(jié)果通過(guò) session.write
發(fā)送回去。此處執(zhí)行表達(dá)式用的是 JDK 6 中提供的 JavaScript 腳本引擎。此處使用到了 I/O 會(huì)話(huà)相關(guān)的方法,會(huì)在下面進(jìn)行說(shuō)明。
接下來(lái)只需要把 I/O 處理器和 I/O 過(guò)濾器配置到 I/O 服務(wù)上就可以了。具體的實(shí)現(xiàn)如 清單 2 所示。
清單 2. 計(jì)算器服務(wù)主程序 CalculatorServer
public class CalculatorServer {
private static final int PORT = 10010;
private static final Logger LOGGER = LoggerFactory
.getLogger(CalculatorServer.class);
public static void main(String[] args) throws IOException {
IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.getFilterChain().addLast("logger", new LoggingFilter());
acceptor.getFilterChain().addLast(
"codec",
new ProtocolCodecFilter(new TextLineCodecFactory(Charset
.forName("UTF-8"))));
acceptor.setHandler(new CalculatorHandler());
acceptor.bind(new InetSocketAddress(PORT));
LOGGER.info("計(jì)算器服務(wù)已啟動(dòng),端口是" + PORT);
}
}
|
清單 2 中,首先創(chuàng)建一個(gè) org.apache.mina.transport.socket.nio.NioSocketAcceptor
的實(shí)例,由它提供 I/O 服務(wù);接著獲得該 I/O 服務(wù)的過(guò)濾器鏈,并添加兩個(gè)新的過(guò)濾器,一個(gè)用來(lái)記錄相關(guān)日志,另外一個(gè)用來(lái)在字節(jié)流和文本之間進(jìn)行轉(zhuǎn)換;最后配置 I/O 處理器。完成這些之后,通過(guò) bind
方法來(lái)在特定的端口進(jìn)行監(jiān)聽(tīng),接收連接。服務(wù)器啟動(dòng)之后,可以通過(guò)操作系統(tǒng)自帶的 Telnet 工具來(lái)進(jìn)行測(cè)試,如 圖 2 所示。在輸入表達(dá)式之后,計(jì)算結(jié)果會(huì)出現(xiàn)在下面一行。
圖 2. 使用 Telnet 工具測(cè)試計(jì)算器服務(wù)
在介紹了簡(jiǎn)單的計(jì)算器服務(wù)這個(gè)應(yīng)用之后,下面說(shuō)明本文中會(huì)使用的復(fù)雜的聯(lián)機(jī)游戲應(yīng)用。
回頁(yè)首
聯(lián)機(jī)游戲示例說(shuō)明
上一節(jié)中給出了一個(gè)簡(jiǎn)單的基于 Apache MINA 的網(wǎng)絡(luò)應(yīng)用的實(shí)現(xiàn),可以用來(lái)熟悉基本的架構(gòu)。而在實(shí)際開(kāi)發(fā)中,網(wǎng)絡(luò)應(yīng)用都是有一定復(fù)雜度的。下面會(huì)以一個(gè)比較復(fù)雜的聯(lián)機(jī)游戲作為示例來(lái)詳細(xì)介紹 Apache MINA 的概念、API 和典型用法。
該聯(lián)機(jī)游戲支持兩個(gè)人進(jìn)行俄羅斯方塊的對(duì)戰(zhàn)。這個(gè)游戲借鑒了 QQ
的“火拼俄羅斯”。用戶(hù)在啟動(dòng)客戶(hù)端之后,需要輸入一個(gè)昵稱(chēng)進(jìn)行注冊(cè)。用戶(hù)可以在“游戲大廳”中查看當(dāng)前已注冊(cè)的所有其它用戶(hù)。當(dāng)前用戶(hù)可以選擇另外的一
個(gè)用戶(hù)發(fā)送游戲邀請(qǐng)。邀請(qǐng)被接受之后就可以開(kāi)始進(jìn)行對(duì)戰(zhàn)。在游戲過(guò)程中,當(dāng)前用戶(hù)可以看到對(duì)方的游戲狀態(tài),即方塊的情況。該游戲的運(yùn)行效果如 圖 3 所示。
圖 3. 聯(lián)機(jī)游戲示例運(yùn)行效果圖
下面開(kāi)始以這個(gè)應(yīng)用為例來(lái)具體介紹 Apache MINA 中的基本概念。先從 I/O 服務(wù)開(kāi)始。
回頁(yè)首
I/O 服務(wù)
I/O 服務(wù)用來(lái)執(zhí)行真正的 I/O 操作,以及管理 I/O 會(huì)話(huà)。根據(jù)所使用的數(shù)據(jù)傳輸方式的不同,有不同的 I/O 服務(wù)的實(shí)現(xiàn)。由于
I/O 服務(wù)執(zhí)行的是輸入和輸出兩種操作,實(shí)際上有兩種具體的子類(lèi)型。一種稱(chēng)為“I/O 接受器(I/O
acceptor)”,用來(lái)接受連接,一般用在服務(wù)器的實(shí)現(xiàn)中;另外一種稱(chēng)為“I/O 連接器(I/O
connector)”,用來(lái)發(fā)起連接,一般用在客戶(hù)端的實(shí)現(xiàn)中。對(duì)應(yīng)在 Apache MINA 中的實(shí)現(xiàn),org.apache.mina.core.service.IoService
是 I/O 服務(wù)的接口,而繼承自它的接口 org.apache.mina.core.service.IoAcceptor
和 org.apache.mina.core.service.IoConnector
則分別表示 I/O 接受器和 I/O 連接器。IoService
接口提供的重要方法如 表 1 所示。
表 1. IoService 中的重要方法
方法 | 說(shuō)明 |
setHandler(IoHandler handler)
|
設(shè)置 I/O 處理器。該 I/O 處理器會(huì)負(fù)責(zé)處理該 I/O 服務(wù)所管理的所有 I/O 會(huì)話(huà)產(chǎn)生的 I/O 事件。 |
getFilterChain()
|
獲取 I/O 過(guò)濾器鏈,可以對(duì) I/O 過(guò)濾器進(jìn)行管理,包括添加和刪除 I/O 過(guò)濾器。 |
getManagedSessions()
|
獲取該 I/O 服務(wù)所管理的 I/O 會(huì)話(huà)。 |
下面具體介紹 I/O 接受器和 I/O 連接器。
I/O 接受器
I/O 接受器用來(lái)接受連接,與對(duì)等體(客戶(hù)端)進(jìn)行通訊,并發(fā)出相應(yīng)的 I/O 事件交給 I/O 處理器來(lái)處理。使用 I/O 接受器的時(shí)候,只需要調(diào)用 bind
方法并指定要監(jiān)聽(tīng)的套接字地址。當(dāng)不再接受連接的時(shí)候,調(diào)用 unbind
停止監(jiān)聽(tīng)即可。關(guān)于 I/O 接受器的具體用法,可以參考 清單 2 中給出的計(jì)算器服務(wù)的實(shí)現(xiàn)。
I/O 連接器
I/O 連接器用來(lái)發(fā)起連接,與對(duì)等體(服務(wù)器)進(jìn)行通訊,并發(fā)出相應(yīng)的 I/O 事件交給 I/O 處理器來(lái)處理。使用 I/O 連接器的時(shí)候,只需要調(diào)用 connect
方法連接指定的套接字地址。另外可以通過(guò) setConnectTimeoutMillis
設(shè)置連接超時(shí)時(shí)間(毫秒數(shù))。
清單 3 中給出了使用 I/O 連接器的一個(gè)示例。
清單 3. I/O 連接器示例
SocketConnector connector = new NioSocketConnector();
connector.setConnectTimeoutMillis(CONNECT_TIMEOUT);
connector.getFilterChain().addLast("logger", new LoggingFilter());
connector.getFilterChain().addLast("protocol",
new ProtocolCodecFilter(new TetrisCodecFactory()));
ConnectFuture connectFuture = connector.connect(new InetSocketAddress(host, port));
connectFuture.awaitUninterruptibly();
|
在 清單 3 中,首先創(chuàng)建一個(gè) Java NIO 的套接字連接器 NioSocketConnector
的實(shí)例,接著設(shè)置超時(shí)時(shí)間。再添加了 I/O 過(guò)濾器之后,通過(guò) connect
方法連接到指定的地址和端口即可。
在介紹完 I/O 服務(wù)之后,下面介紹 I/O 會(huì)話(huà)。
回頁(yè)首
I/O 會(huì)話(huà)
I/O 會(huì)話(huà)表示一個(gè)活動(dòng)的網(wǎng)絡(luò)連接,與所使用的傳輸方式無(wú)關(guān)。I/O
會(huì)話(huà)可以用來(lái)存儲(chǔ)用戶(hù)自定義的與應(yīng)用相關(guān)的屬性。這些屬性通常用來(lái)保存應(yīng)用的狀態(tài)信息,還可以用來(lái)在 I/O 過(guò)濾器和 I/O
處理器之間交換數(shù)據(jù)。I/O 會(huì)話(huà)在作用上類(lèi)似于 Servlet 規(guī)范中的 HTTP 會(huì)話(huà)。
Apache MINA 中 I/O 會(huì)話(huà)實(shí)現(xiàn)的接口是 org.apache.mina.core.session.IoSession
。該接口中比較重要的方法如 表 2 所示。
表 2. IoSession 中的重要方法
方法 | 說(shuō)明 |
close(boolean immediately)
|
關(guān)閉當(dāng)前連接。如果參數(shù) immediately 為 true 的話(huà),連接會(huì)等到隊(duì)列中所有的數(shù)據(jù)發(fā)送請(qǐng)求都完成之后才關(guān)閉;否則的話(huà)就立即關(guān)閉。 |
getAttribute(Object key)
|
從 I/O 會(huì)話(huà)中獲取鍵為 key 的用戶(hù)自定義的屬性。 |
setAttribute(Object key, Object value)
|
將鍵為 key ,值為 value 的用戶(hù)自定義的屬性存儲(chǔ)到 I/O 會(huì)話(huà)中。 |
removeAttribute(Object key)
|
從 I/O 會(huì)話(huà)中刪除鍵為 key 的用戶(hù)自定義的屬性。 |
write(Object message)
|
將消息對(duì)象 message 發(fā)送到當(dāng)前連接的對(duì)等體。該方法是異步的,當(dāng)消息被真正發(fā)送到對(duì)等體的時(shí)候,IoHandler.messageSent(IoSession,Object) 會(huì)被調(diào)用。如果需要的話(huà),也可以等消息真正發(fā)送出去之后再繼續(xù)執(zhí)行后續(xù)操作。 |
在介紹完 I/O 會(huì)話(huà)之后,下面介紹 I/O 過(guò)濾器。
回頁(yè)首
I/O 過(guò)濾器
從 I/O 服務(wù)發(fā)送過(guò)來(lái)的所有 I/O 事件和請(qǐng)求,在到達(dá) I/O 處理器之前,會(huì)先由 I/O 過(guò)濾器鏈中的 I/O
過(guò)濾器進(jìn)行處理。Apache MINA 中的過(guò)濾器與 Servlet
規(guī)范中的過(guò)濾器是類(lèi)似的。過(guò)濾器可以在很多情況下使用,比如記錄日志、性能分析、訪(fǎng)問(wèn)控制、負(fù)載均衡和消息轉(zhuǎn)換等。過(guò)濾器非常適合滿(mǎn)足網(wǎng)絡(luò)應(yīng)用中各種橫切
的非功能性需求。在一個(gè)基于 Apache MINA
的網(wǎng)絡(luò)應(yīng)用中,一般存在多個(gè)過(guò)濾器。這些過(guò)濾器互相串聯(lián),形成鏈條,稱(chēng)為過(guò)濾器鏈。每個(gè)過(guò)濾器依次對(duì)傳入的 I/O
事件進(jìn)行處理。當(dāng)前過(guò)濾器完成處理之后,由過(guò)濾器鏈中的下一個(gè)過(guò)濾器繼續(xù)處理。當(dāng)前過(guò)濾器也可以不調(diào)用下一個(gè)過(guò)濾器,而提前結(jié)束,這樣 I/O
事件就不會(huì)繼續(xù)往后傳遞。比如負(fù)責(zé)用戶(hù)認(rèn)證的過(guò)濾器,如果遇到未認(rèn)證的對(duì)等體發(fā)出的 I/O
事件,則會(huì)直接關(guān)閉連接。這可以保證這些事件不會(huì)通過(guò)此過(guò)濾器到達(dá) I/O 處理器。
Apache MINA 中 I/O 過(guò)濾器都實(shí)現(xiàn) org.apache.mina.core.filterchain.IoFilter
接口。一般來(lái)說(shuō),不需要完整實(shí)現(xiàn) IOFilter
接口,只需要繼承 Apache MINA 提供的適配器 org.apache.mina.core.filterchain.IoFilterAdapter
,并覆寫(xiě)所需的事件過(guò)濾方法即可,其它方法的默認(rèn)實(shí)現(xiàn)是不做任何處理,而直接把事件轉(zhuǎn)發(fā)到下一個(gè)過(guò)濾器。
IoFilter 接口詳細(xì)說(shuō)明
IoFilter
接口提供了 15 個(gè)方法。這 15 個(gè)方法大致分成兩類(lèi),一類(lèi)是與過(guò)濾器的生命周期相關(guān)的,另外一類(lèi)是用來(lái)過(guò)濾 I/O 事件的。第一類(lèi)方法如 表 3 所示。
表 3. IoFilter 中與過(guò)濾器的生命周期相關(guān)的方法
方法 | 說(shuō)明 |
init()
|
當(dāng)過(guò)濾器第一次被添加到過(guò)濾器鏈中的時(shí)候,此方法被調(diào)用。用來(lái)完成過(guò)濾器的初始化工作。 |
onPreAdd(IoFilterChain parent, String name, IoFilter.NextFilter nextFilter)
|
當(dāng)過(guò)濾器即將被添加到過(guò)濾器鏈中的時(shí)候,此方法被調(diào)用。 |
onPostAdd(IoFilterChain parent, String name, IoFilter.NextFilter nextFilter)
|
當(dāng)過(guò)濾器已經(jīng)被添加到過(guò)濾器鏈中之后,此方法被調(diào)用。 |
onPreRemove(IoFilterChain parent, String name, IoFilter.NextFilter nextFilter)
|
當(dāng)過(guò)濾器即將被從過(guò)濾器鏈中刪除的時(shí)候,此方法被調(diào)用。 |
onPostRemove(IoFilterChain parent, String name, IoFilter.NextFilter nextFilter)
|
當(dāng)過(guò)濾器已經(jīng)被從過(guò)濾器鏈中刪除的時(shí)候,此方法被調(diào)用。 |
destroy()
|
當(dāng)過(guò)濾器不再需要的時(shí)候,它將被銷(xiāo)毀,此方法被調(diào)用。 |
在 表 3 中給出的方法中,參數(shù) parent
表示包含此過(guò)濾器的過(guò)濾器鏈,參數(shù) name
表示過(guò)濾器的名稱(chēng),參數(shù) nextFilter
表示過(guò)濾器鏈中的下一個(gè)過(guò)濾器。
第二類(lèi)方法如 表 4 所示。
表 4. IoFilter 中過(guò)濾 I/O 事件的方法
方法 | 說(shuō)明 |
filterClose(IoFilter.NextFilter nextFilter, IoSession session)
|
過(guò)濾對(duì) IoSession 的 close 方法的調(diào)用。 |
filterWrite(IoFilter.NextFilter nextFilter, IoSession session, WriteRequest writeRequest)
|
過(guò)濾對(duì) IoSession 的 write 方法的調(diào)用。 |
exceptionCaught(IoFilter.NextFilter nextFilter, IoSession session, Throwable cause)
|
過(guò)濾對(duì) IoHandler 的 exceptionCaught 方法的調(diào)用。 |
messageReceived(IoFilter.NextFilter nextFilter, IoSession session, Object message)
|
過(guò)濾對(duì) IoHandler 的 messageReceived 方法的調(diào)用。 |
messageSent(IoFilter.NextFilter nextFilter, IoSession session, WriteRequest writeRequest)
|
過(guò)濾對(duì) IoHandler 的 messageSent 方法的調(diào)用。 |
sessionClosed(IoFilter.NextFilter nextFilter, IoSession session)
|
過(guò)濾對(duì) IoHandler 的 sessionClosed 方法的調(diào)用。 |
sessionCreated(IoFilter.NextFilter nextFilter, IoSession session)
|
過(guò)濾對(duì) IoHandler 的 sessionCreated 方法的調(diào)用。 |
sessionIdle(IoFilter.NextFilter nextFilter, IoSession session, IdleStatus status)
|
過(guò)濾對(duì) IoHandler 的 sessionIdle 方法的調(diào)用。 |
sessionOpened(IoFilter.NextFilter nextFilter, IoSession session)
|
過(guò)濾對(duì) IoHandler 的 sessionOpened 方法的調(diào)用。 |
對(duì)于 表 4 中給出的與 I/O 事件相關(guān)的方法,它們都有一個(gè)參數(shù)是 nextFilter
,表示過(guò)濾器鏈中的下一個(gè)過(guò)濾器。如果當(dāng)前過(guò)濾器完成處理之后,可以通過(guò)調(diào)用 nextFilter
中的方法,把 I/O 事件傳遞到下一個(gè)過(guò)濾器。如果當(dāng)前過(guò)濾器不調(diào)用 nextFilter
中的方法的話(huà),該 I/O 事件就不能繼續(xù)往后傳遞。另外一個(gè)共同的參數(shù)是 session
,用來(lái)表示當(dāng)前的 I/O 會(huì)話(huà),可以用來(lái)發(fā)送消息給對(duì)等體。下面通過(guò)具體的實(shí)例來(lái)說(shuō)明過(guò)濾器的實(shí)現(xiàn)。
BlacklistFilter
BlacklistFilter
是 Apache MINA 自帶的一個(gè)過(guò)濾器實(shí)現(xiàn),其功能是阻止來(lái)自特定地址的連接,即所謂的“黑名單”功能。BlacklistFilter
繼承自 IoFilterAdapter
,并覆寫(xiě)了 IoHandler
相關(guān)的方法。清單 4 中給出了部分實(shí)現(xiàn)。
清單 4. 阻止來(lái)自特定地址連接的 BlacklistFilter
public void messageReceived(NextFilter nextFilter, IoSession session, Object message) {
if (!isBlocked(session)) {
nextFilter.messageReceived(session, message);
} else {
blockSession(session);
}
}
private void blockSession(IoSession session) {
session.close(true);
}
|
在 清單 4 中 messageReceived
方法的實(shí)現(xiàn)中,首先通過(guò) isBlocked
來(lái)判斷當(dāng)前連接是否應(yīng)該被阻止,如果不是的話(huà),則通過(guò) nextFilter.messageReceived
把該 I/O 事件傳遞到下一個(gè)過(guò)濾器;否則的話(huà),則通過(guò) blockSession
來(lái)阻止當(dāng)前連接。
使用 ProtocolCodecFilter
ProtocolCodecFilter
用來(lái)在字節(jié)流和消息對(duì)象之間互相轉(zhuǎn)換。當(dāng)該過(guò)濾器接收到字節(jié)流的時(shí)候,需要首先判斷消息的邊界,然后把表示一條消息的字節(jié)提取出來(lái),通過(guò)一定的邏輯轉(zhuǎn)換成
消息對(duì)象,再把消息對(duì)象往后傳遞,交給 I/O
處理器來(lái)執(zhí)行業(yè)務(wù)邏輯。這個(gè)過(guò)程稱(chēng)為“解碼”。與“解碼”對(duì)應(yīng)的是“編碼”過(guò)程。在“編碼”的時(shí)候,過(guò)濾器接收到的是消息對(duì)象,通過(guò)與“解碼”相反的邏
輯,把消息對(duì)象轉(zhuǎn)換成字節(jié),并反向傳遞,交給 I/O 服務(wù)來(lái)執(zhí)行 I/O 操作。
在“編碼”和“解碼”中的一個(gè)重要問(wèn)題是如何在字節(jié)流中判斷消息的邊界。通常來(lái)說(shuō),有三種辦法解決這個(gè)問(wèn)題:
- 使用固定長(zhǎng)度的消息。這種方式實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,只需要每次讀取特定數(shù)量的字節(jié)即可。
- 使用固定長(zhǎng)度的消息頭來(lái)指明消息主體的長(zhǎng)度。比如每個(gè)消息開(kāi)始的 4 個(gè)字節(jié)的值表示了后面緊跟的消息主體的長(zhǎng)度。只需要首先讀取該長(zhǎng)度,再讀取指定數(shù)量的字節(jié)即可。
- 使用分隔符。消息之間通過(guò)特定模式的分隔符來(lái)分隔。每次只要遇到該模式的字節(jié),就表示到了一個(gè)消息的末尾。
具體到示例應(yīng)用來(lái)說(shuō),客戶(hù)端和服務(wù)器之間的通信協(xié)議比較復(fù)雜,有不同種類(lèi)的消息。每種消息的格式都不相同,同類(lèi)消息的內(nèi)容也不盡相同。因此,使用固定長(zhǎng)度的消息頭來(lái)指明消息主體的長(zhǎng)度就成了最好的選擇。
示例應(yīng)用中的每種消息主體由兩部分組成,第一部分是固定長(zhǎng)度的消息類(lèi)別名稱(chēng),第二部分是每種消息的主體內(nèi)容。圖 4 中給出了示例應(yīng)用中一條完整的消息的結(jié)構(gòu)。
圖 4. 示例應(yīng)用中消息的結(jié)構(gòu)
AbstractTetrisCommand
用來(lái)描述聯(lián)機(jī)游戲示例應(yīng)用中的消息。它是一個(gè)抽象類(lèi),是所有具體消息的基類(lèi)。其具體實(shí)現(xiàn)如 清單 5 所示。
清單 5. 聯(lián)機(jī)游戲示例應(yīng)用中的消息 AbstractTetrisCommand
public abstract class AbstractTetrisCommand implements TetrisCommand {
public abstract String getName();
public abstract byte[] bodyToBytes() throws Exception;
public abstract void bodyFromBytes(byte[] bytes) throws Exception;
public byte[] toBytes() throws Exception {
byte[] body = bodyToBytes();
int commandNameLength = Constants.COMMAND_NAME_LENGTH;
int len = commandNameLength + body.length;
byte[] bytes = new byte[len];
String name = StringUtils.rightPad(getName(), commandNameLength,
Constants.COMMAND_NAME_PAD_CHAR);
name = name.substring(0, commandNameLength);
System.arraycopy(name.getBytes(), 0, bytes, 0, commandNameLength);
System.arraycopy(body, 0, bytes, commandNameLength, body.length);
return bytes;
}
}
|
如 清單 5 所示,AbstractTetrisCommand
中定義了 3 個(gè)抽象方法:getName
、bodyToBytes
和 bodyFromBytes
,分別用來(lái)獲取消息的名稱(chēng)、把消息的主體轉(zhuǎn)換成字節(jié)數(shù)組和從字節(jié)數(shù)組中構(gòu)建消息。bodyToBytes
對(duì)應(yīng)于前面提到的“編碼”過(guò)程,而 bodyFromBytes
對(duì)應(yīng)于“解碼”過(guò)程。每種具體的消息都應(yīng)該實(shí)現(xiàn)這 3 個(gè)方法。AbstractTetrisCommand
中的方法 toBytes
封裝了把消息的主體轉(zhuǎn)換成字節(jié)數(shù)組的邏輯,在字節(jié)數(shù)組中,首先是長(zhǎng)度固定為 Constants.COMMAND_NAME_LENGTH
的消息類(lèi)別名稱(chēng),緊接著是每種消息特定的主體內(nèi)容,由 bodyToBytes
方法來(lái)生成。
在介紹完示例應(yīng)用中的消息格式之后,下面將討論具體的“編碼”和“解碼”過(guò)程?!熬幋a”過(guò)程由編碼器來(lái)完成,編碼器需要實(shí)現(xiàn) org.apache.mina.filter.codec.ProtocolEncoder
接口,一般來(lái)說(shuō)繼承自 org.apache.mina.filter.codec.ProtocolEncoderAdapter
并覆寫(xiě)所需的方法即可。清單 6 中給出了示例應(yīng)用中消息編碼器 CommandEncoder
的實(shí)現(xiàn)。
清單 6. 聯(lián)機(jī)游戲示例應(yīng)用中消息編碼器 CommandEncoder
public class CommandEncoder extends ProtocolEncoderAdapter {
public void encode(IoSession session, Object message,
ProtocolEncoderOutput out) throws Exception {
AbstractTetrisCommand command = (AbstractTetrisCommand) message;
byte[] bytes = command.toBytes();
IoBuffer buf = IoBuffer.allocate(bytes.length, false);
buf.setAutoExpand(true);
buf.putInt(bytes.length);
buf.put(bytes);
buf.flip();
out.write(buf);
}
}
|
在 清單 6 中,encode
方法封裝了編碼的邏輯。由于 AbstractTetrisCommand
的 toBytes
已經(jīng)完成了到字節(jié)數(shù)組的轉(zhuǎn)換,encode
方法直接使用即可。首先寫(xiě)入消息主體字節(jié)數(shù)組的長(zhǎng)度,再是字節(jié)數(shù)組本身,就完成了編碼的過(guò)程。
與編碼過(guò)程相比,解碼過(guò)程要相對(duì)復(fù)雜一些。具體的實(shí)現(xiàn)如 清單 7 所示。
清單 7. 聯(lián)機(jī)游戲示例應(yīng)用中消息解碼器 CommandDecoder
public class CommandDecoder extends CumulativeProtocolDecoder {
protected boolean doDecode(IoSession session, IoBuffer in,
ProtocolDecoderOutput out) throws Exception {
if (in.prefixedDataAvailable(4, Constants.MAX_COMMAND_LENGTH)) {
int length = in.getInt();
byte[] bytes = new byte[length];
in.get(bytes);
int commandNameLength = Constants.COMMAND_NAME_LENGTH;
byte[] cmdNameBytes = new byte[commandNameLength];
System.arraycopy(bytes, 0, cmdNameBytes, 0, commandNameLength);
String cmdName = StringUtils.trim(new String(cmdNameBytes));
AbstractTetrisCommand command = TetrisCommandFactory
.newCommand(cmdName);
if (command != null) {
byte[] cmdBodyBytes = new byte[length - commandNameLength];
System.arraycopy(bytes, commandNameLength, cmdBodyBytes, 0,
length - commandNameLength);
command.bodyFromBytes(cmdBodyBytes);
out.write(command);
}
return true;
} else {
return false;
}
}
}
|
在 清單 7 中可以看到,解碼器 CommandDecoder
繼承自 CumulativeProtocolDecoder
。這是 Apache MINA 提供的一個(gè)幫助類(lèi),它會(huì)自動(dòng)緩存所有已經(jīng)接收到的數(shù)據(jù),直到編碼器認(rèn)為可以開(kāi)始進(jìn)行編碼。這樣在實(shí)現(xiàn)自己的編碼器的時(shí)候,就只需要考慮如何判斷消息的邊界即可。如果一條消息的后續(xù)數(shù)據(jù)還沒(méi)有接收到,CumulativeProtocolDecoder
會(huì)自動(dòng)進(jìn)行緩存。在之前提到過(guò),解碼過(guò)程的一個(gè)重要問(wèn)題是判斷消息的邊界。對(duì)于固定長(zhǎng)度的消息來(lái)說(shuō),只需要使用 Apache MINA 的 IoBuffer
的 remaining
方法來(lái)判斷當(dāng)前緩存中的字節(jié)數(shù)目,如果大于消息長(zhǎng)度的話(huà),就進(jìn)行解碼;對(duì)于使用固定長(zhǎng)度消息頭來(lái)指明消息主體的長(zhǎng)度的情況,IoBuffer
提供了 prefixedDataAvailable
方法來(lái)滿(mǎn)足這一需求。prefixedDataAvailable
會(huì)檢查當(dāng)前緩存中是否有固定長(zhǎng)度的消息頭,并且由此消息頭指定長(zhǎng)度的消息主體是否已經(jīng)全部在緩存中。如果這兩個(gè)條件都滿(mǎn)足的話(huà),說(shuō)明一條完整的消息已經(jīng)接收到,可以進(jìn)行解碼了。解碼的過(guò)程本身并不復(fù)雜,首先讀取消息的類(lèi)別名稱(chēng),然后通過(guò) TetrisCommandFactory.newCommand
方法來(lái)生成一個(gè)該類(lèi)消息的實(shí)例,接著通過(guò)該實(shí)例的 bodyFromBytes
方法就可以從字節(jié)數(shù)組中恢復(fù)消息的內(nèi)容,得到一個(gè)完整的消息對(duì)象。每次成功解碼一個(gè)消息對(duì)象,需要調(diào)用 ProtocolDecoderOutput
的 write
把此消息對(duì)象往后傳遞。消息對(duì)象會(huì)通過(guò)過(guò)濾器鏈,最終達(dá)到 I/O 處理器,在 IoHandler.messageReceived
中接收到此消息對(duì)象。如果當(dāng)前緩存的數(shù)據(jù)不足以用來(lái)解碼一條消息的話(huà),doDecode
只需要返回 false
即可。接收到新的數(shù)據(jù)之后,doDecode
會(huì)被再次調(diào)用。
過(guò)濾器鏈
過(guò)濾器只有在添加到過(guò)濾器鏈中的時(shí)候才起作用。過(guò)濾器鏈?zhǔn)沁^(guò)濾器的容器。過(guò)濾器鏈與 I/O 會(huì)話(huà)是一一對(duì)應(yīng)的關(guān)系。org.apache.mina.core.filterchain.IoFilterChain
是 Apache MINA 中過(guò)濾器鏈的接口,其中提供了一系列方法對(duì)其中包含的過(guò)濾器進(jìn)行操作,包括查詢(xún)、添加、刪除和替換等。如 表 5 所示。
表 5. IoFilterChain 接口的方法
方法 | 說(shuō)明 |
addFirst(String name, IoFilter filter)
|
將指定名稱(chēng)的過(guò)濾器添加到過(guò)濾器鏈的開(kāi)頭。 |
addLast(String name, IoFilter filter)
|
將指定名稱(chēng)的過(guò)濾器添加到過(guò)濾器鏈的末尾。 |
contains(String name)
|
判斷過(guò)濾器鏈中是否包含指定名稱(chēng)的過(guò)濾器。 |
get(String name)
|
從過(guò)濾器鏈中獲取指定名稱(chēng)的過(guò)濾器。 |
remove(String name)
|
從過(guò)濾器鏈中刪除指定名稱(chēng)的過(guò)濾器。 |
replace(String name, IoFilter newFilter)
|
用過(guò)濾器 newFilter 替換掉過(guò)濾器鏈中名為 name 的過(guò)濾器。 |
getSession()
|
獲取與過(guò)濾器鏈一一對(duì)應(yīng)的 I/O 會(huì)話(huà)。 |
在介紹完 I/O 過(guò)濾器和過(guò)濾器鏈之后,下面介紹 I/O 處理器。
回頁(yè)首
I/O 處理器
I/O 事件通過(guò)過(guò)濾器鏈之后會(huì)到達(dá) I/O 處理器。I/O 處理器中與 I/O 事件對(duì)應(yīng)的方法會(huì)被調(diào)用。Apache MINA 中 org.apache.mina.core.service.IoHandler
是 I/O 處理器要實(shí)現(xiàn)的接口,一般情況下,只需要繼承自 org.apache.mina.core.service.IoHandlerAdapter
并覆寫(xiě)所需方法即可。IoHandler
接口的方法如 表 6 所示。
表 6. IoHandler 接口的方法
方法 | 說(shuō)明 |
sessionCreated(IoSession session)
|
當(dāng)有新的連接建立的時(shí)候,該方法被調(diào)用。 |
sessionOpened(IoSession session)
|
當(dāng)有新的連接打開(kāi)的時(shí)候,該方法被調(diào)用。該方法在 sessionCreated 之后被調(diào)用。 |
sessionClosed(IoSession session)
|
當(dāng)連接被關(guān)閉的時(shí)候,此方法被調(diào)用。 |
sessionIdle(IoSession session, IdleStatus status)
|
當(dāng)連接變成閑置狀態(tài)的時(shí)候,此方法被調(diào)用。 |
exceptionCaught(IoSession session, Throwable cause)
|
當(dāng) I/O 處理器的實(shí)現(xiàn)或是 Apache MINA 中有異常拋出的時(shí)候,此方法被調(diào)用。 |
messageReceived(IoSession session, Object message)
|
當(dāng)接收到新的消息的時(shí)候,此方法被調(diào)用。 |
messageSent(IoSession session, Object message)
|
當(dāng)消息被成功發(fā)送出去的時(shí)候,此方法被調(diào)用。 |
對(duì)于 表 6 中的方法,有幾個(gè)需要重點(diǎn)的說(shuō)明一下。首先是 sessionCreated
和 sessionOpened
的區(qū)別。sessionCreated
方法是由 I/O 處理線(xiàn)程來(lái)調(diào)用的,而 sessionOpened
是由其它線(xiàn)程來(lái)調(diào)用的。因此從性能方面考慮,不要在 sessionCreated
方法中執(zhí)行過(guò)多的操作。對(duì)于 sessionIdle
,默認(rèn)情況下,閑置時(shí)間設(shè)置是禁用的,也就是說(shuō) sessionIdle
并不會(huì)被調(diào)用??梢酝ㄟ^(guò) IoSessionConfig.setIdleTime(IdleStatus, int)
來(lái)進(jìn)行設(shè)置。
Apache MINA 中的基本概念已經(jīng)介紹完了,下面介紹狀態(tài)機(jī)的使用。
回頁(yè)首
使用狀態(tài)機(jī)
在 I/O 處理器中實(shí)現(xiàn)業(yè)務(wù)邏輯的時(shí)候,對(duì)于簡(jiǎn)單的情況,一般只需要在 messageReceived
方法中對(duì)傳入的消息進(jìn)行處理。如果需要寫(xiě)回?cái)?shù)據(jù)到對(duì)等體的話(huà),用 IoSession.write
方法即可。在另外的一些情況下,客戶(hù)端和服務(wù)器端的通信協(xié)議比較復(fù)雜,客戶(hù)端其實(shí)是有狀態(tài)變遷的。這個(gè)時(shí)候可以用 Apache MINA 提供的狀態(tài)機(jī)實(shí)現(xiàn),可以使得 I/O 處理器的實(shí)現(xiàn)更加簡(jiǎn)單。
狀態(tài)機(jī)中兩個(gè)重要的元素是狀態(tài)以及狀態(tài)之間的遷移。示例應(yīng)用中客戶(hù)端的狀態(tài)以及遷移如 圖 5 所示。
圖 5. 聯(lián)機(jī)游戲示例應(yīng)用中客戶(hù)端的狀態(tài)以及遷移
客戶(hù)端初始化的時(shí)候,其狀態(tài)為“未連接”,表示客戶(hù)端還沒(méi)有在服務(wù)器上面注冊(cè),此時(shí)還不能進(jìn)行游戲;接著用戶(hù)需要輸入一個(gè)昵稱(chēng)來(lái)注冊(cè)到服務(wù)器
上面,完成之后狀態(tài)遷移到“閑置”。此時(shí)客戶(hù)端會(huì)接收到當(dāng)前在線(xiàn)的所有其它用戶(hù)的列表。當(dāng)前用戶(hù)可以邀請(qǐng)其它用戶(hù)和他一塊游戲,也可以接收來(lái)自其它用戶(hù)的
邀請(qǐng)。邀請(qǐng)發(fā)送出去之后,客戶(hù)端的狀態(tài)遷移到“邀請(qǐng)已發(fā)送”。如果接受了其它用戶(hù)的邀請(qǐng),客戶(hù)端的狀態(tài)遷移到“邀請(qǐng)已接收”。如果某個(gè)用戶(hù)的邀請(qǐng)被另外一
個(gè)用戶(hù)接受的話(huà),兩個(gè)客戶(hù)端的狀態(tài)都會(huì)遷移到“游戲中”。
要實(shí)現(xiàn)這樣較為復(fù)雜的狀態(tài)機(jī)的話(huà),只需要在 I/O 處理器中以聲明式的方式定義狀態(tài)和遷移條件就可以了。首先需要聲明狀態(tài)機(jī)中狀態(tài),如 清單 8 所示。
清單 8. 聯(lián)機(jī)游戲示例應(yīng)用中的狀態(tài)聲明
@State public static final String ROOT = "Root";
@State(ROOT) public static final String NOT_CONNECTED = "NotConnected";
@State(ROOT) public static final String IDLE = "Idle";
@State(ROOT) public static final String INVITATION_SENT = "InvitationSent";
@State(ROOT) public static final String INVITATION_ACCEPTED = "InvitationAccepted";
@State(ROOT) public static final String PLAYING = "Playing";
|
如 清單 8 所示,上面定義了一共六個(gè)狀態(tài)。通過(guò)標(biāo)注 @State
就
聲明了一個(gè)狀態(tài)。需要注意的是狀態(tài)之間是可以繼承的。如果狀態(tài)機(jī)接收到一個(gè)事件的時(shí)候,在當(dāng)前狀態(tài)中找不到對(duì)應(yīng)的遷移,就會(huì)在其父狀態(tài)上繼續(xù)查找。狀態(tài)的
繼承在某些情況下是很有用的,比如希望為所有的狀態(tài)都增加同樣的遷移邏輯,就可以直接把遷移條件添加在父狀態(tài)上面。一個(gè)典型的場(chǎng)景就是錯(cuò)誤處理,一般來(lái)
說(shuō),所有的狀態(tài)都需要錯(cuò)誤處理,而錯(cuò)誤處理的邏輯一般都是相同的。把發(fā)生錯(cuò)誤時(shí)候的遷移放在父狀態(tài)中,可以簡(jiǎn)潔的描述這一場(chǎng)景。
定義了狀態(tài)之后,下面應(yīng)該聲明狀態(tài)之間的遷移。如 清單 9 所示。
清單 9. 聯(lián)機(jī)游戲示例應(yīng)用中的狀態(tài)遷移聲明
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = NOT_CONNECTED, next = IDLE)
public void login(TetrisServerContext context, IoSession session, LoginCommand cmd) {
String nickName = cmd.getNickName();
context.nickName = nickName;
session.setAttribute("nickname", nickName);
session.setAttribute("status", UserStatus.IDLE);
sessions.add(session);
users.add(nickName);
RefreshPlayersListCommand command = createRefreshPlayersListCommand();
broadcast(command);
}
@IoHandlerTransition(on = EXCEPTION_CAUGHT, in = ROOT, weight = 10)
public void exceptionCaught(IoSession session, Exception e) {
LOGGER.warn("Unexpected error.", e);
session.close(true);
}
@IoHandlerTransition(in = ROOT, weight = 100)
public void unhandledEvent() {
LOGGER.warn("Unhandled event.");
}
|
清單 9 中,使用標(biāo)注 @IoHandlerTransition
聲明了一個(gè)狀態(tài)遷移。每個(gè)狀態(tài)遷移可以有四個(gè)屬性:on
、in
、next
和 weight
,其中屬性 in
是必須的,其余是可選的。屬性 on
表示觸發(fā)此狀態(tài)遷移的事件名稱(chēng),如果省略該屬性的話(huà),則默認(rèn)為匹配所有事件的通配符。該屬性的值可以是表中給出的 I/O 處理器中能處理的七種事件類(lèi)型。屬性 in
表示狀態(tài)遷移的起始狀態(tài)。屬性 next
表示狀態(tài)遷移的結(jié)束狀態(tài),如果省略該屬性的話(huà),則默認(rèn)為表示當(dāng)前狀態(tài) 的 _self_
。屬性 weight
用來(lái)指明狀態(tài)遷移的權(quán)重。一個(gè)狀態(tài)的所有遷移是按照其權(quán)重升序排列的。對(duì)于當(dāng)前狀態(tài),如果有多個(gè)可能的遷移,排序靠前的遷移將會(huì)發(fā)生。代碼中的第一個(gè)標(biāo)注聲明了如果當(dāng)前狀態(tài)是“未連接”,并且接收到了 MESSAGE_RECEIVED
事件,而且消息的內(nèi)容是一個(gè) LoginCommand
對(duì)象的話(huà),login
方法會(huì)被調(diào)用,調(diào)用完成之后,當(dāng)前狀態(tài)遷移到“閑置”。第二個(gè)標(biāo)注聲明了對(duì)于任何的狀態(tài),如果接收到了 EXCEPTION_CAUGHT
事件,exceptionCaught
方法會(huì)被調(diào)用。最后一個(gè)標(biāo)注聲明了一個(gè)狀態(tài)遷移,其起始狀態(tài)是 ROOT
,表示該遷移對(duì)所有的事件都起作用。不過(guò)它的 weight
是 100,優(yōu)先級(jí)比較低。該狀態(tài)遷移的作用是處理其它沒(méi)有對(duì)應(yīng)狀態(tài)遷移的事件。
使用了 Apache MINA 提供的狀態(tài)機(jī)之后,創(chuàng)建 I/O 處理器的方式也發(fā)生了變化。I/O 處理器的實(shí)例由狀態(tài)機(jī)來(lái)創(chuàng)建,如 清單 10 所示。
清單 10. 在狀態(tài)機(jī)中創(chuàng)建 I/O 處理器
private static IoHandler createIoHandler() {
StateMachine sm = StateMachineFactory.getInstance(
IoHandlerTransition.class).create(ServerHandler.NOT_CONNECTED,
new ServerHandler());
return new StateMachineProxyBuilder().setStateContextLookup(
new IoSessionStateContextLookup(new StateContextFactory() {
public StateContext create() {
return new ServerHandler.TetrisServerContext();
}
})).create(IoHandler.class, sm);
}
|
在 清單 10 中,TetrisServerContext
是提供給狀態(tài)機(jī)的上下文對(duì)象,用來(lái)在狀態(tài)之間共享數(shù)據(jù)。當(dāng)然用 IoSession
也是可以實(shí)現(xiàn)的,不過(guò)上下文對(duì)象的好處是類(lèi)型安全,不需要做額外的類(lèi)型轉(zhuǎn)換。
在介紹完?duì)顟B(tài)機(jī)后,下面介紹一些高級(jí)話(huà)題,包括異步操作以及 Apache MINA 與 JMX 和 Spring 的集成。
回頁(yè)首
高級(jí)話(huà)題
在前面章節(jié)中介紹了 Apache MINA 的基本概念和具體用法,下面討論一些高級(jí)話(huà)題。
異步操作
Apache MINA 中的很多操作都是異步的,比如連接的建立、連接的關(guān)閉、還有數(shù)據(jù)的發(fā)送等。在編寫(xiě)網(wǎng)絡(luò)應(yīng)用的時(shí)候,需要考慮這一點(diǎn)。比如 IoConnector
的 connect
方法,其返回值是 org.apache.mina.core.future.ConnectFuture
類(lèi)的對(duì)象。通過(guò)此對(duì)象,可以查詢(xún)連接操作的狀態(tài)。清單 3 中已經(jīng)使用了 ConnectFuture
。另外一個(gè)常用的是發(fā)送數(shù)據(jù)時(shí)使用的 org.apache.mina.core.future.WriteFuture
,如 清單 11 所示。
清單 11. WriteFuture 的使用
IoSession session = ...; // 獲取 I/O 會(huì)話(huà)對(duì)象
WriteFuture future = session.write("Hello World"); // 發(fā)送數(shù)據(jù)
future.awaitUninterruptibly(); // 等待發(fā)送數(shù)據(jù)操作完成
if(future.isWritten())
{
// 數(shù)據(jù)已經(jīng)被成功發(fā)送
}
else
{
// 數(shù)據(jù)發(fā)送失敗
}
|
由于這樣的需求很常見(jiàn),I/O 處理器中提供了 messageSent
方法,當(dāng)數(shù)據(jù)發(fā)送成功的時(shí)候,該方法會(huì)被調(diào)用。
JMX 集成
Apache MINA 可以集成 JMX 來(lái)對(duì)網(wǎng)絡(luò)應(yīng)用進(jìn)行管理和監(jiān)測(cè)。下面通過(guò)對(duì)前面給出的計(jì)算器服務(wù)進(jìn)行簡(jiǎn)單修改,來(lái)說(shuō)明如何集成 JMX。所需的修改如 清單 12 所示。
清單 12. Apache MINA 與 JMX 的集成
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
IoAcceptor acceptor = new NioSocketAcceptor();
IoServiceMBean acceptorMBean = new IoServiceMBean(acceptor);
ObjectName acceptorName = new ObjectName(acceptor.getClass()
.getPackage().getName()
+ ":type=acceptor,name=" + acceptor.getClass().getSimpleName());
mBeanServer.registerMBean(acceptorMBean, acceptorName);
|
如 清單 12 所示,首先獲取平臺(tái)提供的受控 bean 的服務(wù)器,接著創(chuàng)建受控 bean(MBean)來(lái)包裝想要管理和監(jiān)測(cè)的對(duì)象,這里使用的是 I/O 連接器對(duì)象。最后把創(chuàng)建出來(lái)的受控 bean 注冊(cè)到服務(wù)器即可。
在啟動(dòng)計(jì)算器服務(wù)應(yīng)用的時(shí)候,添加下面的啟動(dòng)參數(shù):-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8084
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
,就啟用了 JMX。接著通過(guò) JVM 提供的“Java 監(jiān)視和管理控制臺(tái)”(運(yùn)行 jconsole
)就可以連接到此應(yīng)用進(jìn)行管理和監(jiān)測(cè)了。監(jiān)測(cè)的結(jié)果如 圖 6 所示。
圖 6. 通過(guò)“Java 監(jiān)視和管理控制臺(tái)”管理和監(jiān)測(cè)基于 Apache MINA 的應(yīng)用
Spring 集成
Apache MINA 可以和流行的開(kāi)源框架 Spring 進(jìn)行集成,由 Spring 來(lái)管理 Apache MINA 中的對(duì)象。與 Spring 集成的方式也比較簡(jiǎn)單,只需要編寫(xiě)相應(yīng)的 Spring 配置文件即可。清單 13 中給出了與 Spring 集成之后的計(jì)算器服務(wù)的配置文件。
清單 13. Apache MINA 與 Spring 集成的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="calculatorHandler" class="calculator.CalculatorHandler" />
<bean id="loggingFilter" class="org.apache.mina.filter.logging.LoggingFilter" />
<bean id="calculatorCodecFilter" class="org.apache.mina.filter.
codec.ProtocolCodecFilter">
<constructor-arg>
<bean class="org.apache.mina.filter.codec.textline.TextLineCodecFactory" />
</constructor-arg>
</bean>
<bean id="filterChainBuilder"
class="org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder">
<property name="filters">
<map>
<entry key="loggingFilter" value-ref="loggingFilter" />
<entry key="codecFilter" value-ref="calculatorCodecFilter" />
</map>
</property>
</bean>
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="java.net.SocketAddress">
<bean class="org.apache.mina.integration.beans.InetSocketAddressEditor"/>
</entry>
</map>
</property>
</bean>
<bean id="ioAcceptor" class="org.apache.mina.transport.socket.nio.NioSocketAcceptor"
init-method="bind" destroy-method="unbind">
<property name="defaultLocalAddress" value=":10010" />
<property name="handler" ref="calculatorHandler" />
<property name="filterChainBuilder" ref="filterChainBuilder" />
</bean>
</beans>
|
清單 13 中創(chuàng)建 I/O 處理器和 I/O 過(guò)濾器的方式很直接。由于不能直接從 I/O 接受器獲取過(guò)濾器鏈,這里創(chuàng)建了一個(gè) org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder
類(lèi)的 bean,用來(lái)構(gòu)建過(guò)濾器鏈。由 Apache MINA 提供的網(wǎng)絡(luò)地址編輯器 org.apache.mina.integration.beans.InetSocketAddressEditor
允許以“主機(jī)名 : 端口
”的形式指定網(wǎng)絡(luò)地址。在聲明 I/O 接受器的時(shí)候,通過(guò) init-method
指明了當(dāng) I/O 接受器創(chuàng)建成功之后,調(diào)用其 bind
方法來(lái)接受連接;通過(guò) destroy-method
聲明了當(dāng)其被銷(xiāo)毀的時(shí)候,調(diào)用其 unbind
來(lái)停止監(jiān)聽(tīng)。
回頁(yè)首
總結(jié)
Apache MINA 是一個(gè)很好的網(wǎng)絡(luò)應(yīng)用框架,它通過(guò)事件驅(qū)動(dòng)的 API 為開(kāi)發(fā)網(wǎng)絡(luò)應(yīng)用提供了良好的架構(gòu)基礎(chǔ),同時(shí)也提供了豐富的
I/O 服務(wù)和 I/O 過(guò)濾器的實(shí)現(xiàn),使得開(kāi)發(fā)網(wǎng)絡(luò)應(yīng)用變得簡(jiǎn)單。本文詳細(xì)介紹了 Apache MINA 中的基本概念,包括 I/O 服務(wù)、I/O
會(huì)話(huà)、I/O 過(guò)濾器和 I/O 處理器等,同時(shí)介紹了如何利用狀態(tài)機(jī)來(lái)實(shí)現(xiàn)邏輯復(fù)雜的 I/O 處理器。除此之外,還討論了 Apache MINA
如何與 JMX 和 Spring 進(jìn)行集成。本文提供了一個(gè)簡(jiǎn)單的計(jì)算器服務(wù)和復(fù)雜的俄羅斯方塊聯(lián)機(jī)游戲作為示例,可以幫助讀者更好的掌握基于
Apache MINA 的網(wǎng)絡(luò)應(yīng)用開(kāi)發(fā)。