【Linux网络】传输层协议TCP(3)滑动窗口、流量控制

hello~ 很高兴见到大家! 这次带来的是Linux网络中关于网络基础这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?

个 人 主 页 : 默|笙


文章目录


一、滑动窗口

1.1 流量控制与窗口探测

  1. 流量控制就是通过 TCP 报文首部中的 16 位窗口大小这个属性,来知晓对方的接收能力,从而调整发送的速度。
  2. 当对方的接收能力为 0 时(即接收窗口大小为 0),发送方如何知道对方什么时候能够重新接收数据?这时候就用到"窗口探测"这个策略了。假设 A 和 B 通信,当 B 的接收缓冲区满了之后,A 会启动一个持续计时器,每隔一段时间向 B 发送一个探测报文。因为确认应答机制,B 收到探测报文后必须给 A 返回一个应答,这个应答报文里就包含 B 当前的窗口大小,A 借此就能知道 B 的恢复情况。

真正的窗口探测报文,通常会携带 1 个字节的有效数据。如果 B 的缓冲区还是满的,B 会拒绝接收这 1 个字节,并回复一个 ACK,里面的窗口大小依然是 0;如果 B 刚好腾出了空间,B 就会收下这 1 个字节,并回复一个包含新窗口大小的 ACK。A 借此就能实现精准的探测。

为什么能传报头?B的接受缓冲区不是满了吗?这里说的"满",指的是内核中的 TCP 接收缓冲区满了,无法再存放有效载荷。而报头(Header)是由系统内核的网络协议栈直接解析的,处理报头既不占用 TCP 接收缓冲区,也不受其容量限制。

  1. 当 B 有了空闲位置之后,它的内核也会主动向 A 发送一个窗口更新通知(Window Update),告知 A 最新的窗口大小。这个通知报文在传输过程中同样有丢失的风险,此时窗口探测机制就可以作为兜底。这两个策略共同工作,确保即使通知丢失,发送方也能通过探测探知接收方的最新窗口状态。

  2. 那么,在第一次发送数据报文时,发送方怎么知道对方的接收能力呢?答案就在三次握手的过程中。在建立连接时,双方发送的 SYN 报文和 SYN-ACK 报文的首部中,都携带了各自的初始窗口大小(Window Size)字段。因此,在连接正式建立、发送第一个数据报文之前,发送方就已经通过三次握手获知了对方的初始接收能力。这也正是三次握手的重要作用之一,它不仅在握手阶段完成了序列号同步,还顺带完成了初始窗口大小的协商与同步。

  3. 流量控制是动态调整的:它不仅会在对方接收能力变小时,限制发送方减慢发送速度;也会在对方接收能力变强(窗口变大)时,允许发送方加快发送速度。

1.2 滑动窗口

介绍

我们知道,TCP 发送数据并不是"发一个报文,等一个应答"的停止等待模式,而是一下子连续发送多个报文,将大大缩短提高性能(就是把多个应答的等待时间叠在一起了)。那么问题来了:这些连续发出的报文存在哪里?而且,发送出去的报文在等到对方的应答(ACK)之前是不能丢弃的(因为如果丢了,对方没收到,就需要重传),它们又保存在哪里?流量控制又是如何精准地做到"不多发、不少发"的?这一切,都跟接下来要讲到的滑动窗口有关。

  1. 滑动窗口位于发送缓冲区中,是发送缓冲区里的一块区域,通信双方的发送缓冲区都有。它把发送缓冲区分成三个部分:已确认区域、窗口区域(即"正发送区域")和待发送区域,这里假设发送缓冲区是满的。发送缓冲区在逻辑上是线性结构,因此滑动窗口自然也是线性的,可以把它当作 char 类型数组来理解。
  2. 已确认区域是已经收到应答(ACK)的报文数据;窗口区域是滑动窗口当前覆盖的范围,表示当前允许发送或已发送但尚未确认的数据;待发送区域则是窗口右侧尚未进入发送窗口的数据。滑动窗口通过两个指针(索引)来定义开始和结束位置:左指针 start_win 指向窗口的起始位置,右指针 end_win 指向窗口的结束位置。
  3. 发送窗口内尚未发送的数据可以直接连续发送,不必"发一个等一个应答再发下一个";发送出去后进入等待应答状态。只有窗口左端的数据收到确认应答后,滑动窗口才会右移,已确认的数据才可以释放或复用;未被确认的数据会一直保留在发送缓冲区中,不能删除。

