您的位置:

Netty拆包粘包处理

一、Netty粘包解决方案

当我们使用Netty进行数据传输时,可能会遇到粘包现象,这种现象在单个包大小很小时很常见。但还是有方法来解决这个问题。下面介绍两种最常见的解决方案。

1、FixedLengthFrameDecoder

该解码器固定读取指定消息长度的数据,如果每条消息都是固定长度的,则适合使用此解码器。

2、DelimiterBasedFrameDecoder

该解码器依赖于分隔符对接收到的数据进行拆分。常用的分隔符有"\r\n"、“\n”、“$”等。当每条消息中都包含有分隔符时,适合使用此解码器。

二、Netty自定义粘包拆包

有些情况下,我们的数据并不能依靠固定长度或分隔符区分消息边界,这时候就需要自定义粘包解包。下面给出代码示例。

public class MyDecoder extends MessageToMessageDecoder<ByteBuf> {
   @Override
   protected void decode(ChannelHandlerContext ctx, ByteBuf in,
                         List<Object> out) throws Exception {
       while (in.readableBytes() >= 4) { //判断是否有一个完整的消息
           int length = in.readInt(); //读取消息长度
           if (in.readableBytes() < length) {//不足一个整包,重置读指针
               in.readerIndex(in.readerIndex() - 4);
               return;
           }
           out.add(in.readBytes(length)); //读取完整的数据包
       }
    }
}

以上示例中,我们继承MessageToMessageDecoder类,并重写decode方法,实现自定义解码。在这个例子中,我们读取了前4个字节作为数据包长度,然后根据长度读取完整的数据包。

三、Netty分包粘包处理

1、使用LengthFieldBasedFrameDecoder

该解码器先从ByteBuf中读取指定长度的整型值,该整型值表示实际数据的长度。基于长度解码器,可以解决TCP协议中粘包和分包问题。下面给出代码示例。

public class MyDecoder extends LengthFieldBasedFrameDecoder {

    public MyDecoder() {
        super(ByteOrder.LITTLE_ENDIAN, 1024, 0, 4, 0, 4, true);
//        参数说明:LITTLE_ENDIAN: should length field byte order be littleEndian
//                  1024: maxFrameLength :表示数据帧最大长度
//                  0: lengthFieldOffset :表示数据长度字段值的起始位置,因为我们在发送数据时在数据包的最前面添加了一个4字节的int数据,所以这里为0
//                  4:lengthFieldLength :表示长度字段所占的字节数
//                  0:lengthAdjustment:一个长度调整值,让解码器从第一个字节开始;这里为0
//                  4:initialBytesToStrip:去掉请求头的几个字段长度;这里为4,因为前面我们在请求头加上了4个字节的int数据表示真实数据的长度信息
//                  true:failFast :如果该参数设置为 true,则表示如果帧长度超过长度限制,就会立即抛出一个 TooLongFrameException,而不是等待后续字节到达,这样可以防止太多的资源浪费在非预期包上
    }

    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        ByteBuf frame = (ByteBuf) super.decode(ctx, in);
        if (frame == null) {
            return null;
        }
        int realLength = frame.readInt();
        ByteBuf data = frame.slice(frame.readerIndex(), realLength);
        frame.release();
        return data;
    }
}

2、使用MessageToMessageCodec

该编码器先编码,再解码,使用起来相对比较复杂。下面给出代码示例。

public class MyMessageCodec extends MessageToMessageCodec<ByteBuf, Object> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {

        ByteBuf byteBuf = ctx.alloc().buffer();
        byteBuf.writeInt(0);  // 占位长度
        byteBuf.writeBytes(msg.toString().getBytes(CharsetUtil.UTF_8));
        byteBuf.setInt(0, byteBuf.readableBytes() - 4); // 记录实际长度
        out.add(byteBuf);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        int length = in.readInt();
        byte[] bytes = new byte[length];
        in.readBytes(bytes);
        out.add(new String(bytes, CharsetUtil.UTF_8));
    }
}

以上代码实现了自定义编解码器。在encode方法中,我们在写入真实数据之前先写入了4个字节的int类型数据,用于记录真实数据的长度信息。在decode方法中,先读取4个字节的int信息,再按照该长度读取真实的数据。