Apache mina 入門(五) —— 斷包,粘包問題解決

通過前面的文章Apache mina 入門(一)— 基礎知識,我們可以知道:Apache Mina Server 是一個網絡通信應用框架,也就是說,它主要是對基於TCP/IP、UDP/IP協議棧的通信框架(當然,也可以提供JAVA 對象的序列化服務、虛擬機管道通信服務等),Mina 可以幫助我們快速開發高性能、高擴展性的網絡通信應用,Mina 提供了事件驅動、異步(Mina 的異步IO 默認使用的是JAVA NIO 作爲底層支持)操作的編程模型。
通過Apache Mina 入門 (二)—— 異步通信機制
Apache mina 入門(三) —— 客戶端同步通訊
Apache mina 入門(四) —— 客戶端長連接方式實現斷線重連監聽
我們可以熟練的處理程序中出現的問題,但Mina 其實還有一個嚴重的問題,那就是斷包問題 —— 自定義或者默認的解碼器每次讀取緩衝的數據是有限制的,即ReadBufferSize的大小,默認是2048個字節,當數據包比較大時將被分成多次讀取,造成斷包。雖然我們有一種粗暴的解決方案,那就是通過acceptor.getSessionConfig().setReadBufferSize(newsize)這種方式來增加默認容量【這樣導致的後果就是數據的處理效率變慢】

在mina中,一般的應用場景用TextLine的Decode和Encode就夠用了(TextLine的默認分割符雖然是\r\n,但其實分隔符是可以自己指定的,如:newTextLineDecoder(charset, decodingDelimiter);)
所以,當我們接收的數據的大小不是很固定,且容易偏大的時候,默認的TextLine就不適合了。這時我們在解析之前就需要判斷數據包是否完整,這樣處理起來就會非常麻煩。那麼Mina 中幸好提供了CumulativeProtocolDecoder
類,從名字上可以看出累積性的協議解碼器,也就是說只要有數據發送過來,這個類就會去讀取數據,然後累積到內部的IoBuffer 緩衝區,但是具體的拆包(把累積到緩衝區的數據解碼爲JAVA 對象)交由子類的doDecode()方法完成,實際上CumulativeProtocolDecoder就是在decode()方法中反覆的調用暴漏給子類實現的doDecode()方法。
具體執行過程如下所示:
A. 你的doDecode()方法返回true 時,CumulativeProtocolDecoder 的decode()方法會首先判斷你是否在doDecode()方法中從內部的IoBuffer 緩衝區讀取了數據【源碼是根據當前數據的讀取位置與之前的位置進行比對,如果相等,則代表未從緩存中讀取數據,反之,則已從緩存區讀取數據】,如果沒有,則會拋出非法的狀態異常【throw new IllegalStateException(“doDecode() can’t return true when buffer is not consumed.”);】,也就是你的doDecode()方法返回true 就表示已經消費過內部的IoBuffer 緩衝區的數據。如果驗證通過,那麼CumulativeProtocolDecoder會檢查緩衝區內是否還有數據未讀取,如果有就繼續調用doDecode()方法【從當前讀取位置繼續往下讀取】,沒有就停止對doDecode()方法的調用。
B. 當你的doDecode()方法返回false 時,CumulativeProtocolDecoder 會停止對doDecode()方法的調用,但此時如果本次數據還有未讀取完的,就將含有剩餘數據的IoBuffer 緩衝區保存到IoSession 中,以便下一次數據到來時可以從IoSession 中提取合併。如果發現本次數據全都讀取完畢,則清空IoBuffer 緩衝區。
簡而言之,當你認爲讀取到的數據已經夠解碼了,則將該部分數據先進行解碼,然後在判斷緩存去是否還有數據,如果有就返回true,否則就返回false。這個CumulativeProtocolDecoder其實最重要的工作就是幫你完成了數據的累積,因爲這個工作是很煩瑣的。

主要代碼爲解碼器:

