上一篇《第三十一篇:TCP协议如何解决丢包的问题,TCP系列六》讲了TCP如何解决丢包问题,本文将为大家讲解TCP是如何提高传输效率,减少传输时延的原理。
1. TCP是如何提高传输效率,减少传输时延的
① 粘包
如果传输的数据过小,例如数据只有1byte,那么为了传输这1byte数据,至少要消耗20字节IP头部+20字节TCP头部=40byte,这还不包括其二层头部所需要的开销,显然这种数据传输效率是很低的。
为了避免资源浪费,提高传输效率,所以在实现TCP的时候,TCP软件内核会将小包的数据合并发送,这样的情况叫做粘包。
既然涉及到粘包,就必然会有拆包的过程;拆包一般在应用层进行,拆包的常用方式有三种:
- 在将数据从应用层传递到传输层之前,发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
- 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
- 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
② 滑动窗口 ( 窗口控制 ) 提高速度
由一个或多个字节组成一个段,TCP以一个段为单位,,每发一个段就进行一次应答处理,如下图所示,这样的传输方式由一个缺点。那就是,包的往返时间越长,网络的吞吐量越低,通信的性能就越低。
那有没有办法解决这个问题呢?答案是有的,那就是报文在不被确认应答的情况下也可以继续发送下一个报文,但是这个未被应答包的大小也不是无限的,而是被限制在一定数量范围内,这个数量叫窗口大小 ,这个技术在TCP中叫做滑动窗口。
2. 滑动窗口
早期的网络通信中,通信双方不会考虑对方的数据处理能力就直接发送数据。导致接收端无法处理数据而丢包,而发送端不断重发,导致网络拥塞,中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。
① 定义
滑动窗口:指无需等待确认应答而可以继续发送数据的最大值,即累计可以不确认应答的数据的最大数值。同时其是是一种流量控制技术,也叫滑动窗口协议 。
② 滑动窗口协议
滑动窗口协议的基本原理就是在任意时刻,发送方都维持了一个连续的允许发送的帧的序号,称为发送窗口;同时,接收方也维持了一个连续的允许接收的帧的序号,称为接收窗口。
****1)发送窗口:发送方维持了一个连续的允许发送的帧的序号,发送窗⼝的实现实际上是操作系统开辟的⼀个缓存空间,发送⽅主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
发送窗口对应计算机的一段缓存,该缓存分为四部分:发送已确认 、 发送未确认 、 未发送但在接收方处理范围的数据部分 、 未发送但超过接收方处理范围的数据部分。
如下图,当发送⽅把#3数据「全部」都⼀下发送出去后,#2发送未确认数据大小达到可⽤窗⼝的⼤⼩,表明可⽤窗⼝耗尽,在没收到ACK 确认之前是⽆法继续发送数据了。
在上图中,24~27数据被ack确认后,可发送窗口向后滑动,如下图
如何计算发送窗口的大小?
发送端必须根据接收端的处理能力来发送数据,才不会导致接收端处理不过来,同时发送端也必须考虑网络拥塞问题(本来网络就很拥塞,还要大量发送数据,这是灾难性的,所以必须通过发送来控制),发送端发送的数据包,接收端会给一个确认包(ACK).确认它收到了。 接收端给发送端发送的确认包(ACK包)中,同时会携带一个窗口的大小。
该窗口大小为接收端窗口大小 = 接收端最大缓存量 - 接收已确认但还未被应用层读取的部分
所以初始发送端的窗口大小=min(接收端窗口大小,拥塞窗口大小)+ 发送未确认大小
****2)接收窗口:接收方也维持了一个连续的允许接收的帧的序号,接收窗⼝的实现实际上也是操作系统开辟的⼀个缓存空间。
- 接收窗口在缓存中分为 :已成功接收并确认部分 、 未收到但是可以接收部分 、 未收到并不可接收的部分 。
- 接收窗口与读取数据的关系
读取速度>窗口 窗口会不断变大直到分配缓存大小
读取速度<窗口 窗口会不断变小,极端情况下为0
读取速度=窗口 窗口不变
3)接收端控制发送端的图示:
- 关闭窗口(Zero Window)
上图,我们可以看到一个处理缓慢的Server(接收端)是怎么把Client(发送端)的TCP 滑动窗口给降成0的。此时,你一定会问,如果Window变成0了,TCP会怎么样?是不是发送端就不发数据了?是的,发送端就不发数据了,你可以想像成"Window Closed",那你一定还会问,如果发送端不发数据了,接收方一会儿Window size 可用了,怎么通知发送端呢?
解决这个问题,TCP使用了零窗口探测技术(Zero Window Probe,缩写为ZWP),也就是说,接收端在接收窗口变为0后会通知发送端,发送端发现接收端窗口大小变为0,会启动一个定时器,在定时器超时后,会发探测报文给接收方,让接收方来ack他的Window尺寸,一般这个值会设置成3次,每次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的TCP实现就会发RST把链接断了。
注意:只要有等待的地方都可能出现DDoS攻击,Zero Window也不例外,一些攻击者会在和HTTP建好链发完GET请求后,就把Window设置为0,然后服务端就只能等待进行ZWP,于是攻击者会并发大量的这样的请求,把服务器端的资源耗尽。
- 糊涂窗口综合症(Silly Window Syndrome)
如果接收⽅太忙了,来不及取⾛接收窗⼝⾥的数据,那么就会导致发送⽅的发送窗⼝越来越⼩。
到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的window,而我们的发送方也会义无反顾地发送这几个字节。
要知道,我们的 TCP + IP 头有 40 个字节,为了传输那⼏个字节的数据,要达上这么⼤的开销,这太不经济了。物理层有最大传输单元(Maximum Transmission Unit,MTU),如果你的网络包可以塞满MTU,那么你可以用满整个带宽,如果不能,那么你就会浪费带宽。(大于MTU的包有两种结局,一种是直接被丢了,另一种是会被重新分块打包发送)。
为了解决这个发送小包的问题,就是避免对小的窗口做出响应,直到有足够大的窗口再响应,这个思路可以同时实现在发送和接收两端。
如果是接收方处理不过来 , 导致滑动窗口过小 , 通常采用如下策略 :
MSS(Max Segment Size),最大消息长度,即段。
当「窗⼝⼤⼩」⼩于 min( MSS,缓存空间/2 ) ,也就是⼩于 MSS 与 1/2 缓存⼤⼩中的最⼩值时,就会向发送⽅通告窗⼝为 0 ,也就阻⽌了发送⽅再发数据过来。
等到接收⽅处理了⼀些数据后,窗⼝⼤⼩ >= MSS,或者接收⽅缓存空间有⼀半可以使⽤,就可以把窗⼝打开让发送⽅发送数据过来。
同时发送⽅通常的策略:
使⽤ Nagle 算法,该算法的思路是延时处理,它用于自动连接许多的小缓冲消息;通过减少必须发送包的个数来增加网络软件系统的效率。即Nagle算法是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
Nagle算法的规则(可参考tcp_output.c文件里tcp_nagle_check函数注释):
(1)如果包长度达到MSS,则允许发送;
(2)如果该包含有FIN,则允许发送;
(3)设置了TCP_NODELAY选项,则允许发送;
(4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;如果设置了TCP_CORK选项,就算设置了TCP_NODELAY选项,TCP程序包也会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去,当然若一定时间后(一般为200ms,该值尚待确认),内核仍然没有组合成一个MTU时也必须发送现有的数据(不可能让数据一直等待吧)。
(5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
注意:TCP_NODELAY 选项,发送数据采用Nagle 算法。这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,使用TCP_NODELAY选项可以禁止Nagle 算法。
另外,Nagle 算法默认是打开的,如果对于⼀些需要⼩数据包交互的场景的程序,⽐如,telnet 或 ssh 这样的交互性⽐较强的程序,则需要关闭 Nagle 算法。