滑动窗口
之前学习了PAR方式的TCP超时和重传,其实在考虑发送方发送数据报的同时,也应该考虑接收方对于数据的处理能力,由此引出本次学习的主题 – 滑动窗口
发送端窗口
滑动窗口按照传输数据方向分为两种,发送端窗口和接收端窗口;下面先看一下发送端窗口👇:
上图分为四个部分:
- 已发送并收到 Ack 确认的数据:1-31 字节
- 已发送未收到 Ack 确认的数据:32-45 字节
- 未发送但总大小在接收方处理范围内:46-51 字节
- 未发送但总大小超出接收方处理范围:52-字节
可用窗口和发送窗口
如上图这里可以引出两个概念:「可用窗口」和「发送窗口」
【 可用窗口 】: 就是上图中的第三部分,属于还未发送,但是在接收端可以处理范围内的部分;
【 发送窗口 】: 就是发送端可以发送的最大报文大小,如上图中的第二部分+第三部分合成发送窗口;
可用窗口耗尽
可用窗口会在一个短暂的停留,当处于未发送并且接受端可以接受范围内的数据传输完成之后,可用窗口耗尽;
当然上面仅仅说的一瞬时的状态,这个状态下,已经发送的报文段还没有确认,并且发送窗口大小没有发生变化,此时发送窗口达到最大状态;
窗口移动
如果在发送窗口中已经发送的报文段已经得到接受端确认之后,那部分数据就会被移除发送窗口,在发送窗口大小不发生变化的情况下,发送窗口向右➡️移动5个字节,因为左边已经发送的5个字节得到确认之后,被移除发送窗口;
可用窗口如何计算
再次引出三个概念:
- SND.WND
SND 指的是发送端,WND指的是window,也就是发送端窗口的意思
- SND.UNA
UNA 就是un ACK的意思,指的是已经发送但是没有没有确认 它指向窗口的第一个字节处
- SND.NXT
NXT 是next的位置,是发送方接下来要发送的位置,它指向可用窗口的第一个字节处
那就很容易得出可用窗口的大小了,计算公式如下:
Usable Window Size = SND.UNA + SND.WND - SND.NXT
接收端窗口
上面介绍了发送端窗口的一些概念,下面👇是接收端窗口的学习:
- 已经接收并且已经确认 :28-31 字节
- 还未接收并且接收端可以接受:32-51 字节
- 还未接收并且超出接收处理能力:51-57 字节
这里引出两个概念:
- RCV.WND
RCV是接收端的意思,WND是接受端窗口的大小
- RCV.NXT
NXT表示的是接受端接收窗口的开始位置,也就是接收方接下来处理的第一个字节;
RCV.WND的大小接受端的内存以及缓冲区大小有关,在某种意义上说,接受端的窗口大小和发送端大小大致相同;
接受端可接收的数据能力可以通过TCP首部的Window字段设置,但是接受端的处理能力是可能随时变化的,所以接受端和服务端的窗口大小大致是一样的;
流量控制
下面👇根据一个例子来阐述流量控制,模拟一个GET请求,客户端向服务端请求一个260字节的文件,大致流程如下,比较繁琐:
这里假设MSS和窗口的大小不发生变化,同时客户端和发送端状态如下:
【 客户端 】: 发送窗口默认360字节 接收窗口设定200字节
【 服务端 】: 发送窗口设定200字节 接收窗口设定360字节
Step1: 客户端发送140字节的数据到服务端
【客户端】发送140字节,【SND.NXT】从1->141
【服务端】状态不变,等待接收客户端传输的140字节
Step2: 服务端接收140字节,发送80字节响应以及ACK
【 客户端 】发送140字节之后等待【 服务端 】的ACK
【 服务端 】可用窗口右移,【RCV.NXT】从1->141
【 服务端 】发送80字节数据,【SND.NXT】从241->321
Step3: 客户端接收响应ACK,并且发送ACK
【 客户端 】发出的140字节得到确认,【SND.UNA】右移140字节
【 客户端 】接收80字节数据,【RCV.NXT】右移80字节,从241->321
Step4: 服务端发送一个280字节的文件,但是280字节超出了客户端的接收窗口,所以客户端分成两部分传输,先传输120字节;
【 服务端 】发送120字节,【SND.NXT】向右移动120字节,从321->441
Step5: 客户端接收文件第一部分,并发送ACK
【 客户端 】接收120字节,【RCV.NXT】从321->441
Step6:服务端接收到第二步80字节的ACK
[ 服务器 ] 80字节得到ACK 【SND.UNA】从241->321
Step7: 服务端接收到第4步的确认
【 服务端 】之前发送文件第一部分的120字节得到确认,【SND.UNA】右移动120,从321->441
Step8: 服务端发送文件第二部分的160字节
【 服务端 】: 发送160字节,【SND.NXT】向右移动160字节,从441->601
Step9: 客户端接收到文件第二部分160字节,同时发送ACK
【 客户端 】接收160字节,【RCV.NXT】向右移动160字节,从441->601
Step10: 服务端收到文件第二部分的ACK
【 服务端 】发送的160字节得到确认,【SND.UNA】向右一定160字节,从441->601;至此客户端收到服务端发送的完整的文件;
上面通过表格列举服务端和客户端每个状态在每个步骤的状态,如果不是很好理解,可以看如下示意图辅助理解:
客户端交互流程
服务端交互流程
上面👆是模拟一个GET请求,服务端发送一个280字节的文件给到客户端,客户端的接收窗口是200字节场景加,客户端和服务端的数据传输与交互流程,通过这个流程来学习滑动窗口的移动状态和流量控制的大致流程;
滑动窗口与操作系统缓冲区
上面👆讲述的时候,都是假设窗口大小是不变的,而实际上,发送端和接受端的滑动窗口的字节数都吃存储在操作系统缓冲区的,操作系统的缓冲区受操作系统控制,当应用进程增加是,每个进程分配的内存减少,缓冲区减少,分配给每个连接的窗口就会压缩。**而且滑动窗口的大小也受应用进程读取缓冲区数据速度有关**;
应用进程读取缓冲区数据不及时造成窗口收缩
step1: 客户端发送140字节
客户端发送到140字节之后,可用窗口收缩到220字节,发送窗口不变
Step2: 服务端接收140字节 但是应用进程仅仅读取40字节
服务端应用进程仅仅读取40字节,仍有100字节占用缓冲区大小,导致接受窗口收缩,服务端发送ACK报文时,在首部Window带上接收窗口的大小260
Step3: 客户端收到确认报文之后,发送窗口收缩到260
Step4: 客户端继续发送180字节数据
客户端发送180字节之后,可用窗口变成80字节
Step5: 服务端接收到180字节
假设应用程序仍然不读取这180字节,最终也导致服务端接收窗口再次收缩180字节,仅剩下80字节,在发送确认报文时,设置首部window=80
Step6: 客户端收到80字节的窗口时,调整发送窗口大小为80字节,可用窗口也是80字节
Step7: 客户端仍然发送80字节到服务端,此时可用窗口为空
Step8: 服务端应用进程继续不读区这80字节的缓冲区数据,最终导致服务端接收窗口大小为0,不能再接收任何数据,同时发送ACK报文;
Step9:客户端收到确认报文之后,调整发送窗口大小为0,这个状态叫做「 窗口关闭 」
窗口收缩导致的丢包
Step1:客户端服务端开始的窗口大小都是360字节,客户端发送140字节数据
客户端发送140字节之后,可用窗口变成220字节
Step2:服务端应用进程骤增,进程缓存区平均分配,造成服务端接收窗口减少,从360变成240字节;
假设接收了140字节之后,应用进程没有读取,那个可用窗口进一步压缩,变成100字节;
Step3:假设同一个连接在没有收到服务端确认之后,又发送了180个字节的数据(Retramission)
先发送了140字节,后发送了180字节,都没有得到确认,客户端可用窗口大小变成40字节
Step4:服务端收到上面👆第三步发送的180字节的数据,但是接受窗口的大小只有100字节,所以不能接收
服务端拒绝接收180字节
Step5:此时客户端才收到之前140字节的确认报文,才知道接收窗口发生了变化
客户端由于没有收到180字节的确认,加入客户端正在准备发送180字节数据,得到接受端的窗口大小是100字节之后,须强制将右侧窗口向左收缩80字节;
窗口关闭
这个例子和上面的例子都发生了「 窗口关闭 」
窗口关闭: 发送端的发送窗口变成0的状态;
上面讲的两种情况一般不会发生的,因为操作系统不会既收缩窗口,同时减少连接缓存;而是一般先使用窗口收缩策略,之后在压缩缓冲区的方式来规避以上问题;
发生窗口关闭之后,发送端不会被动的等待服务端的通知,而是会采用定时嗅探的方式去查看服务端接收窗口是否开放;
Linux中对TCP缓冲区的调整方式
net.ipv4.tcp_rmem = 4096 87380 6291456
读缓存最小值、默认值、最大值,单位字节,覆盖 net.core.rmem_max
net.ipv4.tcp_wmem = 4096 16384 4194304
写缓存最小值、默认值、最大值,单位字节,覆盖net.core.wmem_max
net.ipv4.tcp_mem = 1541646 2055528 3083292
系统无内存压力、启动压力模式阀值、最大值,单位为页的数量
net.ipv4.tcp_moderate_rcvbuf = 1
开启自动调整缓存模式