Apache mina 入门(五) —— 断包,粘包问题解决

来源:互联网 发布:java调用通用短信接口 编辑:程序博客网 时间:2024/06/05 14:39

通过前面的文章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