粘包问题
我们知道,TCP 协议是面向连接的、可靠的、基于字节流的传输层通信协议。其实,TCP作为面向流的协议,不存在“粘包问题”。
什么是粘包
其实这里面有两种含义:
其一是指,由于TCP是面向流的协议,不会按照应用开发者的期望保持send输入数据的边界,导致接收侧有可能一下子收到多个应用层报文,需要应用开发者自己分开。
其二是指,用户数据被TCP发出去的时候,存在多个小尺寸数据被封装在一个TCP报文中发出去的可能性。这种“粘”不是接收侧的效果,而是由于Nagle算法(或者TCP_CORK)的存在,在发送的时候,就把应用开发者多次send的数据,“粘”在一个tcp报文里面发出去了,于是,先被send的数据可能需要等待一段时间,才能跟后面被send的数据一起组成报文发出去。
这两个其实都不是“问题”。
一、针对上面第一点
1、分析
1)TCP 协议是基于字节流的传输层协议,其中不存在消息和数据包的概念。
2)应用层协议没有使用基于长度或者基于终结符的消息边界,导致多个消息的粘连,出现了“粘包”想象。这其实是使用者对于 TCP 的理解有误导致的一个问题。完全可以把这个问题转换为如何设计应用层协议的问题,即如何将TCP流解码为报文数据(stream2datagram)。
3)数据传递
为了说清楚这个问题,我们先来看下client/server之间数据传递的过程。
1、客户端->发送数据
2、服务端->接收数据
通常我们直觉性的认为,客户端直接向网络中传输数据,对端从网络中读取数据,但是这是不正确的。
socket有缓冲区buffer的概念,每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区。客户端send操作仅仅是把数据拷贝到buffer中,也就是说send完成了,数据并不代表已经发送到服务端了,之后才由TCP协议从buffer中发送到服务端。此时服务端的接收缓冲区被TCP缓存网络上来的数据,而后server才从buffer中读取数据。
因此,如果接收数据端的应用层没有及时读取 TCP Recv Buffer 中的数据,就会发生粘包。
2、解决办法
最常见的两种解决方案就是基于长度或者基于终结符(Delimiter)。
二、针对上面第二点
1、分析
1)Nagle 算法是一种通过减少数据包的方式提高 TCP 传输性能的算法。
因为网络 带宽有限,它不会将小的数据块直接发送到目的主机,而是会在本地缓冲区中等待更多待发送的数据,这种批量发送数据的策略虽然会影响实时性和网络延迟,但是能够降低网络拥堵的可能性并减少额外开销。
Nagle算法如下:
- 如果包长度达到MSS(或含有Fin包),立刻发送,否则等待下一个包到来;如果下一包到来后两个包的总长度超过MSS的话,就会进行拆分发送;
- 等待超时(一般为200ms),第一个包没到MSS长度,但是又迟迟等不到第二个包的到来,则立即发送。
几十年前还会发生网络拥塞的问题,但是今天的网络带宽资源不再像过去那么紧张,在默认情况下,Linux 内核都会使用如下的方式默认关闭 Nagle 算法:
TCP_NODELAY = 1
3)除了 Nagle 算法之外,TCP 协议栈中还有另一个用于延迟发送数据的选项 TCP_CORK,如果我们开启该选项,那么当发送的数据小于 MSS 时,TCP 协议就会延迟 200ms 发送该数据或者等待缓冲区中的数据超过 MSS5。无论是 TCP_NODELAY 还是 TCP_CORK,它们都会通过延迟发送数据来提高带宽的利用率,它们会对应用层协议写入的数据进行拆分和重组,而这些机制和配置能够出现的最重要原因是 — TCP 协议是基于字节流的协议,其本身没有数据包的概念,不会按照数据包发送数据。
4)其实99%的情况下禁不禁止都一样,延迟根本不是Nagle算法导致的;就算真有问题,最优解决方案也不是屏蔽Nagle算法。
2、解决办法
设置TCP_NODELAY来屏蔽Nagle算法。不过这是要看情况的,比如:在频繁进行超短的信息交互(比如几个字节)时禁用Nagle算法就是明智之举。
不过,就算关闭 Nagle 算法,如果接收数据端的应用层没有及时读取 TCP Recv Buffer 中的数据,还是会发生粘包。
最后,“粘包”这个词语容易引起歧义,不建议沿用。stream2datagram就是stream2datagram,TCP_NODELAY就是TCP_NODELAY。
参考链接:
https://www.zhihu.com/question/20210025/answer/1744906223
https://segmentfault.com/a/1190000039691657
https://draveness.me/whys-the-design-tcp-message-frame/