aphach mina

http://itindex.net/detail/36989-mina-%E7%BD%91%E7%BB%9C-%E9%80%9A%E4%BF%A1
http://blog.csdn.net/w13770269691/article/details/8614584
http://www.open-open.com/lib/view/open1328680465093.html
http://blog.csdn.net/quyongjin/article/details/9704131

    Apache Mina  是一個開發高性能和高伸縮性網絡應用程序的框架,提供了事件驅動、異步(Mina 的異步IO 默認使用的是JAVA NIO 作爲底層支持)操作的編程模型。官網:http://mina.apache.org/

Mina 的執行流程如下:

(1.) IoService:這個接口在一個線程上負責套接字的建立,擁有自己的Selector,監聽是否有連接被建立。
(2.) IoProcessor:這個接口在另一個線程上,負責檢查是否有數據在通道上讀寫,也就是說它也擁有自己的Selector,這是與我們使用JAVA NIO 編碼時的一個不同之處,通常在JAVA NIO 編碼中,我們都是使用一個Selector,也就是不區分IoService與IoProcessor 兩個功能接口。另外,IoProcessor 負責調用註冊在IoService 上的過濾器,並在過濾器鏈之後調用IoHandler。
(3.) IoFilter:這個接口定義一組攔截器,這些攔截器可以包括日誌輸出、黑名單過濾、數據的編碼(write 方向)與解碼(read 方向)等功能,其中數據的encode 與decode是最爲重要的、也是你在使用Mina 時最主要關注的地方。
(4.) IoHandler:這個接口負責編寫業務邏輯,也就是接收、發送數據的地方。
  1. 簡單的TCPServer:
    (1.) 第一步:編寫IoService
IoAcceptor acceptor=new NioSocketAcceptor();    
acceptor.getSessionConfig().setReadBufferSize(2048);    
acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE,10);    
acceptor.bind(new InetSocketAddress(9123)); 

這段代碼我們初始化了服務端的TCP/IP 的基於NIO 的套接字,然後調用IoSessionConfig設置讀取數據的緩衝區大小、讀寫通道均在10 秒內無任何操作就進入空閒狀態。
(2.) 第二步:編寫過濾器
這裏我們處理最簡單的字符串傳輸,Mina 已經爲我們提供了TextLineCodecFactory 編解碼器工廠來對字符串進行編解碼處理。

acceptor.getFilterChain().addLast("codec",  new ProtocolCodecFilter(new TextLineCodecFactory( Charset.forName("UTF-8"),LineDelimeter.WINDOWS.getValue(), LineDelimiter. WINDOWS.getValue()))    
);  

這段代碼要在acceptor.bind()方法之前執行,因爲綁定套接字(socket)之後就不能再做這些準備工作了。我們傳輸的以換行符爲標識的數據,所以使用了Mina 自帶的換行符編解碼器工廠。

(3.) 第三步:編寫IoHandler

這裏我們只是簡單的打印Client 傳說過來的數據。

public class MyIoHandler extends IoHandlerAdapter {    
// 這裏我們使用的SLF4J作爲日誌門面,至於爲什麼在後面說明。    
private final static Logger log = LoggerFactory    
.getLogger(MyIoHandler.class);    
@Override    
public void messageReceived(IoSession session, Object message)    
throws Exception {    
String str = message.toString();    
log.info("The message received is [" + str + "]");    
if (str.endsWith("quit")) {    
    session.close(true);    
    return;    
        }    
    }    
}    

然後我們把這個IoHandler 註冊到IoService:

acceptor.setHandler(new MyIoHandler());  

當然這段代碼也要在acceptor.bind()方法之前執行。然後我們運行MyServer 中的main 方法,你可以看到控制檯一直處於阻塞狀態,此時,我們用telnet 127.0.0.1 9123 訪問,然後輸入一些內容,當按下回車鍵,你會發現數據在Server 端被輸出,但要注意不要輸入中文,因爲Windows 的命令行窗口不會對傳輸的數據進行UTF-8 編碼。當輸入quit 結尾的字符串時,連接被斷開。這裏注意你如果使用的操作系統,或者使用的Telnet 軟件的換行符是什麼,如果不清楚,可以刪掉第二步中的兩個紅色的參數,使用TextLineCodec 內部的自動識別機制。
2. 簡單的TCPClient:
這裏我們實現Mina 中的TCPClient,因爲前面說過無論是Server 端還是Client 端,在Mina中的執行流程都是一樣的。唯一不同的就是IoService 的Client 端實現是IoConnector
(1.) 第一步:編寫IoClient並註冊過濾器

public static void main(String[] args) {

        IoConnector connector = new NioSocketConnector();
        connector.setConnectTimeoutMillis(30000);
        connector.getFilterChain().addLast(
                "codec",
                new ProtocolCodecFilter(new TextLineCodecFactory(Charset
                        .forName("UTF-8"), LineDelimiter.WINDOWS.getValue(),
                        LineDelimiter.WINDOWS.getValue())));
        connector.connect(new InetSocketAddress("localhost", 9123));

    } 

(2.) 第三步:編寫IoHandler

public class IoHandler extends IoHandlerAdapter {
    private final static Logger LOGGER = LoggerFactory
            .getLogger(IoHandler.class);
    private final String values;

    public IoHandler(String values) {
        this.values = values;
    }

    @Override
    public void sessionOpened(IoSession session) {
        session.write(values);
    }
}

註冊IoHandler:

connector.setHandler(new ClientHandler("你好!\r\n 已經連接!")); 

這裏寫圖片描述
3. 介紹Mina的TCP的主要接口:

通過上面的兩個示例,你應該對Mina 如何編寫TCP/IP 協議棧的網絡通信有了一些感性的認識。
(1.)IoService:

