ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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


📚 本文收录于「流浪」的系列专栏

🐧Linux系统⚙️C++
📊数据结构与算法🐍Python
🔗LangChain & LangGraph🗄️MySQL 数据库
🌿Git 工具🌐计算机网络
🤖LLM💯大厂面试、八股
📚学习筑基专栏

🏠 博客主页:流浪 | 📝 原创首发于 CSDN


前言:篇17 把「收不到 ACK 不等于丢包,要没应答加超时才算丢」讲完了。可靠传输还有几件事:发送方凭什么能连着发、丢了怎么补、网络堵了怎么退,以及连接那头突然没了怎么办。这篇从一只会往前挪的窗口讲起,把滑动、重传、拥塞控制、字节流和异常连接串成一条线。


一、滑动窗口:发送方不等 ACK 也能继续发

1.1 一只会往前挪的取货框

  1. 发送缓冲区像一排货架:应用把数据放上去,TCP 从货架上取货往外发,对方确认一件,这件才算真正交出去。
  2. 如果发一件等一件,一个往返时间(RTT)里只能发一个报文,链路再宽也跑不满。
  3. 窗口就是「不用等确认、可以继续发」的那段额度:框里的货可以先发,框外的得等框往前挪。
  4. 窗口框住的:已经发出去、还没被确认的;还没发、但窗口允许发的。
  5. start和end是两只下标:start指向最早没被确认的字节,end指向窗口右边界;窗口滑动,就是两只下标一起往右走。

窗口不是一堵墙,是一段「允许在途」的额度;额度内尽管发,额度外等确认。

1.2 四段地盘,已确认的那块不用擦

  1. 把发送缓冲区按「发没发、确认没确认」切开,能切出四段:已发已确认、已发未确认、未发可发、未发不可发。
  2. 绿色窗口框住的是中间两段:已发未确认、未发可发;左边是已经签收的,右边是还不能动的。
  3. 已确认的那段,数据已经作废,空间可以立刻拿回来放新数据,不需要把整块缓冲区清一遍——这就是环形缓冲区的用法。
  4. 已发未确认的那段不能删,丢了要靠它重传;这也是后面所有重传机制的底气。
  5. 窗口右边界 = 左边界 + 窗口大小;左边界就是确认号,窗口大小来自接收方报文里的win字段(十六位窗口大小)。
  6. win是 16 位,最大 65535 字节;窗口缩放(window scaling)选项可以把它按比例放大,不然高带宽长距离的链路根本喂不饱。
    • 缩放因子最多 14 位,窗口上限可以抬到 2 的 30 次方字节,具体值在三次握手时协商。

左边界由确认号钉住,右边界由接收方还能收多少决定;两块一夹,就是发送方此刻能发的范围。

1.3 窗口只往右滑,大小却会变

  1. 累计确认:ACK n 表示 n 之前的字节全部收到;接收方不逐个报文回「我收到第几段」,只报「我连续收到了哪」。
  2. 所以窗口只会往右滑,不会往回退;左边界一旦被确认号推过去,就不会再回来。
  3. 窗口大小是另一回事:接收方缓冲区紧,win 就小;缓冲区松,win 就大;最紧的时候 win 可以是 0。
  4. win = 0 时发送方停发,起一个坚持定时器(persist timer)定期去问,免得窗口更新报文丢了,两边互相干等。
  5. 接收方用 win 给发送方限速,这就是流量控制;滑动窗口是流量控制的具体实现。
  6. 实际上窗口会滑会最前面:发送缓冲区是一段连续的内存,可以看成一维数组;如果滑动窗口滑动到缓冲区最后了,前面的数据已经被对方接收并确认了,为了继续发送数据,滑动窗口就要滑回最前面进行下一轮发送
    因此,我们主观视角可以把它看成一个环形的缓冲区

窗口的「滑动」由 ACK 推着走,「大小」由接收方缓冲区牵着走;两个维度分开看,才不会把窗口当成固定长度。