/**
 * 解碼器
 * 繼承CumulativeProtocolDecoder 類,實現對mina 斷包,粘包問題解決
 * 主要思路:
 * 1、判斷當前緩存去中是否存在數據,如果存在,則進行後續處理。
 * 2、獲取報文數據【報文規範爲:長度 (2個字節) + 方法編號(1個字節) + 內容】,先獲取長度,判斷緩存區剩餘數據長度與報文長度是否相等,如果不相等,代表報文數據不完整,
 *      返回 false,需要再次從緩存去讀取
 * 3、相等,則獲取方法編號,內容,獲取到該內容後,則先將該部分完整數據送給handler處理,
 * 4、判斷緩存區是否還存在數據,如果存在,代表該報文後面還粘包了。則返回true.
 * @author liuc
 * @date 2017-12-22
 *
 */
public class ByteArrayDecoder extends CumulativeProtocolDecoder {

    private static final Logger logger = Logger.getLogger(NSProtocalDecoder.class);
    private final Charset charset = Charset.forName("GBK");

    // 請求報文的最大長度 100k
    private int maxPackLength = 102400;

    public int getMaxPackLength() {
        return maxPackLength;
    }

    public void setMaxPackLength(int maxPackLength) {
        if (maxPackLength <= 0) {
            throw new IllegalArgumentException("請求報文最大長度:" + maxPackLength);
        }
        this.maxPackLength = maxPackLength;
    }

    @Override
    protected boolean doDecode(IoSession session, IoBuffer in,
            ProtocolDecoderOutput out) throws Exception {

        //1、先判斷數據是否存在
        if(in.remaining() > 0){
            in.mark();
            //1、獲取長度
            byte[] sizeBytes = new byte[2];
            in.get(sizeBytes,0,2);//讀取2字節 
            byte[] length_byte_arr = new byte[]{0,0,0,0};
            length_byte_arr[2] = sizeBytes[0];
            length_byte_arr[3] = sizeBytes[1];
            //獲取長度
            int length = ByteTools.byteArrayToInt(length_byte_arr);
            //length -2 中 減2是因爲讀取了兩個字節的長度
            if(in.remaining() < length -2 ){
                in.reset();
                //代表報文不完整,需要再次讀取緩衝區的數據
                return false;
            }
            //獲取方法編號
            byte[] funcidBytes = {in.get()};
            byte[] funcid_byte_arr = new byte[]{0,0,0,0};
            funcid_byte_arr[3] = funcidBytes[0];
            //獲取長度
            int funcid = ByteTools.byteArrayToInt(funcid_byte_arr);
            System.out.println("3=================================="+in.remaining());
            //獲取內容
            //讀取報文正文內容
            int oldLimit = in.limit();
            logger.debug("limit:" + (in.position() + length));
            //當前讀取的位置 + 總長度  - 前面讀取的字節長度 
            in.limit(in.position() + length - 3);
            String content = in.getString(charset.newDecoder());
            in.limit(oldLimit);
            logger.debug("報文正文內容:" + content);

            BaseMessageForClient message = new BaseMessageForClient();
            message.setLength(length);
            message.setFuncid(funcid);
            message.setContent(content);
            out.write(message);
            //代表着後續還有包,可以重新進行讀取,但前一個包已經傳送給handler進行處理了
            //該問題稱爲粘包
            if(in.remaining() > 0){
                return true;
            }
        }
        return false;
    }

}

編碼器代碼如下:

public class ByteArrayEncoder extends ProtocolEncoderAdapter {

    public void encode(IoSession session, Object message,
            ProtocolEncoderOutput out) throws Exception {
        // TODO Auto-generated method stub
        BaseMessageForServer basemessage = (BaseMessageForServer)message;

        IoBuffer buffer = IoBuffer.allocate(256);
        buffer.setAutoExpand(true);

        buffer.put(ByteTools.intToByteArray(basemessage.getLength()+3, 2));//包長
        buffer.put(ByteTools.intToByteArray(basemessage.getFuncid(), 1));//方法編號
        buffer.put(basemessage.getContent().getBytes());//內容
        buffer.flip();

        out.write(buffer);
        out.flush();

        buffer.free();

    }

}

源碼下載地址:http://download.csdn.net/download/u012151597/10168974

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