這個接口是服務端IoAcceptor、客戶端IoConnector 的抽象,提供IO 服務和管理IoSession的功能,它有如下幾個常用的方法:
A. TransportMetadata getTransportMetadata():
這個方法獲取傳輸方式的元數據描述信息,也就是底層到底基於什麼的實現,譬如:nio、apr 等。
B. void addListener(IoServiceListener listener):
這個方法可以爲IoService 增加一個監聽器,用於監聽IoService 的創建、活動、失效、空閒、銷燬,具體可以參考IoServiceListener 接口中的方法,這爲你參與IoService 的生命週期提供了機會。
C. void removeListener(IoServiceListener listener):
這個方法用於移除上面的方法添加的監聽器。
D. void setHandler(IoHandler handler):
這個方法用於向IoService 註冊IoHandler,同時有getHandler()方法獲取Handler。
E. Map<Long,IoSession> getManagedSessions():這個方法獲取IoService 上管理的所有IoSession,Map 的key 是IoSession 的id。
F. IoSessionConfig getSessionConfig():
這個方法用於獲取IoSession 的配置對象,通過IoSessionConfig 對象可以設置Socket 連接的一些選項。

(2.)IoAcceptor:
這個接口是TCPServer 的接口,主要增加了void bind()監聽端口、void unbind()解除對套接字的監聽等方法。這裏與傳統的JAVA 中的ServerSocket 不同的是IoAcceptor 可以多次調用bind()方法(或者在一個方法中傳入多個SocketAddress 參數)同時監聽多個端口

(3.)IoConnector:
這個接口是TCPClient 的接口, 主要增加了ConnectFuture connect(SocketAddressremoteAddress,SocketAddress localAddress)方法,用於與Server 端建立連接,第二個參數如果不傳遞則使用本地的一個隨機端口訪問Server 端。這個方法是異步執行的,同樣的,也可以同時連接多個服務端。

(4.)IoSession:

這個接口用於表示Server 端與Client 端的連接,IoAcceptor.accept()的時候返回實例。
這個接口有如下常用的方法:
A. WriteFuture write(Object message):
這個方法用於寫數據,該操作是異步的。
B. CloseFuture close(boolean immediately):
這個方法用於關閉IoSession,該操作也是異步的,參數指定true 表示立即關閉,否則就在所有的寫操作都flush 之後再關閉。
C. Object setAttribute(Object key,Object value):
這個方法用於給我們向會話中添加一些屬性,這樣可以在會話過程中都可以使用,類似於HttpSession 的setAttrbute()方法。IoSession 內部使用同步的HashMap 存儲你添加的自
定義屬性。
D. SocketAddress getRemoteAddress():
這個方法獲取遠端連接的套接字地址。
E. void suspendWrite():
這個方法用於掛起寫操作,那麼有void resumeWrite()方法與之配對。對於read()方法同樣適用。
F. ReadFuture read():
這個方法用於讀取數據, 但默認是不能使用的, 你需要調用IoSessionConfig 的setUseReadOperation(true)纔可以使用這個異步讀取的方法。一般我們不會用到這個方法,因爲這個方法的內部實現是將數據保存到一個BlockingQueue,假如是Server 端,因爲大量的Client 端發送的數據在Server 端都這麼讀取,那麼可能會導致內存泄漏,但對於Client,可能有的時候會比較便利。
G. IoService getService():
這個方法返回與當前會話對象關聯的IoService 實例。
關於TCP連接的關閉:
無論在客戶端還是服務端,IoSession 都用於表示底層的一個TCP 連接,那麼你會發現無論是Server 端還是Client 端的IoSession 調用close()方法之後,TCP 連接雖然顯示關閉, 但主線程仍然在運行,也就是JVM 並未退出,這是因爲IoSession 的close()僅僅是關閉了TCP的連接通道,並沒有關閉Server 端、Client 端的程序。你需要調用IoService 的dispose()方法停止Server 端、Client 端。

(5.)IoSessionConfig:

這個接口用於指定此次會話的配置,它有如下常用的方法:
A. void setReadBufferSize(int size):
這個方法設置讀取緩衝的字節數,但一般不需要調用這個方法,因爲IoProcessor 會自動調整緩衝的大小。你可以調用setMinReadBufferSize()、setMaxReadBufferSize()方法,這樣無論IoProcessor 無論如何自動調整,都會在你指定的區間。
B. void setIdleTime(IdleStatus status,int idleTime):
這個方法設置關聯在通道上的讀、寫或者是讀寫事件在指定時間內未發生,該通道就進入空閒狀態。一旦調用這個方法,則每隔idleTime 都會回調過濾器、IoHandler 中的sessionIdle()方法。
C. void setWriteTimeout(int time):
這個方法設置寫操作的超時時間。
D. void setUseReadOperation(boolean useReadOperation):
這個方法設置IoSession 的read()方法是否可用,默認是false

(6.)IoHandler:

這個接口是你編寫業務邏輯的地方,從上面的示例代碼可以看出,讀取數據、發送數據基本都在這個接口總完成,這個實例是綁定到IoService 上的,有且只有一個實例(沒有給一個IoService 注入一個IoHandler 實例會拋出異常)。它有如下幾個方法:
A. void sessionCreated(IoSession session):
這個方法當一個Session 對象被創建的時候被調用。對於TCP 連接來說,連接被接受的時候調用,但要注意此時TCP 連接並未建立,此方法僅代表字面含義,也就是連接的對象IoSession 被創建完畢的時候,回調這個方法。對於UDP 來說,當有數據包收到的時候回調這個方法,因爲UDP 是無連接的。
B. void sessionOpened(IoSession session):
這個方法在連接被打開時調用,它總是在sessionCreated()方法之後被調用。對於TCP 來說,它是在連接被建立之後調用,你可以在這裏執行一些認證操作、發送數據等。對於UDP 來說,這個方法與sessionCreated()沒什麼區別,但是緊跟其後執行。如果你每隔一段時間,發送一些數據,那麼sessionCreated()方法只會在第一次調用,但是sessionOpened()方法每次都會調用。
C. void sessionClosed(IoSession session) :
對於TCP 來說,連接被關閉時,調用這個方法。對於UDP 來說,IoSession 的close()方法被調用時纔會毀掉這個方法。
D. void sessionIdle(IoSession session, IdleStatus status) :
這個方法在IoSession 的通道進入空閒狀態時調用,對於UDP 協議來說,這個方法始終不會被調用。
E. void exceptionCaught(IoSession session, Throwable cause) :
這個方法在你的程序、Mina 自身出現異常時回調,一般這裏是關閉IoSession。