1.4 延迟应答:晚一点回,窗口反而更大

  1. 接收方不必每收到一段就立刻回 ACK;它可以等几十毫秒,或者每收到两个段回一次,这就是延迟应答(delayed ACK)。
  2. 假设接收缓冲区里还压着 20KB 没被应用读走,剩余空间 24KB;发送方又发来 10KB,剩余 14KB,按规矩这时回的 win 应该是 14KB。
  3. 接收方没急着回,等了一会儿,应用把这 20KB 读走了,剩余空间变成 34KB;这时候再回 ACK,win 就能写 34KB。
  4. 同样是这一来一回,发送方看到的窗口从 14KB 变成 34KB,一次能多发 20KB;ACK 的数量还少了,开销更低。
  5. 代价是发送方要多等一会儿;遇到乱序报文、或者窗口需要立刻调整时,接收方不能拖,得马上回。
  6. 延迟应答不是把窗口变大,是晚一点报,让窗口在这段时间里自己变大。

延迟应答拿一点等待换更大的窗口和更少的 ACK;它不是扩大窗口,是把「确认」和「窗口更新」攒到更划算的时候一起发。

二、三种丢包位置:窗口的左边界停在哪

2.1 先把例子摆好

  1. 从 1001 开始发,发到 5001 这个确认边界,正好四段,每段 1000 字节:1001–2000、2001–3000、3001–4000、4001–5000。
  2. 四段全到,接收方回 ACK 5001;累计确认号到 5001,就表示 5001 之前一个字节都不缺。
  3. 丢包只看三个位置:最左、中间、最右;实际网络里可以自由组合,但先把单点看明白。

看窗口怎么动,只看一件事——第一个没到的字节在哪。

2.2 最左侧的数据丢了:ACK 卡在 1001

  1. 接收方收到 2001–5000,唯独 1001 没到;它不能确认 1001 之后的任何字节,只能回 ACK 1001。
  2. 后面每来一段乱序报文,它再回一次 1001;发送方连着收到三个重复 ACK,就知道 1001 附近丢了。
  3. 发送方的左边界停在 1001,窗口不往前滑;三个重复 ACK 触发快重传,补发 1001–2000。
  4. 补上之后,接收方手里 1001–5000 齐了,累计确认直接跳到 5001,窗口一次滑到 5001。

最左丢,窗口原地不动;它不是不滑,是被第一个空洞卡住了。

2.3 中间丢了:左边界滑到空洞

  1. 1001–2000 到了,接收方回 ACK 2001;接着 2001–3000 丢了。
  2. 接收方又收到 3001–4000、4001–5000,但 2001 没到,只能继续回 ACK 2001。
  3. 发送方的左边界可以滑到 2001——前面的都确认了——但不能再往后;这就是「滑到丢失的位置」,滑到空洞的起点,不是跳过空洞。
  4. 这个例子里,2001 后面只剩两段,凑不够三个重复 ACK,快重传触发不了,最后得等超时。

中间丢,窗口能滑一段,但一定停在空洞口;累计确认只认连续前缀。

2.4 最右侧丢了:同样是一个道理

  1. 4001–5000 丢了,接收方手里 1001–4000 是齐的,回 ACK 4001,左边界滑到 4001。
  2. 后面还有数据时,乱序报文会继续回 ACK 4001,凑够三个就能快重传。
  3. 丢的正好是最后一段时,后面没有报文,凑不出重复 ACK,只能等超时。

右端丢,窗口停在 4001;能不能快重传,取决于后面还有没有足够的报文替它喊冤。

2.5 丢的是 ACK,不是数据:后面的累计确认能盖住

  1. 第一个 ACK(2001)在回程丢了,但后面的 ACK 3001、4001、5001 都到了;累计确认 5001 说明 5001 之前全到,窗口直接滑到 5001。
  2. 数据丢和 ACK 丢要分开:数据丢,窗口被空洞卡住;ACK 丢,后面的累计 ACK 能覆盖。
  3. 如果一段数据里出现多个空洞,累计 ACK 只能告诉发送方「第一个洞在哪」;SACK(选择确认)让接收方把「我收到了哪几段」一起报回去,发送方只补空洞,不用一段段试。

丢 ACK 不可怕,怕的是丢数据;累计确认是打包确认,SACK 是把打包拆开告诉你缺哪块。

三、快重传与超时重传:提前补与兜底补

3.1 超时重传:最后的保险

  1. 发出去的数据开始计时,超过重传超时时间(RTO)还没等到 ACK,就认定丢了,重发。
  2. 每重发一次,等待时间翻倍,这叫指数退避;累计到一定次数还不通,TCP 认定这条路或对端出了问题,直接断开。
  3. 它不会漏,但太慢;丢一次包要干等一个 RTO,吞吐掉得厉害。

超时重传不会漏,但太慢;它是兜底,不是首选。

