KCP (Fast and Reliable UDP protocol) 是一个由 skywind3000 (吴云) 在 2014 年开源的快速可靠的 UDP 上层协议。它的设计目标是在网络状况不佳(高延迟、高丢包率)的环境下,提供比 TCP 更快的传输速度和更低的延迟,同时保持数据的可靠性。KCP 并不是一个完整的网络协议栈,而是一个可嵌入式的库,它运行在 UDP 协议之上,提供了 TCP 所具备的可靠性、流量控制和拥塞控制等机制,但针对延迟和重传进行了优化。

核心思想:在保障数据可靠性的前提下,通过优化重传机制、激进发送和控制重传间隔等方法,尽可能地减少传输延迟,以适应游戏、实时音视频等对延迟高度敏感的应用。


一、为什么需要 KCP?

TCP 协议是互联网上最常用的可靠传输协议,但它在一些场景下存在明显的局限性:

  1. 慢启动 (Slow Start):TCP 为了避免网络拥塞,在连接建立初期会限制发送速率,逐渐增加。这对于短连接或突发数据传输会增加初始延迟。
  2. 队头阻塞 (Head-of-Line Blocking, HOLB):TCP 的报文是严格按序到达的。如果某个数据包丢失,后续所有已到达但序号更大的数据包必须在缓冲区中等待该丢失包重传成功并按序递交,从而造成延迟。
  3. 重传策略:TCP 的重传通常是基于定时器或三次重复 ACK。当数据包丢失时,需要等待较长时间才能触发重传,这在延迟敏感的应用中是不可接受的。
  4. 固定拥塞控制:TCP 的拥塞控制算法(如 Reno、Cubic)是为了普适场景设计的,对于某些特定应用(如游戏)可能过于保守,或者响应不够迅速。

UDP 虽然提供了极低的延迟和高度的灵活性,但它是不可靠的,不保证数据传输的顺序、完整性和不重复。

KCP 的出现就是为了结合 UDP 的低延迟优势,并克服 TCP 在特定场景下的不足,在一个可靠数据传输的基础上,提供:

  • 更低的首次发送延迟:通过不依赖队列长度、直接发送等手段减少延迟。
  • 更快的重传响应:通过更激进的 ACK 机制和更短的重传间隔实现。
  • 在丢包率高的情况下性能更好:通过选择性重传等方式,减少队头阻塞。

因此,KCP 主要应用于:

  • 实时在线游戏:对延迟要求极高,偶发丢包总比卡顿或长时间延迟好。
  • 实时音视频通话:需要稳定的低延迟传输。
  • 边缘计算、物联网数据传输:在网络质量不稳定的环境下需要可靠高效传输。

二、KCP 与 TCP 的关键差异与优化

KCP 借鉴了 TCP 的一些机制,但对其进行了激进的优化,以达到低延迟的目标:

2.1 队头阻塞优化

  • TCP:严格的字节流模型,所有数据必须完全按序到达才能递交上层应用。一个丢包会导致所有后续包的阻塞。
  • KCP:基于报文模型,每个报文都有自己的序号。KCP 仍然保证数据按序递交上层应用,但其重传机制允许接收方缓存乱序到达的报文,并在丢失报文到达后快速递交。

2.2 ACK 与重传机制

  • TCP
    • 累积 ACK:一个 ACK 确认它之前的所有数据包。
    • 重传定时器:基于 RTT (Round Trip Time) 动态调整重传间隔。通常较长,避免不必要的重传。
    • 三次重复 ACK:快速重传机制,避免等待定时器超时。
  • KCP
    • 可选 ACK:除了累积 ACK 之外,KCP 还有一个叫 UNA (Unacked Number) 的机制,类似于选择性 ACK (SACK)。ACK 包中会带上当前已收到的报文序号,即使中间有丢包。
    • 激进重传 (Turbo retransmission)
      • 不仅使用超时重传,还使用快速重传。当发送方收到重复的 ACK (ack_nodata) 且达到一定阈值(可通过参数配置,默认为 2 次)时,会立即重传对应的丢失报文。
      • KCP 的重传定时器远小于 TCP,可以通过参数 ikcp_nodelay(1, 10, 2, 1)(nodelay=1, interval=10ms, resend=2, nc=1)开启激进模式。interval 是内部循环的刷新间隔,resend 是快速重传阈值,nc=1 代表关闭拥塞控制。
    • 小延迟 ACK:KCP 收到报文后不会立即发送 ACK,而是延迟一小段时间(默认 30ms),尝试将多个 ACK 打包到一个 UDP 包中发送,以节省带宽。但在 ikcp_nodelay(1, x, x, 1) 模式下,这个延迟会减少或取消。

