从网络延迟高到流畅观影 慢启动拥塞避免快速重传快速恢复四种算法如何协同解决带宽争抢与丢包问题
嘿,你好呀!我是Agnes,今天想跟你聊聊一个挺有意思的话题。你有没有遇到过这种情况:周末晚上,你兴冲冲地点开一部期待已久的电影,结果画面卡成PPT,右下角那个转圈圈的加载图标像是故意跟你作对一样,转了又转……然后你气得想摔遥控器。
别急,这不是你家的网太差,而是网络世界里正在上演一场”堵车”大戏。而解决这个问题,靠的是四个看似复杂但其实很有逻辑的算法:慢启动、拥塞避免、快速重传、快速恢复。
来,我给你讲个故事,保证你能听懂。
一、先说说什么是”网络堵车”
想象一下,你的电脑就是一辆车,数据就是车里的货物,而互联网就是一条巨大的高速公路。这条路上有很多车(很多用户都在上网),如果所有人都同时猛踩油门冲上去,结果会怎样?
堵车。
当路上车太多,就会发生碰撞——数据就会”丢包”。丢包了怎么办?发送方就得重新发送,于是路上车更多,堵得更厉害。这就像一个恶性循环,你的电影画面当然就卡了。
解决这个问题的核心思想就一个:既要跑得够快,又不能堵死。
而这四个算法,就是交通警察制定的”交通规则”。
二、慢启动:从小步试探开始
慢启动(Slow Start) 是TCP连接的”起步阶段”。
想象你第一次走一条新路,你会怎么做?肯定不会一脚油门踩到底,而是先慢慢开,试探一下路况:这条路有多宽?有多少车?有没有障碍?
在计算机网络里,这个”试探”的过程叫做控制窗口(cwnd)。cwnd决定了你一次能发多少数据包。
慢启动是怎么工作的?
刚开始连接的时候,cwnd的值很小,比如只有 1个数据段(可以理解成1辆小车)。
- 发送1个包 → 收到确认(ACK)→ cwnd变成2
- 发送2个包 → 收到确认 → cwnd变成4
- 发送4个包 → 收到确认 → cwnd变成8
- 发送8个包 → 收到确认 → cwnd变成16
看到了吗?这是指数增长!每过一个RTT(Round Trip Time,数据包往返时间),窗口大小就翻倍。
为什么要这样?因为刚开始的时候,你根本不知道这条路能承载多少流量。慢慢试探,既能尽快利用带宽,又不会一下子把路堵死。
什么时候停止慢启动?
当cwnd达到一个阈值(叫做 ssthresh,慢启动阈值)时,慢启动就结束了,进入下一个阶段:拥塞避免。
这个ssthresh的值可能是由网络中的路由器告诉你的,也可能是默认值(通常是65535字节或者根据MTU计算得出)。
三、拥塞避免:细水长流,稳扎稳打
拥塞避免(Congestion Avoidance) 是TCP的”谨慎巡航阶段”。
如果说慢启动是”试探前进”,那拥塞避免就是”稳扎稳打”。
拥塞避免是怎么工作的?
在拥塞避免阶段,cwnd不再是指数增长,而是线性增长:
- 每经过一个RTT,cwnd只增加 1个数据段
- 比如cwnd是16,下一个RTT变成17,再下一个变成18……
听起来很慢?但这是故意的!
为什么要这么慢?因为一旦你以指数速度增长,很容易突然超过网络的实际承载能力,导致丢包。线性增长就像是在”小心翼翼地探索道路的极限”,既能充分利用带宽,又不会一下子把路堵死。
用代码来理解这个过程
# 模拟TCP拥塞控制算法
class TCPCongestionControl:
def __init__(self):
self.cwnd = 1 # 初始拥塞窗口
self.ssthresh = 65535 # 慢启动阈值(默认值)
self.mode = "slow_start" # 当前模式
def send_packets(self):
"""发送数据包,返回当前窗口大小"""
return self.cwnd
def process_ack(self, is_duplicate=False, is_fast_retransmit=False):
"""处理确认包"""
if self.mode == "slow_start":
# 慢启动:窗口指数增长
self.cwnd *= 2
if self.cwnd >= self.ssthresh:
self.mode = "congestion_avoidance"
self.cwnd = self.ssthresh
elif self.mode == "congestion_avoidance":
# 拥塞避免:窗口线性增长
self.cwnd += 1
return self.cwnd
def process_loss(self, is_triple_dup_ack=False):
"""处理丢包事件"""
if is_triple_dup_ack:
# 快速重传:收到3个重复ACK
self.ssthresh = max(self.cwnd // 2, 2)
self.cwnd = self.ssthresh
self.mode = "fast_recovery"
else:
# 超时丢包:回到慢启动
self.ssthresh = max(self.cwnd // 2, 2)
self.cwnd = 1
self.mode = "slow_start"
def process_fast_recovery_ack(self):
"""快速恢复阶段的确认"""
if self.mode == "fast_recovery":
self.cwnd += 1 # 每收到一个ACK,窗口+1
if self.cwnd >= self.ssthresh:
self.mode = "congestion_avoidance"
self.cwnd = self.ssthresh
这段代码展示了TCP算法的基本逻辑。你不需要完全看懂每一行,但能感受到:算法是动态调整的,根据网络状况不断改变策略。
四、快速重传:不等超时,立刻重传
现在问题来了:如果数据包丢了怎么办?
传统的做法是:发送方发送一个数据包,然后开始计时。如果在规定时间内(RTT)没有收到确认,才重新发送。这个时间叫做 重传超时(RTO)。
但问题是:RTT可能有100毫秒,也可能有500毫秒,甚至更长。 如果每次都等超时才重传,那网络效率就太低了!
快速重传是怎么解决这个问题的?
快速重传(Fast Retransmit) 的核心思想是:不需要等超时,通过”重复确认”来判断丢包。
想象一下这个场景:
发送方 → 接收方:发送包1、包2、包3、包4、包5
接收方:收到包1 → 发送ACK 1
接收方:收到包2 → 发送ACK 2
接收方:收到包3 → 发送ACK 3
接收方:没收到包4 → 发送ACK 3(重复确认!)
接收方:收到包5 → 发送ACK 3(又是重复确认!)
注意:接收方因为没收到包4,所以连续发送了 3个重复的ACK 3。
发送方收到 3个重复ACK 后,立刻就知道:包4丢了!不用等了,马上重传!
这就是快速重传。它比等超时快多了,省下的时间可能是几百毫秒,对于在线视频来说,这几百毫秒就是”卡一下”和”不卡”的区别。
快速重传的触发条件
# 快速重传的触发条件
def check_fast_retransmit(received_acks):
"""
received_acks: 收到的确认列表
例如: [1, 2, 3, 3, 3] 表示收到3个重复ACK
"""
# 统计每个ACK出现的次数
ack_counts = {}
for ack in received_acks:
ack_counts[ack] = ack_counts.get(ack, 0) + 1
# 如果有3个重复ACK,触发快速重传
for ack, count in ack_counts.items():
if count >= 3:
return True, ack # 触发快速重传,需要重传的包是 ack+1
return False, None
这个逻辑简单吧?就是数一数有没有哪个ACK出现了3次。
五、快速恢复:丢包后如何优雅地继续
快速重传只是解决了”发现丢包”的问题,但还有一个问题:丢包之后,窗口该怎么调整?
如果直接把cwnd设为1,那就太保守了,相当于回到起点重新来。但如果完全不调整,又可能再次导致拥塞。
快速恢复(Fast Recovery) 就是为了解决这个问题的。
快速恢复的工作流程
当发生快速重传时,TCP会进入快速恢复模式:
- 设置ssthresh:将ssthresh设为当前cwnd的一半
- 设置cwnd:将cwnd设为新的ssthresh + 3(这3是用来奖励已经发出去的数据包)
- 重传丢失的包
- 在快速恢复阶段:每收到一个重复ACK,cwnd加1(线性增长)
- 收到新的ACK:退出快速恢复,进入拥塞避免
为什么要+3?
这是一个很巧妙的设计。当你收到3个重复ACK时,说明前面的数据包已经到达接收方了,只是确认包丢了。所以你可以认为网络中还有3个包在传输中,给cwnd加上3,相当于承认这些包已经”在路上”了。
class FastRecovery:
def __init__(self):
self.cwnd = 10 # 当前窗口
self.ssthresh = 65535 # 慢启动阈值
def on_triple_dup_ack(self):
"""收到3个重复ACK,触发快速重传+快速恢复"""
# 1. 设置新的阈值
self.ssthresh = max(self.cwnd // 2, 2)
# 2. 设置窗口(ssthresh + 3)
self.cwnd = self.ssthresh + 3
# 3. 重传丢失的包
self.retransmit_lost_packet()
return self.cwnd
def on_duplicate_ack(self):
"""收到额外的重复ACK,窗口+1"""
if self.is_in_fast_recovery:
self.cwnd += 1
return self.cwnd
def on_new_ack(self):
"""收到新的确认,退出快速恢复"""
if self.is_in_fast_recovery:
self.cwnd = self.ssthresh
self.is_in_fast_recovery = False
return "congestion_avoidance"
六、四种算法如何协同工作?
现在我们把四个算法串起来,看看它们是如何协同工作的。
场景模拟:你正在看一部4K电影
假设你正在通过TCP协议从视频网站拉取视频数据。下面是整个过程的模拟:
时间线 事件 cwnd ssthresh 状态
────────────────────────────────────────────────────────────────────────────
0ms 连接建立 1 65535 慢启动
100ms 收到ACK,cwnd翻倍 2 65535 慢启动
200ms 收到ACK,cwnd翻倍 4 65535 慢启动
300ms 收到ACK,cwnd翻倍 8 65535 慢启动
400ms 收到ACK,cwnd翻倍 16 65535 慢启动
500ms 收到ACK,cwnd翻倍 32 65535 慢启动
600ms 收到ACK,cwnd翻倍 64 65535 慢启动
700ms 收到ACK,cwnd翻倍 128 65535 慢启动
800ms 达到阈值,进入拥塞避免 128 65535 拥塞避免
900ms 收到ACK,cwnd+1 129 65535 拥塞避免
1000ms 收到ACK,cwnd+1 130 65535 拥塞避免
... (持续线性增长)
1500ms 发生丢包!收到3个重复ACK 130→68 65 快速恢复
1501ms 重传丢失的包 68 65 快速恢复
1600ms 收到额外ACK,cwnd+1 69 65 快速恢复
1700ms 收到额外ACK,cwnd+1 70 65 快速恢复
1800ms 收到新ACK,退出快速恢复 65 65 拥塞避免
1900ms 收到ACK,cwnd+1 66 65 拥塞避免
... (继续线性增长,但不会再像之前那么激进)
看到了吗?整个过程中,TCP算法根据网络状况不断调整自己的策略:
- 慢启动:快速探测带宽上限
- 拥塞避免:在接近上限时谨慎增长
- 快速重传:发现丢包立即响应
- 快速恢复:丢包后优雅地继续,而不是从头开始
七、用更通俗的比喻来理解
如果你觉得上面的模拟还是有点抽象,那我换个比喻:
比喻:送快递
想象你是一个快递公司的调度员,你的任务是把包裹(数据)从北京送到上海。
慢启动:刚开始,你只派1辆小车去送。到了上海,司机说”一路畅通”。于是你派2辆。第二趟回来又说”畅通”,你派4辆……你一直在试探这条路的承载能力。
拥塞避免:当车多到一定程度(比如128辆),你发现路上开始有点挤了。这时候你不再一次性加很多车,而是每次只加1辆,小心翼翼地试探。
快速重传:有一天,你派了100辆车,结果第50辆车的司机一直没回来确认。但其他99个司机都回来了,而且他们说”第51到100辆都收到了”。你马上意识到:第50辆车可能出事了,不用等了,马上再派一辆去送同样的包裹。
快速恢复:派了新車之后,你不能一下子派100辆回去(那会再次堵死),而是慢慢加车,每次加1辆,直到恢复到正常水平。
八、为什么这些算法能让你的观影体验变好?
回到开头的问题:为什么有了这些算法,你的电影就不卡了?
答案是:这些算法让TCP能够动态适应网络状况,在”充分利用带宽”和”避免拥塞”之间找到平衡。
具体来说:
- 慢启动 让连接能够快速建立,尽快利用可用带宽
- 拥塞避免 让TCP不会因为过于激进而导致网络崩溃
- 快速重传 减少了丢包后的等待时间,避免不必要的延迟
- 快速恢复 让TCP在丢包后能够迅速恢复传输效率,而不是从零开始
这四个算法协同工作,就像一支训练有素的队伍,既不会过于冒进,也不会过于保守。
九、进阶:这些算法在现代网络中的演进
虽然TCP拥塞控制的基本框架从1988年到现在变化不大,但随着时间的推移,也出现了很多改进版本:
| 算法版本 | 提出时间 | 特点 |
|---|---|---|
| TCP Reno | 1990年代 | 引入了快速重传和快速恢复 |
| TCP Cubic | 2006年 | 使用三次函数模型,更适合高速长距离网络 |
| TCP BBR | 2016年 | 基于带宽和延迟建模,而非丢包检测 |
| TCP Vegas | 1990年代 | 基于延迟变化检测拥塞 |
其中,BBR(Bottleneck Bandwidth and Round-trip propagation time) 是比较新的一个算法,由Google提出。它不依赖丢包来判断拥塞,而是直接测量网络的带宽和延迟,更加精准。
但不管怎么演进,慢启动、拥塞避免、快速重传、快速恢复这四个核心思想始终都在,只是实现方式更加精细了。
十、总结:从”卡”到”流畅”的奥秘
好了,说了这么多,我来总结一下:
- 网络延迟高、卡顿,本质上是因为网络拥塞导致丢包
- 慢启动让TCP快速探测带宽,避免一开始就浪费网络资源
- 拥塞避免让TCP在接近带宽上限时谨慎增长,避免过度拥塞
- 快速重传让TCP在发现丢包后立即响应,减少等待时间
- 快速恢复让TCP在丢包后优雅恢复,避免回到起点
这四个算法协同工作,形成了一个自我调节的系统,能够在各种网络条件下找到最优的传输速率。
所以下次你的电影不再卡顿时,别忘了感谢这些在幕后默默工作的算法。它们就像隐形的交通警察,在数据的高速公路上维持秩序,确保每一帧画面都能流畅地送到你的屏幕上。
希望这篇文章能帮你理解TCP拥塞控制的奥秘。如果你还有任何问题,随时问我!😊
