mina框架詳解(二)

上一章我們講解了mina框架的基礎部分(mina框架詳解(一)),包括:mina的整體結構、TCP的主要接口、日誌配置等。這一章講解mina框架的過濾器。

1.過濾器

​ 前面我們看到了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 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)才能繼續前進是一樣的道理。
示例:

package com.dxz.minademo3;

import org.apache.mina.core.filterchain.IoFilter;
import org.apache.mina.core.filterchain.IoFilterChain;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.write.WriteRequest;

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
server session created
%%%%%%%%%%%%%%%%%%%%%%%%%%%sessionOpened
server session Opened
%%%%%%%%%%%%%%%%%%%%%%%%%%%messageReceived
The message received is [你好!]
%%%%%%%%%%%%%%%%%%%%%%%%%%%messageReceived
The message received is [ 大家好!]
%%%%%%%%%%%%%%%%%%%%%%%%%%%sessionIdle

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 日誌過濾器。

2.協議編解碼器

​ 前面說過,協議編解碼器是在使用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 的10、13 字符(/r、/n),就認爲之前的字節就是一個字符串(默認使用UTF-8 編碼)。以上所說的就是各種協議實際上就是網絡七層結構中的應用層協議,它位於網絡層(IP)、傳輸層(TCP)之上,Mina 的協議編解碼器就是讓你實現一套自己的應用層協議棧。

(2-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()就可以了。下面我們開始對上面的短信協議進行編解碼處理。
第一步,協議對象:

package com.dxz.minademo3;

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。

package com.dxz.minademo3;

import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
import org.apache.mina.filter.codec.ProtocolEncoderOutput;

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 其實最重要的工作就是幫你完成了數據的累積,因爲這個工作是很煩瑣的。

package com.dxz.minademo3;

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;

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。
具體代碼示例如下所示:
// 上下文作爲保存狀態的內部類的名字,意思很明顯,就是讓狀態跟隨上下文,在整個調用過程中都可以被保持。

package com.dxz.minademo3;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.AttributeKey;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;

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);
        }
        return ctx;
    }

    private class Context {
        // 狀態變量
    }

    @Override
    protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
        // TODO Auto-generated method stub
        return false;
    }
}

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

package com.dxz.minademo3;

import java.nio.charset.Charset;

import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFactory;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolEncoder;

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 的代碼,如下所示:

package com.dxz.minademo3;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;

import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.util.ReferenceCountingFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

import com.dxz.minademo2.TCPServerHandler;

public class TCPServer3 {

    public static void main(String[] args) throws IOException {

        IoAcceptor acceptor = new NioSocketAcceptor();
        acceptor.getSessionConfig().setReadBufferSize(2048);
        acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);

        // 編寫過濾器

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

        // 設置handler
        acceptor.getFilterChain().addLast("myIoFilter", new ReferenceCountingFilter(new MyIoFilter()));

        // 設置handler
        acceptor.setHandler(new TCPServerHandler());
        
        // 綁定端口
        acceptor.bind(new InetSocketAddress(9124));

        System.out.println(acceptor.getSessionConfig());
    }
}

client

package com.dxz.minademo3;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;

import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

import com.dxz.minademo2.TCPClientHandler;

public class TCPClient3 {

    public static void main(String[] args) {
        IoConnector connector = new NioSocketConnector();
        connector.setConnectTimeoutMillis(30000);

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

        connector.setHandler(new TCPClientHandler("你好!\r\n 大家好!"));
        ConnectFuture future = connector.connect(new InetSocketAddress("localhost", 9124));
        future.addListener(new IoFutureListener<ConnectFuture>() {
            public void operationComplete(ConnectFuture future) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                IoSession session = future.getSession();
                System.out.println("++++++++++++++++++++++++++++");
            }
        });
        System.out.println("*************");

    }

}

最後我們在MyIoHandler 中接收這條短信息:

package com.dxz.minademo3;

import org.apache.mina.core.filterchain.IoFilter;
import org.apache.mina.core.filterchain.IoFilterChain;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.write.WriteRequest;

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");
        SmsObject sms = (SmsObject) message;    
        System.out.println("The message received is [" + sms.getMessage() + "]");
        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);
    }
}

你會看到Server 端的控制檯輸出如下信息:

%%%%%%%%%%%%%%%%%%%%%%%%%%%init
%%%%%%%%%%%%%%%%%%%%%%%%%%%onPreAdd
%%%%%%%%%%%%%%%%%%%%%%%%%%%onPostAdd
%%%%%%%%%%%%%%%%%%%%%%%%%%%sessionCreated
server session created
%%%%%%%%%%%%%%%%%%%%%%%%%%%sessionOpened
server session Opened
-%%%%%%%%%%%%%%%%%%%%%%%%%%%messageReceived
The message received is [你好!Hello World!]
-----------------messageReceived
The message received is [com.dxz.minademo3.SmsObject@2b884014]

