KCP源码解析 (6) 拥塞控制(Congestion Control)

在上一章 底层数据输入处理 (ikcp_input) 中,我们学习了 KCP 如何接收和解析来自网络的原始数据包。我们看到,ikcp_input 不仅处理业务数据,还负责处理 ACK 包,并根据 ACK 包中的时间戳来更新 RTT(往返时间)。

这些信息(如 RTT 和丢包情况)是 KCP 做出高级决策的关键依据。本章,我们将探讨 KCP 最智能、也是其相比 TCP 的一大核心优势所在:拥塞控制

问题的提出:如何在高速公路上安全飙车?

想象一下,你驾驶着一辆超级跑车,行驶在一条通往远方城市的、宽窄不一的高速公路上。你的目标是:在不引发交通事故(网络拥塞)的前提下,尽可能快地到达目的地

  • 如果开得太慢:你会浪费大量时间,无法发挥跑车的性能。这相当于没有充分利用网络带宽。
  • 如果开得太快:前方路段可能很窄,或者有很多车,你鲁莽地冲过去,很容易造成交通堵塞,甚至车毁人亡。这相当于发送速度超过了网络的承载能力,导致大量数据包丢失,反而降低了传输效率。

那么,如何才能找到一个完美的、动态变化的速度呢?你需要一个智能的驾驶辅助系统,能够根据路况(车的多少、路的宽窄)实时调整你的车速。

KCP 的拥塞控制算法,就是这样一个"智能驾驶辅助系统"。它通过一系列精巧的机制,动态地调整自己的发送速率,以适应不断变化的网络状况,从而实现既快又稳的数据传输。

KCP 拥塞控制核心概念

1. 拥塞窗口 (cwnd):发送端的"速度限制"