3.2 快重传:三个重复 ACK 就动手

  1. 接收方收到乱序段,会重复回同一个 ACK;发送方连着收到三个重复 ACK(有的教材把此前那个正常 ACK 也算上,说成四个相同的 ACK),就认定丢包。
  2. 不等 RTO 到期,立刻重传缺失的那一段;后面的段接收方已经缓存,不用重发。
  3. 为什么是三个:一两个重复 ACK 可能只是网络乱序,三个才比较像真丢了;阈值太低会把乱序当丢包,阈值太高又等得久。
  4. 丢包位置越靠右,后面能替它喊冤的报文越少;四段的例子里,只有最左侧丢能凑够三个重复 ACK,中间丢只剩两段、最右丢一段都没有,最后都得靠超时。

快重传是用重复 ACK 提前看出空洞;它赌的是乱序不会连续骗人三次。

3.3 两者不是包含关系,是先后关系

  1. 快重传:丢包被看出来了,提前补;触发条件是重复 ACK。
  2. 超时重传:一直没人告诉你,等到 RTO 兜底;触发条件是超时。
  3. 两者都属于「发现丢包 → 重传」,但触发条件不同,不是谁包含谁。
  4. 快重传之后走快恢复,超时之后回慢启动;两条路的分岔,落在 cwnd 怎么退上。

快重传抢时间,超时重传保底线;两条路一前一后,把丢包补完。

3.4 滑动窗口就是重传的缓存

  1. 发出去没确认的报文必须留在发送缓冲区里,否则丢了没东西可补。
  2. 窗口根据 ACK 决定滑不滑、滑到哪;滑过去的那部分,才是可以释放的。
  3. 没有这份缓存,快重传和超时重传都无从谈起。

窗口不只是「能发多少」的额度,也是「丢了能补」的仓库。

四、流量控制之外:为什么还需要拥塞控制

4.1 只看接收窗口,为什么还会堵

  1. 接收方说「我还能收十箱」,你就发十箱;可中间的路可能只有一座独木桥,十箱同时挤上去,桥就堵了。
  2. 接收窗口(rwnd)只管接收方缓冲区,管不了中间路由器;路由器队列有限,发得太猛会排队溢出、丢包。
  3. 丢包不只发生在「对方没收到」,也可能发生在「网络装不下」。
  4. 上亿个客户端共享同一张网,谁都不退,网络迟早被压垮。

接收窗口是对方的能力,拥塞窗口是网络的能力;前者看得见,后者只能猜。

4.2 丢包多和丢包少,结论不一样

  1. 少量、零散丢包:更像链路误码或抖动,补一下就行,不必大幅退让。
  2. 大量、连续丢包:更像网络已经堵了,必须退让;这时候还按原速重发,只会加重负载,越堵越丢。
  3. 传统 TCP 不区分丢包形态,见到丢包就按拥塞处理;工程上会看形态,现代算法还会看时延和带宽。

丢包是信号,不是判决;少量可能是偶然,大量才说明路真的堵了。

4.3 拥塞窗口 cwnd 与阈值 ssthresh

  1. 拥塞窗口(cwnd)是发送方自己维护的「我敢发多少」,不看对方报文,只按网络反馈调整。
  2. ssthresh 是慢启动转拥塞避免的分界,是阈值,不是窗口;cwnd 涨到它,就换一种涨法。
  3. 发送窗口 = min(rwnd, cwnd);谁紧听谁的。
  4. 实际还能发多少 = min(rwnd, cwnd) − 在途未确认字节;窗口是上限,在途要扣掉。

rwnd 是别人给的(16位窗口大小),cwnd 是自己猜的;发送窗口取两者较小值,谁紧听谁的。

五、慢启动与拥塞避免:cwnd 怎么涨、怎么退

5.1 慢启动:每个 RTT 翻一倍

  1. cwnd 起步很小,早期实现从 1 个 MSS 开始,RFC 6928 之后 Linux 默认 10 个 MSS。
  2. 每收到一个 ACK,cwnd 加 1 个 MSS;一个 RTT(往返时间)内 发出去多少段,就回来多少个 ACK,所以下一个 RTT 窗口翻倍:1、2、4、8、16……第 n 个 RTT 是 2 的 n 次方个 MSS。
  3. 「慢」指的是起点低,不是增速慢;它是一边问路一边加速。
  4. 好处一,试错便宜:先拿一两个报文问路,猜错了也只丢一点,不会一上来把整条路打爆。
  5. 好处二,逼近快:翻倍是对数级,1→2→4→…→128,7 个 RTT 就到 128 个 MSS;线性一次加一个,要 127 个 RTT。
  6. 好处三,知道收油:涨到 ssthresh 就切线性,避免翻倍冲过头。
  7. 代价:翻倍一定会超调,超调那一下容易丢一批;所以后面要配拥塞避免和快恢复。

