Netty-TCP粘包/拆包解决之道

《netty权威指南》学习笔记

TCP粘包/拆包

定义
TCP是个“流”协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分。所以,一个完成的包可能会被TCP拆分为多个包,也有可能把多个小包封装成一个大包,这就是所谓的TCP粘包河拆包问题。
这里写图片描述

解决策略

  • 消息定长:例如每个报文大小固定为200字节,如果不够,空位补空格
  • 在包尾增加回车换行符进行分割,如FTP协议
  • 将消息分为消息头和消息体,消息头中包含消息总长度(或者消息体总长度);–通常的设计思路是在消息头中使用int32来表示消息的总长度;
  • 更复杂的应用协议

TCP粘包/拆包-异常案例

我们来复现异常比较经典的TCP粘包/拆包现场;
客户端循环发送100次请求消息,每发送一条消息就刷新一次,保证每条消息都会被写入Channel中,按照我们的设计,服务端应该会接收到100条查询请求;
客户端核心代码ChannelHandle

@ChannelHandler.Sharable
class TimeClientHandler extends ChannelHandlerAdapter {
    //用来记录反馈的次数
    private int repeat ;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //每次发送消息,结尾增加一个换行符
        byte[] req = ("QUERY TIME ORDER"+System.getProperty("line.separator")).getBytes();
        ByteBuf firstMsg = null;
        //循环发送一百次 请求消息
        for (int i = 0; i < 100; i++) {
            firstMsg = Unpooled.copiedBuffer(req);
            ctx.channel().writeAndFlush(firstMsg);
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] resp = new byte[buf.readableBytes()];
        buf.readBytes(resp);
        String body = new String(resp, "UTF-8").substring(0,resp.length - System.getProperty("line.separator").length());
        System.out.println("Now is:" + body+"; repeat is:" + ++repeat);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.channel().close();
    }
}

服务端核心代码Handler

@ChannelHandler.Sharable
class TimeServerHandler extends ChannelHandlerAdapter {
    //用来记录请求的次数
    private int counter ;
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        //读取消息时,剔除尾部的换行符
        String body = new String(req, "UTF-8").substring(0,req.length - System.getProperty("line.separator").length());
        System.out.println("The time server receive order:" + body + " ; the counter is:"+ ++counter);
        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";

        //返回消息时,结尾增加换行符
        currentTime += System.getProperty("line.separator");
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.writeAndFlush(resp);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

执行的结果如下图:
这里写图片描述
虽然客户端发送了100次请求,但是服务端最终只接收了两次请求,说明客户端发送消息时发生了粘包,由于服务端只接收了两次请求,因此服务端也只会对客户端发送两条应答;
但是从客户端的打印消息来看,客户端只接收了一条应答消息,说明服务端应答消息也发生了应答。

Netty解决TCP粘包/拆包之道

1. LineBasedFrameDecoder
直接在客户端和服务端的handler中增加LineBasedFrameDecoder解码器即可。

ch.pipeline().addLast(new LineBasedFrameDecoder(1024));

LineBasedFrameDecoder的工作原理:它依次遍历ByteBuf中可读字节,判断是否包含有”\n”或者”\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节组成一行。它是以换行符为结束标志的解码器,支持携带结束符和不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度仍然没有发现换行符,就会抛出异常(TooLongFrameException),同时忽略掉之前读取到的异常码流。
这里写图片描述

LineBasedFrameDecoder构造函数-分析

public LineBasedFrameDecoder(final int maxLength) {
  this(maxLength, true, false);
}
/**
  * maxLength 最大长度
  * stripDelimiter 解码帧是否应该去掉分隔符
  * failFast 
  *     ture:无论maxLength部分有没有被read,直接抛出异常;
  *     false:after被读取后抛出异常
  * 
  */
public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast) { }

2.DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder是以指定的分隔符做结束标识的消息解码器。

ByteBuf delimiter =Unpooled.copiedBuffer("$$_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));

上述以”$$_”作为分隔符。
这里写图片描述
DelimiterBasedFrameDecoder构造函数-分析

 public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {
        this(maxFrameLength, true, delimiter);
    }

public DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, ByteBuf delimiter);

//支持多个分隔符
public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf... delimiters)

3.FixedLengthFrameDecoder
FixedLengthFrameDecoder是以固定长度解码器,它能够按照指定的长度对消息进行解码。无论一次接收多少数据报,他都会按照构造函数中指定的长度进行解码,如果是半包消息,则会缓存半包消息,等待下个包到达之后进行拼包,知道读取到一个完整的包。

ch.pipeline().addLast(new FixedLengthFrameDecoder(3));

这里写图片描述

猜你喜欢

转载自blog.csdn.net/it_freshman/article/details/80415553