一、TCP 粘包现象
TCP 粘包是指发送方发送了若干数据包,但是到达接收方之后,数据包的内容全部 “粘连” 在了一起,每个数据包的数据结尾直接和下个数据包的数据连在了一起,无法正常区分。
下面的图片展示了一个 TCP 粘包现象,正常的 3 个数据包因为粘包,被连在了一起,看起来像是一个数据包,结果就很感人了 ...
二、TCP 粘包/拆包 原因
TCP 采用 “异步” 方式发送应用数据,也就是说,当应用程序中调用Send(packet)发送数据时,虽然 Send 函数会立即返回,但是数据并不一定已经到达通信对方了。应用数据具体什么时候发,由应用层下面的传输层 TCP 说了算,TCP 使用了 3 个主要机制 (确认与重传、滑动窗口流量控制、拥塞控制) 来实现可靠性传输,保证应用数据的可靠传输和应用层数据发送顺序和到达顺序一致的语义保证。
下面来展开说一下可能导致 粘包/拆包 问题的原因。
1. 面向字节流的工作特性
TCP 作为传输层,并不了解 (也不关心) 应用层数据的上下文含义,它只会根据通信双方约定的MSS[1]对发送缓冲区的数据包进行拆分。
所以在应用层的视角来看,一个完整的数据包 (可能是一段聊天文字、一个图片、一个视频) 可能会经历不同的发送过程:
2. 缓冲机制
TCP 在发送数据时,会将数据放入发送缓冲区;在接收数据时,会将数据放入接收缓冲区。TCP 会尽可能地将发送缓冲区中的数据打包成一个或多个数据包发送出去,而接收方在读取数据时,也会尽量将接收缓冲区中的数据全部读取出来。这种机制可能导致发送方一次发送的多个数据包被接收方一次性读取,从而引发粘包问题。
为了尽可能提升发送数据和接受处理数据的性能,作为发送方来讲:
作为接收方来讲:
3. Nagle 算法
Nagle 算法原理: 发送方已经发送数据还未被接收方确认之前,期间如果又有小数据生成,先把小数据收集起来,凑满一个 MSS (最大报文段大小) 或者收到接收方 Ack 后再一起发送。通过将小数据包积累成较大的数据包后再发送,从而提高网络效率。
很显然,根据 Nagle 算法的工作机制,在频繁发送小的数据包时 (例如 Telnet, SSH 终端),会产生粘包现象。
下面是在 Go 语言中关闭 TCP Nagle 算法的示例代码。
package mainfunc main() {conn, _ := net.Dial("tcp", "dbwu.tech:443")fd := conn.(*net.TCPConn).File()// 关闭 Nagle 算法syscall.SetsockoptInt(int(fd.Fd()), syscall.IPPROTO_TCP, syscall.TCP_NODELAY, 1)}
❓ 单纯关闭 Nagle 算法并不能解决粘包问题,读者思考一下为什么?