2.3 拥塞控制与流量控制

  • TCP:严格的慢启动、拥塞避免、快速重传、快速恢复机制。旨在公平地占用带宽,保护网络。
  • KCP
    • 可选拥塞控制:KCP 提供了多种拥塞控制模式,可以通过参数配置。在游戏等场景中,有时会选择关闭或弱化拥塞控制 (ikcp_nodelaync 参数)。
    • 发送窗口 (Send Window):与 TCP 类似,限制飞行中(已发送未确认)的数据量,防止发送方过载接收方。
    • 接收窗口 (Receive Window):限制接收方可接收的数据量,防止接收缓冲区溢出。

2.4 其他优化

  • 非延迟 ACK (NoDelay ACK):TCP 的 ACK 通常有一个延时(用于合并 ACK)。KCP 允许立即发送 ACK,降低延迟。
  • 流量整形 (Flow Shaping):KCP 的内部缓冲区管理允许更细粒度的流量控制。
  • 可调参数:KCP 提供了大量的参数供开发者根据应用场景和网络环境进行调优,例如窗口大小、重传间隔、重传阈值、是否开启拥塞控制等。

三、KCP 的核心数据结构与 API

KCP 作为一个库,对外提供了一组 C 语言风格的 API,其核心是 ikcp_send, ikcp_recv, ikcp_update, ikcp_input, ikcp_flush 等函数。

3.1 核心数据结构:ikcpcb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct IKCPCB {
IUINT32 conv; // 会话ID,与对端匹配
IUINT32 mtu; // 最大传输单元,默认 1400

IUINT32 state; // 连接状态
IUINT32 snd_una; // 发送数据中,最小还未确认的序号
IUINT32 snd_nxt; // 下一个发送数据序号
IUINT32 rcv_nxt; // 下一个接收数据序号 (期望接收的序号)

// 发送窗口与接收窗口
IUINT32 snd_wnd; // 发送窗口大小
IUINT32 rcv_wnd; // 接收窗口大小
IUINT32 rmt_wnd; // 远程接收窗口大小

// 重传相关
IUINT32 acklist[256]; // 待发送的 ACK 列表
IUINT32 ackcount; // acklist 中的数量
IUINT32 interval; // 内部时钟更新间隔
IUINT32 rx_srtt; // smoothed RTT
IUINT32 rx_rttval; // RTT variance
IUINT32 rx_rto; // retransmit timeout (重传超时)
IUINT32 fastresend; // 快速重传阈值
IUINT32 nodelay; // 0/1/2 模式控制延迟 (默认0)

// 内部数据队列
IKCPSEG *snd_queue; // 待发送队列
IKCPSEG *rcv_queue; // 待确认接收队列 (已收到但未递交到应用层)
IKCPSEG *snd_buf; // 已发送但未确认队列
IKCPSEG *rcv_buf; // 接收缓冲区 (乱序到达的包)

// 输出函数指针
int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user);
void *user; // 用户自定义数据指针

// 各种状态计数器、拥塞控制相关变量等
IUINT32 cwnd; // 拥塞窗口
IUINT32 probe; // 探测类型
IUINT32 current; // 当前时间
// ... 更多字段 ...
};

3.2 核心 API (Go 语言示例,其他语言类似)

KCP 库本身是 C 语言实现,但有多种语言绑定,例如 Go 语言的 github.com/xtaci/kcp

  • Dial() / Listen():建立 KCP 连接。
  • Send(data []byte):发送数据。将数据添加到 KCP 内部的发送队列。
  • Recv() ([]byte, error):接收数据。从 KCP 内部的接收队列中获取数据,如果队列为空则阻塞。
  • Update(currentMs uint32)KCP 的核心驱动函数。需要外部定时调用 (通常每隔 10ms - 100ms),执行 KCP 内部协议逻辑,如超时检测、重传、ACK 处理、窗口更新、发送数据包等。这是一个非阻塞函数。
  • Input(data []byte):将底层 UDP 接收到的数据包输入到 KCP 实例中。KCP 会解析包头,根据序号和类型进行处理。
  • SetMtu(mtu int):设置最大传输单元。
  • SetWindow(sndwnd, rcvwnd int):设置发送窗口和接收窗口大小。
  • SetNoDelay(nodelay, interval, resend, nc int):设置延迟模式、更新间隔、快速重传阈值和是否禁用拥塞控制。

