ch3.ng

Netty4 TCP协议之自定义解码器,解决分包和粘包


socket流顾名思义它像一种流体,流体的特征是什么?不清楚它的开始和结尾嘛!TCP不了解上层业务数据的具体含义,它只会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

好,现在我们有这样一个需求,客户端要向服务器发送两条指令分别是:

  1. ABCDE
  2. FGHIJ

那么最终我们可能会出现以下这四种情况

  1. 我们分别收到两条指令 ABCDE ***** FGHIJ
  2. 我们也能受到两条指令 ABCDEFG ***** HIJ
  3. 我们也能收到两条指令 ABC ***** DEFGHIJ
  4. 我们只能收到一条指令 ABCDEFGHIJ

当然我们肯定是希望只出现第一种情况,但是现实并没有这么美好。解决办法。。。

肯定是有的。

两种解决方法

  1. 在每发一条指令后我们加一个特殊字符来作为分割位。比如:\r\n但是这样的方式不太好,每次都要每个字节的去寻找这样的结束符,所以性能肯定不会好到哪里去。
  2. 第二种方式像下面这样

假如我们定义了一个这样的传输协议

**
 *
 * +--------+----------+-----------+------+----------+
 * |  包头   |  一级指令 |  二级指令  |  长度 |  数据     |
 * +--------+----------+----------+-------+----------+
 * |  4byte |  2byte   |   2byte  | 4byte |more byte |
 * +--------+----------+----------+-------+----------+
 *

先解释一下:

包头:我们定义一个四个字节的包头,这个包头我们竟可能的让它特殊化比如:

01010101010101010101010101010101

这样的数据作为包头,其他的数据出现这样的数据的可能性将很低,它的特殊性使我们能很正确的找到这个包头。

一级指令和二级指令: 使我们能定义出丰富多样的指令

长度: 这个长度可以表示整个数据的长度,也可以表示剩余数据的长度。没有什么特别的。

数据:参与业务交互的实际数据。

废话不多说先上干货

代码

public class RequestDecoder extends ByteToMessageDecoder {

    private static int BASE_LENTH = 11;
    private static int HEAD = -1431655766;

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        while (true){
            //判断当前缓存中的数据是否满足一条指令的最小基础数据
            if(byteBuf.readableBytes() >= BASE_LENTH){
//                int beginIndex;
                //寻找包头
                while (true){
                    //记录数据包开始的指针位置
                    byteBuf.markReaderIndex();
//                    beginIndex = byteBuf.readerIndex();
                    //判断是否是属于包头
                    if(byteBuf.readInt() == HEAD){
                        break;
                    }
                    //不是包头
                    //指针复位
                    byteBuf.resetReaderIndex();
                    // 缓存指针向后移动一个字节
                    byteBuf.readByte();
                    //判断当前缓存是否依然满足一条指令的最小长度
                    if(byteBuf.readableBytes() < BASE_LENTH){
                        return;
                    }
                }
                //找到了当前的包头
                byte cmdOf1st = byteBuf.readByte();
                short cmdOf2nd = byteBuf.readShort();
                int dataLength = byteBuf.readInt();
                //检验当前指令的数据长度
                if(dataLength < 0){
                    channelHandlerContext.channel().close();
                }
                //判断当前数据是否已经到齐
                if(byteBuf.readableBytes() < dataLength){
                    //没到齐
                    //复位到最开始的地方
                    byteBuf.resetReaderIndex();
//                    byteBuf.readerIndex(beginIndex);
                    return;
                }
                //数据到齐了
                byte[] data = new byte[dataLength];
                byteBuf.readBytes(data);

                Request request = new Request();
                request.setFirstOrder(cmdOf1st);
                request.setSecondOrder(cmdOf2nd);
                request.setData(data);
                list.add(request);
            }else{
                break;
            }
        }
        return;
    }

}

以上的代码基于netty4。 其他的版本大致也都相同。
代码中都有注释,也就不过多的解释了。