深入学习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(); } }