慢启动是拿小窗口问路,问到了就翻倍走;它不是慢,是先从最小风险开始。

5.2 拥塞避免:过了 ssthresh 改成线性

  1. cwnd 涨到 ssthresh 后,每个 RTT 只加 1 个 MSS,不再翻倍。
  2. 线性增长也叫加性增大(additive increase),探得更稳,不会一下撞墙。
  3. 从「翻倍」切到「加一」,切换点就是 ssthresh。

慢启动负责快速逼近,拥塞避免负责贴着上限慢慢试;ssthresh 就是换挡点。

5.3 拥塞发生:超时与快重传两条路

  1. 超时:ssthresh 记为当前 cwnd 的一半,cwnd 回到 1 个 MSS,重新慢启动,这叫 Tahoe。
  2. 三个重复 ACK:快重传 + 快恢复,ssthresh 同样减半,cwnd 降到 ssthresh 附近,直接进拥塞避免,这叫 Reno。
  3. 快恢复的细节:ssthresh = cwnd/2,cwnd = ssthresh + 3 个 MSS(代表已经离开网络的三段),重传丢失段;每再收到一个重复 ACK,cwnd 加 1;收到新数据的 ACK 后,cwnd 回到 ssthresh。
  4. 「归零」是口语,真实值是回到 1 个 MSS;真归零就停发了。
  5. 快恢复的意义:不退回起点,避免吞吐断崖。

超时按最坏情况处理,快重传按「只是丢了一段」处理;一个回起点,一个只退半步。

5.4 AIMD:加性增、乘法减

  1. 加性增:每个 RTT 加 1 个 MSS。
  2. 乘法减:拥塞时 cwnd 减半。
  3. 涨得慢、退得快,画出锯齿。
  4. 为什么减半不是清零:清零太保守,减半能保住一部分已经探到的带宽;多条连接最终能分到相对公平的份额。

AIMD 是小心试探、撞墙退半;用锯齿换稳定,用退让换公平。

5.5 再往后的 BBR:只看丢包不够

  1. BBR 不把丢包当唯一信号,改看带宽和往返时延。
  2. 它试图把窗口维持在瓶颈带宽与 RTT 的乘积附近,也就是带宽时延积(BDP)。
  3. 它的目标是让窗口贴着 BDP,不靠丢包来判断上限。

从「丢包就退」到「按带宽和时延发」,拥塞控制一直在找一个更准的「网络还能吃多少」。

六、TCP 是字节流:粘包不是 TCP 的锅

6.1 水管里没有信封

  1. TCP 只保证字节按顺序到达,不保证「你写几次,对方就读几次」。
  2. 发送方写两次「hello」「world」,接收方可能一次读到「helloworld」,也可能先读到「hel」,再读到「loworld」。
  3. 这不是 TCP 丢包或出错,是字节流本来就没有消息边界;所谓「粘包」「拆包」,是应用层眼里的现象。

TCP 是一条水管,不是一摞信封;水管只保证水按顺序流,不保证一次接到一封信。

6.2 粘包是怎么来的

  1. 发送方连续写小数据,TCP 会把它们攒进发送缓冲区,凑够一段再发。
  2. 接收方应用读得慢,缓冲区里堆了好几段,一次 read 就全读出来了。
  3. 数据超过 MSS,TCP 会拆成多段发;到了接收方,也可能分几次才读全。
  4. 这三种情况都不是协议 bug,是字节流的正常表现。

粘包不是「粘」出来的,是应用层没有约定「一封信到哪儿算完」。

6.3 三种定边界的办法

  1. 固定长度:每条消息长度固定,读满 N 字节就是一条;简单,但浪费空间,长度不灵活。
    • 适合定长指令、二进制协议;消息长度变化时不好用。
  2. 分隔符:用换行、\0等特殊字符分隔;实现简单,但消息里不能出现分隔符,或者要做转义。
    • 适合文本协议;二进制数据里出现分隔符,就得转义。
  3. 长度字段:消息头里先写「后面还有多少字节」,接收方先读头、再读体;最常用,HTTP 的Content-Length就是这种思路。
    • 适合二进制、变长消息;先读固定长度的头,再按长度读体。

