HTTP粘包问题
# TCP粘包
在socket网络编程中,都是端到端通信,由客户端端口+服务端端口+客户端IP+服务端IP+传输协议组成的五元组可以明确的标识一条连接。在TCP的socket编程中,发送端和接收端都有成对的socket。发送端为了将多个发往接收端的包,更加高效的的发给接收端,于是采用了优化算法(Nagle算法),将多次间隔较小、数据量较小的数据,合并成一个数据量大的数据块,然后进行封包。那么这样一来,接收端就必须使用高效科学的拆包机制来分辨这些数据。
# 什么是粘包、拆包?
假设客户端向服务端连续发送了两个数据包,用 packet1 和 packet2 来表示,那么服务端收到的数据可以分为三种,现列举如下
- 第一种:服务端正常接收到这两个数据包 package1 和 package2,即没有发生拆包和粘包
- 第二种:接收端只接收到一个包,由于tcp不会出现丢包的现象。所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
- 第三种:这种情况有两种表现形式,接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。(package1不完整,package2多了package1一部分;package1多了package2的一部分,package2不完整)
# 1.Q:什么是TCP粘包问题?
粘包:多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
简单说就是系统底层并不会按照应用中每次调用writer.write(data)的数据逐个数据包进行发送。
例如:当数据长度大于 MTU(以太网(数据链路层)传输数据方面的限制)时,数据会被拆分成多个包进行传输。
当然这只是其中一个原因,还有很多原因会导致粘包(滑动窗口、Nagle算法),我就不一一介绍了,有兴趣的话可以自行百度。
出现粘包的原因?
出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
先说简单的接收方原因, 接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
再说由发送导致的粘包, 这个比较有意思.
粘包并不是 TCP 协议造成的,它的出现是因为应用层协议设计者对 TCP 协议的错误理解,忽略了 TCP 协议的定义并且缺乏设计应用层协议的经验。我们将从 TCP 协议以及应用层协议出发,分析我们经常提到的 TCP 协议中的粘包是如何发生的:
- TCP 协议是面向字节流的协议,它可能会组合或者拆分应用层协议的数据;
- 应用层协议的没有定义消息的边界导致数据的接收方无法拼接数据;
TCP 协议是面向连接的、可靠的、基于字节流的传输层通信协议,应用层交给 TCP 协议的数据并不会以消息为单位向目的主机传输,这些数据在某些情况下会被组合成一个数据段发送给目标的主机。
Nagle 算法是一种通过减少数据包的方式提高 TCP 传输性能的算法。因为网络 带宽有限,它不会将小的数据块直接发送到目的主机,而是会在本地缓冲区中等待更多待发送的数据,这种批量发送数据的策略虽然会影响实时性和网络延迟,但是能够降低网络拥堵的可能性并减少额外开销。
在早期的互联网中,Telnet 是被广泛使用的应用程序,然而使用 Telnet 会产生大量只有 1 字节负载的有效数据,每个数据包都会有 40 字节的额外开销,带宽的利用率只有 ~2.44%,Nagle 算法就是在当时的这种场景下设计的。
当应用层协议通过 TCP 协议传输数据时,实际上待发送的数据先被写入了 TCP 协议的缓冲区,如果用户开启了 Nagle 算法,那么 TCP 协议可能不会立刻发送写入的数据,它会等待缓冲区中数据超过最大数据 (opens new window)段(MSS)或者上一个数据段被 ACK 时才会发送缓冲区中的数据。

# 粘包、拆包解决办法
特殊字符分隔符协议:可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
EchoServer应用采用的就是这种方式,按照「换行符」进行分割。而这种方式显然不适合更加复杂的场景。比如需要开发的是个代理服务器,代理的数据中如果出现了「分隔符」则会影响我们正确拆包。
定长协议: 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
这种方式虽然可行,但是缺点也很明显,需要通过补0的方式填充数据包,降低了数据传输的效率。
长度编码: 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
最终我们选择使用「长度编码方式」来解决粘包问题。