在TCP编程中,通常Sever端与Client通信时的消息都有着固定的消息格式,称之为协议(protocol),例如FTP协议、Telnet协议等。
那么协议到底是干什么的呢?说白了,协议了就是定义了数据通信的格式。主要是为了解决TCP编程中的粘包和拆包问题。
由于TCP是面向连接的,面向流的,提供高可靠性服务。收发两端都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难以分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。
而UDP则是无连接的,面向消息的,不会使用块的合并优化算法,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
1 TCP粘包、拆包图解
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
- 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
- 服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包
- 服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包
- 服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。
2 粘包、拆包的原因
发生TCP粘包、拆包主要是由于下面一些原因:
- socket缓冲区与滑动窗口
- MSS和MTU分片
- Nagle算法
2.1 socket缓冲区与滑动窗口
socket缓冲区
每个TCP socket在内核中都有一个发送缓冲区(SO_SNDBUF )和一个接收缓冲区(SO_RCVBUF),TCP的全双工的工作模式以及TCP的滑动窗口便是依赖于这两个独立的buffer以及此buffer的填充状态。SO_SNDBUF和SO_RCVBUF 在windows操作系统中默认情况下都是8K。
滑动窗口
TCP链接在三次握手的时候,会将自己的窗口大小(window size)发送给对方,其实就是SO_RCVBUF指定的值。之后在发送数据的时,发送方必须要先确认接收方的窗口没有被填充满,如果没有填满,则可以发送。
综上考虑,粘包、拆包是如何发生的呢?
粘包:假设发送方的每256 bytes表示一个完整的报文,接收方由于数据处理不及时,这256个字节的数据都会被缓存到SO_RCVBUF中。如果接收方的SO_RCVBUF中缓存了多个报文,那么对于接收方而言,这就是粘包。
拆包:考虑另外一种情况,假设接收方的window size只剩了128,意味着发送方最多还可以发送128字节,而由于发送方的数据大小是256字节,因此只能发送前128字节,等到接收方ack后,才能发送剩余字节。这就造成了拆包。
2.2 MSS和MTU分片
MSS是MSS是Maximum Segement Size的缩写,表示TCP报文中data部分的最大长度,是TCP协议在OSI五层网络模型中传输层对一次可以发送的最大数据的限制。
MTU最大传输单元是Maxitum Transmission Unit的简写,是OSI五层网络模型中数据链路层对一次可以发送的最大数据的限制。
当需要传输的数据大于MSS或者MTU时,数据会被拆分成多个包进行传输
2.3 Nagle算法
TCP/IP协议中,无论发送多少数据,总是要在数据(DATA)前面加上协议头(TCP Header+IP Header),同时,对方接收到数据,也需要发送ACK表示确认。
为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
以生活中的例子举例:
你要从深圳去上海, 可以乘飞机, 但是飞机如果专门为你一个人跑一趟, 那代价就大了。 怎么办呢? 飞机就等有两个人的时候, 一次载过去。 不过,还是很浪费, 那就再多等几个人, 直到坐满为止。 那么, 第1, 2, 3, 4, ....n个人, 本来是有时间间隔的, 现在却粘到了一起, 进行批量处理了, 一起发送。
3 粘包、拆包解决策略
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下:
- 消息定长,例如每个报文的大小固定为200字节,如果不够空位补空格
- 包尾增加回车或者空格符等特殊字符进行分割 ,例如FTP协议
- 将消息分为消息头和消息体,消息头中用一个int型数据(4字节),表示消息体长度的字段。在解析时,先读取内容长度Length,其值为实际消息体内容(Content)占用的字节数,之后必须读取到这么多字节的内容,才认为是一个完整的数据报文。