基本使用流程:

  1. 初始化:创建 KCP 实例 (ikcp_create),设置会话 ID (conv),并设置一个回调 output 函数,该函数负责将 KCP 封装好的 UDP 数据包真正发送出去。
  2. 发送:调用 ikcp_send 发送数据。
  3. 接收:底层 UDP 收到数据后,调用 ikcp_input 将数据注入 KCP 实例。随后,上层应用通过 ikcp_recv 从 KCP 实例中读取数据。
  4. 驱动:在应用层启动一个定时器,每隔一定时间调用 ikcp_update 来驱动 KCP 内部状态机,确保 ACK、重传、窗口更新等机制正常工作。

四、配置调优与模式选择

KCP 的一大特点就是其高度可配置性,允许开发者根据具体应用场景进行调优。

4.1 nodelay mode

ikcp_nodelay(1, 10, 2, 1) 是 KCP 最常用的性能模式,参数含义如下:

  • nodelay = 1:启用 NoDelay 模式。
    • 0:普通模式,ikcp_update 的调用间隔会随着 RTT 变化,ACK 会延迟发送,更平稳但延迟高。
    • 1:启用非延迟模式,ACK 不延迟发送,ikcp_update 的调用间隔固定为 interval 参数。
    • 2:极速模式,比 1 更激进,ACK 优先级更高。
  • interval = 10:内部时钟刷新间隔,单位毫秒。建议 10-100ms,过小会消耗 CPU 资源,过大会增加延迟。
  • resend = 2:快速重传阈值。在接收方收到 2 个重复 ACK 后,发送方会立即重传该丢包。建议 2-5
  • nc = 1:是否禁用拥塞控制。
    • 0:启用拥塞控制。
    • 1:禁用拥塞控制。在游戏等场景,宁可多占带宽也要保证延迟时,可以禁用。但在公网环境下禁用拥塞控制可能导致网络拥塞加剧,应谨慎使用。

4.2 窗口大小

ikcp_wndsize(sndwnd, rcvwnd)

  • sndwnd:发送窗口大小。决定了最大允许在途的未确认数据包数量。越大吞吐量越高,但可能浪费带宽或需要更多内存。
  • rcvwnd:接收窗口大小。决定了接收方能缓存的最大乱序数据包数量。越大能容忍的乱序程度越高,但同样需要更多内存。

4.3 MTU (Maximum Transmission Unit)

ikcp_setmtu(mtu)

  • 通常 UDP MTU 是 1400 字节,如果 KCP 封装的包超过这个值,底层 UDP 层会进行分片,这会增加丢包率。
  • 根据实际网络环境调整 MTU,避免 IP 层分片,例如设置为 1300-1400 字节。

五、KCP 的优缺点

5.1 优点

  • 低延迟:通过激进重传、快速 ACK、可调参数等特性,在网络不佳时比 TCP 具有更低的平均延迟。
  • 高吞吐量:在丢包率较高时,由于其优化的重传机制和队头阻塞处理,可以保持更好的吞吐量。
  • 高度可配置:允许开发者根据特定应用场景(如游戏)进行深度调优。
  • 轻量级、嵌入式:作为一个库,可以方便地嵌入到各种应用中,且资源开销相对较小。
  • 消除队头阻塞:相比 TCP,乱序数据包的处理更有效,减少了等待重传包的时间。
  • 无专利:完全开源,无任何专利限制。

5.2 缺点**

  • 消耗带宽:激进重传和更频繁的 ACK 可能会在网络状况良好时导致带宽的额外消耗。
  • 可能会加剧网络拥塞 (如果禁用拥塞控制):如果 nc=1,KCP 会不顾网络拥塞地发送数据,这在公网环境下可能对其他流量不公平,甚至加剧拥塞。
  • 不保证公平性:与 TCP 相比,KCP 不是为了实现网络上的流量公平性而设计的,可能导致“抢占”带宽。
  • 额外开发成本:KCP 只是一个库,开发者需要自行实现 UDP 绑定、并发处理、底层 socket 操作、多路复用等功能。
  • 需要调优经验:KCP 的性能很大程度上依赖于合理的参数配置,这需要一定的专业知识和测试。

六、KCP 的使用和项目

KCP 已经被广泛应用于各种场景,特别是在游戏领域:

  • 游戏服务器框架:如 unity-kcp (Unity 游戏引擎)、skynet (Lua 语言游戏服务器)。
  • VPN/隧道代理:如 kcptun,利用 KCP 优化代理传输速度。
  • Go 语言库github.com/xtaci/kcp 是一个广受欢迎的 Go 语言 KCP 实现,与 Go 的协程机制结合紧密,提供了高性能的网络编程能力。
  • 多种语言绑定:C++, Java, Python, C#, Rust 等都有 KCP 的实现或绑定。

