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