F. void messageReceived(IoSession session, Object message) :
接收到消息時調用的方法,也就是用於接收消息的方法,一般情況下,message 是一個IoBuffer 類,如果你使用了協議編解碼器,那麼可以強制轉換爲你需要的類型。通常我們都是會使用協議編解碼器的, 就像上面的例子, 因爲協議編解碼器是
TextLineCodecFactory,所以我們可以強制轉message 爲String 類型。
G. void messageSent(IoSession session, Object message) :
當發送消息成功時調用這個方法,注意這裏的措辭,發送成功之後,也就是說發送消息是不能用這個方法的。
發送消息的時機:
發送消息應該在sessionOpened()messageReceived()方法中調用IoSession.write()方法完成。因爲在sessionOpened()方法中,TCP 連接已經真正打開,同樣的在messageReceived()方法TCP 連接也是打開狀態,只不過兩者的時機不同。sessionOpened()方法是在TCP 連接建立之後,接收到數據之前發送;messageReceived()方法是在接收到數據之後發送,你可以完成依據收到的內容是什麼樣子,決定發送什麼樣的數據。因爲這個接口中的方法太多,因此通常使用適配器模式IoHandlerAdapter,覆蓋你所感興趣的方法即可。

(7.)IoBuffer:

這個接口是對JAVA NIO 的ByteBuffer 的封裝,這主要是因爲ByteBuffer 只提供了對基本數據類型的讀寫操作,沒有提供對字符串等對象類型的讀寫方法,使用起來更爲方便,另外,ByteBuffer 是定長的,如果想要可變,將很麻煩。IoBuffer 的可變長度的實現類似於StringBuffer。IoBuffer 與ByteBuffer 一樣,都是非線程安全的。本節的一些內容如果不清楚,可以參考java.nio.ByteBuffer 接口。這個接口有如下常用的方法:
A. static IoBuffer allocate(int capacity,boolean useDirectBuffer):
這個方法內部通過SimpleBufferAllocator 創建一個實例,第一個參數指定初始化容量,第二個參數指定使用直接緩衝區還是JAVA 內存堆的緩存區,默認爲false。
B. void free():
釋放緩衝區,以便被一些IoBufferAllocator 的實現重用,一般沒有必要調用這個方法,除非你想提升性能(但可能未必效果明顯)。
C. IoBuffer setAutoExpand(boolean autoExpand):
這個方法設置IoBuffer 爲自動擴展容量,也就是前面所說的長度可變,那麼可以看出長度可變這個特性默認是不開啓的。
D. IoBuffer setAutoShrink(boolean autoShrink):
這個方法設置IoBuffer 爲自動收縮,這樣在compact()方法調用之後,可以裁減掉一些沒有使用的空間。如果這個方法沒有被調用或者設置爲false,你也可以通過調用shrink()方法手動收縮空間。

E. IoBuffer order(ByteOrder bo):
這個方法設置是Big Endian 還是Little Endian,JAVA 中默認是Big Endian,C++和其他語言一般是Little Endian。
F. IoBuffer asReadOnlyBuffer():
這個方法設置IoBuffer 爲只讀的。
G. Boolean prefixedDataAvailable(int prefixLength,int maxDataLength):
這個方法用於數據的最開始的124 個字節表示的是數據的長度的情況,

prefixLentgh表示這段數據的前幾個字節(只能是124 的其中一個),代表的是這段數據的長度,
maxDataLength 表示最多要讀取的字節數。返回結果依賴於等式
remaining()-prefixLength>=maxDataLength,也就是總的數據-表示長度的字節,剩下的字節數要比打算讀取的字節數大或者相等。
H. String getPrefixedString(int prefixLength,CharsetDecoder decoder):
如果上面的方法返回true,那麼這個方法將開始讀取表示長度的字節之後的數據,注意要保持這兩個方法的prefixLength 的值是一樣的。
G、H 兩個方法在後面講到的PrefixedStringDecoder 中的內部實現使用。
IoBuffer 剩餘的方法與ByteBuffer 都是差不多的,額外增加了一些便利的操作方法,例如:
IoBuffer putString(String value,CharsetEncoder encoder)可以方便的以指定的編碼方式存儲字符串、InputStream asInputStream()方法從IoBuffer 剩餘的未讀的數據中轉爲輸入流等。

(8.)IoFuture:

在Mina 的很多操作中,你會看到返回值是XXXFuture,實際上他們都是IoFuture 的子類,看到這樣的返回值,這個方法就說明是異步執行的,主要的子類有ConnectFuture、CloseFuture 、ReadFuture 、WriteFuture 。這個接口的大部分操作都和
java.util.concurrent.Future 接口是類似的,譬如:await()awaitUninterruptibly()等,一般我們常用awaitUninterruptibly()方法可以等待異步執行的結果返回。這個接口有如下常用的方法:
A. IoFuture addListener(IoFutureListener<?> listener):
這個方法用於添加一個監聽器, 在異步執行的結果返回時監聽器中的回調方法operationComplete(IoFuture future),也就是說,這是替代awaitUninterruptibly()方法另一種等待異步執行結果的方法,它的好處是不會產生阻塞。
B. IoFuture removeListener(IoFutureListener<?> listener):
這個方法用於移除指定的監聽器。
C. IoSession getSession():
這個方法返回當前的IoSession。舉個例子,我們在客戶端調用connect()方法訪問Server 端的時候,實際上這就是一個異步執行的方法,也就是調用connect()方法之後立即返回,執行下面的代碼,而不管是否連接成功。那麼如果我想在連接成功之後執行一些事情(譬如:獲取連接成功後的IoSession對象),該怎麼辦呢?按照上面的說明,你有如下兩種辦法:

第一種

ConnectFuture future = connector.connect(new InetSocketAddress(    
HOSTNAME, PORT));    
// 等待是否連接成功,相當於是轉異步執行爲同步執行。    
future.awaitUninterruptibly();    
// 連接成功後獲取會話對象。如果沒有上面的等待,由於connect()方法是異步的,session    
可能會無法獲取。    
session = future.getSession(); 

第二種

ConnectFuture future = connector.connect(new InetSocketAddress(
                HOSTNAME, PORT));
        future.addListener(new IoFutureListener<ConnectFuture>() {
            @Override
            public void operationComplete(ConnectFuture future) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                IoSession session = future.getSession();
                System.out.println("++++++++++++++++++++++++++++");
            }
        });
        System.out.println("*************");

這裏寫圖片描述

爲了更好的看清楚使用監聽器是異步的,而不是像awaitUninterruptibly()那樣會阻塞主線程的執行,我們在回調方法中暫停5 秒鐘,然後輸出+++,在最後輸出。我們執行代碼之後,你會發現首先輸出(這證明了監聽器是異步執行的),然後IoSession 對象Created,系統暫停5 秒,然後輸出+++,最後IoSession 對象Opened,也就是TCP 連接建立。

4.日誌配置:
前面的示例代碼中提到了使用SLF4J 作爲日誌門面,這是因爲Mina 內部使用的就是SLF4J,你也使用SLF4J 可以與之保持一致性。Mina 如果想啓用日誌跟蹤Mina 的運行細節,你可以配置LoggingFilter 過濾器,這樣你可
以看到Session 建立、打開、空閒等一系列細節在日誌中輸出,默認SJF4J 是按照DEBUG級別輸出跟蹤信息的,如果你想給某一類別的Mina 運行信息輸出指定日誌輸出級別,可以調用LoggingFilter 的setXXXLogLevel(LogLevel.XXX)。

例:

[java] view plain copy
LoggingFilter lf = new LoggingFilter();    
lf.setSessionOpenedLogLevel(LogLevel.ERROR);    
acceptor.getFilterChain().addLast("logger", lf);    

這裏IoSession 被打開的跟蹤信息將以ERROR 級別輸出到日誌。

5.過濾器:

前面我們看到了LoggingFilter、ProtocolCodecFilter 兩個過濾器,一個負責日誌輸出,一個負責數據的編解碼,通過最前面的Mina 執行流程圖,在IoProcessor 與IoHandler 之間可以有很多的過濾器,這種設計方式爲你提供可插拔似的擴展功能提供了非常便利的方式,目前的Apache CXF、Apache Struts2 中的攔截器也都是一樣的設計思路。Mina 中的IoFilter 是單例的,這與CXF、Apache Struts2 沒什麼區別。IoService 實例上會綁定一個DefaultIoFilterChainBuilder 實例,DefaultIoFilterChainBuilder 會把使用內部的EntryImpl 類把所有的過濾器按照順序連在一起,組成一個過濾器鏈。
DefaultIoFilterChainBuilder 類如下常用的方法:
A. void addFirst(String name,IoFilter filter):
這個方法把過濾器添加到過濾器鏈的頭部,頭部就是IoProcessor 之後的第一個過濾器。同樣的addLast()方法把過濾器添加到過濾器鏈的尾部。
B. void addBefore(String baseName,String name,IoFilter filter):
這個方法將過濾器添加到baseName 指定的過濾器的前面,同樣的addAfter()方法把過濾器添加到baseName 指定的過濾器的後面。這裏要注意無論是那種添加方法,每個過濾器的名字(參數name)必須是唯一的。
C. IoFilter remove(Stirng name):
這個方法移除指定名稱的過濾器,你也可以調用另一個重載的remove()方法,指定要移除的IoFilter 的類型。
D. List<Entry> getAll():
這個方法返回當前IoService 上註冊的所有過濾器。默認情況下,過濾器鏈中是空的,也就是getAll()方法返回長度爲0 的List,但實際Mina內部有兩個隱藏的過濾器:HeadFilter、TailFilter,分別在List 的最開始和最末端,很明顯,TailFilter 在最末端是爲了調用過濾器鏈之後,調用IoHandler。但這兩個過濾器對你來說是透明的,可以忽略它們的存在。編寫一個過濾器很簡單,你需要實現IoFilter 接口,如果你只關注某幾個方法,可以繼承IoFilterAdapter 適配器類。IoFilter 接口中主要包含兩類方法,一類是與IoHandler 中的方法名一致的方法,相當於攔截IoHandler 中的方法,另一類是IoFilter 的生命週期回調方法,這些回調方法的執行順序和解釋如下所示:

(1.)init()在首次添加到鏈中的時候被調用,但你必須將這個IoFilter 用
ReferenceCountingFilter 包裝起來,否則init()方法永遠不會被調用。
(2.)onPreAdd()在調用添加到鏈中的方法時被調用,但此時還未真正的加入到鏈。
(3.)onPostAdd()在調用添加到鏈中的方法後被調,如果在這個方法中有異常拋出,則過濾器會立即被移除,同時destroy()方法也會被調用(前提是使用ReferenceCountingFilter包裝)。
(4.)onPreRemove()在從鏈中移除之前調用。
(5.)onPostRemove()在從鏈中移除之後調用。
(6.)destory()在從鏈中移除時被調用,使用方法與init()要求相同。
無論是哪個方法,要注意必須在實現時調用參數nextFilter 的同名方法,否則,過濾器鏈的執行將被中斷,IoHandler 中的同名方法一樣也不會被執行,這就相當於Servlet 中的Filter 必須調用filterChain.doFilter(request,response)才能繼續前進是一樣的道理。

示例:

public class MyIoFilter implements IoFilter {
    @Override
    public void destroy() throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%�stroy");
    }

    @Override
    public void exceptionCaught(NextFilter nextFilter, IoSession session,
            Throwable cause) throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%exceptionCaught");
        nextFilter.exceptionCaught(session, cause);
    }

    @Override
    public void filterClose(NextFilter nextFilter, IoSession session)
            throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%filterClose");
        nextFilter.filterClose(session);
    }

    @Override
    public void filterWrite(NextFilter nextFilter, IoSession session,
            WriteRequest writeRequest) throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%filterWrite");
        nextFilter.filterWrite(session, writeRequest);
    }

    @Override
    public void init() throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%init");
    }

    @Override
    public void messageReceived(NextFilter nextFilter, IoSession session,
            Object message) throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%messageReceived");
        nextFilter.messageReceived(session, message);
    }

    @Override
    public void messageSent(NextFilter nextFilter, IoSession session,
            WriteRequest writeRequest) throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%messageSent");
        nextFilter.messageSent(session, writeRequest);
    }

    @Override
    public void onPostAdd(IoFilterChain parent, String name,
            NextFilter nextFilter) throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%onPostAdd");
    }

    @Override
    public void onPostRemove(IoFilterChain parent, String name,
            NextFilter nextFilter) throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%onPostRemove");
    }

    @Override
    public void onPreAdd(IoFilterChain parent, String name,
            NextFilter nextFilter) throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%onPreAdd");
    }

    @Override
    public void onPreRemove(IoFilterChain parent, String name,
            NextFilter nextFilter) throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%onPreRemove");
    }

    @Override
    public void sessionClosed(NextFilter nextFilter, IoSession session)
            throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%sessionClosed");
        nextFilter.sessionClosed(session);
    }

    @Override
    public void sessionCreated(NextFilter nextFilter, IoSession session)
            throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%sessionCreated");
        nextFilter.sessionCreated(session);
    }

    @Override
    public void sessionIdle(NextFilter nextFilter, IoSession session,
            IdleStatus status) throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%sessionIdle");
        nextFilter.sessionIdle(session, status);
    }

    @Override
    public void sessionOpened(NextFilter nextFilter, IoSession session)
            throws Exception {
        System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%sessionOpened");
        nextFilter.sessionOpened(session);
    }
}

我們將這個攔截器註冊到上面的TCPServer 的IoAcceptor 的過濾器鏈中的最後一個:

acceptor.getFilterChain().addLast("myIoFilter",    
new ReferenceCountingFilter(new MyIoFilter())); 

這裏寫圖片描述

這裏我們將MyIoFilter 用ReferenceCountingFilter 包裝起來,這樣你可以看到init()destroy()方法調用。我們啓動客戶端訪問,然後關閉客戶端,你會看到執行順序如下所示:
init onPreAdd onPostAdd sessionCreated sessionOpened messageReceived filterClose sessionClosed onPreRemove onPostRemove destroy。
IoHandler 的對應方法會跟在上面的對應方法之後執行,這也就是說從橫向(單獨的看一個過濾器中的所有方法的執行順序)上看,每個過濾器的執行順序是上面所示的順序;從縱向(方法鏈的調用)上看,如果有filter1、filter2 兩個過濾器,sessionCreated()方法的執行順序如下所示:

filter1-sessionCreated filter2-sessionCreated IoHandler-sessionCreated。
這裏你要注意init、onPreAdd、onPostAdd 三個方法並不是在Server 啓動時調用的,而是IoSession 對象創建之前調用的,也就是說IoFilterChain.addXXX()方法僅僅負責初始化過濾器並註冊過濾器,但並不調用任何方法,包括init()初始化方法也是在IoProcessor 開始工作的時候被調用。IoFilter 是單例的,那麼init()方法是否只被執行一次呢?這個是不一定的,因爲IoFilter是被IoProcessor 調用的,而每個IoService 通常是關聯多個IoProcessor,所以IoFilter的init()方法是在每個IoProcessor 線程上只執行一次。關於Mina 的線程問題,我們後面會詳細討論,這裏你只需要清楚,init()destroy()的調用次數與IoProceesor 的個數有關,假如一個IoService 關聯了3 個IoProcessor,有五個併發的客戶端請求,那麼你會看到三次init()方法被調用,以後將不再會調用。Mina中自帶的過濾器:
過濾器 說明
BlacklistFilter 設置一些IP 地址爲黑名單,不允許訪問。
BufferedWriteFilter 設置輸出時像BufferedOutputStream 一樣進行緩衝。
CompressionFilter 設置在輸入、輸出流時啓用JZlib 壓縮。
ConnectionThrottleFilter 這個過濾器指定同一個IP 地址(不含端口號)上的請求在多長的毫秒值內可以有一個請求,如果小於指定的時間間隔就有連續兩個請求,那麼第二個請求將被忽略(IoSession.close())。正如Throttle 的名字一樣,調節訪問的頻率這個過濾器最好放在過濾器鏈的前面。
FileRegionWriteFilter 如果你想使用File 對象進行輸出,請使用這個過濾器。要注意,你需要使用WriteFuture 或者在
messageSent() 方法中關閉File 所關聯的FileChannel 通道。
StreamWriteFilter 如果你想使用InputStream 對象進行輸出,請使用這個過濾器。要注意,你需要使用WriteFuture或者在messageSent()方法中關閉File 所關聯的
FileChannel 通道。NoopFilter 這個過濾器什麼也不做,如果你想測試過濾器鏈是否起作用,可以用它來測試。
ProfilerTimerFilter 這個過濾器用於檢測每個事件方法執行的時間,所以最好放在過濾器鏈的前面。
ProxyFilter 這個過濾器在客戶端使用ProxyConnector 作爲實現時,會自動加入到過濾器鏈中,用於完成代理功能。
RequestResponseFilter 暫不知曉。

SessionAttributeInitializingFilter 這個過濾器在IoSession 中放入一些屬性(Map),通常放在過濾器的前面,用於放置一些初始化的信息。
MdcInjectionFilter 針對日誌輸出做MDC 操作,可以參考LOG4J 的MDC、NDC 的文檔。
WriteRequestFilter CompressionFilter、RequestResponseFilter 的基類,用於包裝寫請求的過濾器。
還有一些過濾器,會在各節中詳細討論,這裏沒有列出,譬如:前面的LoggingFilger 日誌過濾器。