定边界的活是应用层干的:固定长度、分隔符、长度字段,三选一,或者组合用。

6.4 序列化不等于定边界

  1. 序列化是把结构体、对象变成字节流;反序列化是把字节流还原回来。
  2. 序列化本身不解决「一条消息到哪儿算完」;protobuf 这类格式也要在消息前加长度,JSON 也要靠分隔符或长度字段。
  3. 「在 TCP 协议里明确报文边界」这句话不准确:TCP 不保存边界,边界由应用层协议定。

TCP 只管送字节,不管分信件;边界、序列化、协议格式,都是应用层自己的事。

七、TCP 异常:进程没了、机器关了、网线拔了

7.1 进程终止与机器重启

  1. 正常情况下,进程退出或调用 close,内核会释放文件描述符,替它发一个 FIN;这和正常关闭没有区别。
  2. 机器正常重启、关机,操作系统会关掉所有进程,内核替每条连接发 FIN,也走四次挥手。
  3. 一端先关,只说明「我不发了」,另一端还可以继续发,这叫半关闭;双方都关完,连接才彻底结束。

进程没了、机器关了,只要内核还能发 FIN,对端看到的就是一次正常关闭。

7.2 掉电与断网

  1. 机器突然掉电、网线被拔,来不及发 FIN;对端不知道你没了,还以为连接活着。
  2. 如果对端往这条连接写数据,数据发出去没人确认,只能一遍遍重传,最后超时报错;如果对面已经重启,收到旧连接的报文会用 RST 回绝,连接直接重置。
  3. 如果双方都不写数据,这条连接就会一直挂着,直到保活机制发现对面不在了。

掉电和断网没有告别;对端只能靠「写不出去」或「保活探测」才知道你走了。

7.3 保活定时器

  1. TCP 内置保活定时器(keepalive):连接闲置一段时间后,主动发探测报文,看对面还在不在。
  2. Linux 默认闲置 2 小时才开始探,每隔 75 秒探一次,连续 9 次没有响应才释放连接。
  3. 它能兜底,但很慢;两个小时的空窗,很多业务等不起。

保活定时器是 TCP 自带的「定期点名」,能发现死连接,但默认节奏太慢。

7.4 应用层的心跳

  1. HTTP 长连接会定期检测对方状态;QQ 这类应用断线后会定期重连。
  2. 应用层心跳比 TCP 保活更灵活:间隔、次数、超时都能按业务调,还能顺带做鉴权、负载上报。
  3. 所以「TCP 对异常连接有很强的容错」要打个折扣:它能靠重传和保活兜底,但发现死连接可能很慢,工程上更常用应用层心跳。

TCP 能兜住数据重传,但发现死连接很慢;真要快速感知,还得靠应用层心跳。

八、全篇总结

1. 窗口滑的是确认
  1. 窗口框住已发未确认和可发未发,左边界由 ACK 钉住,右边界由 win 决定。
  2. 已确认区可复用,不必清空;窗口大小可大可小,最小到 0。
2. 丢包看位置
  1. 最左丢,ACK 卡在空洞;中间丢,左边界滑到空洞;最右丢,停在最后一段。
  2. 数据丢会被空洞卡住,ACK 丢会被后面的累计确认盖住;多个空洞靠 SACK。
3. 重传分提前与兜底
  1. 快重传靠三个重复 ACK 提前补,超时重传靠 RTO 兜底。
  2. 两者是先后关系,不是包含关系;滑动窗口就是重传的缓存。
4. 拥塞控制是给网络让路
  1. rwnd 管对方,cwnd 管网络;发送窗口取两者较小值。
  2. 大面积丢包要退让,不能立即重发;少量丢包可能只是误码。
5. 慢启动探路,AIMD 收场
  1. 慢启动每 RTT 翻倍,到 ssthresh 转线性。
  2. 超时回 1 个 MSS,快重传走快恢复;加性增、乘法减。
6. 字节流没有边界,异常连接靠保活
  1. TCP 不保存消息边界,粘包是应用层没定好边界;固定长度、分隔符、长度字段三选一。
  2. 进程退出、机器重启会发 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 的可靠不是一次发对,而是发错了能补、堵住了能退。评论区聊聊你抓包时见过最夸张的窗口。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进