TCP的粘包问题
Last updated
Last updated
TCP
选项之MSS
MSS
(Maximum Segment Size,最大报文段大小)的概念是指TCP
层所能够接收的最大段大小,该值只包括TCP
段的数据部分,不包括选项部分。在TCP
首部有一个MSS
选项,在三次握手过程中,TCP
发送端使用该选项告诉对方自己所能接受的最大段大小。 MSS
选项只能出现在 SYN 段中。
MSS
选项虽然作为一个选项存在,原则上讲是可有可无的,但是目前绝大多数的TCP
通信过程中都会携带该选项,可以说它是TCP
中最重要的一个选项了。关于MSS
的内容,主要包括两个方面:
SYN段和SYN+ACK
段中携带的MSS
选项是如何确定的(有些资料中会提到的RMSS
,即接收MSS
)?
连接建立后的发包过程中,MSS
是如何发挥作用的(有些资料中会提到的SMSS
,即发送MSS
)?
从客户端和服务器端的三次握手过程中可以看到RMSS
是如何确定的。从连接态的数据发送过程可以看到SMSS
是如何发乎作用的。
MSS
是软件层的概念,它是由软件控制的,MTU
是硬件(比如网卡出口)的属性,是指二层链路层帧携带的数据最大大小。
TCP
之Nagle
算法Nagle
算法是为了减少广域网的小分组数目,从而减小网络拥塞的出现;
该算法要求一个tcp
连接上最多只能有一个未被确认的未完成的小分组,在该分组ack
到达之前不能发送其他的小分组,tcp
需要收集这些少量的分组,并在ack
到来时以一个分组的方式发送出去;其中小分组的定义是小于MSS
的任何分组;
该算法的优越之处在于它是自适应的,确认到达的越快,数据也就发送的越快;而在希望减少微小分组数目的低速广域网上,则会发送更少的分组;
ACK
如果tcp
对每个数据包都发送一个ack
确认,那么只是一个单独的数据包为了发送一个ack
代价比较高,所以tcp
会延迟一段时间,如果这段时间内有数据发送到对端,则捎带发送ack
,如果在延迟ack
定时器触发时候,发现ack
尚未发送,则立即单独发送;
延迟ACK
好处:
避免糊涂窗口综合症;
发送数据的时候将ack
捎带发送,不必单独发送ack
;
如果延迟时间内有多个数据段到达,那么允许协议栈发送一个ack
确认多个报文段;
试想如下典型操作,写-写-读,即通过多个写小片数据向对端发送单个逻辑的操作,两次写数据长度小于MSS
,当第一次写数据到达对端后,对端延迟ack
,不发送ack
,而本端因为要发送的数据长度小于MSS
,所以nagle
算法起作用,数据并不会立即发送,而是等待对端发送的第一次数据确认ack
;这样的情况下,需要等待对端超时发送ack
,然后本段才能发送第二次写的数据,从而造成延迟;
使用TCP
套接字选项TCP_NODELAY
可以关闭套接字选项;
如下场景考虑关闭Nagle
算法:对端不向本端发送数据,并且对延时比较敏感的操作,这种操作没法捎带ack
;
对于如上写-写-读操作,优先使用其他方式,而不是关闭Nagle
算法:
使用writev
,而不是两次调用write
,单个writev
调用会使tcp
输出一次而不是两次,只产生一个tcp
分节,这是首选方法;
把两次写操作的数据复制到单个缓冲区,然后对缓冲区调用一次write;
关闭Nagle
算法,调用write两次;有损于网络,通常不考虑;
长连接:Client方与Server方先建立通讯连接,连接建立后 不断开, 然后再进行报文发送和接收。
短连接:Client方与Server每进行一次报文收发时进行通讯连接,交易完毕后立即断开连接。此种方式常用于一点对多点通讯,比如多个Client连接一个Server。
TCP
的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包更有效的发到对方,使用了优化方法(Nagle
算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。
对于UDP
,不会使用块的合并优化算法,实际上是由于UDP
支持的是一对多的模式,所以接收端的skbuff
(套接字缓冲区)采用了链式结构来记录每一个到达的UDP
包,在每个UDP
包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。所以UDP
不会出现粘包问题。
保护消息边界,就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包。而面向流则是指无消息保护边界的,如果发送端连续发送数据,接收端有可能在一次接收动作中,会接收两个或者更多的数据包。
例如,我们连续发送三个数据包,大小分别是2k,4k ,8k,这三个数据包,都已经到达了接收端的网络堆栈中,如果使用UDP
协议,不管我们使用多大的接收缓冲区去接收数据,我们必须有三次接收动作,才能够把所有的数据包接收完,而使用TCP
协议,我们只要把接收的缓冲区大小设置在14k以上,我们就能够一次把所有的数据包接收下来,只需要有一次接收动作(指应用层的接受动作,缓冲区也是应用层的)。
误认为
TCP
丢包的问题:很多人在使用tcp
协议通讯的时候,并不清楚tcp
是基于流的传输,当连续发送数据的时候,如果他们使用的缓冲区足够大,他们有可能会一次接收到两个甚至更多的数据包,而很多人往往会忽视这一点,只解析检查了第一个数据包,而已经接收的其他数据包却被忽略了。
(1)、如果利用tcp
每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http
协议)。
关闭连接主要是要双方都发送close连接(参考tcp
关闭协议)。如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如"hello give me sth abour yourself"
,然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符。
(2)、如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok
,也不用考虑粘包。
(3)、如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
"hellogive me sth abour yourself"
"Don'tgive me sth abour yourself"
如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hellogive me sth abour yourselfDon't give me sth abour yourself"
这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在包头加一个数据长度之类的,以确保接收。
TCP
粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
发送端需要等缓冲区满才发送出去,造成粘包
接收方不及时接收缓冲区的包,造成多个包接收
发送方引起的粘包是由TCP
协议本身造成的,TCP
为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP
会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。
接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包。
不是所有的粘包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把粘连的包分开(简称分包)。但在实际工程应用中,传输的数据一般为带结构的数据,这时就需要做分包处理。
在处理定长结构数据的粘包问题时,分包算法比较简单;在处理不定长结构数据的粘包问题时,分包算法就比较复杂。特别是粘在一起的包有不完整的包的粘包情况,由于一包数据内容被分在了两个连续的接收包中,处理起来难度较大。实际工程应用中应尽量避免出现粘包现象。
应用层调用write方法,将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个SO_SNDBUF
的限制,如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小,则数据需要进行多次的发送。
TCP
所传输的报文段有MSS
的限制,如果套接字缓冲区的大小大于MSS
,也会导致消息的分割发送。
由于链路层最大发送单元MTU
,在IP层会进行数据的分片。
这些情况都会导致一个完整的应用层数据被分割成多次发送,导致接收对等方不是按完整数据包的方式来接收数据。
粘包问题本质上是因为无法区分包界限,解决包界限的问题主要有以下几种方式:
消息数据的定长,比如定长100字节,不足补空格,接收方收到后解析100字节数据即为完整数据。但这样的做的缺点是浪费了部分存储空间和带宽。
消息数据使用特定分割符区分界限,比如包尾加上\r\n
标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n
,则会误判为消息的边界。
把消息数据分成消息头和消息体,消息头带消息的长度,接收方收到后根据消息头中的长度解析数据。
在实际开发中很多网络框架对TCP
拆包粘包问题的解决做了很多支持,比如netty
中LineBasedFrameDecoder
解析器就是利用换号符号做分割。
(1)TCP
为了保证可靠传输,尽量减少额外开销(每次发包都要验证),因此采用了流式传输,面向流的传输,相对于面向消息的传输,可以减少发送包的数量,从而减少了额外开销。但是,对于数据传输频繁的程序来讲,使用TCP
可能会容易粘包。当然,对接收端的程序来讲,如果机器负荷很重,也会在接收缓冲里粘包。这样,就需要接收端额外拆包,增加了工作量。因此,这个特别适合的是数据要求可靠传输,但是不需要太频繁传输的场合(两次操作间隔100ms
,具体是由TCP
等待发送间隔决定的,取决于内核中的socket的写法)。
(2)UDP
,由于面向的是消息传输,它把所有接收到的消息都挂接到缓冲区的接受队列中,因此,它对于数据的提取分离就更加方便,但是,它没有粘包机制,因此,当发送数据量较小的时候,就会发生数据包有效载荷较小的情况,也会增加多次发送的系统发送开销(系统调用,写硬件等)和接收开销。因此,应该最好设置一个比较合适的数据包的包长,来进行UDP
数据的发送。(UDP
最大载荷为1472,因此最好能每次传输接近这个数的数据量,这特别适合于视频,音频等大块数据的发送,同时,通过减少握手来保证流媒体的实时性)