Apache Mina學習2

Apache Mina ,一個高性能 Java 異步併發網絡通訊框架。利用 Mina 可以高效地完成以下任務:

  • TCP/IP 和 UDP/IP 通訊
  • 串口通訊
  • VM 間的管道通訊
  • SSL/TLS
  • JXM 集成
  • IoC 容器集成( Spring 、 Pico 等)
  • 狀態機

Mina 的 API 當前主要有三個分支,分別是:

  • 2.0.x 目前處於 SVN trunk 上的版本, Mina 社區對該版本的 API 進行了全新的設計
  • 1.1.x 爲當前用於產品開發的版本,適用於 5.0 以上的 JDK ,最新版本爲 1.1.5
  • 1.0.x 是 1.1.x 的 JDK 1.4 的兼容版本,最新版本爲 1.0.8

這裏將要介紹的是 2.0.x 版。雖然當前的穩定版本還是 1.1.x ,但是按照 Mina 團隊之前的開發計劃, 2.0.x 即將在 08 年夏季正式發佈,並且在 2.0.x 中對 Spring 等 IoC 的集成進行了簡化,添加了基於 OGNL 的 JMX 遠程管理支持,使用基於 Java Annotation 的全新 API 大大簡化了狀態機編程,新的基於 Apache APR 的基礎 I/O 組件促進了進一步的效率提升(據官方評測, APR 的效率較之 Sun NIO 要高出約 10%)。由於這一系列的重大改進,使得 2.0.x 成爲十分令人期待的一個版本,無論是 Mina 新手還是老用戶,如果你對這個項目抱有興趣,便很有必要提前對這個版本進行一些瞭解。

首先讓我們對異步 I/O 做一些基本的瞭解。異步 I/O 模型大體上可以分爲兩種,反應式( Reactive )模型和前攝式( Proactive )模型:

傳統的 select / epoll / kqueue 模型,以及 Java NIO 模型,都是典型的反應式模型,即應用代碼對 I/O 描述符進行註冊,然後等待 I/O 事件。當某個或某些 I/O 描述符所對應的 I/O 設備上產生 I/O 事件(可讀、可寫、異常等)時,系統將發出通知,於是應用便有機會進行 I/O 操作並避免阻塞。由於在反應式模型中應用代碼需要根據相應的事件類型採取不同的動作,最常見的結構便是嵌套的 if {...} else {...}  或 switch ,並常常需要結合狀態機來完成複雜的邏輯。

前攝式模型則恰恰相反。在前攝式模型中,應用代碼主動地投遞異步操作而不管 I/O 設備當前是否可讀或可寫。投遞的異步 I/O 操作被系統接管,應用代碼也並不阻塞在該操作上,而是指定一個回調函數並繼續自己的應用邏輯。當該異步操作完成時,系統將發起通知並調用應用代碼指定的回調函數。在前攝式模型中,程序邏輯由各個回調函數串聯起來:異步操作 A 的回調發起異步操作 B ,B 的回調再發起異步操作 C ,以此往復。 Mina 便是一個前攝式的異步 I/O 框架。

前攝式模型相較於反射式模型往往更加難以編程。然而在具有原生異步 I/O 支持的操作系統中(例如支持 IO Completion Port 的 Win32 系統),採用前攝式模型往往可以取得比反應式模型更佳的效率。在沒有原生異步 I/O 支持的系統中,也可以使用傳統的反應式 API 對前攝式模型予以模擬。在現代的軟硬件系統中,使用 epoll 和 kqueue 的前攝式模型實現同樣可以輕鬆解決 C10K 問題。前攝式模型的一個顯著優勢是在實現複雜邏輯的時候不需要藉助於狀態機。因爲狀態機已經隱含在由回調串聯起來的異步操作鏈當中了。如果上述內容難以理解,可以參考 Boost.Asio ,這是一個相當優秀的跨平臺 C++ 前攝式 I/O 模型實現。

