【从0开始学习计算机网络】| TCP 是怎么保证数据不丢、网络不堵的?

发布时间:2026/10/3 7:36:30
【从0开始学习计算机网络】| TCP 是怎么保证数据不丢、网络不堵的? 个人主页:一条泥憨鱼(欢迎各位大佬莅临)❄️《数据结构》 ❄️《AI与Agent那些事》❄️《从0开始学计算机网络》 ❄️《后端开发》前言你肯定遇到过这种情况打开一个网页进度条冲到 80% 突然不动了你盯着屏幕等了三四秒心里开始嘀咕是不是网断了然后它又突然一下加载完了。这个卡一下又冲上去的现象不是你的网坏了也不是浏览器抽风。它背后是TCP 的两套机制在互相拉扯一套负责数据别丢一套负责网络别堵。这两套机制配合起来我们下载文件才不会下错看网页才不会乱序。但这里有个问题网络本身是会丢包的路由器也会堵车凭什么 TCP 就能保证数据不丢、不乱接下来就把 TCP 的可靠性机制和拥塞控制四个阶段慢启动、拥塞避免、快重传、快恢复讲清楚。先搞懂 TCP 到底在解决什么问题要理解 TCP得先知道它下面的那个协议——IP——有多不靠谱。你可以把 IP 想象成一个只管扔包裹不管结果的快递员。你把包裹交给它它负责往目的地扔但- 包裹可能丢网络丢包- 包裹可能后发先到乱序- 包裹可能重复送重复包- 它也不知道对方到底收没收到IP 协议就这德行它只负责尽力而为不保证任何东西。那问题来了我们下载一个 100MB 的文件分成几万个数据包发出去中间丢了几个为什么文件还能完好无损因为 TCP 在 IP 之上又加了一层靠谱快递公司。IP 负责扔包裹TCP 负责给包裹编号、要求对方签收、丢了就重发、乱了就重排。具体来说TCP 保证可靠传输靠四个手段第一序号。每个字节的数据都有一个编号。比如你要发 1000 字节第一个字节编号是 1第二个是 2……这样接收方就知道哪个包该排在哪个位置。第二确认应答ACK。接收方每收到一段数据就回一个 ACK意思是我收到编号 X 之前的所有数据了。发送方收到 ACK就知道这段数据安全到达了。第三超时重传。如果发送方发出去一个包等了很久没收到对应的 ACK就认为它丢了重新发一遍。第四滑动窗口。这个稍微复杂点简单说就是不用发一个等一个可以一次发一批提高效率。这里先点到为止你只要知道它是用来提升传输速度的就行。举个具体例子。发送方发了三个包序号 1、序号 2、序号 3。接收方收到 1 和 3但 2 丢了。它会回两个 ACK我要序号 2收到 1 之后期待的、我要序号 2收到 3 之后发现缺了 2还是期待 2。发送方一看对方一直在喊我要 2就知道 2 丢了重发一遍。这套机制保证了数据不丢、不乱。但这里有个新问题如果发送方不管不顾一次把几万个包全扔出去网络受得了吗受不了。这就是拥塞控制要解决的问题。慢启动——为什么一开始要慢慢来这里有个反直觉的点TCP 发数据不是一上来就猛发而是从一个很小的量开始慢慢加。为什么要这样想象你往一个杯子里倒水。你不知道杯子多大也不知道水会不会溢出来。聪明的做法是先倒一点点看看水面到哪了再决定下一步倒多少。如果你一上来就整壶倒下去大概率洒一桌。TCP 也是这个思路。它维护一个叫拥塞窗口cwnd的东西你可以理解成我一次最多能发多少数据而不会把网络搞堵。这里要注意区分两个窗口- 拥塞窗口cwnd管的是网络堵不堵是发送方自己估算的- 接收窗口rwnd管的是对方收不收得下是接收方告诉发送方的发送方实际能发多少取这两个的最小值。也就是说既不能超过网络的承载能力也不能超过对方的接收能力。好回到慢启动。它一开始把 cwnd 设成很小比如 1 个 MSSMSS 是一个数据包最大能装多少数据通常是 1460 字节左右。然后- 发出去 1 个包收到 ACKcwnd 变成 2- 发出去 2 个包收到 2 个 ACKcwnd 变成 4- 发出去 4 个包收到 4 个 ACKcwnd 变成 8- ……看出来了吗1、2、4、8、16……每过一个往返时间RTT窗口就翻一倍。这是指数增长。为什么叫慢启动因为它起点很低从 1 开始。但它的增长速度其实一点都不慢是指数级的。那什么时候停TCP 设了一个阈值叫慢启动阈值ssthresh。当 cwnd 涨到 ssthresh说明网络可能快到临界点了就不能再翻倍了得换个策略。这个新策略就是拥塞避免。拥塞避免——快到临界点了改成稳步走慢启动的指数增长很爽但也很危险。你想cwnd 从 1 涨到 1024 只需要 10 个 RTT如果继续翻倍很快就会把网络压垮。所以到了 ssthresh 之后TCP 切换成拥塞避免策略不再翻倍改成每过一个 RTTcwnd 只加 1。这就是从指数增长变成了线性增长。打个比方你在高速上开车前面路很空你可以猛踩油门加速慢启动。但快到收费站了你得从加速改成匀速稳稳地往前挪拥塞避免。如果你还猛踩油门前面一堵你就得急刹甚至追尾。那什么时候会撞车呢答案是丢包。在 TCP 看来丢包就是网络在喊我堵了因为正常情况下数据包不应该丢。一旦丢了说明中间某个路由器的缓冲区满了开始丢包了。丢包有两种情况对应两种不同的处理方式-超时重传等了很久超过 RTO 时间都没收到 ACK说明可能丢得很严重网络堵得厉害-快重传连续收到 3 个重复 ACK说明只是丢了一个包网络还没完全瘫这两种情况处理方式不一样尤其是第二种诞生了快重传和快恢复。快重传与快恢复——丢了一个包别从头再来这是全文最容易被讲糊涂的地方我们分两步说。快重传不用等超时立刻重发前面说过接收方收到乱序的包时会重复发同一个 ACK。比如发送方发了 1、2、3、4、5结果 3 丢了。接收方收到 1、2回 ACK我要 3收到 4发现缺 3还是回 ACK我要 3收到 5还是回 ACK我要 3。发送方一看收到了 3 个我要 3的重复 ACK立刻意识到3 丢了于是不等超时马上重传 3。这就是快重传。它的好处是快不用傻等 RTO 超时省了好几秒。用生活类比你去取快递对方一直跟你说我要第 5 个说了一遍又一遍你就知道第 5 个没送到赶紧补发不用等到对方投诉。快恢复砍一半别回到起点重传之后接下来怎么办按照老思路既然丢包了说明网络堵了那就把 cwnd 打回 1重新慢启动。但这样太狠了明明只是丢了一个包网络还在正常工作你直接回到起点效率损失太大。所以 TCP 引入了快恢复既然只是丢了一个包说明网络没完全瘫那就把 cwnd 砍一半而不是打回 1然后直接进入拥塞避免线性增长。对比一下- 超时重传严重情况cwnd 打回 1重新慢启动- 快重传 快恢复轻微情况cwnd 砍半直接拥塞避免这样一来cwnd 的曲线就变成了经典的锯齿形状慢启动指数上升 → 拥塞避免线性上升 → 丢包 → 砍半 → 再线性上升 → 再丢包 → 再砍半……每次砍半之后网络又能撑一段时间然后再次丢包再次砍半。这就是 TCP 拥塞控制的稳态。把四个阶段串起来到这里四个阶段就讲完了。我们用一条时间线回顾一下1. 慢启动cwnd 从 1 开始指数增长1→2→4→8……快速探测网络容量2. 拥塞避免cwnd 到达 ssthresh 后改成线性增长每个 RTT 加 1稳扎稳打3. 快重传收到 3 个重复 ACK立刻重传丢失的包不等超时4. 快恢复cwnd 砍半直接进入拥塞避免不回到起点这些机制你写代码的时候基本不用管操作系统内核已经替你做好了。但理解它们能帮你解释很多玄学现象- 为什么小文件传输感觉特别慢因为慢启动阶段 cwnd 还很小没涨起来就传完了- 为什么弱网环境下要调 TCP 参数因为默认的初始 cwnd 和 ssthresh 可能不适合高延迟网络- 为什么 QUIC/HTTP3 要绕开 TCP 重做一套拥塞控制因为 TCP 在内核里改起来慢而且队头阻塞问题不好解决如果你想把今天讲的东西亲眼看看可以试试这两个工具- ss -i在 Linux 上敲这个命令能看到当前连接的 cwnd、ssthresh、RTT 等参数-Wireshark抓包神器能看到每一个数据包、每一个 ACK甚至能画出 cwnd 随时间变化的曲线打开 Wireshark 抓一次自己访问网页的流量你会对今天讲的这些有完全不一样的理解。