TCP的粘包问题

✏️ 1、TCP选项之MSS

MSS(Maximum Segment Size,最大报文段大小)的概念是指TCP层所能够接收的最大段大小,该值只包括TCP段的数据部分,不包括选项部分。在TCP首部有一个MSS选项,在三次握手过程中,TCP发送端使用该选项告诉对方自己所能接受的最大段大小。 MSS 选项只能出现在 SYN 段中。

MSS选项虽然作为一个选项存在,原则上讲是可有可无的,但是目前绝大多数的TCP通信过程中都会携带该选项,可以说它是TCP中最重要的一个选项了。关于MSS的内容,主要包括两个方面:

  1. SYN段和SYN+ACK段中携带的MSS选项是如何确定的(有些资料中会提到的RMSS,即接收MSS)?

  2. 连接建立后的发包过程中,MSS是如何发挥作用的(有些资料中会提到的SMSS,即发送MSS)?

从客户端和服务器端的三次握手过程中可以看到RMSS是如何确定的。从连接态的数据发送过程可以看到SMSS是如何发乎作用的。

MSS 是软件层的概念,它是由软件控制的,MTU 是硬件(比如网卡出口)的属性,是指二层链路层帧携带的数据最大大小。

✏️ 2、TCPNagle算法

Nagle算法是为了减少广域网的小分组数目,从而减小网络拥塞的出现;

该算法要求一个tcp连接上最多只能有一个未被确认的未完成的小分组,在该分组ack到达之前不能发送其他的小分组,tcp需要收集这些少量的分组,并在ack到来时以一个分组的方式发送出去;其中小分组的定义是小于MSS的任何分组;

该算法的优越之处在于它是自适应的,确认到达的越快,数据也就发送的越快;而在希望减少微小分组数目的低速广域网上,则会发送更少的分组;

🖋️ 2.1、延迟ACK

如果tcp对每个数据包都发送一个ack确认,那么只是一个单独的数据包为了发送一个ack代价比较高,所以tcp会延迟一段时间,如果这段时间内有数据发送到对端,则捎带发送ack,如果在延迟ack定时器触发时候,发现ack尚未发送,则立即单独发送;

延迟ACK好处:

  1. 避免糊涂窗口综合症;

  2. 发送数据的时候将ack捎带发送,不必单独发送ack

  3. 如果延迟时间内有多个数据段到达,那么允许协议栈发送一个ack确认多个报文段;

🖋️ 2.2、当Nagle遇上延迟ACK

试想如下典型操作,写-写-读,即通过多个写小片数据向对端发送单个逻辑的操作,两次写数据长度小于MSS,当第一次写数据到达对端后,对端延迟ack,不发送ack,而本端因为要发送的数据长度小于MSS,所以nagle算法起作用,数据并不会立即发送,而是等待对端发送的第一次数据确认ack;这样的情况下,需要等待对端超时发送ack,然后本段才能发送第二次写的数据,从而造成延迟;

🖋️ 2.3、关闭Nagle算法

使用TCP套接字选项TCP_NODELAY可以关闭套接字选项;

如下场景考虑关闭Nagle算法:对端不向本端发送数据,并且对延时比较敏感的操作,这种操作没法捎带ack

对于如上写-写-读操作,优先使用其他方式,而不是关闭Nagle算法:

  • 使用writev,而不是两次调用write,单个writev调用会使tcp输出一次而不是两次,只产生一个tcp分节,这是首选方法;

  • 把两次写操作的数据复制到单个缓冲区,然后对缓冲区调用一次write;

  • 关闭Nagle算法,调用write两次;有损于网络,通常不考虑;

✏️ 3、长连接和短连接

长连接:Client方与Server方先建立通讯连接,连接建立后 不断开, 然后再进行报文发送和接收。

短连接:Client方与Server每进行一次报文收发时进行通讯连接,交易完毕后立即断开连接。此种方式常用于一点对多点通讯,比如多个Client连接一个Server。

✏️ 4、TCP的粘包问题

TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。

对于UDP,不会使用块的合并优化算法,实际上是由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。所以UDP不会出现粘包问题。

🖋️ 4.1、保护消息边界

保护消息边界,就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包。而面向流则是指无消息保护边界的,如果发送端连续发送数据,接收端有可能在一次接收动作中,会接收两个或者更多的数据包。