拥塞窗口(Congestion Window,cwnd 是 KCP 自我施加的一个"速度限制"。它代表了在收到对方确认(ACK)之前,KCP 最多可以发送多少个未被确认的数据包。

  • cwnd 越大,意味着 KCP 可以在同一时间"在途"(已发送但未确认)的数据包越多,发送速率就越高。
  • cwnd 越小,意味着 KCP 变得越保守,发送速率就越低。

KCP 的所有拥塞控制策略,核心目标就是动态地调整 cwnd 的大小

在 KCP 的控制块 ikcpcb 中,cwnd 是一个关键字段:

c 复制代码
// ikcp.h: IKCPCB 结构体中的相关字段
struct IKCPCB
{
	// ...
	IUINT32 cwnd;       // 拥塞窗口大小 (Congestion Window)
	IUINT32 ssthresh;   // 慢启动阈值 (Slow Start Threshold)
	// ...
};

我们很快会讲到 ssthresh 的作用。

2. 慢启动与拥塞避免

KCP 采用了一套与 TCP 类似的、经过验证的策略来调整 cwnd

慢启动 (Slow Start)

当连接刚刚建立,或者网络刚刚从一次拥塞中恢复时,KCP 对当前的网络路况一无所知。这时它会采用"慢启动"策略,像一个小心翼翼的司机,先探探路。

  • 策略 :以一个很小的 cwnd 值开始(比如 cwnd = 1)。每收到一个对新数据的 ACK,就把 cwnd 的值加一。
  • 效果cwnd 的大小会随着每个 RTT(往返时间)近似指数级增长(1 -> 2 -> 4 -> 8...)。这使得 KCP 可以在网络状况良好时,迅速地把速度提起来,快速达到网络的可用带宽上限。

拥塞避免 (Congestion Avoidance)

cwnd 增长到一个特定的阈值------慢启动阈值 (ssthresh) 之后,KCP 认为自己已经接近了网络的承载极限,不能再那么"激进"了。此时,它会切换到"拥塞避免"模式。

  • 策略cwnd 不再指数增长,而是线性增长 。大致可以理解为,每个 RTT 周期,cwnd 只增加 1。
  • 效果:这就像司机在高速上已经把车速提得很快了,他会选择缓慢地、一点一点地踩油门,而不是一脚踩到底,以求更精细地逼近最高安全速度。

3. 拥塞避免: 对丢包的反应

如果在路上发生了"事故"------即 KCP 检测到了丢包(通过超时重传或快速重传),这强烈地暗示网络已经发生了拥塞。此时,KCP 会立刻踩下"紧急刹车":

  1. 将慢启动阈值 ssthresh 大幅降低 :通常设置为当前拥塞窗口 cwnd 的一半。这相当于说:"刚才开到这个速度就出事了,下次的安全速度上限得定低一点。"
  2. 将拥塞窗口 cwnd 重置为一个很小的值:通常重置为初始值(比如 1)。
  3. 重新进入"慢启动"阶段:从低速开始,重新探测网络容量。

这个过程保证了 KCP 在造成拥堵后,能迅速降低对网络的压力,并重新开始寻找合适的发送速率。

内部探秘:拥塞控制的实现

KCP 的拥塞控制逻辑主要分散在两个地方:

  • ikcp_input :当收到 ACK 时,根据当前阶段(慢启动或拥塞避免)来增加 cwnd
  • ikcp_flush :当检测到丢包(需要重传)时,减小 cwndssthresh

让我们用一个状态图来描绘这个过程:

graph TD A(慢启动
cwnd 指数增长) -- cwnd >= ssthresh --> B(拥塞避免
cwnd 线性增长) B -- 检测到丢包 --> C{紧急处理} A -- 检测到丢包 --> C C -- ssthresh = cwnd / 2
cwnd = 1 --> A

窗口增长的实现 (ikcp_input)

ikcp_input 收到有效的 ACK 包,并且确认有新的数据被对方接收(即 snd_una 前进了),它就会执行窗口增长逻辑。

c 复制代码
// ikcp.c: ikcp_input 中处理 ACK 后的拥塞窗口增长逻辑 (简化版)
int ikcp_input(ikcpcb *kcp, const char *data, long size)
{
    // ... 省略包解析 ...

    // 如果 snd_una 前进了 (即有新的数据包被确认)
	if (_itimediff(kcp->snd_una, prev_una) > 0) {
		IUINT32 mss = kcp->mss;
		if (kcp->cwnd < kcp->ssthresh) {
			// 1. 慢启动阶段: cwnd 指数增长
			kcp->cwnd++;
			kcp->incr += mss;
		} else {
			// 2. 拥塞避免阶段: cwnd 线性增长
			if (kcp->incr < mss) kcp->incr = mss;
			kcp->incr += (mss * mss) / kcp->incr + (mss / 16);
			if ((kcp->cwnd + 1) * mss <= kcp->incr) {
				kcp->cwnd++;
			}
		}
		// 确保 cwnd 不超过对方的接收窗口
		if (kcp->cwnd > kcp->rmt_wnd) {
			kcp->cwnd = kcp->rmt_wnd;
		}
	}
    return 0;
}

这段代码清晰地展示了 KCP 是如何根据 cwndssthresh 的关系,来选择不同的增长策略的。

窗口减小的实现 (ikcp_flush)

当 核心更新与刷新机制 (ikcp_update & ikcp_flush) 中的 ikcp_flush 函数发现有数据包超时或者需要快速重传时,它会设置一个 lostchange 标志。在函数末尾,它会根据这些标志来执行窗口减小逻辑。

c 复制代码
// ikcp.c: ikcp_flush 中检测到丢包后的处理逻辑 (简化版)
void ikcp_flush(ikcpcb *kcp)
{
    int lost = 0;   // 标志是否发生超时重传
    int change = 0; // 标志是否发生快速重传

    // ... 遍历 snd_buf,如果需要重传,则设置 lost=1 或 change=1 ...

    // --- 函数末尾的拥塞处理 ---
	if (change) { // 快速重传触发的拥塞避免
		kcp->ssthresh = (kcp->snd_nxt - kcp->snd_una) / 2;
		// ... cwnd 调整 ...
	}

	if (lost) { // 超时重传触发的慢启动
        // 将慢启动阈值减半
		kcp->ssthresh = cwnd / 2;
		if (kcp->ssthresh < IKCP_THRESH_MIN)
			kcp->ssthresh = IKCP_THRESH_MIN;
        // 将拥塞窗口重置为1,重新开始慢启动
		kcp->cwnd = 1;
		kcp->incr = kcp->mss;
	}
}

这段代码就是 KCP 的"紧急刹车"机制。一旦检测到网络拥塞的迹象,它就会果断地降低发送速率,避免情况恶化。

KCP 的"激进模式"开关:ikcp_nodelay

与 TCP 不同,KCP 赋予了开发者极大的控制权。通过 ikcp_nodelay 函数,你可以精细地调整 KCP 的行为,甚至完全关闭拥塞控制。

c 复制代码
// ikcp.h
int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc);
  • nc: (No Congestion control) 当 nc 设置为 1 时,KCP 的拥塞控制将被完全关闭cwnd 将不再受算法限制,发送速度只受对方接收窗口 rmt_wnd 的限制。