上面是簡單的編解碼示例,對於想簡單理解編解碼的已經足夠用了。若是想更深入瞭解編解碼的請看下面兩部分,若是不想看的,可以直接跳到第3部分:線程模型的配置。


(2-2.)複雜的解碼器:
下面我們講解一下如何在解碼器中保存狀態變量,也就是真正的實現上面所說的Context。
我們假設這樣一種情況,有兩條短信:
M sip:wap.fetion.com.cn SIP-C/2.0
S: 1580101xxxx
R: 1889020xxxx
L: 21
Hello World!
M sip:wap.fetion.com.cn SIP-C/2.0
S: 1580101xxxx
R: 1889020xxxx
L: 21
Hello World!
他們按照上面的顏色標識發送,也就是說紅色部分、藍色部分、綠色部分分別發送(調用三次IoSession.write()方法),那麼如果你還用上面的CmccSipcDecoder,將無法工作,因爲第一次數據流(紅色部分)發送過取時,數據是不完整的,無法解析出一條短信息,當二次數據流(藍色部分)發送過去時,已經可以解析出第一條短信息了,但是第二條短信還是不完整的,需要等待第三次數據流(綠色部分)的發送。注意:由於模擬數據發送的規模性問題很麻煩,所以這裏採用了這種極端的例子說明問題,雖不具有典型性,但很能說明問題,這就足夠了,所以不要追究這種發送消息是否在真實環境中存在,更不要追究其合理性。

CmccSispcDecoder 類改爲如下的寫法:

public class CmccSipcDecoder extends CumulativeProtocolDecoder {    
    private final Charset charset;    
    private final AttributeKey CONTEXT = new AttributeKey(getClass(), "context");    
    public CmccSipcDecoder(Charset charset) {    
    	this.charset = charset;    
    }    
    @Override    
    protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) 
    throws Exception {    
    Context ctx = getContext(session);    
    CharsetDecoder cd = charset.newDecoder();    
    int matchCount = ctx.getMatchCount();    
    int line = ctx.getLine();    
    IoBuffer buffer = ctx.innerBuffer;    
    String statusLine = ctx.getStatusLine(),    
    sender = ctx.getSender(),    
    receiver = ctx.getReceiver(),    
    length = ctx.getLength(),    
    sms = ctx.getSms();    
    while (in.hasRemaining()) {    
    byte b = in.get();    
    matchCount++;    
    buffer.put(b);    
    if (line < 4 && b == 10) {    
    if (line == 0) {    
    buffer.flip();    
    statusLine = buffer.getString(matchCount, cd);    
    statusLine = statusLine.substring(0,    
    statusLine.length() - 1);    
    matchCount = 0;    
    buffer.clear();    
    ctx.setStatusLine(statusLine);    
    }    
    if (line == 1) {    
    buffer.flip();    
    sender = buffer.getString(matchCount, cd);    
    sender = sender.substring(0, sender.length() - 1);    
    matchCount = 0;    
    buffer.clear();    
    ctx.setSender(sender);    
    }    
    if (line == 2) {    
    buffer.flip();    
    receiver = buffer.getString(matchCount, cd);    
    receiver = receiver.substring(0, receiver.length() -    
    1);    
    matchCount = 0;    
    buffer.clear();    
    ctx.setReceiver(receiver);    
    }    
    if (line == 3) {    
    buffer.flip();    
    length = buffer.getString(matchCount, cd);    
    length = length.substring(0, length.length() - 1);    
    matchCount = 0;    
    buffer.clear();    
    ctx.setLength(length);    
    }    
    line++;    
    } else if (line == 4) {    
    if (matchCount == Long.parseLong(length.split(": ")[1]))    
    {    
    buffer.flip();    
    sms = buffer.getString(matchCount, cd);    
    ctx.setSms(sms);    
    // 由於下面的break,這裏需要調用else外面的兩行代碼    
    ctx.setMatchCount(matchCount);    
    ctx.setLine(line);    
    break;    
    }    
    }    
    ctx.setMatchCount(matchCount);    
    ctx.setLine(line);    
    }    
    if (ctx.getLine() == 4    
    && Long.parseLong(ctx.getLength().split(": ")[1]) == ctx    
    .getMatchCount()) {    
    SmsObject smsObject = new SmsObject();    
    smsObject.setSender(sender.split(": ")[1]);    
    smsObject.setReceiver(receiver.split(": ")[1]);    
    smsObject.setMessage(sms);    
    out.write(smsObject);    
    ctx.reset();    
    return true;    
    } else {    
    return false;    
    }    
    }    
    private Context getContext(IoSession session) {    
    Context context = (Context) session.getAttribute(CONTEXT);    
    if (context == null){    
    context = new Context();    
    session.setAttribute(CONTEXT, context);    
    }    
    return context;    
    }    
    private class Context {    
    private final IoBuffer innerBuffer;    
    private String statusLine = "";    
    private String sender = "";    
    private String receiver = "";    
    private String length = "";    
    private String sms = "";    
    public Context() {    
    innerBuffer = IoBuffer.allocate(100).setAutoExpand(true);    
    }    
    private int matchCount = 0;    
    private int line = 0;    
    public int getMatchCount() {    
    return matchCount;    
    }    
    public void setMatchCount(int matchCount) {    
    this.matchCount = matchCount;    
    }    
    public int getLine() {    
    return line;    
    }    
    public void setLine(int line) {    
    this.line = line;    
    }    
    public String getStatusLine() {    
    return statusLine;    
    }    
    public void setStatusLine(String statusLine) {    
    this.statusLine = statusLine;    
    }    
    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 getLength() {    
    return length;    
    }    
    public void setLength(String length) {    
    this.length = length;    
    }    
    public String getSms() {    
    return sms;    
    }    
    public void setSms(String sms) {    
    this.sms = sms;    
    }    
    public void reset() {    
    this.innerBuffer.clear();    
    this.matchCount = 0;     
    this.line = 0;    
    this.statusLine = "";    
    this.sender = "";    
    this.receiver = "";    
    this.length = "";    
    this.sms = "";    
    }    
   }    
}    