當然,對於程序員來說,還是直接看代碼來得最爲直接: Show me the code! 好,以下我們以官方文檔上的一個簡單的 TCP Time Server 爲示例對 Mina 的基本服務器編程予以剖析。該服務器的功能是監聽本地所有接口的 8150 端口,當有客戶端連接建立時便向客戶端以文本方式發送當前時間,並關閉連接。使用 Time Server 的目的在於

  • 邏輯簡單(更甚於 Unix Network Programming 中的 Echo Server ),易於實現
  • 只需實現服務器端代碼,客戶端可有普通 telnet 程序代替
  • 使用文本協議,可利用 Mina 內置的 TextLineCodecFactory 來作爲協議解析器

Time Server 源碼分析

以下便是完整的服務端代碼,稍後再逐行進行分析:

1 package test.mina.time.server;
2 
3 import java.io.IOException;
4 import java.net.InetSocketAddress;
5 import java.util.Date;
6 
7 import org.apache.commons.logging.Log;
8 import org.apache.commons.logging.LogFactory;
9 import org.apache.mina.common.IoAcceptor;
10 import org.apache.mina.common.IoHandlerAdapter;
11 import org.apache.mina.common.IoSession;
12 import org.apache.mina.filter.codec.ProtocolCodecFilter;
13 import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
14 import org.apache.mina.filter.logging.LoggingFilter;
15 import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
16 
17 public class TimeServer {
18 
19 static Log log = LogFactory.getLog( TimeServer.class );
20 
21 public static void main( final String[] args ) {
22 final IoAcceptor acceptor = new NioSocketAcceptor();
23 
24 acceptor.setHandler( new IoHandlerAdapter() {
25 
26 @Override
27 public void messageSent( final IoSession session,
28 final Object message )
29 throws Exception {
30 session.close();
31 }
32 
33 @Override
34 public void sessionOpened( final IoSession session )
35 throws Exception {
36 session.write( new Date() );
37 }
38 
39 } );
40 
41 acceptor.getFilterChain().addLast( "codec",
42 new ProtocolCodecFilter( new TextLineCodecFactory() ) );
43 acceptor.getFilterChain().addLast( "logging", new LoggingFilter() );
44 
45 try {
46 acceptor.bind( new InetSocketAddress( 8150 ) );
47 }
48 catch( final IOException e ) {
49 log.error( "Bind error: ", e );
50 }
51 }
52 
53 }


建立監聽器

22 final IoAcceptor acceptor = new NioSocketAcceptor();


在傳統服務端編程中,對於一個 TCP 服務器,我們需要先建立一個監聽套接字。在 Mina 中,我們創建的並不是一個監聽套接字,而是一個監聽套接字工廠,或者稱之爲“監聽器( acceptor )”。該概念映射到 Mina API 中,就是 IoAcceptor 接口及其各個實現類。

傳統的 BSD Socket API 中的監聽套接字以及其在 Java 中的對等物 java.net.ServerSocket 都是套接字工廠,其任務是在某個本地地址上進行監聽,並在有客戶端連接到來時產生一個與客戶端進行通信的套接字。而 Mina  的 IoAcceptor 作爲監聽套接字的工廠可以接受一個包含多個本地地址的集合, IoAcceptor 會自行鍼對這個集合中的每個本地地址分別創建監聽套接字並進行監聽,並且在監聽器銷燬時進行適當的資源清理。這樣便省去了我們建立自行維護多個監聽套接字的麻煩。

監聽套接字由 IoAcceptor 來接管,那麼服務端接受客戶端連接後產生的套接字又由誰接管呢?在 Mina 的術語中,一個 TCP 連接被稱作一個“會話( session )”,對應的 Mina API 是 IoSession 接口。每當服務端接受一個客戶端連接,便會創建出一個新的 IoSession 對象,通過該對象就可以對新建立的 TCP 連接進行各種操作。

在這個示例中,我們選用基於 Java NIO 的監聽器實現 NioSocketAcceptor 。之後,每當服務器接受一個客戶端連接, IoAccetpor 都會產生一個代表客戶端和服務器 TCP 連接的 IoSession 對象。

設置事件回調

