TCP拥塞控制和流量控制
2025/12/21大约 15 分钟
TCP拥塞控制和流量控制
基本概念对比
流量控制(Flow Control)
- 目的:防止发送方发送速度过快,导致接收方缓冲区溢出
- 对象:点对点(发送方 → 接收方)
- 机制:滑动窗口(接收方通告窗口大小)
- 关注点:接收方的处理能力
- 解决问题:接收方来不及处理
拥塞控制(Congestion Control)
- 目的:防止过多数据注入网络,避免网络拥塞
- 对象:全局性(发送方 → 网络 → 接收方)
- 机制:慢启动、拥塞避免、快速重传、快速恢复
- 关注点:网络的承载能力
- 解决问题:网络拥堵
| 对比项 | 流量控制 | 拥塞控制 |
|---|---|---|
| 控制对象 | 接收方处理能力 | 网络承载能力 |
| 控制范围 | 端到端 | 全局网络 |
| 控制方法 | 滑动窗口 | 拥塞窗口+算法 |
| 窗口大小决定 | 接收方通告 | 发送方计算 |
| 目标 | 保护接收方 | 保护网络 |
| 触发条件 | 接收缓冲区满 | 网络拥塞 |
流量控制详解
核心机制:滑动窗口(Sliding Window)
工作原理:
- 接收方在TCP头部的窗口字段(16位)通告接收窗口大小
- 发送方根据接收窗口大小控制发送量
- 接收方处理数据后,更新窗口大小并通告给发送方
- 动态调整发送速率
窗口大小计算:
发送窗口 = min(接收窗口rwnd, 拥塞窗口cwnd)滑动窗口示意图:
发送方窗口:
┌────────────────────────────────────────────────┐
│ 已发送已确认 │ 已发送未确认 │ 可发送 │ 不可发送 │
└────────────────────────────────────────────────┘
↑ ↑
LastByteAcked LastByteAcked + AdvertisedWindow
接收方窗口:
┌────────────────────────────────────────────────┐
│ 已接收已确认 │ 可接收 │ 不可接收 │
└────────────────────────────────────────────────┘
↑ ↑
LastByteRead LastByteRead + RcvBuffer窗口更新过程:
时刻1: 接收方窗口 = 4KB
发送方发送 2KB 数据
时刻2: 接收方收到 2KB,未处理,窗口 = 2KB
通知发送方:rwnd = 2KB
时刻3: 应用读取 1KB 数据,窗口 = 3KB
通知发送方:rwnd = 3KB
时刻4: 应用读取所有数据,窗口 = 4KB
通知发送方:rwnd = 4KB零窗口问题:
- 接收方缓冲区满,通告窗口为0
- 发送方停止发送数据
- 为防止死锁,发送方启动持续定时器
- 定期发送零窗口探测报文(ZWP)
- 接收方回复当前窗口大小
拥塞控制详解
核心变量:
- cwnd(拥塞窗口):发送方维护,表示网络能承受的数据量
- ssthresh(慢启动阈值):区分慢启动和拥塞避免的阈值
- rwnd(接收窗口):接收方通告的窗口大小
实际发送窗口:
发送窗口 = min(cwnd, rwnd)四个核心算法:
1. 慢启动(Slow Start)
目的:试探网络容量,指数增长拥塞窗口
过程:
初始: cwnd = 1 MSS (最大报文段大小,通常1460字节)
ssthresh = 64KB (初始阈值)
每收到一个ACK: cwnd = cwnd + 1 MSS
结果: cwnd 指数增长: 1 → 2 → 4 → 8 → 16 → 32 ...增长示意:
RTT1: 发送 1 个包 ━━
收到 1 个ACK
cwnd = 2
RTT2: 发送 2 个包 ━━ ━━
收到 2 个ACK
cwnd = 4
RTT3: 发送 4 个包 ━━ ━━ ━━ ━━
收到 4 个ACK
cwnd = 8退出条件:
- cwnd >= ssthresh:进入拥塞避免
- 发生丢包:进入拥塞处理
2. 拥塞避免(Congestion Avoidance)
目的:cwnd接近网络容量时,线性增长避免拥塞
过程:
条件: cwnd >= ssthresh
每个RTT: cwnd = cwnd + 1 MSS
结果: cwnd 线性增长: 8 → 9 → 10 → 11 → 12 ...加性增长(AIMD:Additive Increase):
每个RTT增加 1 MSS
慢慢试探网络容量上限3. 快速重传(Fast Retransmit)
目的:快速检测丢包,不等超时
触发条件:收到3个重复ACK
过程:
发送: 1, 2, 3, 4, 5
接收: 1, 3, 4, 5 (丢失包2)
接收方响应:
收到包3: 发送ACK=2 (期望收到2)
收到包4: 发送ACK=2 (仍期望收到2)
收到包5: 发送ACK=2 (仍期望收到2)
发送方收到3个重复ACK=2:
立即重传包2 (不等超时)优势:
- 比超时重传更快
- 减少等待时间
- 提高吞吐量
4. 快速恢复(Fast Recovery)
目的:从快速重传恢复,避免cwnd骤降
过程(TCP Reno版本):
收到3个重复ACK时:
1. ssthresh = cwnd / 2 // 新阈值为当前的一半
2. cwnd = ssthresh + 3 MSS // 拥塞窗口减半
3. 重传丢失的报文段
4. 收到新的ACK后,cwnd = ssthresh // 进入拥塞避免完整状态转换:
开始
↓
慢启动 (cwnd指数增长)
↓
cwnd >= ssthresh?
是 ↓ 否 ↓
拥塞避免 继续慢启动
(线性增长)
↓
收到3个重复ACK
↓
快速重传
↓
快速恢复
↓
拥塞避免拥塞控制完整过程图
cwnd
|
60| ╱╲
| ╱ ╲
50| ╱ ╲
| ╱ ╲
40| ssthresh ╱ ╲
| ------- ╱ ╲
30| ╱╲╱ ╲
| ╱ ↘
20| ╱ ︙(快速恢复)
| ╱ ︙
10| ╱ ↓
| ╱ ︙ (拥塞避免)
1|╱___________________________________________→
|_______________________________________________> 时间
慢 拥塞 3个 快速恢复 拥塞避免
启动 避免 重复ACK
说明:
1. 慢启动: 指数增长
2. 达到ssthresh: 转入拥塞避免
3. 拥塞避免: 线性增长
4. 丢包(3个重复ACK): ssthresh减半,进入快速恢复
5. 快速恢复: 重传后继续拥塞避免超时重传时的处理
超时表示严重拥塞:
发生超时时:
1. ssthresh = max(cwnd/2, 2 MSS) // 阈值设为当前的一半
2. cwnd = 1 MSS // 窗口重置为1
3. 重新进入慢启动与快速重传的区别:
- 快速重传:轻度拥塞,cwnd减半
- 超时重传:严重拥塞,cwnd重置为1
Golang代码示例
package main
import (
"fmt"
"math"
"time"
)
// ============ 流量控制模拟 ============
// 接收方
type Receiver struct {
buffer []byte
bufferSize int
dataSize int // 当前缓冲区数据量
}
func NewReceiver(bufferSize int) *Receiver {
return &Receiver{
buffer: make([]byte, bufferSize),
bufferSize: bufferSize,
dataSize: 0,
}
}
// 接收数据
func (r *Receiver) Receive(data []byte) (bool, int) {
dataLen := len(data)
availableSpace := r.bufferSize - r.dataSize
if dataLen > availableSpace {
fmt.Printf("接收方: 缓冲区不足,需要%d字节,只有%d字节\n",
dataLen, availableSpace)
return false, availableSpace
}
// 接收数据
r.dataSize += dataLen
fmt.Printf("接收方: 接收%d字节,缓冲区使用: %d/%d\n",
dataLen, r.dataSize, r.bufferSize)
return true, availableSpace - dataLen
}
// 应用读取数据
func (r *Receiver) Read(size int) int {
if size > r.dataSize {
size = r.dataSize
}
r.dataSize -= size
fmt.Printf("应用层: 读取%d字节,缓冲区使用: %d/%d\n",
size, r.dataSize, r.bufferSize)
return r.bufferSize - r.dataSize
}
// 获取接收窗口大小
func (r *Receiver) GetReceiveWindow() int {
return r.bufferSize - r.dataSize
}
// 发送方
type Sender struct {
receiveWindow int
}
func NewSender(initialWindow int) *Sender {
return &Sender{
receiveWindow: initialWindow,
}
}
// 更新接收窗口
func (s *Sender) UpdateWindow(window int) {
s.receiveWindow = window
fmt.Printf("发送方: 更新接收窗口为 %d 字节\n", window)
}
// 发送数据
func (s *Sender) Send(size int) []byte {
if size > s.receiveWindow {
fmt.Printf("发送方: 请求发送%d字节,但窗口只有%d字节\n",
size, s.receiveWindow)
size = s.receiveWindow
}
if size <= 0 {
fmt.Println("发送方: 窗口为0,无法发送")
return nil
}
fmt.Printf("发送方: 发送%d字节\n", size)
return make([]byte, size)
}
func flowControlDemo() {
fmt.Println("=== 流量控制演示 ===\n")
// 创建接收方(缓冲区10KB)
receiver := NewReceiver(10240)
sender := NewSender(receiver.GetReceiveWindow())
// 场景1: 正常发送
fmt.Println("场景1: 正常发送")
data := sender.Send(4096)
if data != nil {
receiver.Receive(data)
sender.UpdateWindow(receiver.GetReceiveWindow())
}
fmt.Println()
// 场景2: 多次发送,缓冲区逐渐填满
fmt.Println("场景2: 连续发送")
for i := 0; i < 3; i++ {
data := sender.Send(3000)
if data != nil {
success, newWindow := receiver.Receive(data)
if success {
sender.UpdateWindow(newWindow)
}
}
time.Sleep(100 * time.Millisecond)
}
fmt.Println()
// 场景3: 接收方缓冲区满,窗口为0
fmt.Println("场景3: 缓冲区满")
data = sender.Send(2000)
fmt.Println()
// 场景4: 应用读取数据,窗口增大
fmt.Println("场景4: 应用读取数据")
newWindow := receiver.Read(5000)
sender.UpdateWindow(newWindow)
fmt.Println()
// 场景5: 窗口恢复后继续发送
fmt.Println("场景5: 继续发送")
data = sender.Send(4000)
if data != nil {
receiver.Receive(data)
sender.UpdateWindow(receiver.GetReceiveWindow())
}
}
// ============ 拥塞控制模拟 ============
const MSS = 1460 // 最大报文段大小 (字节)
// TCP拥塞控制状态
type CongestionState int
const (
SlowStart CongestionState = iota
CongestionAvoidance
FastRecovery
)
// TCP拥塞控制
type TCPCongestionControl struct {
cwnd float64 // 拥塞窗口 (单位: MSS)
ssthresh float64 // 慢启动阈值 (单位: MSS)
state CongestionState // 当前状态
dupAckCnt int // 重复ACK计数
}
func NewTCPCongestionControl() *TCPCongestionControl {
return &TCPCongestionControl{
cwnd: 1.0, // 初始为1个MSS
ssthresh: 64.0, // 初始阈值64个MSS (约93KB)
state: SlowStart,
dupAckCnt: 0,
}
}
// 收到新的ACK
func (t *TCPCongestionControl) OnNewAck() {
t.dupAckCnt = 0 // 重置重复ACK计数
switch t.state {
case SlowStart:
// 慢启动: 每个ACK增加1 MSS (指数增长)
t.cwnd += 1.0
fmt.Printf("慢启动: cwnd = %.1f MSS (%.0f KB)\n",
t.cwnd, t.cwnd*MSS/1024)
// 达到阈值,转入拥塞避免
if t.cwnd >= t.ssthresh {
t.state = CongestionAvoidance
fmt.Printf("达到阈值,转入拥塞避免\n")
}
case CongestionAvoidance:
// 拥塞避免: 每个RTT增加1 MSS (线性增长)
// 近似: 每个ACK增加 1/cwnd MSS
t.cwnd += 1.0 / t.cwnd
fmt.Printf("拥塞避免: cwnd = %.1f MSS (%.0f KB)\n",
t.cwnd, t.cwnd*MSS/1024)
case FastRecovery:
// 快速恢复: 收到新ACK,进入拥塞避免
t.cwnd = t.ssthresh
t.state = CongestionAvoidance
fmt.Printf("快速恢复完成,转入拥塞避免: cwnd = %.1f MSS\n", t.cwnd)
}
}
// 收到重复ACK
func (t *TCPCongestionControl) OnDuplicateAck() {
t.dupAckCnt++
fmt.Printf("收到重复ACK (第%d个)\n", t.dupAckCnt)
if t.dupAckCnt == 3 {
// 收到3个重复ACK: 快速重传 + 快速恢复
fmt.Printf("收到3个重复ACK,触发快速重传和快速恢复\n")
// 更新阈值和窗口
t.ssthresh = math.Max(t.cwnd/2.0, 2.0)
t.cwnd = t.ssthresh + 3.0
t.state = FastRecovery
fmt.Printf("快速恢复: ssthresh = %.1f, cwnd = %.1f MSS\n",
t.ssthresh, t.cwnd)
} else if t.state == FastRecovery {
// 快速恢复期间的重复ACK
t.cwnd += 1.0
}
}
// 超时重传
func (t *TCPCongestionControl) OnTimeout() {
fmt.Printf("发生超时!严重拥塞\n")
// 更新阈值
t.ssthresh = math.Max(t.cwnd/2.0, 2.0)
// 重置窗口
t.cwnd = 1.0
// 回到慢启动
t.state = SlowStart
t.dupAckCnt = 0
fmt.Printf("超时重传: ssthresh = %.1f, cwnd = %.1f MSS (重新慢启动)\n",
t.ssthresh, t.cwnd)
}
// 获取当前状态
func (t *TCPCongestionControl) GetState() string {
states := []string{"慢启动", "拥塞避免", "快速恢复"}
return states[t.state]
}
func congestionControlDemo() {
fmt.Println("\n=== 拥塞控制演示 ===\n")
tcp := NewTCPCongestionControl()
// 场景1: 慢启动阶段
fmt.Println("场景1: 慢启动阶段")
fmt.Printf("初始状态: cwnd=%.1f, ssthresh=%.1f\n\n", tcp.cwnd, tcp.ssthresh)
for i := 0; i < 10; i++ {
tcp.OnNewAck()
if tcp.state == CongestionAvoidance {
break
}
}
fmt.Println()
// 场景2: 拥塞避免阶段
fmt.Println("场景2: 拥塞避免阶段")
for i := 0; i < 5; i++ {
tcp.OnNewAck()
}
fmt.Println()
// 场景3: 快速重传和快速恢复
fmt.Println("场景3: 丢包,快速重传")
tcp.OnDuplicateAck()
tcp.OnDuplicateAck()
tcp.OnDuplicateAck() // 第3个重复ACK
fmt.Println()
// 场景4: 快速恢复后收到新ACK
fmt.Println("场景4: 快速恢复后收到新ACK")
tcp.OnNewAck()
fmt.Println()
// 场景5: 超时重传
fmt.Println("场景5: 超时重传(严重拥塞)")
oldCwnd := tcp.cwnd
tcp.OnTimeout()
fmt.Printf("cwnd从%.1f降到%.1f\n", oldCwnd, tcp.cwnd)
fmt.Println()
// 场景6: 重新慢启动
fmt.Println("场景6: 重新慢启动")
for i := 0; i < 5; i++ {
tcp.OnNewAck()
}
}
// ============ 完整模拟 ============
func fullSimulation() {
fmt.Println("\n=== 完整TCP传输模拟 ===\n")
tcp := NewTCPCongestionControl()
receiver := NewReceiver(65536) // 64KB接收缓冲区
fmt.Println("模拟TCP传输过程:\n")
// 模拟多个RTT
for rtt := 1; rtt <= 20; rtt++ {
fmt.Printf("--- RTT %d ---\n", rtt)
// 计算可发送的数据量
cwndBytes := int(tcp.cwnd * MSS)
rwnd := receiver.GetReceiveWindow()
sendWindow := cwndBytes
if rwnd < sendWindow {
sendWindow = rwnd
}
fmt.Printf("cwnd=%.1f MSS (%.0f KB), rwnd=%d KB, 发送窗口=%d KB\n",
tcp.cwnd, float64(cwndBytes)/1024, rwnd/1024, sendWindow/1024)
// 发送数据
if sendWindow > 0 {
// 模拟发送
dataToSend := sendWindow
if dataToSend > 8192 { // 限制每次最多8KB
dataToSend = 8192
}
// 模拟网络状况
if rtt == 10 {
// 模拟丢包
fmt.Println("模拟丢包!")
tcp.OnDuplicateAck()
tcp.OnDuplicateAck()
tcp.OnDuplicateAck()
} else if rtt == 15 {
// 模拟超时
fmt.Println("模拟超时!")
tcp.OnTimeout()
} else {
// 正常收到ACK
receiver.Receive(make([]byte, dataToSend))
tcp.OnNewAck()
// 模拟应用读取数据
if rtt%3 == 0 {
receiver.Read(4096)
}
}
}
fmt.Printf("状态: %s\n\n", tcp.GetState())
time.Sleep(50 * time.Millisecond)
}
}
func main() {
// 流量控制演示
flowControlDemo()
time.Sleep(1 * time.Second)
// 拥塞控制演示
congestionControlDemo()
time.Sleep(1 * time.Second)
// 完整模拟
fullSimulation()
}实际案例分析
案例1:视频流传输
问题: 视频播放卡顿
原因分析:
- 流量控制: 播放器接收缓冲区满,rwnd减小
- 拥塞控制: 网络拥堵,cwnd减小
解决:
- 增大接收缓冲区
- 使用自适应码率
- 优化拥塞控制算法(BBR)案例2:文件下载
问题: 下载速度慢
原因分析:
- cwnd增长太慢
- 初始ssthresh太小
解决:
- 增大初始cwnd
- 调整TCP参数
- 使用TCP Fast Open相关面试题
Q1: 流量控制和拥塞控制的区别是什么?
答案:
- 控制对象不同:
- 流量控制:针对接收方,防止接收缓冲区溢出
- 拥塞控制:针对网络,防止网络拥塞
- 控制范围不同:
- 流量控制:端到端,点对点
- 拥塞控制:全局性,涉及整个网络路径
- 实现机制不同:
- 流量控制:滑动窗口(接收方通告rwnd)
- 拥塞控制:多种算法(慢启动、拥塞避免等,发送方维护cwnd)
- 目的不同:
- 流量控制:匹配发送方和接收方的速度
- 拥塞控制:匹配发送速度和网络容量
Q2: 为什么慢启动叫"慢"启动,但却是指数增长?
答案:
- 名字由来:相对于一开始就用大窗口发送,从1个MSS开始是"慢"的
- 实际增长:虽然是指数增长,但起点很小(1 MSS)
- 对比:
- 早期TCP:一开始就用最大窗口发送
- 慢启动:从1开始,逐步试探网络容量
- 目的:避免一开始就造成网络拥塞
- 历史:1988年引入,解决了当时严重的网络拥塞崩溃问题
Q3: 什么是拥塞窗口和接收窗口?如何共同作用?
答案:
拥塞窗口(cwnd):
- 发送方维护
- 反映网络容量
- 通过拥塞控制算法动态调整
- 防止网络拥塞
接收窗口(rwnd):
- 接收方通告
- 反映接收方缓冲区大小
- 随接收方处理速度变化
- 防止接收方溢出
共同作用:
实际发送窗口 = min(cwnd, rwnd)
场景1: cwnd=10KB, rwnd=20KB → 发送窗口=10KB (受网络限制)
场景2: cwnd=20KB, rwnd=10KB → 发送窗口=10KB (受接收方限制)Q4: TCP的Nagle算法和延迟确认有什么关系?
答案:
Nagle算法:
- 发送方优化,减少小包数量
- 规则:如果有未确认数据,缓存小包直到收到ACK
- 目的:提高网络效率
延迟确认(Delayed ACK):
- 接收方优化,减少ACK数量
- 规则:收到数据后不立即发ACK,等待200ms或收到2个包
- 目的:减少ACK报文数量
相互影响:
问题场景:
1. 发送方发送小包(Nagle等待ACK)
2. 接收方延迟ACK(等待200ms)
3. 双方互相等待,延迟增加
解决方案:
- 禁用Nagle: TCP_NODELAY
- 减少延迟ACK时间
- 发送方凑够一个MSS再发送Q5: 快速重传为什么要等3个重复ACK?为什么不是2个或4个?
答案:
为什么是3个:
- 平衡误判率:
- 1-2个重复ACK:可能是乱序,不是丢包
- 3个重复ACK:大概率是丢包
- 4个或更多:延迟太长,失去"快速"意义
统计学依据:
- 实验表明,网络中包乱序导致1-2个重复ACK较常见
- 3个重复ACK表示至少有4个包乱序,概率很小
- 因此3个是较好的阈值
权衡:
重复ACK数 优点 缺点
1-2个 反应最快 误判率高
3个 平衡点 标准选择 ✓
4个以上 误判率低 延迟太大Q6: TCP BBR算法是什么?与传统拥塞控制有何不同?
答案:
BBR (Bottleneck Bandwidth and RTT):
- Google 2016年提出
- 基于瓶颈带宽和RTT的拥塞控制算法
核心思想:
- 不基于丢包:传统算法靠丢包判断拥塞
- 主动探测:测量瓶颈带宽和最小RTT
- 目标:BDP = 瓶颈带宽 × 最小RTT
优势:
- 高吞吐:更好利用带宽
- 低延迟:不等丢包才降速
- 适应性强:适合高延迟、高带宽网络
对比:
传统算法(Reno/Cubic):
- 填满缓冲区 → 丢包 → 降速 → 再增速
- 锯齿状吞吐量曲线
- 高延迟(缓冲区膨胀)
BBR:
- 测量带宽和RTT
- 维持在最优点
- 平稳的吞吐量
- 低延迟应用:
- YouTube、Google服务
- Linux内核4.9+支持
Q7: 如何调优TCP参数提升性能?
答案:
接收缓冲区:
# Linux系统
sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216" # 最小 默认 最大
sysctl -w net.ipv4.tcp_wmem="4096 16384 16777216"初始拥塞窗口:
# 增大初始cwnd(默认10,可改为更大)
ip route change default via <gateway> dev eth0 initcwnd 30拥塞控制算法:
# 查看可用算法
sysctl net.ipv4.tcp_available_congestion_control
# 设置为BBR
sysctl -w net.ipv4.tcp_congestion_control=bbrTCP Fast Open:
# 启用TFO(减少握手延迟)
sysctl -w net.ipv4.tcp_fastopen=3应用层优化:
// Go代码设置TCP参数
conn, _ := net.Dial("tcp", "example.com:80")
tcpConn := conn.(*net.TCPConn)
// 禁用Nagle算法(低延迟场景)
tcpConn.SetNoDelay(true)
// 设置缓冲区大小
tcpConn.SetReadBuffer(1024 * 1024) // 1MB
tcpConn.SetWriteBuffer(1024 * 1024) // 1MBQ8: 什么情况下会发生拥塞?如何检测?
答案:
拥塞原因:
- 链路容量不足:带宽被占满
- 路由器缓冲区溢出:队列满导致丢包
- 突发流量:瞬间大量数据
- 慢速接收方:接收方处理不过来
检测方法:
丢包检测:
- 重复ACK(3个)
- 超时重传(RTO)
延迟检测:
- RTT增大
- RTT方差增大
吞吐量下降:
- 实际带宽远低于链路带宽
工具检测:
# 查看TCP统计
netstat -s | grep -i retrans
# 查看丢包率
ping -c 100 example.com | grep loss
# tcpdump分析
tcpdump -i eth0 -nn | grep "retransmission"拥塞指标:
轻度拥塞: 偶尔丢包,RTT略增
中度拥塞: 频繁丢包,RTT明显增加
重度拥塞: 大量丢包,超时重传频繁关键点总结
流量控制:
- 机制:滑动窗口
- 窗口:接收方通告rwnd
- 目的:保护接收方
- 问题:零窗口处理
拥塞控制:
- 四大算法:慢启动、拥塞避免、快速重传、快速恢复
- 窗口:发送方维护cwnd
- 目的:保护网络
- 核心:动态调整发送速率
实际发送窗口:
发送窗口 = min(cwnd, rwnd)状态转换:
慢启动(指数增长) → 拥塞避免(线性增长)
↓ ↓
达到阈值 收到3个重复ACK
↓
快速重传+快速恢复
↓
回到拥塞避免优化方向:
- 增大初始窗口
- 使用BBR算法
- 调整缓冲区大小
- 启用TCP Fast Open