這裏我們做了如下的幾步操作:
(1.) 所有記錄狀態的變量移到了Context 內部類中,包括記錄讀到短信協議的哪一行的line。每一行讀取了多少個字節的matchCount,還有記錄解析好的狀態行、發送者、接受者、短信內容、累積數據的innerBuffer 等。這樣就可以在數據不能完全解碼,等待下一次doDecode()方法的調用時,還能承接上一次調用的數據。
(2.) 在 doDecode()方法中主要的變化是各種狀態變量首先是從Context 中獲取,然後操作之後,將最新的值setXXX()到Context 中保存。
(3.) 這裏注意doDecode()方法最後的判斷,當認爲不夠解碼爲一條短信息時,返回false,也就是在本次數據流解碼中不要再調用doDecode()方法;當認爲已經解碼出一條短信息時,輸出短消息,然後重置所有的狀態變量,返回true,也就是如果本次數據流解碼中還有沒解碼完的數據,繼續調用doDecode()方法。下面我們對客戶端稍加改造,來模擬上面的紅、藍、綠三次發送聊天短信息的情況:
MyClient:


ConnectFuture future = connector.connect(new InetSocketAddress(HOSTNAME, PORT));    
future.awaitUninterruptibly();    
session = future.getSession();    
for (int i = 0; i < 3; i++) {    
    SmsObject sms = new SmsObject();    
    session.write(sms);    
    System.out.println("****************" + i);    
} 

這裏我們爲了方便演示,不在IoHandler 中發送消息,而是直接在MyClient 中發送,你要注意的是三次發送都要使用同一個IoSession,否則就不是從同一個通道發送過去的了。
CmccSipcEncoder:

public void encode(IoSession session, Object message,ProtocolEncoderOutput out) throws Exception {    
    SmsObject sms = (SmsObject) message;    
    CharsetEncoder ce = charset.newEncoder();    
    String statusLine = "M sip:wap.fetion.com.cn SIP-C/2.0";    
    String sender = "15801012253";    
    String receiver = "15866332698";    
    String smsContent = "你好!Hello World!";    
    IoBuffer buffer = IoBuffer.allocate(100).setAutoExpand(true);    
    buffer.putString(statusLine + '/n', ce);    
    buffer.putString("S: " + sender + '/n', ce);    
    buffer.putString("R: " + receiver + '/n', ce);    
    buffer.flip();    
    out.write(buffer);    
    IoBuffer buffer2 = IoBuffer.allocate(100).setAutoExpand(true);    
    buffer2.putString("L: " + (smsContent.getBytes(charset).length)    
    + "/n",ce);    
    buffer2.putString(smsContent, ce);    
    buffer2.putString(statusLine + '/n', ce);    
    buffer2.flip();    
    out.write(buffer2);    
    IoBuffer buffer3 = IoBuffer.allocate(100).setAutoExpand(true);    
    buffer3.putString("S: " + sender + '/n', ce);    
    buffer3.putString("R: " + receiver + '/n', ce);    
    buffer3.putString("L: " + (smsContent.getBytes(charset).length)    
    + "/n",ce);    
    buffer3.putString(smsContent, ce);    
    buffer3.putString(statusLine + '/n', ce);    
    buffer3.flip();    
    out.write(buffer3);    
}    