24 acceptor.setHandler( new IoHandlerAdapter() {
25 
26 @Override
27 public void messageSent( final IoSession session,
28 final Object message )
29 throws Exception {
30 session.close();
31 }
32 
33 @Override
34 public void sessionOpened( final IoSession session )
35 throws Exception {
36 session.write( new Date() );
37 }
38 
39 } );


然後,我們要讓這個 IoAcceptor 創建出的 IoSession 對象知道在各種事件發生時應該如何進行處理。這實際上就是文章開頭所描述的反應式模型的應用層接口。只是反應式模型中的事件分支判斷部分被 Mina 封裝了起來,只暴露出包含了各種事件回調的 IoHandler 接口。可供處理的事件回調包括:

  • sessionCreated(IoSession)

IoSession 對象被創建時的回調,一般用於進行會話初始化操作。注意,與sessionOpened(IoSession) 回調不同, IoSession 對象的創建並不意味着對應的底層 TCP 連接的建立,而僅僅代表它的字面意思:一個 IoSession 對象被創建出來了。

  • sessionOpened(IoSession)

IoSession 對象被打開時的回調。在 TCP 中,該事件是在 TCP 連接建立時觸發的,一般可用於發起連接建立後的握手、認證等操作。

  • sessionClosed(IoSession)

IoSession 對象被關閉時的回調。在 TCP 中,該事件是在 TCP 連接斷開時觸發的。一般可用於會話資源的清理等操作。

  • sessionIdle(IoSession, IdleStatus)

IoSession 對象超時時的回調。當一個 IoSession 對象在指定的超時時長內沒有讀寫事件發生,就會觸發該事件,一般可用於通知服務器斷開長時間閒置的連接等處理。具體的超時設置可由 IoService.setWriteIdleTime(int) 、IoService.setReadIdleTime(int) 和 IoService.setBothIdleTime(int) 設置。

  • messageReceived(IoSession, Object)

當接收到 IoSession 對端發送的數據時的回調。

  • messageSent(IoSession, Object)

當發送給 IoSession 對端的數據發送成功時的回調。

  • exceptionCaught(IoSession, Throwable)

當會話過程中出現異常時的回調。通常用於錯誤處理。

然而,並非每個應用都對所有這些事件感興趣,要實現所有這些方法未免繁瑣,因此 Mina 提供了抽象類IoHandlerAdapter ,它實現了各個事件的默認處理——也就是不處理。因此,通常我們只需要繼承IoHandlerAdapter 並覆蓋需要處理的事件回調就可以了。在 Timer Server 示例中,回想一下我們設計的功能——當有客戶端連接建立時便向客戶端以文本方式發送當前時間,並關閉連接。爲了實現這個功能,我們需要實現兩個事件回調:首先,在 sessionOpened(IoSession) 事件回調中向對端發送當前日期,其次,在messageSent(IoSession, Object) 事件回調中關閉連接。以上便是第 24 至 39 行所創建的匿名類完成的事情。

需要注意的是 sessionOpened 方法中的這一行:


這裏的 IoSession.write(Object) 方法便是一個異步方法。對該方法的調用並不會阻塞,而是向 Mina 投遞了一個異步寫操作,並返回一個可用於對已投遞異步寫操作進行控制的 WriteFuture 對象。例如,通過調用WriteFuture 的 await 方法或 awaitUninterruptibly() ,我們就可以同步等待該異步操作的完成。

配置過濾器鏈

41 acceptor.getFilterChain().addLast( "codec",
42 new ProtocolCodecFilter( new TextLineCodecFactory() ) );
43 acceptor.getFilterChain().addLast( "logging", new LoggingFilter() );


接下來,是對過濾器鏈的配置。過濾器鏈可以被當作一條兩端分別連接服務器和客戶端的管道,管道中首尾相接地裝上零個或多個過濾器。每個過濾器都可對通過的數據進行任意的操作,包括增加、刪除、更新、類型轉換等。先裝上的過濾器更靠近遠程端點(客戶端),後裝上的更靠近本地端點(服務器)。 41 至 43 行先後向IoAcceptor 的過濾器鏈中添加了兩個過濾器,分別名爲“ codec ”和“ logging ”。後者很好理解,其作用就是對IoSession 對象上發生的各種事件進行日誌記錄。而前者就要多費一些口舌來解釋了。

