一文带你搞定TCP流量控制 tcp流量控制算法
摘要
- 理想的流量控制
- 实际的流量控制
- 窗口关闭
- 糊涂窗口综合征
理想的流量控制
什么是流量控制?
流量控制就是发送方不能无脑的给接收方发送数据,它需要根据接收方的处理能力来发送数据。
理想下的流量控制?
理想意味着在实际中不存在,这里只是简单的说一下流量控制的作用,我们假设的理想通信发生条件为:
- 客户端是接收方、服务端是发送方
- 接收窗口和发送窗口相同,为200
- 接收方和发送方在通信过程中始终保持相同的窗口大小
- 客户端发送请求数据
- 服务端收到请求后,发送确认报文和80字节的数据,可用窗口减少为120(200-80),SND.NXT指针变为321,表示发送方下次发送序列号为321的数据
- 客户端收到数据后,接收窗口右移80字节,RCV.NXT为321,表示客户端段期望下一个收到的报文的序列号为321,并且发送ACK报文给服务端
- 服务端发送120字节的数据,可用窗口变为0,服务端无法发送数据
- 客户端收到120字节的数据,接收窗口右移120字节,接着发送ACK报文给客户端
- 服务端收到序列号为241(长度为80字节)的ACK报文,因此SND.UNA右移80字节变为321(241+80),可用窗口变为80字节
- 服务端收到序列号321(长度为120字节)的ACK报文,因此SND.UNA右移120字节变为441(321+120),可用窗口变为200字节
- 服务端发送160字节的数据,可用窗口变为40字节,并且SND.NXT右移160个字节变为601(441+160)
- 客户端在收到数据后,接收窗口右移160字节,接着发送ACK报文给服务端
- 服务端在收到ACK报文后,SND.UNAK右移160字节,并且可用窗口再次恢复为200字节
实际的流量控制
操作系统缓冲区和滑动窗口的关系
由于我们发送的数据都是暂存在操作系统的内存缓冲区,所以滑动窗口会收操作系统缓冲区的影响:
- 操作系统内存缓冲区会动态调整,影响窗口大小
- 如果应用程序如果无法及时从缓冲区读取内容,也会导致缓冲区减少,此时滑动窗口需要适当调小,避免缓冲区内容溢出。
应用程序无法及时读取缓存
假设以下场景:
- 客户端为发送方,服务端为接收方,初始化发送窗口和接收窗口都为360
- 服务端繁忙,收到客户端数据后,应用层无法及时读取数据
- 客户端发送140字节的数据,可用窗口变为220(360-140),SND.NXT右移140变为141(1+140)
- 服务端收到140字节的数据,应用程序只读取了40节数据,还有100字节存在于缓冲区中,于是接收窗口变为260(360-100),服务端在给客户端发送ACK报文时会把新的窗口大小告知客户端。
- 客户端在收到ACK报文以后,发送窗口减少为260,并且由于收到了数据的ACK,因此SND.UNA会右移140个字节变为141,此时可用窗口为260
- 客户端继续发送180字节的数据,SND.NXT变为321,可用窗口变为80(260-80)
- 服务端在收到数据后,应用进程没有读取任何数据,于是接收窗口从260缩小为80(260-180),并且在发送ACK报文时告知客户端
- 客户端收到ACK报文以后,会将发送窗口减少为80,可用窗口此时也是80,因此到这里客户端最多只能发送80字节的数据给服务端
- 客户端发送最后80字节的数据给服务端,可用窗口变为0
- 服务端收到80字节的数据后,应用进程依然没有读取任何数据,于是接收窗口减少为0,并在发送ACK报文告知客户端
- 客户端收到ACK报文以后,发现窗口大小为0,因此将发送窗口减少为0。
这里会有个问题,窗口变为0也就是发生了窗口关闭。
操作系统缓冲区变化
当服务器资源紧张,操作系统有可能会减少缓冲区的大小,如果此时应用程序还无法读取数据,那么将会出现数据包丢失现象。
- 客户端发送140字节数据,可用窗口变为220(360-140)
- 服务端收到140字节数据后,但是系统资源紧张,操作系统减少120字节的缓冲区,并且应用层没有读取任何数据,于是接收窗口变为100(360-120-140),然后发送ACK报文告知客户端
- 在收到服务端的ACK报文前,客户端又发送了180字节的数据,可用窗口减少到40
- 服务器收到180字节的数据后,由于接收窗口只有100字节,超出了缓冲区的大小,因此会丢弃该数据包
- 客户端此时收到之前的ACK报文,会将发送窗口减少为100,此时可用窗口出现了负值-80 =(100 -(321 - 141))
在上述情况中,减少缓存先于收缩窗口发生,出现丢包现象。
为了防止上述情况,TCP规定是先收缩窗口,过段时间再减少缓存,这样避免丢包。
窗口关闭
什么是窗口关闭
窗口大小为0,阻止发送方给接收方发送数据,直到窗口变为非0才能恢复发送。
窗口关闭的危险
窗口关闭以后发送端无法发送数据给接收端,只有当接收端处理完数据以后,这时候窗口回复,发送ACK报文信息给客户端,客户端才能恢复发送。但是一旦该ACK报文丢失,那么发送方会一直等待接收方的非0窗口通知,接收方也一直在等待发送方的数据,容易造成死锁现象。
如何解决窗口关闭带来的死锁?
只要TCP连接的一方收到对方0窗口的通知,就启动计时器,如果计时器超时就会发送窗口探测报文给对端,对端会给出自己的接收窗口大小。
- 如果收到的窗口依然为0,发送方重启启动持续计时器
- 如果收到的窗口不为0,恢复正常发送
窗口探测的次数一般为3次,每次大约30~60s。如果三次以后接收窗口还是0,有的TCP实现就会发送RST报文来中止连接。
糊涂窗口综合征
什么是糊涂窗口综合征?
如果接收方太忙,来不及取走缓冲区的数据,发送方的窗口会越来越小,最后如果接收方空出几个字节并告诉发送方现在有几个字节的窗口,发送方便会发送这几个字节,这就是糊涂窗口综合征。
糊涂窗口综合征的缺点?
TCP+IP头部大约有40个字节,为了几个字节数据,需要加上头部相对大的开销,性价比极低。
糊涂窗口综合征的原因是?
- 接收方会告知发送方一个小的窗口
- 发送方可以发送小数据
如何避免接收方告知小窗口?
接收方在告知窗口时会采取一种策略:
- 当窗口大小小于min(MSS,缓存空间/2),就会向发送方告知窗口为0,避免发送方发送数据,等到接收方处理完一些数据后,窗口大小>=MSS或者有一半以上缓存空间可以使用时,就可以把窗口打开让发送方发送数据。
如何避免发送方发送小数据?
发送方在发送数据时采用Nagle算法,该算法的思路是延时处理,满足以下两个条件才可以发送数据:
- 窗口大小>=MSS或是数据大小>=MSS
- 收到之前发送数据的ACK报文
对于telnet或ssh这种小数据包交互场景的应用程序,需要关闭Nagle算法。
// 在Java中,对Socket进行以下设置可以关闭Nagle算法。
Socket socket = new Socket();
socket.setTcpNoDelay(true);