6.協議編解碼器:

前面說過,協議編解碼器是在使用Mina 的時候你最需要關注的對象,因爲在網絡傳輸的數據都是二進制數據(byte),而你在程序中面向的是JAVA 對象,這就需要你實現在發送數據時將JAVA 對象編碼二進制數據,而接收數據時將二進制數據解碼爲JAVA 對象(這個可不是JAVA 對象的序列化、反序列化那麼簡單的事情)。Mina 中的協議編解碼器通過過濾器ProtocolCodecFilter 構造,這個過濾器的構造方法需要一個ProtocolCodecFactory,這從前面註冊TextLineCodecFactory 的代碼就可以看出來。
ProtocolCodecFactory 中有如下兩個方法:
public interface ProtocolCodecFactory {
ProtocolEncoder getEncoder(IoSession session) throws Exception;
ProtocolDecoder getDecoder(IoSession session) throws Exception;
}
因此,構建一個ProtocolCodecFactory 需要ProtocolEncoder、ProtocolDecoder 兩個實例。你可能要問JAVA 對象和二進制數據之間如何轉換呢?這個要依據具體的通信協議,也就是Server 端要和Client 端約定網絡傳輸的數據是什麼樣的格式,譬如:第一個字節表示數據長度,第二個字節是數據類型,後面的就是真正的數據(有可能是文字、有可能是圖片等等),然後你可以依據長度從第三個字節向後讀,直到讀取到指定第一個字節指定長度的數據。
簡單的說,HTTP 協議就是一種瀏覽器與Web 服務器之間約定好的通信協議,雙方按照指定的協議編解碼數據。我們再直觀一點兒說,前面一直使用的TextLine 編解碼器就是在讀取網絡上傳遞過來的數據時,只要發現哪個字節裏存放的是ASCII 的1013 字符(/r、/n),就認爲之前的字節就是一個字符串(默認使用UTF-8 編碼)。以上所說的就是各種協議實際上就是網絡七層結構中的應用層協議,它位於網絡層(IP)、傳輸層(TCP)之上,Mina 的協議編解碼器就是讓你實現一套自己的應用層協議棧。

(6-1.)簡單的編解碼器示例:
下面我們舉一個模擬電信運營商短信協議的編解碼器實現,假設通信協議如下所示:
M sip:wap.fetion.com.cn SIP-C/2.0
S: 1580101xxxx
R: 1889020xxxx

L: 21
Hello World!
這裏的第一行表示狀態行,一般表示協議的名字、版本號等,第二行表示短信的發送號碼,第三行表示短信接收的號碼,第四行表示短信的字節數,最後的內容就是短信的內容。上面的每一行的末尾使用ASC II 的10(/n)作爲換行符,因爲這是純文本數據,協議要
求雙方使用UTF-8 對字符串編解碼。實際上如果你熟悉HTTP 協議,上面的這個精簡的短信協議和HTTP 協議的組成是非常像的,第一行是狀態行,中間的是消息報頭,最後面的是消息正文。在解析這個短信協議之前,你需要知曉TCP 的一個事項,那就是數據的發送沒有規模性,所謂的規模性就是作爲數據的接收端,不知道到底什麼時候數據算是讀取完畢,所以應用層協議在制定的時候,必須指定數據讀取的截至點。一般來說,有如下三種方式設置數據讀取的長度:
(1.)使用分隔符,譬如:TextLine 編解碼器。你可以使用/r、/n、NUL 這些ASC II 中的特殊的字符來告訴數據接收端,你只要遇見分隔符,就表示數據讀完了,不用在那裏傻等着不知道還有沒有數據沒讀完啊?我可不可以開始把已經讀取到的字節解碼爲指定的數據類型了啊?
(2.)定長的字節數,這種方式是使用長度固定的數據發送,一般適用於指令發送,譬如:數據發送端規定發送的數據都是雙字節,AA 表示啓動、BB 表示關閉等等。
(3.)在數據中的某個位置使用一個長度域,表示數據的長度,這種處理方式最爲靈活,上面的短信協議中的那個L 就是短信文字的字節數,其實HTTP 協議的消息報頭中的Content-Length 也是表示消息正文的長度,這樣數據的接收端就知道我到底讀到多長的
字節數就表示不用再讀取數據了。相比較解碼(字節轉爲JAVA 對象,也叫做拆包)來說,編碼(JAVA 對象轉爲字節,也叫做打包)就很簡單了,你只需要把JAVA 對象轉爲指定格式的字節流,write()就可以了。下面我們開始對上面的短信協議進行編解碼處理。

第一步,協議對象:


public class SmsObject {
    private String sender;// 短信發送者
    private String receiver;// 短信接受者
    private String message;// 短信內容

    public String getSender() {
        return sender;
    }

    public void setSender(String sender) {
        this.sender = sender;
    }

    public String getReceiver() {
        return receiver;
    }