上面的這段代碼要配合MyClient來操作,你需要做的是在MyClient中的紅色輸出語句處設置斷點,然後第一調用時CmccSipcEncoder中註釋掉藍、綠色的代碼,也就是發送兩條短信息的第一部分(紅色的代碼),依次類推,也就是MyClient的中的三次斷點中,分別執行CmccSipcEncoder中的紅、藍、綠三段代碼,也就是模擬兩條短信的三段發送。你會看到Server端的運行結果是:當MyClient第一次到達斷點時,沒有短信息被讀取到,當MyClient第二次到達斷點時,第一條短信息輸出,當MyClient第三次到達斷點時,第二條短信息輸出。

Mina中自帶的解碼器:
解碼器 說明
CumulativeProtocolDecoder 累積性解碼器,上面我們重點說明了這個解碼器的用法。
SynchronizedProtocolDecoder 這個解碼器用於將任何一個解碼器包裝爲一個線程安全的解碼器,用於解決上面說的每次執行decode()方法時可能線程不是上一次的線程的問題,但這樣會在高併發時,大大降低系統的性能。
TextLineDecoder 按照文本的換行符( Windows:/r/n 、Linux:/n、Mac:/r)解碼數據。
PrefixedStringDecoder 這個類繼承自CumulativeProtocolDecoder類,用於讀取數據最前端的1、2、4 個字節表示後面的數據長度的數據。譬如:一個段數據的前兩個字節表示後面的真實數據的長度,那麼你就可以用這個方法進行解碼。

(2-3.)多路分離的解碼器:
假設一段數據發送過來之後,需要根據某種條件決定使用哪個解碼器,而不是像上面的例子,固定使用一個解碼器,那麼該如何做呢?幸好Mina 提供了org.apache.mina.filter.codec.demux 包來完成這種多路分離(Demultiplexes)的解碼工作,也就是同時註冊多個解碼器,然後運行時依據傳入的數據決定到底使用哪個解碼器來工作。所謂多路分離就是依據條件分發到指定的解碼器,譬如:上面的短信協議進行擴展,可以依據狀態行來判斷使用1.0 版本的短信協議解碼器還是2.0版本的短信協議解碼器。
下面我們使用一個簡單的例子,說明這個多路分離的解碼器是如何使用的,需求如下所示:
(1.) 客戶端傳入兩個int 類型的數字,還有一個char 類型的符號。
(2.) 如果符號是+,服務端就是用1 號解碼器,對兩個數字相加,然後把結果返回給客戶端。
(3.) 如果符號是-,服務端就使用2 號解碼器,將兩個數字變爲相反數,然後相加,把結果返回給客戶端。
Demux 開發編解碼器主要有如下幾個步驟:
A. 定義Client 端、Server 端發送、接收的數據對象。
B. 使用Demux 編寫編碼器是實現MessageEncoder接口,T 是你要編碼的數據對象,這個MessageEncoder 會在DemuxingProtocolEncoder 中調用。
C. 使用Demux 編寫編碼器是實現MessageDecoder 接口,這個MessageDecoder 會在DemuxingProtocolDecoder 中調用。

D. 在 DemuxingProtocolCodecFactory 中調用addMessageEncoder()、addMessageDecoder()方法組裝編解碼器。
MessageEncoder的接口如下所示:

public interface MessageEncoder<T> {    
    void encode(IoSession session, T message, ProtocolEncoderOutput out) throws Exception;    
} 

你注意到消息編碼器接口與在ProtocolEncoder 中沒什麼不同,區別就是Object message被泛型具體化了類型,你不需要手動的類型轉換了。
MessageDecoder的接口如下所示:

public interface MessageDecoder {    
    static MessageDecoderResult OK = MessageDecoderResult.OK;    
    static MessageDecoderResult NEED_DATA = MessageDecoderResult.NEED_DATA;    
    static MessageDecoderResult NOT_OK = MessageDecoderResult.NOT_OK;    
    MessageDecoderResult decodable(IoSession session, IoBuffer in);    
    MessageDecoderResult decode(IoSession session, IoBuffer in,    
    ProtocolDecoderOutput out) throws Exception;    
    void finishDecode(IoSession session, ProtocolDecoderOutput out) throws Exception;    
}

