【网络编程】KCP——可靠的 UDP 传输协议——的知识汇总

文章目录

  • 前言
  • [UDP 协议](#UDP 协议)
    • [UDP 的关键指标/特性](#UDP 的关键指标/特性)
    • [UDP 的典型应用场景](#UDP 的典型应用场景)
  • [KCP 协议的基础](#KCP 协议的基础)
    • [KCP 的构造](#KCP 的构造)
    • [KCP 协议特性](#KCP 协议特性)
  • [KCP 的可靠传输机制------ARQ](#KCP 的可靠传输机制——ARQ)
  • [KCP 的激进重传策略------低延迟设计!](#KCP 的激进重传策略——低延迟设计!)
    • [**1、 快速重传 (Fast Retransmit):跳过漫长的等待**](#1、 快速重传 (Fast Retransmit):跳过漫长的等待)
    • [**2、选择性重传 (Selective Repeat / SACK-like Behavior):精准打击,避免浪费**](#2、选择性重传 (Selective Repeat / SACK-like Behavior):精准打击,避免浪费)
    • [低延迟 ACK 机制能综合实现 "选择性重传" + "快速重传"](#低延迟 ACK 机制能综合实现 “选择性重传” + “快速重传”)
    • [**3、激进的超时重传 (Aggressive RTO)------解决尾包延迟问题**](#3、激进的超时重传 (Aggressive RTO)——解决尾包延迟问题)
    • 低延迟的本质就是用带宽换时间
  • [再低延迟也总有意外------KCP 超时淘汰机制](#再低延迟也总有意外——KCP 超时淘汰机制)
    • [KCP 滑动窗口](#KCP 滑动窗口)
    • [KCP 的超时淘汰机制](#KCP 的超时淘汰机制)
  • [KCP 应对网络拥塞的方案------非退让流量控制](#KCP 应对网络拥塞的方案——非退让流量控制)
  • 总结

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接

前言

我是零声学院的小学徒,我虽然不是计算机科班出身,但我是学数学的,我认为对于一个我不熟悉的东西是需要作知识汇总的,尽量让自己理解清楚事情的全貌,并且对知识作出恰当的比喻和类比,以达本意。我将仿效上一篇介绍 TCP 协议栈的文章笔法(原文链接 在此),详细的介绍在工程上(尤其是游戏工程)具有广泛应用的可靠、低延迟的通信协议------KCP.

先运行过去写过的代码(与 UDP 协议相关),执行过程使用 Wireshark 去抓包,记录一路上的 UDP 报文。从现实的 UDP 报文出发,先介绍 UDP 协议栈的简单简陋,同时又看到它的低延迟潜力,只要我们在此基础上,稍做设计,便能设计出可靠的低延迟传输协议------KCP。 而后逐步地深入介绍 KCP 协议栈的各个机制的内容,给大家一个对 KCP 的整体性的认知。事不宜迟,我们现在开始。

我过去曾写过一篇文章,它是介绍远程 DNS 域名查询服务的,原文链接 在此。在那篇文章中,传输层是采用 UDP 协议的,读者可查看那篇文章的代码,套接字使用数据报模式就是使用 UDP 的开始,

c 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 这是一个通讯方式的参数设定
	// AF_INET 表示使用 IPv4 的地址协议。
	// SOCK_DGRAM 表示"数据报套接字"
	// 0 指代自动选择与 type 匹配的默认协议。对于 SOCK_DGRAM,默认协议是 UDP(等价于显式指定 IPPROTO_UDP)。
	// 返回的整数是一个文件描述符,唯一标识当前创建的套接字。
	// 后续通过此描述符进行网络操作:bind():绑定 IP 和端口(服务端)。sendto():发送数据。recvfrom():接收数据。close():关闭套接字。
	if (sockfd < 0) {   
		return  -1;    // 这表示通讯状态设置不成功
	}

如果读者要问假如我想使用 TCP 协议怎么办?那也很简单,套接字使用数据流模式就 OK 了。

c 复制代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

回到正题上,我们把使用 UDP 传输协议的 DNS 的域名查询代码运行一下,利用这个程序去查询零声教育的域名

bash 复制代码
qiming@qiming:~/share/CTASK/TCP_test/DNS_test$ ./dns www.0voice.com
connect : 0
www.0voice.com has address 43.139.121.27
	Time to live: 0 minutes , 0 seconds
qiming@qiming:~/share/CTASK/TCP_test/DNS_test$

零声教育的域名是 43.139.121.27。此时,我们早已开启 Wireshark 来进行网络抓包了

于是找到了相匹配的两份 UDP 报文

我们先从 UDP 协议讲起,随后就会将 KCP 协议。

UDP 协议

我们亲眼看到真实的 UDP 报文后,我们再从理论的 UDP 报文来看

c 复制代码
0                   1                   
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5   <-- 对齐 2 个字节 (16 位)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    Source Port                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    Destination Port           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Length                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|      checksum                 |	<-- UDP 报文的头部(共 8 个字节)           
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Data Payload (可变长度)    |	<-- 数据荷载,实际数据,对齐值为 2 个字节
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段名称 长度(字节) 说明
源端口号 2 发送方端口号,用于标识发送方的应用程序。
目的端口号 2 接收方端口号,用于标识接收方的应用程序。
长度 2 UDP报文的总长度,包括UDP头部和数据部分,以字节为单位。最小值为8字节(仅头部)。
检验和 2 用于检测UDP报文在传输过程中是否出现错误。
数据 可变 应用层提供的数据,长度可变。

UDP 报文是极其简单的,没有复杂的东西。

UDP 的关键指标/特性

1、无连接:

  • 核心特性。 发送数据报之前不需要像 TCP 那样进行"三次握手"建立连接。
  • 意义: 通信开销极小,延迟非常低(发送第一个数据包前几乎没有准备时间)。适用于需要快速响应的场景。

2、不可靠交付:

  • 核心特性。 UDP 协议本身不提供任何机制来保证数据报一定能到达目的地。
  • 具体表现:
    • 不确认 (No Acknowledgement): 发送方发送后不会等待接收方的确认 (ACK)。
    • 不重传 (No Retransmission): 如果数据报丢失(网络拥塞、路由器丢弃、校验失败等),发送方不会自动重传。
    • 不保证顺序 (No Ordering Guarantee): 后发送的数据报可能比先发送的早到达接收方。UDP 不负责重新排序。
    • 不防止重复 (No Duplication Prevention): 网络问题可能导致同一个数据报被多次送达接收方(通常由底层 IP 网络引起)。
  • 意义: 可靠性保证(如果需要)必须由应用程序自己实现(例如,在应用层添加序列号、超时重传、确认机制),或者应用程序能够容忍这些情况(如直播丢几帧画面)。

3、无拥塞控制:

  • UDP 没有内置的机制来感知网络拥塞并调整发送速率。
  • 意义: UDP 发送方会尽可能快地按照应用程序的要求发送数据,无论网络当前是否拥塞。这可能导致:
    • 加剧拥塞: 在网络已经拥塞时,UDP 流量会继续冲击网络,可能使情况恶化,影响自身和其他协议(尤其是 TCP)的性能。
    • 公平性问题: 对 TCP 流不公平,因为 TCP 会主动降低速率,而 UDP 不会。
  • 应用程序如果需要在拥塞环境下良好运行,必须自己实现某种形式的速率控制和拥塞感知。

4、无流量控制:

  • UDP 没有机制让接收方告诉发送方"慢一点,我处理不过来了"。
  • 意义: 如果发送方发送速度远快于接收方的处理能力(或接收方应用程序读取数据的速度),接收方的缓冲区会溢出,导致数据报被丢弃。应用程序需要自行处理速率匹配问题。

5、面向数据报 (Message-Oriented):

  • UDP 每次发送和接收的是一个完整的数据报(报文)。
  • 意义:
    • 保留消息边界: 应用程序发送多少次 sendto 调用,接收方就需要对应多少次 recvfrom 调用。每个 recvfrom 调用返回一个完整的数据报(或报错)。数据报之间是独立的。
    • 与 TCP 的字节流模型形成鲜明对比(TCP 没有消息边界,数据是连续的字节流)。

6、头部开销小:

  • 固定头部仅 8 字节,远小于 TCP 头部的 20 字节(通常)或更多(带选项)。
  • 意义: 对于传输小数据包的应用(如 DNS 查询、心跳包),UDP 的协议开销比例更低,效率更高。

7、支持广播和多播(对局域网内的所有主机可见):

  • UDP 数据报可以发送到:
    • 单播 (Unicast): 单个目标主机(最常见)。
    • 广播 (Broadcast): 同一子网内的所有主机(目的 IP 地址为特定子网的广播地址,如 192.168.1.255)。
    • 多播 (Multicast): 订阅了特定多播组的一组主机(目的 IP 地址为 D 类地址 224.0.0.0 - 239.255.255.255)。
  • 意义: 这是 TCP 无法做到的(TCP 严格是点对点连接)。UDP 是实现如网络发现、视频会议、实时数据分发(股票行情)等多播/广播应用的基石。路由器需要支持 IGMP 等协议来管理多播组成员关系。

UDP vs TCP 关键对比

特性 UDP TCP
连接 无连接 面向连接 (三次握手)
可靠性 不可靠 (不确认、不重传、不排序、不保序) 可靠 (确认、重传、排序、保序)
交付保证 尽力而为交付 (Best Effort) 保证按序、无差错、不丢失、不重复的交付
流量控制 有 (滑动窗口)
拥塞控制 有 (多种复杂算法)
数据模型 面向数据报 (保留消息边界) 面向字节流 (无消息边界)
头部大小 小 (8 字节) 大 (通常 20 字节 + 选项)
速度/延迟 快 (连接建立开销小,无确认重传延迟) 相对较慢 (连接建立/断开开销,确认重传延迟)
广播/多播 支持 不支持 (仅单播)
适用场景 实时应用、简单查询、多播广播、容忍丢包 Web 浏览、文件传输、邮件、需要可靠性的应用

UDP 的典型应用场景

1、对延迟敏感,能容忍少量丢失的应用:

  • 实时音视频流 (VoIP, Video Conferencing, Live Streaming)
  • 在线游戏 (状态更新、位置同步)
  • 实时金融行情推送

2、简单查询/响应,数据量小:

  • DNS (域名解析)
  • DHCP (动态获取 IP 地址)
  • SNMP (网络管理)
  • TFTP (简单文件传输)

3、多播和广播应用:

  • 服务发现 (如 mDNS/Bonjour)
  • 路由协议更新 (如 RIP)
  • 流媒体分发

4、在应用层实现可靠性的场景:

  • 某些自定义协议或基于 UDP 构建的可靠传输协议 (如 QUIC - HTTP/3 的基础, DCCP, 部分 VPN 协议)。

UDP 是一种极其精简、快速但"不可靠"的传输层协议。它的核心价值在于低延迟低开销,适用于那些对时效性要求极高、能够容忍一定程度数据丢失或由应用层自行处理可靠性的场景。理解其报文结构(特别是端口号、长度、校验和的作用)以及无连接、不可靠、无流控/拥控的特性,是正确使用 UDP 或选择 UDP 还是 TCP 的关键。虽然 UDP 本身简单,但在其之上构建可靠的应用层协议(如 QUIC)是当前网络协议发展的一个重要方向。

KCP 协议的基础

我们知道 UDP 的传输机制与 TCP 是极其不一样的,强调 低延迟低开销,在游戏等领域有着广泛的应用。那么,如何在 UDP 基础之上,作些改动,让其变得更加可靠但依旧低延迟低开销呢?KCP 协议通信方案!

KCP的全称是Quick UDP Protocol,即"快速用户数据报协议"。KCP 协议是一种基于 UDP 的可靠传输协议,由国内开发者 skywind3000 设计并开源。它的核心目标是在保证一定可靠性的前提下,尽可能地降低传输延迟,特别适合对实时性要求高的网络应用。

KCP 通过 24 字节精简头部 + 双队列缓冲管理 (发送/接收队列 + 缓冲区) + 时间戳驱动更新(滑动窗口的超时淘汰机制),在 UDP 上实现了低延迟可靠传输。其核心代码约 1000 行 C 语言(见 GitHub),适合嵌入实时应用(如游戏引擎)。开发者需注意它不是 TCP 的替代品,而是为特定场景优化的传输层增强工具。

KCP 的构造

KCP 协议通过自定义报文头在 UDP 之上构建可靠传输,其设计追求简洁高效和低延迟。以下是 KCP 报文结构、核心字段和代码实现的关键解析:

c 复制代码
struct kcp_segment_header {
    uint32_t conv;     // 会话 ID(虚拟连接标识)
    uint8_t  cmd;      // 命令类型(见下表)
    uint8_t  frg;      // 分片编号(0=最后一个分片,>0 表示后续还有分片)
    uint16_t wnd;      // 接收窗口剩余大小(接收方能接收的数据量)
    uint32_t ts;       // 时间戳(发送时刻,用于 RTT 计算)
    uint32_t sn;       // 序列号(当前包的序号)
    uint32_t una;      // 待确认序号(发送方尚未收到的 ACK 的起始序号)
    uint32_t len;      // 数据长度(不含头部的数据部分长度)
};

✅ 头部固定 24 字节,后续紧跟用户数据(若有)。

网络套接字socket 与 KCP 控制块相关联(这与 TCP 一样,TCP 协议栈维护 TCB,即 TCP 控制块),它主要是放置 KCP 协议栈的内容(里面当然是包括 "滑动窗口" 的)。

c 复制代码
// KCP 会话控制块(简化版)
struct ikcpcb {
    // 连接标识
    uint32_t conv;
    
    // 窗口管理
    uint32_t snd_una;  // 最早未确认包序号
    uint32_t snd_nxt;  // 下一个待发包序号
    uint32_t rcv_nxt;  // 下一个待接收包序号
    
    // 队列管理
    struct IQUEUEHEAD snd_queue; // 待发送队列
    struct IQUEUEHEAD rcv_queue; // 乱序接收队列
    struct IQUEUEHEAD snd_buf;   // 已发送等待 ACK 队列
    struct IQUEUEHEAD rcv_buf;   // 按序接收队列
    
    // 超时管理
    uint32_t current;    // 当前时间戳
    uint32_t rx_rttval;  // RTT 波动值
    uint32_t rx_srtt;    // 平滑 RTT
    uint32_t rx_rto;     // 当前 RTO 值
    
    // 配置参数
    uint32_t mtu;        // 最大传输单元(默认 1400)
    uint32_t fastresend; // 快速重传触发阈值(默认 2)
    uint32_t nodelay;    // 是否启用无延迟模式(0/1)
};

KCP 协议特性

以下是 KCP 协议的关键特点和原理:

  • 底层使用 UDP:
    • KCP 本身不处理底层网络传输,它构建在 UDP 之上。这意味着它继承了 UDP 的无连接、低开销、无拥塞控制(默认)等特性。
    • 它通过自己的机制在 UDP 之上实现了可靠传输。
  • 核心机制:ARQ (自动重传请求):
    • 和 TCP 一样,KCP 也使用确认号、超时重传、序列号等机制来保证数据包的可靠、有序到达。
    • 它实现了自己的滑动窗口来进行流量控制。
  • 追求低延迟的关键设计:
    • 更激进的重传策略: 这是 KCP 降低延迟的核心。
      • 快速重传: 当检测到丢包(比如收到 3 个重复的 ACK)时,KCP 会立即重传丢失的包,而不像标准的 TCP RTO (重传超时) 那样需要等待一个较长的超时时间(通常至少 200ms)。KCP 的 RTO 最小值可以设置得非常低(如 10ms 甚至更低)。
      • 选择性重传: KCP 可以选择性地只重传真正丢失的数据包,而不是像 TCP SACK 出现之前那样可能需要重传整个窗口的数据(尽管现代 TCP 也支持 SACK)。
    • 非退让的流控: KCP 的流量控制算法(窗口大小调整)不像 TCP 那样对丢包反应"剧烈"。TCP 在发生拥塞(丢包)时会大幅减小拥塞窗口(cwnd),导致传输速率骤降。KCP 的窗口调整可以配置得更平滑,减少因单次丢包造成的传输速率波动和恢复时间,从而保持更稳定的低延迟。用户可以根据网络状况灵活配置其拥塞控制策略的激进程度。
    • 更小的传输单元: KCP 鼓励更小的数据分片传输,有助于降低单个包丢失对整个传输的影响。
    • 更快的 ACK: KCP 可以配置为更及时地发送 ACK 确认包,减少发送端等待确认的时间。

性能方面的优点:

  • 显著降低延迟: 在存在一定丢包和波动的网络环境下(如移动网络、跨国网络),KCP 通常能比 TCP 提供低得多的端到端延迟。
  • 可配置性强: 开发者可以通过调整一系列参数(如 RTO 最小值、快速重传触发条件、窗口大小、是否启用 nodelay 等)来精细控制 KCP 的行为,在延迟、带宽利用率和抗丢包能力之间进行权衡,以适应不同的应用场景和网络条件。
  • 用户态实现: 作为应用层协议,易于集成和调试,不依赖操作系统内核。

性能方面的缺点/代价:

  • 更高的带宽消耗: 更激进的重传和更快的 ACK 意味着在网络状况不佳时,KCP 会比 TCP 产生更多的冗余数据包(重传包、ACK包),占用更多带宽。
  • 可能加剧网络拥塞: 如果配置过于激进且网络本身已经拥塞,KCP 的大量重传可能会进一步恶化网络状况(需要合理配置流控和拥塞避免策略)。TCP 的"退让"特性在维持整个网络稳定性方面是有价值的。
  • 实现复杂度: 应用开发者需要理解并合理配置 KCP 的参数才能达到最佳效果,这比直接使用 TCP 稍复杂。
  • 可靠性依赖实现: 其可靠性完全依赖于自身的 ARQ 实现,不像 TCP 经过了几十年的广泛验证和优化。

主要应用场景:

  • 实时网络游戏: 尤其是动作类、MOBA、FPS 等对延迟极其敏感的游戏,是 KCP 最经典的应用领域。手游中使用尤其广泛。
  • 实时音视频通话: 如视频会议、直播连麦等,需要低延迟保证流畅互动。
  • 弱网环境优化: 在移动网络(4G/5G)、长距离链路(跨国)、或质量不稳定的 Wi-Fi 环境下,提升应用的响应速度和流畅性。
  • 需要低延迟的远程控制/操作: 如云桌面、远程机器人控制等。
  • 替代 TCP 在某些特定延迟敏感场景的应用。

那么 KCP 与 TCP 有何相同点呢?

KCP 与 TCP 的基础可靠性机制相同

机制 作用 KCP & TCP 实现方式
序列号 (Sequence Number) 标识数据包顺序 每个数据包携带唯一序列号,接收方按序重组
确认应答 (ACK) 告知发送方数据接收状态 接收方发送 ACK 包(含已接收的最大连续序列号)
超时重传 (RTO) 应对丢包 未收到 ACK 的包在超时后重传
滑动窗口 (Sliding Window) 流量控制与批量传输 限制发送方可发送的数据量,通过 ACK 推进窗口释放空间

✅ 两者均通过 序列号 + ACK + 重传 + 滑动窗口 实现可靠传输(本质是 ARQ 自动重传请求)

KCP 与 TCP 的滑动窗口的核心逻辑相同

概念 作用 KCP & TCP 一致性
发送窗口 (snd_una) 跟踪最早未确认包 窗口左边界,收到 ACK 后向右移动
接收窗口 (rcv_nxt) 期待接收的下一个包序号 接收方按序交付数据后向右移动
窗口大小 (snd_wnd/rcv_wnd) 控制传输速率 限制发送速率(避免淹没接收方)

✅ 窗口滑动逻辑完全一致:ACK 推动发送窗口,连续数据推动接收窗口。

KCP 与 TCP 的基础组件相同

组件 功能 KCP & TCP
重传计时器 检测丢包超时 每个发送包独立计时,超时触发重传
缓冲区管理 缓存已发送未确认/乱序到达的数据 发送/接收端均维护队列
校验和 检测数据损坏 可选支持(KCP 依赖下层 UDP 校验,TCP 强制)

总结:异同本质

维度 KCP TCP
基础结构 ✅ 与 TCP 完全相同 ✅ 标准滑动窗口
窗口单位 ❗ 包数量(非字节) 字节数
控制权 ❗ 应用层可编程 内核固定控制
拥塞控制 ❗ 需开发者实现 内置完善算法
ACK 机制 ❗ 支持无延迟 ACK + 精确重传信息 依赖系统实现和扩展

KCP 的可靠传输机制------ARQ

KCP 协议的核心 ARQ(自动重传请求)策略是 选择性重传(Selective Repeat, SR),但在此基础上进行了深度优化和激进参数调整。其实,这点跟 TCP 没啥区别。

三种 ARQ 机制对比

ARQ(Automatic Repeat reQuest) 是可靠传输协议的核心机制,其三种经典实现方式------停等式(Stop-and-Wait)、回退 N 帧(Go-Back-N) 和选择性重传(Selective Repeat)------构成了现代网络协议(如 TCP、KCP)的基础。以下是它们的详细对比及与 KCP/TCP 的关联:

特性 停等式 (Stop-and-Wait) 回退 N 帧 (Go-Back-N, GBN) 选择性重传 (Selective Repeat, SR)
发送窗口大小 1 个包 N 个包(N>1) N 个包(N>1)
接收窗口大小 1 个包 1 个包 N 个包
重传策略 超时后重发当前包 超时后重发所有未确认包 仅重传丢失的包
乱序处理 丢弃乱序包 丢弃乱序包 缓存乱序包
传输效率 ⭐ 最低(每包需单独确认) ⭐⭐ 中等(批量发送但重传开销大) ⭐⭐⭐ 最高(仅重传丢失包)
带宽利用率 低(RTT 内只能发 1 个包) 中(流水线发送,但重传冗余) 高(精准重传 + 乱序缓存)
典型场景 极弱网络(如卫星链路) 早期 TCP(如 Tahoe) 现代 TCP(SACK)/ KCP

停等式 (Stop-and-Wait) 有点小捞,它太笨了。❗ 问题:每个包必须等 ACK 到达才能发下一个,效率极低。

回退 N 帧 (Go-Back-N, GBN) 处理的批量很大。❗ 问题:单包丢失导致大量冗余重传(尤其高延迟网络)。

选择性重传 (Selective Repeat, SR) 就具有相当的现代性了。✅ 优势:仅重传丢失包(SN=2),接收方缓存乱序包(3,4,5)。

KCP 的选择性重传

一、基础机制:选择性重传(SR)

特性 KCP 的实现
重传对象 仅重传真正丢失的包(非整个窗口)
乱序处理 接收方缓存乱序到达的包,待缺失包到达后按序提交
确认机制 ACK 包携带精确的丢包信息(类似 TCP SACK,但为原生支持)
效率优势 单包丢失不影响后续包传输,避免回退 N 帧(GBN)的冗余重传

✅ 核心逻辑:"谁丢传谁" ------ 通过 ACK 包中的序列号信息精准定位丢失包。

二、KCP 对 SR 的增强策略

1.、激进快速重传

  • 触发条件:收到 2 个重复 ACK(默认值,可配置)即重传(TCP 需 3 个)。
  • 优势:将丢包检测时间从 RTT + 超时等待 压缩至 ≈1 RTT。

2、超时重传优化

  • RTO 最小值:可设为 5-10ms(TCP 通常 ≥200ms),加速兜底重传。
  • RTO 计算:启用 nodelay 模式时,RTO = RTT + 1×RTTVAR(更贴近当前延迟)。

3、ACK 无延迟反馈

  • 机制:启用 ack_nodelay 时,收到数据包立即回复 ACK(TCP 常延迟 40ms 等待捎带)。
  • 效果:进一步减少丢包判定时间(尤其对单向数据流)。

4、 窗口淘汰机制

  • 问题场景:最早发送的包(如 SN=10)多次重传失败,阻塞窗口滑动。
  • KCP 方案:
    • 若后续包(如 SN=15)被确认 → 强制将窗口左边界推进到 SN=16。
    • 放弃旧包的重传,避免传输停滞(牺牲少数包可靠性换流动性)。

KCP 的激进重传策略------低延迟设计!

KCP 的低延迟主要来源于它对 丢包 这一网络传输最大敌人的处理方式。TCP 为了保证网络的全局公平性和稳定性,对丢包的反应是相对"保守"和"缓慢"的。而 KCP 的设计哲学是:为了应用的实时性,可以更早、更主动、更精准地重传丢失的数据包,即使这意味着可能消耗更多的带宽

1、 快速重传 (Fast Retransmit):跳过漫长的等待

  • 原理: 当接收方发现接收到的数据包序列号不是连续的(即出现"空洞"),它会立即向发送方发送一个 ACK 包,指出它期望接收的下一个序列号(即空洞的开始位置)。如果发送方连续收到 3个(默认值,可配置) 或更多针对同一个数据包的重复 ACK (DupACK),它就立即推断该数据包很可能丢失了。
  • 与 TCP 的关键区别:
    • 触发阈值更低: TCP 通常也需要 3 个 DupACK 触发快速重传(NewReno 及以后算法)。看起来一样?不!关键在于时间窗口。
    • RTO 最小值极低: TCP 的 RTO (Retransmission Timeout) 计算复杂,其最小值通常被限制在 200ms 左右(Linux 默认)。这意味着,即使触发了快速重传,TCP 也可能在等待 RTO 超时后才行动,或者其 RTO 本身就很大。KCP 的 RTO 最小值 (minrto) 可以由用户直接设置,典型值低至 10ms 甚至 5ms。即使没有 DupACK,KCP 的常规超时重传等待时间也远短于 TCP。
    • 反应速度: 当收到足够的 DupACK 时,KCP 不等待任何定时器超时,立即重传怀疑丢失的数据包。而 TCP 在收到 DupACK 后,虽然会启动快速重传,但可能受到拥塞窗口调整、RTO 计算等因素影响,响应速度不如 KCP 直接和激进。
  • 延迟收益: 在发生单包或少量丢包时,KCP 能在 几十毫秒内 完成重传(收到 DupACK -> 立即重发),而 TCP 可能因为 RTO 较大或算法保守需要等待 几百毫秒。这对于实时应用(如游戏中一个关键操作指令丢失)是质的飞跃。

2、选择性重传 (Selective Repeat / SACK-like Behavior):精准打击,避免浪费

  • 原理: 当接收方检测到多个不连续的数据包丢失时,它需要告知发送方具体是哪些包丢了。KCP 在其 ACK 包中携带了接收窗口内所有缺失数据段的起始和结束序列号信息(类似于 TCP 的 SACK 选项,但实现方式不同)。
  • 与 TCP 的关键区别:
    • TCP 早期版本 (Reno): 在收到 DupACK 进入快速恢复阶段时,如果只丢失一个包,效果尚可。但如果一个窗口内丢失多个包,Reno 在收到部分新数据的 ACK 后就会错误地认为所有丢失包都已重传成功,退出快速恢复,导致剩余丢失包必须等待 RTO 超时(即所谓的"重传超时"问题),造成巨大的延迟(几百毫秒到几秒)。
    • TCP 现代版本 (SACK): 通过 SACK 选项也能实现选择性确认,解决了 Reno 的多个丢包问题。KCP 与 TCP SACK 在"选择性"理念上相似,但 KCP 的实现通常更轻量和直接,并且其整体重传策略(结合极低的 RTO 和快速响应)更为激进。
  • 延迟收益: 在发生多个数据包连续丢失的情况下,KCP 能通过接收方精确的反馈,一次性重传所有已知丢失的包,避免了像 TCP Reno 那样因多次 RTO 超时或等待导致的累积延迟。即使对比 TCP SACK,KCP 更快的触发和重传动作也能进一步压缩恢复时间。

低延迟 ACK 机制能综合实现 "选择性重传" + "快速重传"

想象一个场景:在我们中学时代,监考老师给我们点名,他们会一个一个地点,点一个学生就喊一句 "到",当点到下一位,无任何人应答的时候,老师立马就把这个学生记到迟到缺席的 "黑名单" 里,并通知班主任叫他赶快来,在此期间一直等他,直到老师不耐烦了,把它记作缺考(超时淘汰)。这其实就是一个低延迟 ACK 。

以下这个图示正是一个低延迟 ACK 的工作原理展示。

c 复制代码
时间线 (向下流动)        发送方 (Sender)                    接收方 (Receiver)                    事件说明
-------------------    ---------------------------    ---------------------------    --------------------------------
         |                 发送 Seq1                    ->                            |
         |                 发送 Seq2                    ->                            |
         |                 发送 Seq3 (丢失)              -> (丢失)                    **Seq3 在网络中丢失**
         |                 发送 Seq4                    ->                            |
         |                 发送 Seq5                    ->                            |
         |                                            <-         ACK1 (确认Seq1)     **低延迟 ACK!**
         |                                            <-         ACK2 (确认Seq2)     **低延迟 ACK!**
         |                                            <-         ACK2 (重复ACK)       **收到 Seq4 (乱序),立即重复 ACK2 索要 Seq3**
         |                                            <-         ACK2 (重复ACK)       **收到 Seq5 (乱序),立即重复 ACK2 索要 Seq3**
         |                                                                          **发送方收到第2个重复ACK(2) (假设fastresend=2)**
         |                 重传 Seq3                   ->                            **快速重传触发! (只重传丢失的Seq3)**
         |                                            <-         ACK3 (确认Seq3)      **低延迟 ACK! Seq3到达**
         |                                                                          **接收方已缓存 Seq4, Seq5,现在可交付 Seq3, Seq4, Seq5**
         |                                            <-         ACK5 (确认Seq5)      **累积确认/选择性ACK (确认Seq3,4,5)**
         |                 发送 Seq6                   ->                            | 继续发送新数据
         |                                            <-         ACK6 (确认Seq6)      |
         |                  ...                        ...                           ...

当 KCP 收到一个数据包时,它不会等待interval定时器到期或等待有数据要回传时捎带 ACK,而是立即生成一个 ACK 包发送回去。

更及时的 ACK 反馈:减少发送端的等待

  • 原理: KCP 提供了灵活的 ACK 发送策略:
    • ack_nodelay: 启用后(推荐),当 KCP 收到一个数据包时,它不会等待interval定时器到期或等待有数据要回传时捎带 ACK,而是立即生成一个 ACK 包发送回去。
    • 禁用时,会尝试稍作等待(interval内)以便捎带数据或合并 ACK。
  • 与 TCP 的关键区别: TCP 通常采用捎带确认策略,即接收方在发送数据时,才把 ACK 信息放在回传数据包的首部中一起发回。如果没有数据要回传,TCP 会设置一个小的延迟定时器(通常几十毫秒)等待,希望有数据可以捎带。如果没有,才单独发送一个纯 ACK 包。这个延迟是为了减少纯 ACK 包的数量。
  • 延迟收益: 启用 ack_nodelay 后,发送方能更快地知道数据包是否成功到达。这带来两个好处:
    • 加速快速重传触发: 丢失包的 DupACK 能更快地返回到发送方。
    • 推进发送窗口: 确认包能更快地释放发送窗口的空间,允许发送方发送后续的新数据,提高吞吐量和降低整体传输完成时间。
    • 代价:可能增加少量纯 ACK 包的带宽消耗。

3、激进的超时重传 (Aggressive RTO)------解决尾包延迟问题

  • 原理: KCP 使用自己独立的 RTO 计算算法(基于 RTT 测量和变化)。虽然算法本身与 TCP 的 Karn/Partridge 算法类似,但其核心参数可由用户精细控制:
    • minrto: 允许设置的最小 RTO 值(如 10ms)。TCP 通常有系统级下限(200ms)。
    • nodelay: 启用后(通常推荐启用),会显著改变 RTO 计算方式:
      • 第一次 RTO 计算:RTO = RTT + max(clock_drift, 4 * RTTVAR)
      • 后续 RTO 计算:RTO = RTT + 1 * RTTVAR (非常接近最新的 RTT 测量值)
    • interval: KCP 内部轮询间隔(如 10ms)。这决定了它检查超时的频率。
  • 与 TCP 的关键区别:
    • 用户可配置性: 开发者可以根据网络环境和应用需求,将 minrto 设置得非常低(如 10ms),而 TCP 的内核参数调整通常受限且影响全局。
    • 更快的检测: 更小的 interval 和 minrto 意味着 KCP 能更快地检测到超时(可能没有触发快速重传的丢包,如尾包丢失)。
    • 更激进的增长: nodelay 模式下的 RTO 计算比标准 TCP 更激进,增长幅度小,使得超时重传的等待时间更接近最新的 RTT 测量值,而不是像 TCP 那样指数级退避导致 RTO 迅速膨胀到秒级。
  • 延迟收益: 对于那些无法触发快速重传(如只丢失一个包且后面没有新数据,无法产生 DupACK)的丢包场景,KCP 能在 10ms - 几十ms 级别 的超时后立即重传,而 TCP 至少需要等待 200ms 以上。这解决了"尾包延迟"问题。

协同效应:如何合力实现低延迟

  • 丢包检测快: 通过 ack_nodelay 和精确的序列号/ACK机制,接收方能快速发现丢包并通知发送方 (DupACK)。
  • 决策快: 收到足够 DupACK (阈值可配,默认3) 后,KCP 立即决策重传,不等待定时器。
  • 重传动作快: 利用选择性重传信息,只重传丢失的包。
  • 兜底快: 即使没有触发快速重传(如尾包丢失),极低的 minrto 和快速的内部轮询 (interval) 也能保证在极短时间(如 10-30ms)后通过超时重传补发。
  • 恢复快: 一旦丢失的包被重传并确认,发送窗口能迅速前进(因为 ACK 回来得也快 - ack_nodelay),后续数据流能很快跟上,避免了 TCP 因 RTO 膨胀和窗口骤减导致的传输速率"雪崩"和缓慢恢复。

低延迟的本质就是用带宽换时间

KCP 激进重传策略的核心思想是:不惜以可能增加带宽消耗(发送更多重传包和更及时的纯 ACK 包)和潜在加剧网络拥塞为代价,换取传输延迟的极大降低 。 它通过一系列精心设计的、高度可配置的机制,将网络传输中最耗时的环节------丢包检测、决策、重传和恢复------的时间压缩到了极致(毫秒级),从而满足了实时应用对低延迟的苛刻要求。这种"带宽换时间"的权衡在实时游戏、音视频通信等场景下通常是值得的,因为用户体验对延迟的敏感度远高于对少量额外带宽消耗的敏感度。

再低延迟也总有意外------KCP 超时淘汰机制

我们先设想一个情景:当我们在玩英雄联盟的时候,如果我们的电脑卡顿(ping 值很高),英雄在漂移,情况持续了 3 min,网络恢复正常之后,系统并没有把我们电脑卡顿过程时候,别的英雄击杀我们的片段给出,而是快速跳过,最后只显示我们在泉水之中。这,就是 KCP 超时淘汰机制,极其具有实用性和工程性。

KCP 滑动窗口

KCP 超时淘汰机制是从属于 KCP 滑动窗口的管理机制的,其结构图示如下

c 复制代码
发送方 (Sender)                                                  接收方 (Receiver)
+---------------------------------------------------+         +---------------------------------------------------+
|                **发送缓冲区 (Send Buffer)**        |         |                **接收缓冲区 (Recv Buffer)**        |
| +-----------------------------------------------+ |         | +-----------------------------------------------+ |
| | 已发送 **已确认** (Delivered & Acked)          | |         | | 已接收 **已交付应用** (Delivered to App)        | |
| | [SND.UNA 之前的数据]                           | |         | | [RCV.NXT 之前的数据]                           | |
| |                                               | |         | |                                               | |
| +-----------------------------------------------+ |         | +-----------------------------------------------+ |
| | 已发送 **未确认** (In Flight / Outstanding)     | |  ---->  | | 已接收 **待交付** (Received, Not Delivered)   | |
| | [**发送窗口 (Sending Window)** 的核心区域]      | | 数据包   | | [**接收窗口 (Receiving Window)** 可用空间前部]| |
| | * SND.UNA (发送未确认起点)                      | |  ---->  | | * RCV.NXT (期望接收起点)                      | |
| | * SND.NXT (下一个发送位置) -->                  | |         | |                                               | |
| |                                                | |         | |                                               | |
| +-----------------------------------------------+ |         | +-----------------------------------------------+ |
| | **允许发送但尚未发送** (Sendable)                | |         | | **可用接收空间** (Available Space)            | |
| | [发送窗口内,SND.NXT 之后的部分]                 | |         | | [**接收窗口 (Receiving Window)** 核心]        | |
| | * 发送窗口右边界 = SND.UNA + Win                | |         | | * 窗口大小 = RCV.WND                         | |
| |   Win = min(拥塞窗口 cwnd, 接收方通告窗口 rwnd)  | |         | | * 右边界 = RCV.NXT + RCV.WND                 | |
| +-----------------------------------------------+ |         | +-----------------------------------------------+ |
| | **不允许发送** (Not Sendable)                  | |         | | **不可用空间** (Not Available)               | |
| | [超出当前发送窗口]                              | |         | | [超出当前接收窗口]                           | |
| +-----------------------------------------------+ |         | +-----------------------------------------------+ |
+---------------------------------------------------+         +---------------------------------------------------+

1、发送窗口 (SND_WND) 的约束:

  • KCP 发送端维护一个发送窗口,它定义了当前允许发送的最大数据范围(从 snd_una 到 snd_una + snd_wnd)。
  • snd_una 指向最早一个尚未被确认(Acknowledged)的数据包的序列号。它是窗口的左边界。
  • 只有序列号落在 [snd_una, snd_una + snd_wnd) 范围内的数据包才允许被发送(包括首次发送和重传)。

2、窗口的推进 (Advancing):

  • 当接收方 ACK 了 snd_una 指向的包(以及按序到达的后续包)时,snd_una 就会向右移动(增大),发送窗口也随之整体向右滑动。
  • 新"暴露"出来的窗口空间允许发送新的数据包。

3、"丢包太久不重发"的本质:窗口淘汰

  • 关键决策点: 为了不让整个传输流程被这些顽固的旧丢包卡死(阻塞新数据的发送),KCP 会执行一个策略:强制将 snd_una 推进到接收方已 ACK 的最小连续序列号之后的位置。

KCP 的超时淘汰机制

我接下来通过图示简单介绍一下超时淘汰机制。

图 1:初始状态(正常传输)

c 复制代码
发送窗口范围: [10, 11, 12, 13, 14, 15]  (snd_una=10, snd_wnd=6)
已发送未确认: █  █  █  █  █  █ 
          	10  11 12 13 14 15  <-- 序列号
状态:所有包已发送,等待 ACK。

图 2:发生丢包(包 10,11,12 丢失)------选择性重传

c 复制代码
发送窗口范围: [10, 11, 12, 13, 14, 15]  
已发送未确认: ✖  ✖   ✖  █   █   █ 
            10 11  12 13  14  15
状态:
  - 包 13,14,15 被接收方收到,但无法 ACK(因包 10 未到,不连续)
  - 包 10,11,12 超时重传多次(🔥)仍失败。

图 3:触发淘汰机制(强制推进窗口)

c 复制代码
接收方反馈:ACK 13,14,15(非连续确认)
KCP 决策:放弃包 10,11,12(重传失败)!

新窗口范围: [16, 17, 18, 19, 20, 21]  (snd_una=16, snd_wnd=6)
已发送未确认: █  █  █  █  █  █ 
            16 17 18 19  20 21
淘汰影响:
  - 包 10,11,12 被移出窗口 ❌(不再重传)
  - 新数据包 16-21 立即发送 ✅

图 4:新丢包处理(包 17 丢失)

c 复制代码
窗口范围: [16, 17, 18, 19, 20, 21]  
已发送未确认: █  ✖  █  █  █  █ 
            16 17 18 19  20 21
状态:
  - 包 17 丢失 → 触发 **快速重传**(仍在窗口内)
  - 包 18,19,20,21 正常传输。

关键决策点: 为了不让整个传输流程被这些顽固的旧丢包卡死(阻塞新数据的发送),KCP 会执行一个策略:强制将 snd_una 推进到接收方已 ACK 的最小连续序列号之后的位置。在本案例中:

1、当前 snd_una = 10。

2、包 10, 11, 12 丢失且重传多次失败。

3、接收方收到了包 13, 14, 15 并 ACK 了它们(但无法 ACK 10, 11, 12)。

3、KCP 发现 13, 14, 15 被 ACK 了,但 10 这个关键点一直没 ACK。

4、KCP 判定 10, 11, 12 重传无望,决定将 snd_una 直接推进到 16(假设 15 是当前收到的最大连续包)。

后果:

5、序列号 10, 11, 12 的包被移出发送窗口 ([snd_una=16, snd_una+snd_wnd)) 的范围。

6、这些包不再有资格被重传,因为 KCP 的重传队列只维护窗口内的包。

7、发送窗口腾出了大量空间,新的数据包(序列号 >=16)可以立即被发送,传输得以继续。

与 TCP 的核心差异

机制 KCP TCP
窗口淘汰 ✅ 主动放弃超时旧包,推进窗口 ❌ 坚持重传直到成功或断开
阻塞避免 ⚡ 新数据不受旧丢包影响 🐢 旧丢包阻塞整个窗口
适用场景 实时应用(容忍部分丢包 ) 高可靠性传输(如文件下载)

KCP 应对网络拥塞的方案------非退让流量控制

让我们用一个"高速公路应急车道管理"的比喻,来形象地理解 KCP 的非退让流量控制的必要性,并将其与传统 TCP 的退让式(AIMD) 控制进行对比。

场景设定

  • 道路: 一条繁忙的高速公路,有 常规车道 和 应急车道。
  • 车辆: 代表需要传输的数据包。
  • 交通状况: 代表网络状况。偶尔有小事故(轻微丢包),也可能有大拥堵(严重拥塞)。
  • 交通管理中心 (TCP): 遵循严格的安全规则。一旦监测到任何事故(丢包),就认为可能发生大拥堵(严重拥塞),立即采取激进措施。
  • 特种物流车队 (KCP): 运输的是高时效性、高优先级的物资(如救援物资、紧急医疗用品)。它们的核心任务是尽最大可能、以最快速度、稳定地将物资送达,即使路况不完美。它们拥有特殊的通行策略。

传统 TCP (退让式/AIMD,拥塞控制配合慢启动) 的比喻

  • 策略 ("宁可错杀,不可放过"):
    • 只要高速摄像头(丢包检测)拍到任何一起小剐蹭事故(一个丢包),交通管理中心 (TCP) 就立刻、武断地认定整条高速即将瘫痪(严重拥塞)。
    • 行动:
      • 立即关闭一半常规车道! (乘性减 - 拥塞窗口减半)。所有车辆(数据包)瞬间只能使用一半的道路资源。
      • 然后极其缓慢地、一条一条地重新开放车道。 (加性增 - 拥塞窗口缓慢线性增长)。需要很长时间才能恢复到原来的通行能力。
  • 后果:
    • 交通能力瞬间腰斩: 即使事故很小且很快处理完,车流(吞吐量)也暴跌。大量无辜车辆被堵在路上。
    • 恢复极其缓慢: 重新开放车道的过程非常保守和缓慢,整体运输效率低下。
    • 对高时效物资是灾难: 特种物流车队 (重要数据) 即使没有卷入事故,也被迫降速、延误,无法完成紧急任务。
  • 必要性 (在公共道路上): 对于普通交通,这种策略是必要的,以防止自私的司机(应用)过度占用道路导致全局崩溃(拥塞崩溃)。安全第一,公平性优先。

KCP (非退让流控) 的比喻

  • 策略 ("精准处置,保持运力"):

    • 特种车队有自己的指挥中心 (KCP协议栈)。
    • 当车队收到报告,某辆特定运输车(一个数据包,如 Seq102)在某个路段发生了小事故(丢失)。
    • 行动:
      • 立即派遣一辆备用车 (快速重传) 走应急车道 (选择性重传) 去替换那辆故障车。目标:最快速度补上缺失的物资。
      • 绝不关闭主车道! (非退让 - 发送窗口 snd_wnd 不缩减)。其他所有运输车(后续数据包)继续在主车道 (常规车道) 上按计划行驶,只要目的地仓库 (接收方) 还有空间 (rcv_wnd)。
      • 可能微调车速 (调整 interval/RTO): 指挥中心可能会命令整个车队稍微降低一点车速(比如从 120km/h 降到 100km/h),给处理事故留出更多余量,避免连续出事。但这只是调速,不是封路!
      • 利用一切可用空间: 一旦目的地仓库报告有更多空位(通告更大的 rcv_wnd),指挥中心立刻允许派出更多运输车(增大 snd_wnd),填满所有可用车道。
  • 后果:

    • 运力影响最小化: 只有出事的那一辆车需要替换,其他车辆几乎不受影响。整体运输量(吞吐量)保持高位且稳定。
    • 时效性最大化: 高优先级物资总能尽快利用可用道路资源送达。平均延迟低,延迟波动小。
    • 快速恢复潜力: 一旦备用车抵达(重传成功)或路况变好,车队可以瞬间提速或加车(利用更大的 snd_wnd),恢复到甚至超过之前的运力。

必要性 (对特种车队):

  • 任务优先: 对于救援物资、紧急医疗用品(实时音视频、游戏指令、金融交易),送达的时效性和稳定性是最高优先级。一次大的延误或吞吐量暴跌可能导致任务失败(通话卡顿、游戏卡死、交易超时)。
  • 容忍小事故: 这些小事故(少量丢包)在任务背景下是可以接受的代价(音频偶尔沙沙声、画面小马赛克),只要主体物资(大部分数据包)能持续、快速、稳定地送达。
  • 专用/可控道路: 特种车队通常在有优先权、路况相对可控的路线上行驶(如专线、VPN、P2P内网),或者其流量占比不高。它们不会(或不应该)主动去挤垮公共道路(公网核心)。在这种场景下,"自私"一点,优先保障自己的关键任务,是必要且合理的。
  • 避免过度反应: 关闭一半车道(窗口减半)对于处理一个小事故是灾难性的过度反应,完全违背了特种车队的核心使命。

以下是非退让流量控制的程序执行图例(大家要结合低延迟 ACK 去理解),当出现丢包的时候,稍微降低传输速度(RTO 稍微增大,但窗口没有像 TCP 那样大幅缩减),当接收方告知缓冲区充足的时候,发送方增大发送窗口的大小。

c 复制代码
时间线 (向下流动)    发送方 (Sender)                                        接收方 (Receiver) / 网络事件
------------------  ----------------------------------------------        ------------------------------------------------
         |          **状态:**
         |          `snd_una = 100` (下一个期望ACK是100)
         |          `rcv_wnd (来自接收方) = 10` (最新通告)
         |          `snd_wnd = min(rcv_wnd, max) = 10`
         |          **发送窗口: [100, 110)**
         |          **发送: Seq100, Seq101, Seq102, Seq103, Seq104** ->      **网络正常**
         |          **发送窗口内剩余: 5 slots (105-109)**
         |          **发送: Seq105, Seq106, Seq107** ->                     **网络正常**
         |          **发送窗口内剩余: 2 slots (108-109)**
         |                                                                **Seq102 丢失! (网络拥塞开始)**
         |                                                                **接收方收到:**
         |                                                                Seq100 -> **立即发送 ACK(101)** (确认100)
         |                                                                Seq101 -> **立即发送 ACK(102)** (确认101)
         |                                                                Seq103 (乱序!) -> **立即发送 ACK(102)** (DUP-ACK, 索要102)
         |                                                                Seq104 (乱序!) -> **立即发送 ACK(102)** (DUP-ACK)
         |          <- **收到 ACK(101)** (`snd_una` 推进到101)              
         |             **发送窗口变为: [101, 111)**
         |             **发送: Seq108, Seq109** -> (用完当前窗口)            **Seq108, Seq109 可能到达或丢失**
         |          <- **收到 ACK(102)** (`snd_una` 推进到102)              
         |             **发送窗口变为: [102, 112)**
         |             **无新数据,或应用层暂无数据**
         |          <- **收到 ACK(102) (DUP-ACK 1)**                      **接收方处理Seq105, Seq106...**
         |          <- **收到 ACK(102) (DUP-ACK 2) [假设fastresend=2]**   **触发快速重传条件!**
         |          **!! 关键点 !!** 
         |          **检测到 Seq102 丢失 (通过 DUP-ACKs)**
         |          **但 `snd_wnd` 保持不变 = 10 (非退让!)**
         |          **动作: 立即重传 Seq102** ->                          **重传Seq102到达**
         |          **可能动作: 轻微增加 RTO 或 减小 `interval` (稍慢发送新数据)**
         |                                                                **接收方收到 Seq102:**
         |                                                                **立即发送 ACK(108)** (确认102-107? 或累积ACK)
         |                                                                **(同时,应用层消费数据,假设释放了空间)**
         |                                                                **接收方 `rcv_wnd` 增大 (e.g., =15)**
         |          <- **收到 ACK(108)** (`snd_una` 推进到108)             
         |             **发送窗口变为: [108, 118)** (因为 `snd_wnd` 仍为10)
         |          <- **收到新通告 `rcv_wnd = 15`**                       
         |          **!! 关键点 !!** 
         |          **更新 `snd_wnd = min(15, max) = 15`** (窗口**扩大**了!)
         |          **发送窗口变为: [108, 123)** (立刻获得更大发送潜力)
         |          **发送: Seq110, Seq111, ... (直到填满新窗口)** ->       **网络拥塞可能缓解或持续...**
         |          **... 继续 ...**

总结

KCP 的各项机制之间是有相当大的重合,但归根结底,其底层运作机理是

  1. 低延迟 ACK :有快速重传、选择性重传的效果,即可靠的 UDP 传输。
  2. 超时重传机制:第一个计时器(在滑动窗口上的设置)是对低延迟 ACK 的补充,解决尾包延迟问题。
  3. 滑动窗口:有选择性重传的功效,有序的流量控制。
  4. 非退让流量控制:这是滑动窗口上的设置。容忍小错误、瑕疵,以换取传输流畅与稳定,用户体验更好
  5. 超时淘汰机制:第二个计时器(在滑动窗口上的设置),是对严重超时的包的断舍离处理。

我还会写几篇文章去介绍 KCP 协议的 C/C++ 代码实现。毕竟如果只有课本理论,那么会很单调的。话又说回来,源代码是有 1000 多行的,加上杂七杂八的应用,代码可能直逼 2000 行。故而深入解析 KCP 协议代码是一个长期的工程,我会继续跟进,但是近期不会写,我要花更多的事件去理解这些代码,而后给大家去讲述。