滑动窗口的正常工作流程

  1. 首先要确认滑动窗口的大小。这个滑动窗口的大小依据对方的接收能力(通知窗口cwnd)和拥塞窗口而定:
    win_size(滑动窗口容量) = min(对方接收能力cwnd, rwnd(拥塞窗口大小,现在暂不做考虑,只看对方接收能力)),不因待发数据少而变小,滑动窗口允许有空位。
    窗口通过移动两个指针来确定:
    start_win = 初始序列号 + 1 / 应答报文中的确认序号,
    end_win = start_win + win_size
    。

    确认序号的意义就是让窗口左边界能够向前滑动,释放已确认数据占用的窗口空间。

  2. 假如初始序列号是 1001,对方的接收能力是 4000,要发送的数据设为无穷,则

    win_size = min(4000, 无穷) = 4000,

    start_win = 1001,end_win = 5001。

    这个窗口内的数据可以连续发出去,之后就是等待应答报文了。(如图)

  1. 当收到确认应答之后,窗口就要更新:start_win = 确认序号 = 2001。

    如果对方刚把数据放进接收缓冲区就被应用层读取上去,那么接收能力仍是 4000,窗口大小不变,但整体向右移动,此时 end_win = 2001 + 4000 = 6001。

    如果对方没有及时读取数据,那么它的接收能力就是 3000,start_win = 5001,窗口缩小了。

    窗口也有变大的时候,比如接收能力变大时,例如应用层把其他数据读取上去。

  2. 滑动窗口有没有可能左移?研究两个指针就可以了:

    start_win 不收到对应的确认序号是不会移动的,而确认序号的含义是"该序号之前的数据已经接收完毕",所以 start_win 没有可能左移,而且左边的数据也已经被清理或复用了。

    再看右指针 end_win = start_win + win_size。正常情况下 end_win 也只会向右移动,因为窗口右边界不应收缩。这里要注意:win_size 并不是发送前完全固定不变,它会随接收能力变化;如果 win_size 变小,按公式 end_win 可能比原来小,但 TCP 设计上应避免已通告窗口的右边界左移,发送方也不应把窗口右边界回退。所以结论是:两个指针正常都只向右移动------start_win 不左移是因为确认序号只前进,end_win 不左移是因为窗口右边界不收缩。

  3. 滑动窗口的大小通常取决于对方的接收能力以及之后要讲到的拥塞窗口,因为一般要发送的数据会很多,滑动窗口机制是实现流量控制的重要方式。接收方通过通告窗口(rwnd)告诉发送方还能接收多少数据,发送方据此调整滑动窗口,从而控制发送速率,适配接收方的处理能力。

异常情况

情况一:报文已送达,应答丢了
  1. 如果报文送到了,但是应答丢了怎么办?如图所示,信道良好时,总有应答能够传递到 A。即使 ACK=1001、ACK=3001、ACK=4001 丢了,只要 ACK=5001、ACK=6001 到达,就能根据累计确认判断:该确认序号之前的数据都已经收到,前面的报文无需重传。对于最新发送的报文,它传过去后如果迟迟收不到对应的确认应答,就由超时重传机制兜底:超时后重传该未确认报文。这就是确认序号机制可以大大减少因应答丢失而触发的重传次数的原因。
情况二:报文丢了
  1. 如果报文丢了,主机 B 丢失了 1001~2000 这段数据,而后面的数据都收到了,那么主机 B 对后续报文返回的确认序号都会是 ACK=1001,表示 1001 之前的数据已收到,期望下次从 1001 开始发送。主机 A 一旦收到三个这样的重复 ACK(冗余 ACK),就判断该报文丢失,立即触发快速重传。等主机 B 接收到 1001~2000 这段数据后,会把已缓存的后续数据拼接起来,并按累计确认机制更新确认序号,通常直接更新到连续收到的数据末尾。超时重传机制作为兜底,与快速重传一起保证数据传输的可靠性。

快速重传(Fast Retransmit) 是 TCP 在"疑似丢包"时提前重传的一种机制:发送方收到三个冗余 ACK(同一个确认序号重复到达三次)后,不再等超时定时器到期,而是立即重传对应报文。

  1. 其他更复杂的丢包情况,通常可以看作"报文丢失"和"应答丢失"这两种基本情况的组合;解决方法仍然是上述机制的组合运用:依靠累计确认减少因 ACK 丢失导致的重传,依靠冗余 ACK 触发快速重传,并由超时重传兜底,保证数据最终可靠传输。