協議編解碼器

我們知道, TCP 本身只是一個可靠字節流協議, TCP 層面上的二進制數據流不具備任何的邊界和結構,只是純粹的字節流。而在應用層面上,我們在不同通訊節點間處理和交換的——也就是應用構建人員直接關心的——是應用域對象(application domain object)。這就產生了矛盾:應用構建人員需要具有特定類型的域對象來適應具體問題域的需求,而在 TCP 層面,我們手裏只有一股股死板的二進制數據流。爲了解決這種矛盾,爲底層的二進制流賦予個性,向上層應用提供鮮活的域對象,我們就需要將二者互相轉換。於是就引入了一對自古以來就繁瑣乏味的工作:打包和拆包。

 

打包,就是將域對象轉換爲二進制數據包,各個數據包首尾相接,形成二進制數據流;拆包,就是從無包邊界的二進制數據流中將數據包一個個拆分出來並轉換成相應的域對象。由於 TCP 沒有包邊界,相對於打包而言,拆包的工作尤其乏味和易錯。爲了對這些操作進行適度的封裝以便重用, Mina 提供了一個有力的工具——協議編解碼器( protocol codec )。簡而言之,協議編解碼器的職責,就是打包和拆包。針對一種類型的域對象,我們需要編寫一個編碼器( encoder )和一個解碼器( decoder ),分別用於打包和拆包。將這對編碼器和解碼器通過一個 ProtocolCodecFactory 包裝起來,就組成了一個協議編解碼器。最後,再用一個ProtocolCodecFilter 將這個協議編解碼器包裝成一個過濾器,就可以將之插入過濾器鏈中,來實現二進制 TCP 數據流與應用域對象的自動轉換了。

解釋完了原理,我們再回到 Time Server 的示例中來。我們的 Time Server 很簡單,但是麻雀雖小五臟俱全,這裏也同樣存在着打包和拆包的問題。首先我們來確定一下域對象。仔細觀察一下 24 至 39 行中構造的IoHandlerAdapter 匿名子類,我們就可以發現,在整個客戶端服務器會話過程中,除了 TCP 建立和斷開過程中的握手消息以外,唯一的數據 I/O 就是在 sessionOpened(IoSession) 事件回調中由服務器向客戶端發送的 Date 對象。但是,爲了利用 Mina 本身提供的 TextLineCodecFactory ,我們並不採用 Date 作爲域對象類型,而採用 String ,藉助於 Date.toString() 方法,這個選擇並不會導致什麼問題。TextLineCodecFactory 提供了一套面向字符串文本行的協議編解碼器。它將每個傳入編碼器的字符串作爲單獨的一行文本打包進 TCP 流,並通過解碼器將 TCP 流中的文本以行尾單位轉換爲 String 對象。這樣,就方便地實現了 Time Server 的打包和拆包。

至此,我們已經完成了 Time Server 的大部分編碼工作:我們通過 IoAcceptor 創建了監聽套接字,爲後續將要產生的 IoSession 對象設置了相應的事件回調處理,還配置了過濾器鏈,並在過濾器鏈中嵌入了TextLineCodecFactory 協議編解碼器。圖 1 描述出了 Time Server 的結構與數據流向:

圖 1. Apache Mina 2.0.x Time Server

綁定監聽套接字
45 try {
46 acceptor.bind( new InetSocketAddress( 8150 ) );
47 }
48 catch( final IOException e ) {
49 log.error( "Bind error: ", e );
50 }

好了,萬事俱備只欠東風。最後,我們只需要打開監聽端口,就萬事大吉了。上文中我們提到過可以爲IoAcceptor 設置多個監聽地址,但這裏我們只需要監聽通配地址 0.0.0.0 上的 8150 端口就可以了,因此直接在IoAcceptor.bind(SocketAddress) 中指定該監聽地址即可。