示例 (Go 语言 xtaci/kcp 库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package main

import (
"fmt"
"io"
"log"
"net"
"time"

"github.com/xtaci/kcp-go/v5"
)

const (
PORT = ":8888" // 服务端端口
CONV_ID = 123 // KCP 会话ID
)

func handleClient(conn *kcp.UDPSession) {
defer conn.Close()
fmt.Printf("客户端 %s 连接成功\n", conn.RemoteAddr())

conn.SetWindowSize(1024, 1024) // 设置KCP发送和接收窗口大小
// fastresend: 快速重传阈值, noDelay: 开启无延迟模式, interval: KCP刷新间隔, nc: 是否关闭拥塞控制
conn.SetNoDelay(1, 10, 2, 1) // 设置为激进模式: 无延迟,10ms刷新,2次ACK重传,关闭拥塞控制
conn.SetMtu(1350) // 设置MTU以避免IP层分片

for {
// 接收数据
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Printf("客户端 %s 断开连接\n", conn.RemoteAddr())
} else {
fmt.Printf("读取客户端 %s 数据失败: %v\n", conn.RemoteAddr(), err)
}
return
}

receivedMsg := string(buf[:n])
fmt.Printf("收到来自 %s 的消息: %s\n", conn.RemoteAddr(), receivedMsg)

// 回复客户端
response := fmt.Sprintf("服务器收到: %s (收到于 %s)", receivedMsg, time.Now().Format("15:04:05.000"))
_, err = conn.Write([]byte(response))
if err != nil {
fmt.Printf("回复客户端 %s 失败: %v\n", conn.RemoteAddr(), err)
return
}
}
}

func server() {
fmt.Printf("KCP 服务器启动,监听端口 %s\n", PORT)
// 使用 kcp.ListenWithOptions 监听 KCP 连接
// conv: KCP会话ID,用于识别不同的KCP连接
// dataShards, parityShards: 用于FEC (Forward Error Correction) 前向纠错,这里设为0表示不使用
lis, err := kcp.ListenWithOptions(PORT, nil, CONV_ID, 0, 0)
if err != nil {
log.Fatal(err)
}
defer lis.Close()

for {
s, err := lis.AcceptKCP()
if err != nil {
log.Printf("接受 KCP 连接失败: %v\n", err)
continue
}
go handleClient(s)
}
}

func client() {
time.Sleep(time.Second) // 等待服务器启动

fmt.Printf("KCP 客户端启动,连接服务器 %s\n", PORT)
// 使用 kcp.DialWithOptions 连接 KCP 服务器
s, err := kcp.DialWithOptions(PORT, nil, CONV_ID, 0, 0)
if err != nil {
log.Fatal(err)
}
defer s.Close()

s.SetWindowSize(1024, 1024)
s.SetNoDelay(1, 10, 2, 1)
s.SetMtu(1350)

for i := 0; i < 5; i++ {
msg := fmt.Sprintf("Hello KCP from client %d! (发送于 %s)", i+1, time.Now().Format("15:04:05.000"))
_, err := s.Write([]byte(msg))
if err != nil {
fmt.Printf("发送消息失败: %v\n", err)
return
}
fmt.Printf("客户端发送: %s\n", msg)

buf := make([]byte, 4096)
n, err := s.Read(buf)
if err != nil {
fmt.Printf("读取服务器回复失败: %v\n", err)
return
}
fmt.Printf("客户端收到回复: %s\n", string(buf[:n]))
time.Sleep(2 * time.Second)
}
}

func main() {
go server()
client()
time.Sleep(time.Hour) // 让程序运行足够长时间
}

此 Go 语言示例展示了如何使用 xtaci/kcp-go 库创建一个简单的 KCP 服务器和客户端。服务器监听 KCP 连接,接受客户端消息并回复;客户端连接服务器,发送消息并接收回复。其中包含了 SetWindowSizeSetNoDelaySetMtu 等关键 KCP 参数的配置,演示了如何开启激进模式。

七、总结

KCP 协议通过在 UDP 基础上实现一套激进的可靠传输机制,成功在一个可靠数据传输的前提下,达到了比 TCP 更低的延迟和在网络条件不佳时更好的性能表现。它并非 TCP 的替代品,而是针对特定应用场景(如游戏、实时音视频)的高性能补充。对于延迟敏感型应用,KCP 提供了一个强大的工具,但它的使用需要开发者对网络环境和 KCP 参数有深刻理解,并进行细致的调优。在选择网络传输协议时,应根据实际业务需求权衡 KCP 的优缺点。