(1.)decodable()方法有三個返回值,分別表示如下的含義:
A. MessageDecoderResult.NOT_OK:表示這個解碼器不適合解碼數據,然後檢查其它解碼器,如果都不滿足會拋異常;
B. MessageDecoderResult.NEED_DATA:表示當前的讀入的數據不夠判斷是否能夠使用這個解碼器解碼,然後再次調用decodable()方法檢查其它解碼器,如果都是NEED_DATA,則等待下次輸入;
C. MessageDecoderResult.OK: 表示這個解碼器可以解碼讀入的數據, 然後則調用MessageDecoder 的decode()方法。這裏注意decodable()方法對參數IoBuffer in 的任何操作在方法結束之後,都會復原,也就是你不必擔心在調用decode()方法時,position 已經不在緩衝區的起始位置。這個方法相當於是預讀取,用於判斷是否是可用的解碼器。
(2.)decode()方法有三個返回值,分別表示如下的含義:
A. MessageDecoderResult.NOT_OK:表示解碼失敗,會拋異常;

B. MessageDecoderResult.NEED_DATA:表示數據不夠,需要讀到新的數據後,再次調用decode()方法。
C. MessageDecoderResult.OK:表示解碼成功。

代碼演示:
(1.)客戶端發送的數據對象:

package com.dxz.minademo4;

public class SendMessage {
    private int i = 0;
    private int j = 0;
    private char symbol = '+';

    public char getSymbol() {
        return symbol;
    }

    public void setSymbol(char symbol) {
        this.symbol = symbol;
    }

    public int getI() {
        return i;
    }

    public void setI(int i) {
        this.i = i;
    }

    public int getJ() {
        return j;
    }

    public void setJ(int j) {
        this.j = j;
    }
}

(2.)服務端發送的返回結果對象:

package com.dxz.minademo4;

public class ResultMessage {
    private int result = 0;

    public int getResult() {
        return result;
    }

    public void setResult(int result) {
        this.result = result;
    }
}

(3.)客戶端使用的SendMessage的編碼器:

package com.dxz.minademo4;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoderOutput;
import org.apache.mina.filter.codec.demux.MessageEncoder;

public class SendMessageEncoder implements MessageEncoder<SendMessage> {
    @Override
    public void encode(IoSession session, SendMessage message, ProtocolEncoderOutput out) throws Exception {
        IoBuffer buffer = IoBuffer.allocate(10);
        buffer.putChar(message.getSymbol());
        buffer.putInt(message.getI());
        buffer.putInt(message.getJ());
        buffer.flip();
        out.write(buffer);
    }
}

這裏我們的SendMessage、ResultMessage 中的字段都是用長度固定的基本數據類型,這樣IoBuffer 就不需要自動擴展了,提高性能。按照一個char、兩個int 計算,這裏的IoBuffer只需要10 個字節的長度就可以了。
(4.)服務端使用的SendMessage的1號解碼器:

package com.dxz.minademo4;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.filter.codec.demux.MessageDecoder;
import org.apache.mina.filter.codec.demux.MessageDecoderResult;

public class SendMessageDecoderPositive implements MessageDecoder {
    @Override
    public MessageDecoderResult decodable(IoSession session, IoBuffer in) {
        if (in.remaining() < 2)
            return MessageDecoderResult.NEED_DATA;
        else {
            char symbol = in.getChar();
            if (symbol == '+') {
                return MessageDecoderResult.OK;
            } else {
                return MessageDecoderResult.NOT_OK;
            }
        }
    }

    @Override
    public MessageDecoderResult decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
        SendMessage sm = new SendMessage();
        sm.setSymbol(in.getChar());
        sm.setI(in.getInt());
        sm.setJ(in.getInt());
        out.write(sm);
        return MessageDecoderResult.OK;
    }

    @Override
    public void finishDecode(IoSession session, ProtocolDecoderOutput out) throws Exception {
        // undo
    }
}

因爲客戶端發送的SendMessage 的前兩個字節(char)就是符號位,所以我們在decodable()方法中對此條件進行了判斷,之後讀到兩個字節,並且這兩個字節表示的字符是+時,才認爲這個解碼器可用。
(5.)服務端使用的SendMessage的2號解碼器:

package com.dxz.minademo4;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.filter.codec.demux.MessageDecoder;
import org.apache.mina.filter.codec.demux.MessageDecoderResult;

public class SendMessageDecoderNegative implements MessageDecoder {
    @Override
    public MessageDecoderResult decodable(IoSession session, IoBuffer in) {
        if (in.remaining() < 2)
            return MessageDecoderResult.NEED_DATA;
        else {
            char symbol = in.getChar();
            if (symbol == '-') {
                return MessageDecoderResult.OK;
            } else {
                return MessageDecoderResult.NOT_OK;
            }
        }
    }

