滑动窗口

# 滑动窗口 >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 > 开启自动调整缓存模式