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

你可能感兴趣的:(Apache,Mina)