    @Override
    public MessageDecoderResult decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
        SendMessage sm = new SendMessage();
        sm.setSymbol(in.getChar());
        sm.setI(-in.getInt());
        sm.setJ(-in.getInt());
        out.write(sm);
        return MessageDecoderResult.OK;
    }

    @Override
    public void finishDecode(IoSession session, ProtocolDecoderOutput out) throws Exception {
        // undo
    }
}

(6.)服務端使用的ResultMessage的編碼器:

package com.dxz.minademo4;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoderOutput;
import org.apache.mina.filter.codec.demux.MessageEncoder;

public class ResultMessageEncoder implements MessageEncoder<ResultMessage> {
    @Override
    public void encode(IoSession session, ResultMessage message, ProtocolEncoderOutput out) throws Exception {
        IoBuffer buffer = IoBuffer.allocate(4);
        buffer.putInt(message.getResult());
        buffer.flip();
        out.write(buffer);
    }
}

(7.)客戶端使用的ResultMessage的解碼器:

package com.dxz.minademo4;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.filter.codec.demux.MessageDecoder;
import org.apache.mina.filter.codec.demux.MessageDecoderResult;

public class ResultMessageDecoder implements MessageDecoder {
    @Override
    public MessageDecoderResult decodable(IoSession session, IoBuffer in) {
        if (in.remaining() < 4)
            return MessageDecoderResult.NEED_DATA;
        else if (in.remaining() == 4)
            return MessageDecoderResult.OK;
        else
            return MessageDecoderResult.NOT_OK;
    }

    @Override
    public MessageDecoderResult decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
        ResultMessage rm = new ResultMessage();
        rm.setResult(in.getInt());
        out.write(rm);
        return MessageDecoderResult.OK;
    }

    @Override
    public void finishDecode(IoSession session, ProtocolDecoderOutput out) throws Exception {
        // undo
    }
}

(8.)組裝這些編解碼器的工廠:

package com.dxz.minademo4;

import org.apache.mina.filter.codec.demux.DemuxingProtocolCodecFactory;

public class MathProtocolCodecFactory extends DemuxingProtocolCodecFactory {
    public MathProtocolCodecFactory(boolean server) {
        if (server) {
            super.addMessageEncoder(ResultMessage.class, ResultMessageEncoder.class);
            super.addMessageDecoder(SendMessageDecoderPositive.class);
            super.addMessageDecoder(SendMessageDecoderNegative.class);
        } else {
            super.addMessageEncoder(SendMessage.class, SendMessageEncoder.class);
            super.addMessageDecoder(ResultMessageDecoder.class);
        }
    }
}

這個工廠類我們使用了構造方法的一個布爾類型的參數,以便其可以在Server 端、Client端同時使用。我們以Server 端爲例,你可以看到調用兩次addMessageDecoder()方法添加了1 號、2 號解碼器,其實DemuxingProtocolDecoder 內部在維護了一個MessageDecoder數組,用於保存添加的所有的消息解碼器,每次decode()的時候就調用每個MessageDecoder的decodable()方法逐個檢查,只要發現一個MessageDecoder 不是對應的解碼器,就從數組中移除,直到找到合適的MessageDecoder,如果最後發現數組爲空,就表示沒找到對應的MessageDecoder,最後拋出異常。
(9.)Server端:

package com.dxz.minademo4;

import java.net.InetSocketAddress;

import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

public class Server {
    public static void main(String[] args) throws Exception {
        IoAcceptor acceptor = new NioSocketAcceptor();
        LoggingFilter lf = new LoggingFilter();
        acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 5);
        acceptor.getFilterChain().addLast("logger", lf);
        acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new MathProtocolCodecFactory(true)));
        acceptor.setHandler(new ServerHandler());
        acceptor.bind(new InetSocketAddress(9123));
    }
}

(10.)Server端使用的IoHandler:

package com.dxz.minademo4;

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

public class ServerHandler extends IoHandlerAdapter {
    private final static Logger log = LoggerFactory.getLogger(ServerHandler.class);

    @Override
    public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
        session.close(true);
    }

    @Override
    public void messageReceived(IoSession session, Object message) throws Exception {
        SendMessage sm = (SendMessage) message;
        log.info("The message received is [ " + sm.getI() + " " + sm.getSymbol() + " " + sm.getJ() + " ]");
        ResultMessage rm = new ResultMessage();
        rm.setResult(sm.getI() + sm.getJ());
        session.write(rm);
    }
}

(11.)Client端:

package com.dxz.minademo4;

import java.net.InetSocketAddress;

import org.apache.mina.core.service.IoConnector;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