例如,我们连续发送三个数据包,大小分别是2k,4k ,8k,这三个数据包,都已经到达了接收端的网络堆栈中,如果使用UDP协议,不管我们使用多大的接收缓冲区去接收数据,我们必须有三次接收动作,才能够把所有的数据包接收完,而使用TCP协议,我们只要把接收的缓冲区大小设置在14k以上,我们就能够一次把所有的数据包接收下来,只需要有一次接收动作(指应用层的接受动作,缓冲区也是应用层的)。

误认为TCP丢包的问题:很多人在使用tcp协议通讯的时候,并不清楚tcp是基于流的传输,当连续发送数据的时候,如果他们使用的缓冲区足够大,他们有可能会一次接收到两个甚至更多的数据包,而很多人往往会忽视这一点,只解析检查了第一个数据包,而已经接收的其他数据包却被忽略了。

🖋️ 4.2、 什么时候需要考虑粘包问题

(1)、如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议)。

关闭连接主要是要双方都发送close连接(参考tcp关闭协议)。如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如"hello give me sth abour yourself",然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符。

(2)、如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包。

(3)、如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:

  1. "hellogive me sth abour yourself"

  2. "Don'tgive me sth abour yourself"

如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hellogive me sth abour yourselfDon't give me sth abour yourself"这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在包头加一个数据长度之类的,以确保接收。

🖋️ 4.3、 粘包出现原因

TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。

  1. 发送端需要等缓冲区满才发送出去,造成粘包

  2. 接收方不及时接收缓冲区的包,造成多个包接收

发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。

接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包。

不是所有的粘包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把粘连的包分开(简称分包)。但在实际工程应用中,传输的数据一般为带结构的数据,这时就需要做分包处理。

在处理定长结构数据的粘包问题时,分包算法比较简单;在处理不定长结构数据的粘包问题时,分包算法就比较复杂。特别是粘在一起的包有不完整的包的粘包情况,由于一包数据内容被分在了两个连续的接收包中,处理起来难度较大。实际工程应用中应尽量避免出现粘包现象。

包分片的原因:

  • 应用层调用write方法,将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个SO_SNDBUF的限制,如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小,则数据需要进行多次的发送。

  • TCP所传输的报文段有MSS的限制,如果套接字缓冲区的大小大于MSS,也会导致消息的分割发送。

  • 由于链路层最大发送单元MTU,在IP层会进行数据的分片。

这些情况都会导致一个完整的应用层数据被分割成多次发送,导致接收对等方不是按完整数据包的方式来接收数据。

🖋️ 4.4、解决方案

粘包问题本质上是因为无法区分包界限,解决包界限的问题主要有以下几种方式:

  1. 消息数据的定长,比如定长100字节,不足补空格,接收方收到后解析100字节数据即为完整数据。但这样的做的缺点是浪费了部分存储空间和带宽。

  2. 消息数据使用特定分割符区分界限,比如包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。

  3. 把消息数据分成消息头和消息体,消息头带消息的长度,接收方收到后根据消息头中的长度解析数据。

在实际开发中很多网络框架对TCP拆包粘包问题的解决做了很多支持,比如nettyLineBasedFrameDecoder解析器就是利用换号符号做分割。

🖋️ 4.5、总结

(1)TCP为了保证可靠传输,尽量减少额外开销(每次发包都要验证),因此采用了流式传输,面向流的传输,相对于面向消息的传输,可以减少发送包的数量,从而减少了额外开销。但是,对于数据传输频繁的程序来讲,使用TCP可能会容易粘包。当然,对接收端的程序来讲,如果机器负荷很重,也会在接收缓冲里粘包。这样,就需要接收端额外拆包,增加了工作量。因此,这个特别适合的是数据要求可靠传输,但是不需要太频繁传输的场合(两次操作间隔100ms,具体是由TCP等待发送间隔决定的,取决于内核中的socket的写法)。

(2)UDP,由于面向的是消息传输,它把所有接收到的消息都挂接到缓冲区的接受队列中,因此,它对于数据的提取分离就更加方便,但是,它没有粘包机制,因此,当发送数据量较小的时候,就会发生数据包有效载荷较小的情况,也会增加多次发送的系统发送开销(系统调用,写硬件等)和接收开销。因此,应该最好设置一个比较合适的数据包的包长,来进行UDP数据的发送。(UDP最大载荷为1472,因此最好能每次传输接近这个数的数据量,这特别适合于视频,音频等大块数据的发送,同时,通过减少握手来保证流媒体的实时性

Last updated