警告:这是一个非常"危险"的选项。关闭拥塞控制意味着 KCP 会像一辆刹车失灵的跑车一样,无视路况,只管猛踩油门。这在内部局域网或者网络质量极好的情况下可能可以获得极致的低延迟,但在公共互联网上,这种行为极易导致网络拥堵,对你自己的应用和其他使用者都会造成严重影响。请务必谨慎使用!

总结

在本章中,我们深入了解了 KCP 的"智能驾驶系统"------拥塞控制。

  • 核心目标:在不造成网络拥堵的前提下,尽可能快地发送数据。
  • 核心机制 :通过动态调整拥塞窗口 (cwnd) 来控制发送速率。
  • 核心策略
    • 慢启动 :在初期以指数方式快速提升 cwnd,探测网络带宽。
    • 拥塞避免 :当 cwnd 达到慢启动阈值 (ssthresh) 后,转为线性增长,精细调整速率。
    • 丢包响应 :一旦检测到丢包,立即将 ssthresh 减半,并将 cwnd 重置为很小的值,重新开始慢启动。
  • 控制权 :KCP 提供了 ikcp_nodelay 接口,允许开发者调整甚至关闭拥塞控制,以适应不同的应用场景。

KCP 正是凭借这套高效、智能的拥塞控制算法,才能在保证可靠性的同时,实现比 TCP 更低的平均延迟和更平稳的传输表现。

到目前为止,我们已经探讨了 KCP 的用户接口、核心状态、更新机制、队列管理和拥塞控制。我们反复提到 KCP 在内部处理的是一个个"数据段"。这些数据段到底长什么样?它们包含了哪些信息,使得 KCP 能够实现如此复杂的功能?

下一章,我们将深入到 KCP 的最基本组成单位------KCP 数据段 (IKCPSEG),解剖它的内部结构。

相关推荐
帽儿山的枪手6 小时前
HVV期间,如何使用SSH隧道绕过内外网隔离限制?
linux·网络协议·安全
charlie1145141917 小时前
设计自己的小传输协议 导论与概念
c++·笔记·qt·网络协议·设计·通信协议
(Charon)8 小时前
【C语言网络编程】HTTP 客户端请求(基于 Socket 的完整实现)
网络·网络协议·http
程序员编程指南9 小时前
Qt 并行计算框架与应用
c语言·数据库·c++·qt·系统架构
努力的小帅11 小时前
C++_红黑树树
开发语言·数据结构·c++·学习·算法·红黑树
CN-Dust11 小时前
【C++】指针
开发语言·c++
逐花归海.11 小时前
『 C++ 入门到放弃 』- 哈希表
数据结构·c++·程序人生·哈希算法·散列表
筏.k11 小时前
C++现代Redis客户端库redis-plus-plus详解
c++·redis
程序员编程指南12 小时前
Qt 多线程调试技巧与常见问题
c语言·开发语言·c++·qt
徐归阳12 小时前
第十一天:不定方程求解
c++·visual studio