通过滑动窗口来看情况二
  1. 通过滑动窗口来看丢失情况,可以把丢失情况分为三类:最左侧数据丢失、中间数据丢失、最右侧数据丢失。如果是最左侧数据丢失,左指针 start_win 不会移动,因为确认序号一直等于当前的 start_win;在重传并收到新的应答之后,左指针才会向右移动。假如下一次后续数据都收到了,那么下一次确认序号会因累计确认机制直接更新为 5001;假使接收能力为 2000,那么就是 start_win = 5001,end_win = 5001 + 2000 = 7001。

  2. 如果是中间数据丢失,假如是 2001~3000 这段数据丢了,那么确认序号会是 2001,发送方收到后把 start_win 推进到 2001;此时 2001~3000 就成为当前窗口最左侧的未确认数据,这不就是最左侧数据丢失的情况吗?最右侧数据丢失也是同样:前面的数据确认后,丢失段会成为新的窗口左端未确认数据。所以,从"当前窗口最左侧未确认数据"的角度看,数据丢失都可以概括为一种情况,那就还是最左侧数据丢失。区别主要在于:中间丢失常能由后续数据触发冗余 ACK,进而快速重传;最右侧丢失可能更多依赖超时重传兜底。

滑动窗口的边界?
  1. 最后一点,滑动窗口一直向右移动,到发送缓冲区边界了怎么办?可以把发送缓冲区理解成一个环形队列。逻辑上,滑动窗口的右边界在序列空间里持续向右推进;物理上,缓冲区用固定大小的环形队列实现,队头对应已确认区域,队尾对应待发送区域,窗口在队列中向右滑动。已确认的数据从队头出队并释放空间,新数据从队尾入队;指针走到数组末尾时,通过取模回绕到开头,复用左侧已释放的空间。未确认的数据继续留在队列中。这样,滑动窗口在逻辑序列空间上一直向右移动,在环形队列中则循环使用物理空间。缓冲区满时,窗口右边界受可用空间限制,新数据暂不能入队,等确认释放空间后再继续推进。

  2. 总结:连续发出的报文保存在发送缓冲区中,窗口内未被确认的数据会一直保留;收到确认应答后,窗口左边界向右移动,已确认的数据才可以删除或复用。流量控制依靠滑动窗口实现:接收方通过通告窗口告知接收能力,发送方据此调整发送窗口大小。

  3. 为什么发送数据不直接一整个发送4000字节,而是要拆成好几段(通常按MSS,比如每段不超过约1460字节)进行发送?这主要和数据链路层的MTU限制有关,MSS通常由MTU推导,MAC帧最大长度是底层原因之一。


今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~

让我们共同努力, 一起走下去!

相关推荐
xiaoye-duck2 小时前
《C++ 项目:手写大模型接入 SDK》ChatGPT接口设计(中):全量响应实现和测试
网络·c++·语言模型·chatgpt
Linux界最忧郁之人3 小时前
华为云ECS&IMS&AS实战|弹性云服务器、私有镜像、弹性伸缩完整实验
linux·运维·服务器·centos·华为云·openstack
whyutianict_vv3 小时前
零基础转 AI 运维:投入清单、技术路线与核对表
linux·运维·人工智能·ai
无言听风雨丶3 小时前
systemd 与信号 / 僵尸进程:Linux 进程管理的底层逻辑与排错
linux·服务器·网络
DevOpenClub4 小时前
高考一分一段 API:分数查位次、科类枚举与历史对照
linux·数据库·高考
小贾要学习4 小时前
【C++后端开发项目】Reactor 模型 / C++ 仿写 muduo HTTP 服务器:让底层网络库真正跑起网页
linux·服务器·c++·http·网络编程·muduo·reactor模型
g5zhu58964 小时前
没有公网ip的情况下,本地能ping服务器服务器不能ping本地,让服务器能访问本地某端口的简单方法
服务器·网络协议·tcp/ip
Blockbuater_drug4 小时前
OpenMM分子动力学模拟02 安装:Linux:conda 一条命令装通 GPU 加速的分子模拟环境
linux·conda·cuda·gpu驱动·pythonpath·openmm安装
SDWAN_Cheap4 小时前
BGP是什么,广播IP是什么?两个容易混淆的概念拆解
网络·tcp/ip·bgp·广播ip