Mina对粘包和断包的处理
mina对编解码的支持,在解码过程中,不得不面对的一个问题就是TCP的粘包和断包,先说下什么是粘包和断包。
TCP通讯是面向数据流的通讯,我们将数据流理解为一支竹竿,数据包就相当于竹竿中的每一节,那么我们的解码过程就相当于对竹竿进行分解的过程。竹竿就是多个数据包的“粘包”,断包就是指竹节中间断开,我们需要将它拼接成为一个完整的竹节,如果不能拼接起来就要废弃这部分。
粘包:
断包:
对粘包的处理相对比较简单,只需要依据数据包的格式进行数据流的分割即可;对于断包的处理我们需要将断包的数据保存起来,等待接收下次的数据进行拼接。
通常情况下我们要考虑粘包和断包同时出现的情况下的解码代码编写。有两种实现方式:
1.继承CumulativeProtocolDecoder类,实现doDecode方法。
2.实现ProtocolDecoder接口,自己解决粘包和断包的问题。
如果子类返回false,父类doDecode就结束递归调用,否则,继续调用子类的doDecode方法。
1.继承CumulativeProtocolDecoder类,实现doDecode方法。
doDecode方法一方面判断数据包是否符合解码要求(数据包可能过短,数据包格式不合要求都可能不能通过解码要求),不符合刚返回false;另一方面对于符合解码要求的数据进行数据解码,并返回true
源码:
- public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
- if (!session.getTransportMetadata().hasFragmentation()) {
- while (in.hasRemaining()) {
- // 判断是否符合解码要求,不符合则中断并返回
- if (!doDecode(session, in, out)) {
- break;
- }
- }
- return;
- }
- boolean usingSessionBuffer = true;
- // 取得上次断包数据
- IoBuffer buf = (IoBuffer) session.getAttribute(BUFFER);
- // If we have a session buffer, append data to that; otherwise
- // use the buffer read from the network directly.
- if (buf != null) { // 如果有断包数据
- boolean appended = false;
- // Make sure that the buffer is auto-expanded.
- if (buf.isAutoExpand()) {
- try {
- // 将断包数据和当前传入的数据进行拼接
- buf.put(in);
- appended = true;
- } catch (IllegalStateException e) {
- // A user called derivation method (e.g. slice()),
- // which disables auto-expansion of the parent buffer.
- } catch (IndexOutOfBoundsException e) {
- // A user disabled auto-expansion.
- }
- }
- if (appended) {
- buf.flip();// 如果是拼接的数据,将buf置为读模式
- } else {
- // Reallocate the buffer if append operation failed due to
- // derivation or disabled auto-expansion.
- //如果buf不是可自动扩展的buffer,刚通过数据拷贝的方式将断包数据和当前数据进行拼接
- buf.flip();
- IoBuffer newBuf = IoBuffer.allocate(buf.remaining() + in.remaining()).setAutoExpand(true);
- newBuf.order(buf.order());
- newBuf.put(buf);
- newBuf.put(in);
- newBuf.flip();
- buf = newBuf;
- // Update the session attribute.
- session.setAttribute(BUFFER, buf);
- }
- } else {
- buf = in;
- usingSessionBuffer = false;
- }
- for (;;) {
- int oldPos = buf.position();
- boolean decoded = doDecode(session, buf, out);// 进行数据的解码操作
- if (decoded) {
- // 如果符合解码要求并进行了解码操作,
- // 则当前position和解码前的position不可能一样
- if (buf.position() == oldPos) {
- throw new IllegalStateException("doDecode() can't return true when buffer is not consumed.");
- }
- // 如果已经没有数据,则退出循环
- if (!buf.hasRemaining()) {
- break;
- }
- } else {// 如果不符合解码要求,则退出循环
- break;
- }
- }
- // if there is any data left that cannot be decoded, we store
- // it in a buffer in the session and next time this decoder is
- // invoked the session buffer gets appended to
- if (buf.hasRemaining()) {
- if (usingSessionBuffer && buf.isAutoExpand()) {
- buf.compact();
- } else {
- //如果还有没处理完的数据(一般为断包),刚将此数据存入session中,以便和下次数据进行拼接。
- storeRemainingInSession(buf, session);
- }
- } else {
- if (usingSessionBuffer) {
- removeSessionBuffer(session);
- }
- }
- }
上面的处理过程可以这样理解:
1.取得断包数据,如果有断包数据,就和当前数据拼接。
2.进行数据解码操作。
3.将可以进行解码操作的数据解码完成后,如果还有数据,则将剩余数据存入session中,等待下次数据到来,从步骤1开始再次执行。
public boolean doDecode(IoSessionsession, IoBufferin, ProtocolDecoderOutputout) {
//判断接收到的数据是否为空
//方框中的代码并没有真正地进行数据解码,只是读取了数据包的前八位,用来决定buffer的大小和数据包的长度
if (in.remaining() > 0) { //声明byte数组,用来储存前八字节 byte[] sizeBytes = newbyte[8]; //标记当前buffer指针位置 in.mark(); System.out.println("数据占用的buffer大小: " + in.remaining()); //移动指针位置,得到buffer中的前八字节(5-8字节为数据包长度) in.get(sizeBytes, 0, 8); System.out.println("数据占用的buffer大小: " + in.remaining()); //转码byte到int,获得数据长度,存入size变量 intsize = (int) DataTypeChangeHelper.unsigned4BytesToInt(sizeBytes, 4); System.out.println("数据长度: " + size); //重置buffer指针位置到之前mark()方法标记的位置(即pos=0) in.reset(); System.out.println("数据占用的buffer大小: " + in.remaining()); |
//程序继续运行,进入if判断
//如果传入的是一个完整的数据包,并小于2MB时,in.remaining(buffer大小)应该等于数据包长度(size的值)
//if情况(断包):当buffer的大小比数据包size小的时候,说明这是一个断包,此时数据应传入session中,程序返回false,结束此方法,等待下一个数据包的传入。
if (in.remaining() <size) {
return false; in.remaining()就是一个累加器,假设说1G就是数据包的大小,从2M逐渐累计到1G后,才提交到Session的Buffer, Session的Buffer就是按照实际的数据包大小Size来判断是否对这个数据包的大小进行一次编码。return false表示不提交到Session的Buffer,继续让父类调用数据包来进行累计达到1G.
}
//else情况:如果数据包是完整的,凑够了1G,开始进行真正的数据解码,进入else
else {
//创建byte数组,长度为真正的数据包长度
byte[] bytes = newbyte[size];
System.out.println("else size: " + size);
System.out.println("else before: " + in.remaining());
//读取全部数据包内容到buffer
in.get(bytes, 0, size);(需要看源码,理解通过session的1G的BUFFER不断加载2M的数据包。sessionBuf.put(in);
System.out.println("else after: " + in.remaining());
//将buffer中的内容解码,转换成用户可以读取的packagedata
PackageDatapack = packetFilterComponent.getDataFromBuffer(IoBuffer.wrap(bytes)); out.write(pack); |
//if判断(粘包):当buffer中转换完数据包长度的字节后,正常情况下,应该剩余0个字节;
//当剩余超过0个字节时,说明有粘包现象,此时程序应该返回true,让父类程序再执行一遍,进行下一次解析,再需要父类调一次。
if (in.remaining() > 0) {
return true;// in.remaining()是编码之后Session的buffer还剩下数据,就是粘了下一个数据包的一部分,需要让父类继续调用接收数据包。
}
}
}
returnfalse;
}
收包流程
1. 先预设一个buffer(2M),开始收数据(假设数据1G)
2. buffer收到数据后马上放到IOsession,等够了一个1G后进行统一解码
实际测试中发现,buffer也是变化的,最多能到几十M
断包的含义:size大小是3M,实际来了2.5M,就等下一次再来0.5M
粘包的含义:处理完buffer里面的后发现,还有其他的包来了,就等下次来了一起处理。