深入学习Netty(5)——Netty是如何解决TCP粘包/拆包问题的?

深入学习Netty(5)——Netty是如何解决TCP粘包/拆包问题的?

前言

  学习Netty避免不了要去了解TCP粘包/拆包问题,熟悉各个编解码器是如何解决TCP粘包/拆包问题的,同时需要知道TCP粘包/拆包问题是怎么产生的。

  在此博文前,可以先学习了解前几篇博文:

  • 深入学习Netty(1)——传统BIO编程
  • 深入学习Netty(2)——传统NIO编程
  • 深入学习Netty(3)——传统AIO编程
  • 深入学习Netty(4)—-Netty编程入门

  参考资料《Netty In Action》、《Netty权威指南》(有需要的小伙伴可以评论或者私信我)

  博文中所有的代码都已上传到Github,欢迎Star、Fork

 


 

一、TCP粘包/拆包

1.什么是TCP粘包/拆包问题?

引用《Netty权威指南》原话,可以很清楚解释什么是TCP粘包/拆包问题。

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

  一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是TCP粘包/拆包。

假设服务端分别发送两个数据包P1和P2给服务端,由于服务端读取一次的字节数目是不确定的,所以可能会发生五种情况:

              

  • 服务端分两次读取到两个独立的数据包;
  • 服务端一次接收到两个数据包,P1和P2粘合在一起,被称为TCP粘包;
  • 服务端分两次读取到两个数据包,第一次读取到完整的P1包和P2包的部分内容,第二次读取到P2包的剩余内容,被称之为TCP拆包;
  • 服务端分两次读取到两个数据包,第一次读取到了P1包的部分内容P1_1,第二次读取到了P1包的剩余内容P1_2和P2包的整包
  • 其实还有最后一种可能,就是服务端TCP接收的滑动窗非常小,而数据包P1/P2非常大,很有可能服务端需要分多次才能将P1/P2包接收完全,期间发生多次拆包。

2.TCP粘包/拆包问题发生的原因

 TCP是以流动的方式传输数据,传输的最小单位为一个报文段(segment)。主要有如下几个指标影响或造成TCP粘包/拆包问题,分别为MSS、MTU、缓冲区,以及Nagle算法的影响。

(1)MSS(Maximum Segment Size)指的是连接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),超过这个量要分成多个报文段。

(2)MTU限制了一次最多可以发送1500个字节,而TCP协议在发送DATA时,还会加上额外的TCP Header和IP Header,因此刨去这两个部分,就是TCP协议一次可以发送的实际应用数据的最大大小,即MSS长度=MTU长度-IP Header-TCP Header。

(3)TCP为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制,来接收数据。

由于有上述的原因,所以会造成拆包/粘包的具体原因如下:

(1)拆包发生原因

  • 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包
  • 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包

(2)粘包发生原因

  • 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包

二、TCP粘包/拆包问题解决策略

1.常用的解决策略

由于底层TCP是无法理解上层业务数据,所以在底层是无法保证数据包不被拆分和重组的,所以只能通过上层应用协议栈设计来解决

(1)消息定长,例如每个报文的大小固定长度200字节,不够空位补空格

(2)在包尾增加回车换行符进行分割,例如FTP协议

(3)将消息分为消息头和消息体,消息头中包含表示消息总长度的字段

(4)更复杂的应用层协议

2.TCP粘包异常问题案例

(1)TimeServerHandler

public class TimeServerHandler extends ChannelInboundHandlerAdapter {
    private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());


    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());
        // 每收到一条消息计数器就加1, 理论上应该接收到100条
        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 = currentTime + System.getProperty("line.separator");
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.writeAndFlush(resp);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warning("Unexpected exception from downstream: " + cause.getMessage());
        ctx.close();
    }
}
hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » 深入学习Netty(5)——Netty是如何解决TCP粘包/拆包问题的?