IoAcceptor.bind(SocketAddress) 並不僅僅是傳統 BSD Socket API 中的 socket / bind / listen / accept 經典操作序列中的 bind ,而是集四者於一身,以達到簡化編程的目的。

Run!

編譯後,一個熱騰騰的 Time Server 就新鮮出爐了!讓我們來跑跑看。首先配置一下日誌策略,將日誌輸出指向標準輸出, log4j.xml 內容如下:

1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd" >
3 <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
4 <appender name="stdout" class="org.apache.log4j.ConsoleAppender">
5 <layout class="org.apache.log4j.PatternLayout">
6 <param name="ConversionPattern" value="%p - %c{1} - %m%n" />
7 </layout>
8 </appender>
9 <root>
10 <level value="info" />
11 <appender-ref ref="stdout" />
12 </root>
13 </log4j:configuration>

現在運行服務器,再打開終端,用 telnet 連接服務器:

$ telnet localhost 8150

一切正常的話,將獲得類似如下的輸出:

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Tue Jan 15 15:50:38 CST 2008
Connection closed by foreign host.

標註出的這行,就是服務器發送過來的當前服務器時間。同時,服務器端將在標準輸出上有類似如下的日誌輸出:

INFO - LoggingFilter - [/127.0.0.1:3080] CREATED
INFO - LoggingFilter - [/127.0.0.1:3080] OPENED
INFO - LoggingFilter - [/127.0.0.1:3080] SENT: Tue Jan 15 15:50:38 CST 2008
INFO - LoggingFilter - [/127.0.0.1:3080] CLOSED

可以看到標註出的與客戶端輸出對應的日誌輸出行。

我們還可以做一個小小的改動來詳細地看一下協議編解碼器的工作過程:將兩個過濾器的添加順序對掉一下。對掉之前,日誌過濾器在協議編解碼過濾器之上(見圖 1 ),因此,在日誌中輸出的是 Date 對象,更具體的說,是 Date.toString() 的結果。對掉之後,日誌過濾器位於協議編解碼過濾器之下,我們便可以看到由編碼器編碼後的日期字符串的字節序列(對應的時間字符串是“ Tue Jan 15 16:34:36 CST 2008 ”):

INFO - LoggingFilter - [/192.168.80.180:60144] CREATED
INFO - LoggingFilter - [/192.168.80.180:60144] OPENED
INFO - LoggingFilter - [/192.168.80.180:60144] SENT: HeapBuffer[pos=0 lim=29 cap=32: 54 75 65 20 4A 61 6E 20 31 35 20 31 36 3A 33 34...]
INFO - LoggingFilter - [/192.168.80.180:60144] SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
INFO - LoggingFilter - [/192.168.80.180:60144] CLOSED

小結

區區 53 行代碼,我們便用 Mina 實現了一個全功能的併發網絡時間服務器。值得注意的是,這並不是一個如同 UNP 中的第一個 Echo Server 示例那樣的以同步方式串行處理客戶端請求的迭代式服務器,而是一個基於 Java NIO 多路複用機制的高性能異步併發服務器。

Time Server 在併發策略上採用的是默認的單線程策略。我們可以通過在過濾器鏈中插入一個 ExecutorFilter來啓用線程池來完成 IoHandler 中定義的事件回調操作。當在事件處理過程中存在文件 I/O 或數據庫操作等耗時較長的同步阻塞操作時,採用多線程的併發策略可以獲取更高的併發度。在 Mina 1.1.x 中,除了ExecutorFilter 的方式,每個 IoService (各種 IoAcceptor 和以後將要介紹的 IoConnector 都是IoService 的一種)具備一個 ThreadModel 域,可以使用特定的線程模型來制定併發策略。然而這種方式增加了編程的複雜度,因此在 2.0.x 中被去除了。

在後續的文章中,還將對 Mina 的客戶端編程、 SSL/TLS 編程以及 Spring 、 JMX 集成等內容進行介紹。



文章轉載自:http://blog.csdn.net/dankes/article/details/2525759

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章