public class Client {
    public static void main(String[] args) throws Throwable {
        IoConnector connector = new NioSocketConnector();
        connector.setConnectTimeoutMillis(30000);
        connector.getFilterChain().addLast("logger", new LoggingFilter());
        connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(new MathProtocolCodecFactory(false)));
        connector.setHandler(new ClientHandler());
        connector.connect(new InetSocketAddress("localhost", 9123));
    }
}

(12.)Client端的IoHandler:

package com.dxz.minademo4;

import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

    @Override
    public void sessionOpened(IoSession session) throws Exception {
        SendMessage sm = new SendMessage();
        sm.setI(100);
        sm.setJ(99);
        sm.setSymbol('+');
        session.write(sm);
    }

    @Override
    public void messageReceived(IoSession session, Object message) {
        ResultMessage rs = (ResultMessage) message;
        LOGGER.info(String.valueOf(rs.getResult()));
    }
}

你嘗試改變ClientHandler中的Synbol中的紅色代碼中的正負號,會看到服務端使用了兩個不同的解碼器對其進行處理。

The message received is [ 100 + 99 ]
The message received is [ -100 - -99 ]

3.線程模型配置:
Mina 中的很多執行環節都使用了多線程機制,用於提高性能。Mina 中默認在三個地方使用了線程:
(1.) IoAcceptor:
這個地方用於接受客戶端的連接建立,每監聽一個端口(每調用一次bind()方法),都啓用一個線程,這個數字我們不能改變。這個線程監聽某個端口是否有請求到來,一旦發現,則創建一個IoSession 對象。因爲這個動作很快,所以有一個線程就夠了。
(2.) IoConnector:
這個地方用於與服務端建立連接,每連接一個服務端(每調用一次connect()方法),就啓用一個線程,我們不能改變。同樣的,這個線程監聽是否有連接被建立,一旦發現,則創建一個IoSession 對象。因爲這個動作很快,所以有一個線程就夠了。
(3.) IoProcessor:
這個地方用於執行真正的IO 操作,默認啓用的線程個數是CPU 的核數+1,譬如:單CPU 雙核的電腦,默認的IoProcessor 線程會創建3 個。這也就是說一個IoAcceptor 或者IoConnector 默認會關聯一個IoProcessor 池,這個池中有3 個IoProcessor。因爲IO 操作耗費資源,所以這裏使用IoProcessor 池來完成數據的讀寫操作,有助於提高性能。這也就是前面說的IoAccetor、IoConnector 使用一個Selector,而IoProcessor 使用自己單獨的Selector 的原因。那麼爲什麼IoProcessor 池中的IoProcessor 數量只比CPU 的核數大1 呢?因爲IO 讀寫操作是耗費CPU 的操作,而每一核CPU 同時只能運行一個線程,因此IoProcessor 池中的IoProcessor 的數量並不是越多越好。

這個IoProcessor 的數量可以調整,如下所示:
IoAcceptor acceptor=new NioSocketAcceptor(5);
IoConnector connector=new NioSocketConnector(5);
這樣就會將IoProcessor 池中的數量變爲5 個,也就是說可以同時處理5 個讀寫操作。還記得前面說過Mina 的解碼器要使用IoSession 保存狀態變量,而不是Decoder 本身,這是因爲Mina 不保證每次執行doDecode()方法的都是同一個IoProcessor 這句話嗎?其實這個問題的根本原因是IoProcessor 是一個池,每次IoSession 進入空閒狀態時(無讀些數據發生),IoProcessor 都會被回收到池中,以便其他的IoSession 使用,所以當IoSession從空閒狀態再次進入繁忙狀態時,IoProcessor 會再次分配給其一個IoProcessor 實例,而此時已經不能保證還是上一次繁忙狀態時的那個IoProcessor 了。你還會發現IoAcceptor 、IoConnector 還有一個構造方法, 你可以指定一個java.util.concurrent.Executor 類作爲線程池對象,那麼這個線程池對象是做什麼用的呢?其實就是用於創建(1.)、(2.)中的用於監聽是否有TCP 連接建立的那個線程,默認情況下,使用Executors.newCachedThreadPool()方法創建Executor 實例,也就是一個無界的線程池(具體內容請參看JAVA 的併發庫)。大家不要試圖改變這個Executor 的實例,也就是使用內置的即可,否則可能會造成一些莫名其妙的問題,譬如:性能在某個訪問量級別時,突然下降。因爲無界線程池是有多少個Socket 建立,就分配多少個線程,如果你改爲Executors 的其他創建線程池的方法,創建了一個有界線程池,那麼一些請求將無法得到及時響應,從而出現一些問題。

