TCP的粘包问题
Last updated
Was this helpful?
Last updated
Was this helpful?
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,因此最好能每次传输接近这个数的数据量,这特别适合于视频,音频等大块数据的发送,同时,通过减少握手来保证流媒体的实时性)