    public void setReceiver(String receiver) {
        this.receiver = receiver;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

第二步,編碼器:
在Mina 中編寫編碼器可以實現ProtocolEncoder,其中有encode()、dispose()兩個方法需要實現。這裏的dispose()方法用於在銷燬編碼器時釋放關聯的資源,由於這個方法一般我們並不關心,所以通常我們直接繼承適配器ProtocolEncoderAdapter。


public class CmccSipcEncoder extends ProtocolEncoderAdapter {
    private final Charset charset;

    public CmccSipcEncoder(Charset charset) {
        this.charset = charset;
    }

    @Override
    public void encode(IoSession session, Object message,
            ProtocolEncoderOutput out) throws Exception {
        SmsObject sms = (SmsObject) message;
        CharsetEncoder ce = charset.newEncoder();
        IoBuffer buffer = IoBuffer.allocate(100).setAutoExpand(true);
        String statusLine = "M sip:wap.fetion.com.cn SIP-C/2.0";
        String sender = sms.getSender();
        String receiver = sms.getReceiver();
        String smsContent = sms.getMessage();
        buffer.putString(statusLine + "/n", ce);
        buffer.putString("S: " + sender + "/n", ce);
        buffer.putString("R: " + receiver + "/n", ce);
        buffer.putString("L: " + (smsContent.getBytes(charset).length) + "/n",
                ce);
        buffer.putString(smsContent, ce);
        buffer.flip();
        out.write(buffer);
    }
}

這裏我們依據傳入的字符集類型對message 對象進行編碼,編碼的方式就是按照短信協議拼裝字符串到IoBuffer 緩衝區,然後調用ProtocolEncoderOutput 的write()方法輸出字節流。這裏要注意生成短信內容長度時的紅色代碼,我們使用String 類與Byte[]類型之間的轉換方法獲得轉爲字節流後的字節數。
編碼器的編寫有以下幾個步驟:
A. 將 encode()方法中的message 對象強制轉換爲指定的對象類型;
B. 創建IoBuffer 緩衝區對象,並設置爲自動擴展;
C. 將轉換後的message 對象中的各個部分按照指定的應用層協議進行組裝,並put()到IoBuffer 緩衝區;
D. 當你組裝數據完畢之後,調用flip()方法,爲輸出做好準備,切記在write()方法之前,要調用IoBuffer 的flip()方法,否則緩衝區的position 的後面是沒有數據可以用來輸出的,你必須調用flip()方法將position 移至0,limit 移至剛纔的position。這個flip()方法的含義請參看java.nio.ByteBuffer。
E. 最後調用ProtocolEncoderOutput 的write()方法輸出IoBuffer 緩衝區實例。

第三步,解碼器:

在Mina 中編寫解碼器,可以實現ProtocolDecoder 接口,其中有decode()finishDecode()dispose()三個方法。這裏的finishDecode()方法可以用於處理在IoSession 關閉時剩餘的未讀取數據,一般這個方法並不會被使用到,除非協議中未定義任何標識數據什麼時候截止的約定,譬如:Http 響應的Content-Length 未設定,那麼在你認爲讀取完數據後,關閉TCP連接(IoSession 的關閉)後,就可以調用這個方法處理剩餘的數據,當然你也可以忽略調剩餘的數據。同樣的,一般情況下,我們只需要繼承適配器ProtocolDecoderAdapter,關注decode()方法即可。但前面說過解碼器相對編碼器來說,最麻煩的是數據發送過來的規模,以聊天室爲例,一個TCP 連接建立之後,那麼隔一段時間就會有聊天內容發送過來,也就是decode()方法會被往復調用,這樣處理起來就會非常麻煩。那麼Mina 中幸好提供了CumulativeProtocolDecoder類,從名字上可以看出累積性的協議解碼器,也就是說只要有數據發送過來,這個類就會去讀取數據,然後累積到內部的IoBuffer 緩衝區,但是具體的拆包(把累積到緩衝區的數據解碼爲JAVA 對象)交由子類的doDecode()方法完成,實際上CumulativeProtocolDecoder就是在decode()反覆的調用暴漏給子類實現的doDecode()方法。
具體執行過程如下所示:
A. 你的doDecode()方法返回true 時,CumulativeProtocolDecoder 的decode()方法會首先判斷你是否在doDecode()方法中從內部的IoBuffer 緩衝區讀取了數據,如果沒有,則會拋出非法的狀態異常,也就是你的doDecode()方法返回true 就表示你已經消費了本次數據(相當於聊天室中一個完整的消息已經讀取完畢),進一步說,也就是此時你必須已經消費過內部的IoBuffer 緩衝區的數據(哪怕是消費了一個字節的數據)。如果驗證過通過,那麼CumulativeProtocolDecoder 會檢查緩衝區內是否還有數據未讀取,如果有就繼續調用doDecode()方法,沒有就停止對doDecode()方法的調用,直到有新的數據被緩衝。

B. 當你的doDecode()方法返回false 時,CumulativeProtocolDecoder 會停止對doDecode()方法的調用,但此時如果本次數據還有未讀取完的,就將含有剩餘數據的IoBuffer 緩衝區保存到IoSession 中,以便下一次數據到來時可以從IoSession 中提取合併。如果發現本次數據全都讀取完畢,則清空IoBuffer 緩衝區。簡而言之,當你認爲讀取到的數據已經夠解碼了,那麼就返回true,否則就返回false。這個CumulativeProtocolDecoder 其實最重要的工作就是幫你完成了數據的累積,因爲這個工作是很煩瑣的。

public class CmccSipcDecoder extends CumulativeProtocolDecoder {
    private final Charset charset;

    public CmccSipcDecoder(Charset charset) {
        this.charset = charset;
    }

    @Override
    protected boolean doDecode(IoSession session, IoBuffer in,
            ProtocolDecoderOutput out) throws Exception {
        IoBuffer buffer = IoBuffer.allocate(100).setAutoExpand(true);
        CharsetDecoder cd = charset.newDecoder();
        int matchCount = 0;
        String statusLine = "", sender = "", receiver = "", length = "", sms = "";
        int i = 1;
        while (in.hasRemaining()) {
            byte b = in.get();
            buffer.put(b);
            if (b == 10 && i < 5) {
                matchCount++;
                if (i == 1) {
                    buffer.flip();
                    statusLine = buffer.getString(matchCount, cd);
                    statusLine = statusLine.substring(0,
                            statusLine.length() - 1);
                    matchCount = 0;
                    buffer.clear();
                }
                if (i == 2) {
                    buffer.flip();
                    sender = buffer.getString(matchCount, cd);
                    sender = sender.substring(0, sender.length() - 1);
                    matchCount = 0;
                    buffer.clear();
                }
                if (i == 3) {
                    buffer.flip();
                    receiver = buffer.getString(matchCount, cd);
                    receiver = receiver.substring(0, receiver.length() - 1);
                    matchCount = 0;
                    buffer.clear();
                }
                if (i == 4) {
                    buffer.flip();
                    length = buffer.getString(matchCount, cd);
                    length = length.substring(0, length.length() - 1);
                    matchCount = 0;
                    buffer.clear();
                }
                i++;
            } else if (i == 5) {
                matchCount++;
                if (matchCount == Long.parseLong(length.split(": ")[1])) {
                    buffer.flip();
                    sms = buffer.getString(matchCount, cd);
                    i++;
                    break;
                }
            } else {
                matchCount++;
            }
        }
        SmsObject smsObject = new SmsObject();
        smsObject.setSender(sender.split(": ")[1]);
        smsObject.setReceiver(receiver.split(": ")[1]);
        smsObject.setMessage(sms);
        out.write(smsObject);
        return false;
    }
}

我們的這個短信協議解碼器使用/n(ASCII 的10 字符)作爲分解點,一個字節一個字節的讀取,那麼第一次發現/n 的字節位置之前的部分,必然就是短信協議的狀態行,依次類推,你就可以解析出來發送者、接受者、短信內容長度。然後我們在解析短信內容時,使用獲取到的長度進行讀取。全部讀取完畢之後, 然後構造SmsObject 短信對象, 使用ProtocolDecoderOutput 的write()方法輸出,最後返回false,也就是本次數據全部讀取完畢,告知CumulativeProtocolDecoder 在本次數據讀取中不需要再調用doDecode()方法了。這裏需要注意的是兩個狀態變量i、matchCount,i 用於記錄解析到了短信協議中的哪一行(/n),matchCount 記錄在當前行中讀取到了哪一個字節。狀態變量在解碼器中經常被使用,我們這裏的情況比較簡單,因爲我們假定短信發送是在一次數據發送中完成的,所以狀態變量的使用也比較簡單。假如數據的發送被拆成了多次(譬如:短信協議的短信內容、消息報頭被拆成了兩次數據發送),那麼上面的代碼勢必就會存在問題,因爲當第二次調用doDecode()方法時,狀態變量i、matchCount 勢必會被重置,也就是原來的狀態值並沒有被保存。那麼我們如何解決狀態保存的問題呢?答案就是將狀態變量保存在IoSession 中或者是Decoder 實例自身,但推薦使用前者,因爲雖然Decoder 是單例的,其中的實例變量保存的狀態在Decoder 實例銷燬前始終保持,但Mina 並不保證每次調用doDecode()方法時都是同一個線程(這也就是說第一次調用doDecode()是IoProcessor-1 線程,第二次有可能就是IoProcessor-2 線程),這就會產生多線程中的實例變量的可視性(Visibility,具體請參考JAVA 的多線程知識)問題。IoSession中使用一個同步的HashMap 保存對象,所以你不需要擔心多線程帶來的問題。使用IoSession 保存解碼器的狀態變量通常的寫法如下所示:
A. 在解碼器中定義私有的內部類Context,然後將需要保存的狀態變量定義在Context 中存儲。
B. 在解碼器中定義方法獲取這個Context 的實例,這個方法的實現要優先從IoSession 中獲取Context。
具體代碼示例如下所示:
// 上下文作爲保存狀態的內部類的名字,意思很明顯,就是讓狀態跟隨上下文,在整個調用過程中都可以被保持。


public class XXXDecoder extends CumulativeProtocolDecoder{    
private final AttributeKey CONTEXT =    
new AttributeKey(getClass(), "context" );    
public Context getContext(IoSession session){    
Context ctx=(Context)session.getAttribute(CONTEXT);    
        if(ctx==null){    
        ctx=new Context();    
        session.setAttribute(CONTEXT,ctx);    
            }    
        }    
private class Context {    
//狀態變量    
    }    
}  

注意這裏我們使用了Mina 自帶的AttributeKey 類來定義保存在IoSession 中的對象的鍵值,這樣可以有效的防止鍵值重複。另外,要注意在全部處理完畢之後,狀態要復位,譬如:聊天室中的一條消息讀取完畢之後,狀態變量要變爲初始值,以便下次處理時重新使用。

第四步,編解碼工廠:

public class CmccSipcCodecFactory implements ProtocolCodecFactory {
    private final CmccSipcEncoder encoder;
    private final CmccSipcDecoder decoder;

    public CmccSipcCodecFactory() {
        this(Charset.defaultCharset());
    }

    public CmccSipcCodecFactory(Charset charSet) {
        this.encoder = new CmccSipcEncoder(charSet);
        this.decoder = new CmccSipcDecoder(charSet);
    }

    @Override
    public ProtocolDecoder getDecoder(IoSession session) throws Exception {
        return decoder;
    }

    @Override
    public ProtocolEncoder getEncoder(IoSession session) throws Exception {
        return encoder;
    }
}

實際上這個工廠類就是包裝了編碼器、解碼器,通過接口中的getEncoder()、getDecoder()方法向ProtocolCodecFilter 過濾器返回編解碼器實例,以便在過濾器中對數據進行編解碼處理。
第五步,運行示例:
下面我們修改最一開始的示例中的MyServer、MyClient 的代碼,如下所示:

MyServer的代碼更改

acceptor.getFilterChain().addLast(    
                "codec",    
                new ProtocolCodecFilter(new CmccSipcCodecFactory(Charset    
                .forName("UTF-8"))));     

MyClient 的代碼更改

connector.getFilterChain().addLast(
                "codec",
                new ProtocolCodecFilter(new CmccSipcCodecFactory(Charset
                        .forName("UTF-8")))); 

然後我們在ClientHandler 中發送一條短信:

    public void sessionOpened(IoSession session){    
    SmsObject sms = new SmsObject();    
    sms.setSender("15801012253");    
    sms.setReceiver("18869693235");    
    sms.setMessage("你好!Hello World!");    
    session.write(sms);    
    }  

最後我們在MyIoHandler 中接收這條短信息:
public void messageReceived(IoSession session, Object message)
throws Exception {
SmsObject sms = (SmsObject) message;
log.info(“The message received is [” + sms.getMessage() + “]”);
}

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