资讯动态

计算机网络篇18:传输层——TCP 滑动窗口、快速重传与拥塞控制,发送窗口为什么取 min

发布时间:2026/11/6 1:47:47 来源:尧图企业网站定制
本文收录于「流浪」的系列专栏Linux系统⚙️C数据结构与算法PythonLangChain LangGraph️MySQL 数据库Git 工具计算机网络LLM大厂面试、八股学习筑基专栏 博客主页流浪 原创首发于 CSDN前言篇17 把「收不到 ACK 不等于丢包要没应答加超时才算丢」讲完了。可靠传输还有几件事发送方凭什么能连着发、丢了怎么补、网络堵了怎么退以及连接那头突然没了怎么办。这篇从一只会往前挪的窗口讲起把滑动、重传、拥塞控制、字节流和异常连接串成一条线。一、滑动窗口发送方不等 ACK 也能继续发1.1 一只会往前挪的取货框发送缓冲区像一排货架应用把数据放上去TCP 从货架上取货往外发对方确认一件这件才算真正交出去。如果发一件等一件一个往返时间RTT里只能发一个报文链路再宽也跑不满。窗口就是「不用等确认、可以继续发」的那段额度框里的货可以先发框外的得等框往前挪。窗口框住的已经发出去、还没被确认的还没发、但窗口允许发的。start和end是两只下标start指向最早没被确认的字节end指向窗口右边界窗口滑动就是两只下标一起往右走。窗口不是一堵墙是一段「允许在途」的额度额度内尽管发额度外等确认。1.2 四段地盘已确认的那块不用擦把发送缓冲区按「发没发、确认没确认」切开能切出四段已发已确认、已发未确认、未发可发、未发不可发。绿色窗口框住的是中间两段已发未确认、未发可发左边是已经签收的右边是还不能动的。已确认的那段数据已经作废空间可以立刻拿回来放新数据不需要把整块缓冲区清一遍——这就是环形缓冲区的用法。已发未确认的那段不能删丢了要靠它重传这也是后面所有重传机制的底气。窗口右边界 左边界 窗口大小左边界就是确认号窗口大小来自接收方报文里的win字段十六位窗口大小。win是 16 位最大 65535 字节窗口缩放window scaling选项可以把它按比例放大不然高带宽长距离的链路根本喂不饱。缩放因子最多 14 位窗口上限可以抬到 2 的 30 次方字节具体值在三次握手时协商。左边界由确认号钉住右边界由接收方还能收多少决定两块一夹就是发送方此刻能发的范围。1.3 窗口只往右滑大小却会变累计确认ACK n 表示 n 之前的字节全部收到接收方不逐个报文回「我收到第几段」只报「我连续收到了哪」。所以窗口只会往右滑不会往回退左边界一旦被确认号推过去就不会再回来。窗口大小是另一回事接收方缓冲区紧win 就小缓冲区松win 就大最紧的时候 win 可以是 0。win 0 时发送方停发起一个坚持定时器persist timer定期去问免得窗口更新报文丢了两边互相干等。接收方用 win 给发送方限速这就是流量控制滑动窗口是流量控制的具体实现。实际上窗口会滑会最前面发送缓冲区是一段连续的内存可以看成一维数组如果滑动窗口滑动到缓冲区最后了前面的数据已经被对方接收并确认了为了继续发送数据滑动窗口就要滑回最前面进行下一轮发送因此我们主观视角可以把它看成一个环形的缓冲区窗口的「滑动」由 ACK 推着走「大小」由接收方缓冲区牵着走两个维度分开看才不会把窗口当成固定长度。1.4 延迟应答晚一点回窗口反而更大接收方不必每收到一段就立刻回 ACK它可以等几十毫秒或者每收到两个段回一次这就是延迟应答delayed ACK。假设接收缓冲区里还压着 20KB 没被应用读走剩余空间 24KB发送方又发来 10KB剩余 14KB按规矩这时回的 win 应该是 14KB。接收方没急着回等了一会儿应用把这 20KB 读走了剩余空间变成 34KB这时候再回 ACKwin 就能写 34KB。同样是这一来一回发送方看到的窗口从 14KB 变成 34KB一次能多发 20KBACK 的数量还少了开销更低。代价是发送方要多等一会儿遇到乱序报文、或者窗口需要立刻调整时接收方不能拖得马上回。延迟应答不是把窗口变大是晚一点报让窗口在这段时间里自己变大。延迟应答拿一点等待换更大的窗口和更少的 ACK它不是扩大窗口是把「确认」和「窗口更新」攒到更划算的时候一起发。二、三种丢包位置窗口的左边界停在哪2.1 先把例子摆好从 1001 开始发发到 5001 这个确认边界正好四段每段 1000 字节1001–2000、2001–3000、3001–4000、4001–5000。四段全到接收方回 ACK 5001累计确认号到 5001就表示 5001 之前一个字节都不缺。丢包只看三个位置最左、中间、最右实际网络里可以自由组合但先把单点看明白。看窗口怎么动只看一件事——第一个没到的字节在哪。2.2 最左侧的数据丢了ACK 卡在 1001接收方收到 2001–5000唯独 1001 没到它不能确认 1001 之后的任何字节只能回 ACK 1001。后面每来一段乱序报文它再回一次 1001发送方连着收到三个重复 ACK就知道 1001 附近丢了。发送方的左边界停在 1001窗口不往前滑三个重复 ACK 触发快重传补发 1001–2000。补上之后接收方手里 1001–5000 齐了累计确认直接跳到 5001窗口一次滑到 5001。最左丢窗口原地不动它不是不滑是被第一个空洞卡住了。2.3 中间丢了左边界滑到空洞1001–2000 到了接收方回 ACK 2001接着 2001–3000 丢了。接收方又收到 3001–4000、4001–5000但 2001 没到只能继续回 ACK 2001。发送方的左边界可以滑到 2001——前面的都确认了——但不能再往后这就是「滑到丢失的位置」滑到空洞的起点不是跳过空洞。这个例子里2001 后面只剩两段凑不够三个重复 ACK快重传触发不了最后得等超时。中间丢窗口能滑一段但一定停在空洞口累计确认只认连续前缀。2.4 最右侧丢了同样是一个道理4001–5000 丢了接收方手里 1001–4000 是齐的回 ACK 4001左边界滑到 4001。后面还有数据时乱序报文会继续回 ACK 4001凑够三个就能快重传。丢的正好是最后一段时后面没有报文凑不出重复 ACK只能等超时。右端丢窗口停在 4001能不能快重传取决于后面还有没有足够的报文替它喊冤。2.5 丢的是 ACK不是数据后面的累计确认能盖住第一个 ACK2001在回程丢了但后面的 ACK 3001、4001、5001 都到了累计确认 5001 说明 5001 之前全到窗口直接滑到 5001。数据丢和 ACK 丢要分开数据丢窗口被空洞卡住ACK 丢后面的累计 ACK 能覆盖。如果一段数据里出现多个空洞累计 ACK 只能告诉发送方「第一个洞在哪」SACK选择确认让接收方把「我收到了哪几段」一起报回去发送方只补空洞不用一段段试。丢 ACK 不可怕怕的是丢数据累计确认是打包确认SACK 是把打包拆开告诉你缺哪块。三、快重传与超时重传提前补与兜底补3.1 超时重传最后的保险发出去的数据开始计时超过重传超时时间RTO还没等到 ACK就认定丢了重发。每重发一次等待时间翻倍这叫指数退避累计到一定次数还不通TCP 认定这条路或对端出了问题直接断开。它不会漏但太慢丢一次包要干等一个 RTO吞吐掉得厉害。超时重传不会漏但太慢它是兜底不是首选。3.2 快重传三个重复 ACK 就动手接收方收到乱序段会重复回同一个 ACK发送方连着收到三个重复 ACK有的教材把此前那个正常 ACK 也算上说成四个相同的 ACK就认定丢包。不等 RTO 到期立刻重传缺失的那一段后面的段接收方已经缓存不用重发。为什么是三个一两个重复 ACK 可能只是网络乱序三个才比较像真丢了阈值太低会把乱序当丢包阈值太高又等得久。丢包位置越靠右后面能替它喊冤的报文越少四段的例子里只有最左侧丢能凑够三个重复 ACK中间丢只剩两段、最右丢一段都没有最后都得靠超时。快重传是用重复 ACK 提前看出空洞它赌的是乱序不会连续骗人三次。3.3 两者不是包含关系是先后关系快重传丢包被看出来了提前补触发条件是重复 ACK。超时重传一直没人告诉你等到 RTO 兜底触发条件是超时。两者都属于「发现丢包 → 重传」但触发条件不同不是谁包含谁。快重传之后走快恢复超时之后回慢启动两条路的分岔落在 cwnd 怎么退上。快重传抢时间超时重传保底线两条路一前一后把丢包补完。3.4 滑动窗口就是重传的缓存发出去没确认的报文必须留在发送缓冲区里否则丢了没东西可补。窗口根据 ACK 决定滑不滑、滑到哪滑过去的那部分才是可以释放的。没有这份缓存快重传和超时重传都无从谈起。窗口不只是「能发多少」的额度也是「丢了能补」的仓库。四、流量控制之外为什么还需要拥塞控制4.1 只看接收窗口为什么还会堵接收方说「我还能收十箱」你就发十箱可中间的路可能只有一座独木桥十箱同时挤上去桥就堵了。接收窗口rwnd只管接收方缓冲区管不了中间路由器路由器队列有限发得太猛会排队溢出、丢包。丢包不只发生在「对方没收到」也可能发生在「网络装不下」。上亿个客户端共享同一张网谁都不退网络迟早被压垮。接收窗口是对方的能力拥塞窗口是网络的能力前者看得见后者只能猜。4.2 丢包多和丢包少结论不一样少量、零散丢包更像链路误码或抖动补一下就行不必大幅退让。大量、连续丢包更像网络已经堵了必须退让这时候还按原速重发只会加重负载越堵越丢。传统 TCP 不区分丢包形态见到丢包就按拥塞处理工程上会看形态现代算法还会看时延和带宽。丢包是信号不是判决少量可能是偶然大量才说明路真的堵了。4.3 拥塞窗口 cwnd 与阈值 ssthresh拥塞窗口cwnd是发送方自己维护的「我敢发多少」不看对方报文只按网络反馈调整。ssthresh 是慢启动转拥塞避免的分界是阈值不是窗口cwnd 涨到它就换一种涨法。发送窗口 min(rwnd, cwnd)谁紧听谁的。实际还能发多少 min(rwnd, cwnd) − 在途未确认字节窗口是上限在途要扣掉。rwnd 是别人给的(16位窗口大小)cwnd 是自己猜的发送窗口取两者较小值谁紧听谁的。五、慢启动与拥塞避免cwnd 怎么涨、怎么退5.1 慢启动每个 RTT 翻一倍cwnd 起步很小早期实现从 1 个 MSS 开始RFC 6928 之后 Linux 默认 10 个 MSS。每收到一个 ACKcwnd 加 1 个 MSS一个 RTT(往返时间)内 发出去多少段就回来多少个 ACK所以下一个 RTT 窗口翻倍1、2、4、8、16……第 n 个 RTT 是 2 的 n 次方个 MSS。「慢」指的是起点低不是增速慢它是一边问路一边加速。好处一试错便宜先拿一两个报文问路猜错了也只丢一点不会一上来把整条路打爆。好处二逼近快翻倍是对数级1→2→4→…→1287 个 RTT 就到 128 个 MSS线性一次加一个要 127 个 RTT。好处三知道收油涨到 ssthresh 就切线性避免翻倍冲过头。代价翻倍一定会超调超调那一下容易丢一批所以后面要配拥塞避免和快恢复。慢启动是拿小窗口问路问到了就翻倍走它不是慢是先从最小风险开始。5.2 拥塞避免过了 ssthresh 改成线性cwnd 涨到 ssthresh 后每个 RTT 只加 1 个 MSS不再翻倍。线性增长也叫加性增大additive increase探得更稳不会一下撞墙。从「翻倍」切到「加一」切换点就是 ssthresh。慢启动负责快速逼近拥塞避免负责贴着上限慢慢试ssthresh 就是换挡点。5.3 拥塞发生超时与快重传两条路超时ssthresh 记为当前 cwnd 的一半cwnd 回到 1 个 MSS重新慢启动这叫 Tahoe。三个重复 ACK快重传 快恢复ssthresh 同样减半cwnd 降到 ssthresh 附近直接进拥塞避免这叫 Reno。快恢复的细节ssthresh cwnd/2cwnd ssthresh 3 个 MSS代表已经离开网络的三段重传丢失段每再收到一个重复 ACKcwnd 加 1收到新数据的 ACK 后cwnd 回到 ssthresh。「归零」是口语真实值是回到 1 个 MSS真归零就停发了。快恢复的意义不退回起点避免吞吐断崖。超时按最坏情况处理快重传按「只是丢了一段」处理一个回起点一个只退半步。5.4 AIMD加性增、乘法减加性增每个 RTT 加 1 个 MSS。乘法减拥塞时 cwnd 减半。涨得慢、退得快画出锯齿。为什么减半不是清零清零太保守减半能保住一部分已经探到的带宽多条连接最终能分到相对公平的份额。AIMD 是小心试探、撞墙退半用锯齿换稳定用退让换公平。5.5 再往后的 BBR只看丢包不够BBR 不把丢包当唯一信号改看带宽和往返时延。它试图把窗口维持在瓶颈带宽与 RTT 的乘积附近也就是带宽时延积BDP。它的目标是让窗口贴着 BDP不靠丢包来判断上限。从「丢包就退」到「按带宽和时延发」拥塞控制一直在找一个更准的「网络还能吃多少」。六、TCP 是字节流粘包不是 TCP 的锅6.1 水管里没有信封TCP 只保证字节按顺序到达不保证「你写几次对方就读几次」。发送方写两次「hello」「world」接收方可能一次读到「helloworld」也可能先读到「hel」再读到「loworld」。这不是 TCP 丢包或出错是字节流本来就没有消息边界所谓「粘包」「拆包」是应用层眼里的现象。TCP 是一条水管不是一摞信封水管只保证水按顺序流不保证一次接到一封信。6.2 粘包是怎么来的发送方连续写小数据TCP 会把它们攒进发送缓冲区凑够一段再发。接收方应用读得慢缓冲区里堆了好几段一次 read 就全读出来了。数据超过 MSSTCP 会拆成多段发到了接收方也可能分几次才读全。这三种情况都不是协议 bug是字节流的正常表现。粘包不是「粘」出来的是应用层没有约定「一封信到哪儿算完」。6.3 三种定边界的办法固定长度每条消息长度固定读满 N 字节就是一条简单但浪费空间长度不灵活。适合定长指令、二进制协议消息长度变化时不好用。分隔符用换行、\0等特殊字符分隔实现简单但消息里不能出现分隔符或者要做转义。适合文本协议二进制数据里出现分隔符就得转义。长度字段消息头里先写「后面还有多少字节」接收方先读头、再读体最常用HTTP 的Content-Length就是这种思路。适合二进制、变长消息先读固定长度的头再按长度读体。定边界的活是应用层干的固定长度、分隔符、长度字段三选一或者组合用。6.4 序列化不等于定边界序列化是把结构体、对象变成字节流反序列化是把字节流还原回来。序列化本身不解决「一条消息到哪儿算完」protobuf 这类格式也要在消息前加长度JSON 也要靠分隔符或长度字段。「在 TCP 协议里明确报文边界」这句话不准确TCP 不保存边界边界由应用层协议定。TCP 只管送字节不管分信件边界、序列化、协议格式都是应用层自己的事。七、TCP 异常进程没了、机器关了、网线拔了7.1 进程终止与机器重启正常情况下进程退出或调用 close内核会释放文件描述符替它发一个 FIN这和正常关闭没有区别。机器正常重启、关机操作系统会关掉所有进程内核替每条连接发 FIN也走四次挥手。一端先关只说明「我不发了」另一端还可以继续发这叫半关闭双方都关完连接才彻底结束。进程没了、机器关了只要内核还能发 FIN对端看到的就是一次正常关闭。7.2 掉电与断网机器突然掉电、网线被拔来不及发 FIN对端不知道你没了还以为连接活着。如果对端往这条连接写数据数据发出去没人确认只能一遍遍重传最后超时报错如果对面已经重启收到旧连接的报文会用 RST 回绝连接直接重置。如果双方都不写数据这条连接就会一直挂着直到保活机制发现对面不在了。掉电和断网没有告别对端只能靠「写不出去」或「保活探测」才知道你走了。7.3 保活定时器TCP 内置保活定时器keepalive连接闲置一段时间后主动发探测报文看对面还在不在。Linux 默认闲置 2 小时才开始探每隔 75 秒探一次连续 9 次没有响应才释放连接。它能兜底但很慢两个小时的空窗很多业务等不起。保活定时器是 TCP 自带的「定期点名」能发现死连接但默认节奏太慢。7.4 应用层的心跳HTTP 长连接会定期检测对方状态QQ 这类应用断线后会定期重连。应用层心跳比 TCP 保活更灵活间隔、次数、超时都能按业务调还能顺带做鉴权、负载上报。所以「TCP 对异常连接有很强的容错」要打个折扣它能靠重传和保活兜底但发现死连接可能很慢工程上更常用应用层心跳。TCP 能兜住数据重传但发现死连接很慢真要快速感知还得靠应用层心跳。八、全篇总结1. 窗口滑的是确认窗口框住已发未确认和可发未发左边界由 ACK 钉住右边界由 win 决定。已确认区可复用不必清空窗口大小可大可小最小到 0。2. 丢包看位置最左丢ACK 卡在空洞中间丢左边界滑到空洞最右丢停在最后一段。数据丢会被空洞卡住ACK 丢会被后面的累计确认盖住多个空洞靠 SACK。3. 重传分提前与兜底快重传靠三个重复 ACK 提前补超时重传靠 RTO 兜底。两者是先后关系不是包含关系滑动窗口就是重传的缓存。4. 拥塞控制是给网络让路rwnd 管对方cwnd 管网络发送窗口取两者较小值。大面积丢包要退让不能立即重发少量丢包可能只是误码。5. 慢启动探路AIMD 收场慢启动每 RTT 翻倍到 ssthresh 转线性。超时回 1 个 MSS快重传走快恢复加性增、乘法减。6. 字节流没有边界异常连接靠保活TCP 不保存消息边界粘包是应用层没定好边界固定长度、分隔符、长度字段三选一。进程退出、机器重启会发 FIN掉电、断网没有 FIN只能靠重传、保活或应用层心跳。九、文末面试题9.1 推导题1. 发送窗口为什么取 min(rwnd, cwnd)只取其中一个会怎样答推导rwnd 是接收方还能收多少cwnd 是发送方估计网络还能塞多少两个都是上限。只取 rwnd接收方是舒服了可中间网络可能已经堵了继续猛发只会让路由器丢包只取 cwnd网络是舒服了可接收方缓冲区可能已经满了发过去也装不下。取两者较小值等于同时听两个瓶颈的话谁紧就按谁的速度来。2. 快重传为什么等三个重复 ACK而不是一个答推导一个重复 ACK 可能只是报文乱序接收方先收到了后面的段并不代表前面的段真丢了两个也可能是乱序。连续三个重复 ACK说明后面已经有好几段都到了、唯独那个空洞一直没补上这才比较像真丢包。阈值定在三个是在「早点重传」和「别把乱序当丢包」之间取平衡定得太低会频繁误重传定得太高又要多等。3. 拥塞发生后为什么 cwnd 减半而不是清零AIMD 为什么能收敛答推导清零等于把好不容易探到的带宽全扔掉吞吐会断崖式下跌而且接下来又要从 1 个 MSS 慢慢爬代价太大。减半保留了「刚才这条路的承载能力大概在哪」的信息只退一半既能快速缓解拥塞又不至于浪费已探到的容量。AIMD 的加性增让每条连接每个 RTT 只加一点乘法减让拥塞时所有连接一起退多条流在反复「加一点、退一半」的过程中会收敛到相对公平的带宽份额。9.2 真题1. TCP 滑动窗口的作用是什么窗口大小由什么决定答推导 · 已对照解析转述TCP 不采用发一个等一个的方式而是在发送缓冲区上划一个窗口窗口内的数据可以连续发送不必等前一个 ACK。窗口左边界是最早未确认的字节右边界由接收方报文里的 win 字段决定也就是接收方缓冲区还能收多少窗口大小因此由接收方的接收能力决定。发送方按 ACK 推动左边界右移窗口跟着滑动这就是流量控制。【真题·转述自 50道经典计算机网络面试题】2. 流量控制和拥塞控制有什么区别答推导 · 已对照解析转述流量控制是端到端的接收方用 win 告诉发送方「我还能收多少」防止发送方发得太快把接收缓冲区撑爆拥塞控制是面向网络的发送方用 cwnd 估算「路上还能塞多少」防止过多数据注入网络把路由器队列撑爆。前者看接收方后者看整条链路发送窗口取 min(rwnd, cwnd)。【真题·转述自 50道经典计算机网络面试题】3. 慢启动和拥塞避免有什么区别答推导 · 已对照解析转述慢启动从很小的 cwnd 起步每收到一个 ACK 加 1 个 MSS一个 RTT 翻倍按 2 的 n 次方增长用来快速探出网络能承受多少cwnd 涨到 ssthresh 后进入拥塞避免每个 RTT 只加 1 个 MSS线性增长避免翻倍冲过头。一个是指数探路一个是线性试探切换点是 ssthresh。【真题·转述自 50道经典计算机网络面试题】4. 快重传和快恢复的过程是怎样的答推导 · 已对照解析转述接收方收到乱序段会重复回同一个 ACK发送方连续收到三个重复 ACK就认定缺失段丢了不等 RTO 到期立刻重传这就是快重传。快恢复把 ssthresh 设为 cwnd 的一半cwnd 降到 ssthresh 附近Reno 里先设为 ssthresh 3 个 MSS重传丢失段收到新数据的 ACK 后cwnd 回到 ssthresh直接进入拥塞避免不用像超时那样回到 1 个 MSS。【真题·转述自 50道经典计算机网络面试题】5. TCP 粘包和拆包是怎么产生的怎么解决答推导 · 已对照解析转述TCP 是面向字节流的不保存消息边界发送方连续写的小数据可能被攒成一段发接收方读得慢可能一次读出多段超过 MSS 又会拆段于是应用层看到「粘包」或「拆包」。解决思路是应用层自己定边界固定长度、分隔符、或者在消息头里放长度字段HTTP 的 Content-Length 就是长度字段。【真题·转述自 50道经典计算机网络面试题】6. TCP 的保活机制是怎么回事如何判断连接已经断开答推导 · 已对照解析转述TCP 内置保活定时器连接闲置一段时间后发探测报文Linux 默认闲置 2 小时开始探每隔 75 秒一次连续 9 次没响应就释放连接。它能发现死连接但太慢实际工程更常用应用层心跳按业务需要设置间隔和超时。判断连接断开还可以靠读写返回值读到 0 表示对端正常关闭写失败或超时表示连接异常。【真题·转述自 如何判断 TCP 连接是否可用】结语窗口滑的是确认cwnd 涨的是试探TCP 的可靠不是一次发对而是发错了能补、堵住了能退。评论区聊聊你抓包时见过最夸张的窗口。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