下面我們完整的綜述一下Mina 的工作流程:
(1.) 當 IoService 實例創建的時候,同時一個關聯在IoService上的IoProcessor 池、線程池也被創建;
(2.) 當 IoService 建立套接字(IoAcceptor 的bind()或者是IoConnector 的connect()方法被調用)時,IoService 從線程池中取出一個線程,監聽套接字端口;
(3.) 當 IoService 監聽到套接字上有連接請求時,建立IoSession 對象,從IoProcessor池中取出一個IoProcessor 實例執行這個會話通道上的過濾器、IoHandler;
(4.) 當這條IoSession 通道進入空閒狀態或者關閉時,IoProcessor 被回收。上面說的是Mina 默認的線程工作方式,那麼我們這裏要講的是如何配置IoProcessor 的多線程工作方式。因爲一個IoProcessor 負責執行一個會話上的所有過濾器、IoHandler,也就是對於IO 讀寫操作來說,是單線程工作方式(就是按照順序逐個執行)。假如你想讓某個事件方法(譬如:sessionIdle()、sessionOpened()等)在單獨的線程中運行(也就是非IoProcessor 所在的線程),那麼這裏就需要用到一個ExecutorFilter 的過濾器。你可以看到IoProcessor 的構造方法中有一個參數是java.util.concurrent.Executor,也就是可以讓IoProcessor 調用的過濾器、IoHandler 中的某些事件方法在線程池中分配的線程上獨立運行,而不是運行在IoProcessor 所在的線程。

例:
acceptor.getFilterChain().addLast(“exceutor”, new ExecutorFilter());
我們看到是用這個功能,簡單的一行代碼就可以了。那麼ExecutorFilter 還有許多重載的構造方法,這些重載的有參構造方法,參數主要用於指定如下信息:
(1.) 指定線程池的屬性信息,譬如:核心大小、最大大小、等待隊列的性質等。你特別要關注的是ExecutorFilter 內部默認使用的是OrderedThreadPoolExecutor 作爲線程池的實現,從名字上可以看出是保證各個事件在多線程執行中的順序(譬如:各個事件方法的執行是排他的,也就是不可能出現兩個事件方法被同時執行;messageReceived()總是在sessionClosed() 方法之前執行), 這是因爲多線程的執行是異步的, 如果沒有OrderedThreadPoolExecutor 來保證IoHandler 中的方法的調用順序,可能會出現嚴重的問題。但是如果你的代碼確實沒有依賴於IoHandler 中的事件方法的執行順序,那麼你可以使用UnorderedThreadPoolExecutor 作爲線程池的實現。因此,你也最好不要改變默認的Executor 實現,否則,事件的執行順序就會混亂,譬如:messageReceived()、messageSent()方法被同時執行。
(2.) 哪些事件方法被關注,也就哪些事件方法用這個線程池執行。線程池可以異步執行的事件類型是位於IoEventType 中的九個枚舉值中除了SESSION_CREATED 之外的其餘八個,這說明Session 建立的事件只能與IoProcessor 在同一個線程上執行。

public enum IoEventType {    
    SESSION_CREATED,    
    SESSION_OPENED,    
    SESSION_CLOSED,    
    MESSAGE_RECEIVED,    
    MESSAGE_SENT,    
    SESSION_IDLE,    
    EXCEPTION_CAUGHT,    
    WRITE,    
    CLOSE,    
}

默認情況下,沒有配置關注的事件類型,有如下六個事件方法會被自動使用線程池異步執行:

IoEventType.EXCEPTION_CAUGHT,
IoEventType.MESSAGE_RECEIVED,
IoEventType.MESSAGE_SENT,
IoEventType.SESSION_CLOSED,
IoEventType.SESSION_IDLE,
IoEventType.SESSION_OPENED

其實ExecutorFilter 的工作機制很簡單,就是在調用下一個過濾器的事件方法時,把其交給Executor 的execute(Runnable runnable)方法來執行,其實你自己在IoHandler 或者某個過濾器的事件方法中開啓一個線程,也可以完成同樣的功能,只不過這樣做,你就失去了程序的可配置性,線程調用的代碼也會完全耦合在代碼中。但要注意的是絕對不能開啓線程讓其執行sessionCreated()方法。如果你真的打算使用這個ExecutorFilter,那麼最好想清楚它該放在過濾器鏈的哪個位置,針對哪些事件做異步處理機制。一般ExecutorFilter 都是要放在ProtocolCodecFilter 過濾器的後面,也就是不要讓編解碼運行在獨立的線程上,而是要運行在IoProcessor 所在的線程,因爲編解碼處理的數據都是由IoProcessor 讀取和發送的,沒必要開啓新的線程,否則性能反而會下降。一般使用ExecutorFilter 的典型場景是將業務邏輯(譬如:耗時的數據庫操作)放在單獨的線程中運行,也就是說與IO 處理無關的操作可以考慮使用ExecutorFilter 來異步執行。

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