概述
在早先博客我通过 NIO 实现时间服务器时曾提到:TCP 可能造成粘包/拆包问题,但由于之前的示例相对简单,因此没有做预防处理。本篇博客我就来简单聊一下 TCP 粘包/拆包 问题以及 Netty 如何解决该问题。
什么是 TCP 粘包/拆包问题
TCP 是一种流传输协议,所谓流传输就是指数据包之间没有界限,所有数据都是连在一起的。顶层应用层通过 TCP 发送数据时,TCP 并不了解该数据具体含义,它只会根据当前缓冲区的实际情况进行数据包的划分。这样就可能导致一个完整的业务数据包被拆分为多个TCP包发送或几个完整的数据包黏在一个TCP包中发送,这就是 TCP 的粘包/拆包问题。
举个例子:客户端通过 TCP 发送两个数据包 D1、D2 到服务端,此时对于服务端来说可能存在以下五种情况:
- 服务端分两次读取到两个独立的数据包,分别是 D1、D2,没有粘包、拆包问题
- 服务端一次接收到两个数据包,分别是 D1、D2,发生粘包问题
- 服务端分两次读取到两个数据包,第一个数据包记录完整的 D1 数据和 D2 部分数据,第二个数据包记录剩余 D2 数据,发生拆包问题
- 服务端分两次读取到两个数据包,第一个数据包记录部分 D1 数据,第二个数据包记录剩余 D1 数据和完整的 D2 数据,发生拆包问题
- 服务端分多次读取到多个数据包,每个数据包只记录部分数据,发生拆包问题
对于 TCP 不太了解的读者可以 点击这里 查看我之前关于 TCP 整理的博客,下文很多概念会涉及 TCP 相关知识。
粘包/拆包问题发送的原因
发送粘包/拆包问题原因主要集中在以下三点:
- 应用程序 write() 写入的字节数大于套接口发送缓冲区大小:发送的数据过大,超过此时发送缓冲区大小,此时就必须将数据拆分成多个包发送
- 进行 MSS 大小的 TCP 分段:MSS 即最大报文长度,即要发送的数据大小已超过最大的报文长度
- 以太网帧的 payload 大于 MTU 进行 IP 分片:发送的数据过大,超过 IP 层的最大传输单元
MSS(最大报文长度)和 MTU(最大传输单元)的区别和联系:
1、MTU 应用于数据链路层,不针对某个协议,MSS 主要针对 TCP 协议
2、MTU 限制了数据链路层可以传输的数据大小,因此间接影响了上层(网络层)的数据包大小
3、由于 MSS 受 MTU 限制,因此 MSS 值一定小于 MTU
根据 TCP 的流量控制,MSS 会随着网络环境的改变适应性改变,其中它的值受客户端和服务端共同影响。
粘包/拆包问题的解决策略
由于底层 TCP 无法理解上层应用层数据,所以在底层是无法完全解决粘包/拆包问题的。一般只能通过上层应用层来解决:
- 消息定长:固定每个报文的大小长度
- 包尾增加换行符,但此时需要特别注意:应用数据中不能包含换行符
- 消息分为消息头和消息体,消息头中包含属性记录数据长度
- 更复杂的应用层协议
总得来说,解决该问题的核心思路在于让 TCP 知道数据包之间的间隔。
粘包异常示例
这里我给出可能导致粘包问题的代码,其中省略 Netty 客户端、服务端代码,只保留具体 I/O 处理类:
客户端处理类:
public class TimeClientHandler extends ChannelHandlerAdapter {
/**
* 用来记录客户端发送的记录数
*/
private int counter;
private byte[] bytes = "QUERY TIME ORDER\n".getBytes();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = null;
for (int i = 0; i < 100; i++)