kcp

English | 简体中文

KCP(A Fast and Reliable ARQ Protocol)的 Ruby 绑定, 以原生 C 扩展形式提供。KCP 是一个用 C 实现的快速可靠传输协议(ARQ), 相比 TCP 在丢包/弱网场景下有更低的延迟和更好的实时性。

特性

  • 原生 C 扩展:直接编译 skywind3000/kcp 的 C 源码,性能与 C 一致
  • 跨平台:通过 mkmf + Ruby 工具链构建,支持 x86_64 / arm64macOS / Linux / Windows
  • 字节流模式stream = 1):数据按有序字节流交付,无消息边界
  • 拉取式接口:引擎不碰 socket,你通过 output/input 自己收发 UDP 数据报
  • 零运行时依赖(除 Ruby >= 3.0)

安装

# Gemfile
gem 'kcp'
gem install kcp

构建时会自动编译 ext/kcp 下的 C 源码(ikcp.c + kcp_native.c), 由 Ruby 的 mkmf 负责选择对应平台的编译器与架构(Windows 用 MSVC/MinGW,macOS/Linux 用 clang/gcc,arm/x64 自动匹配)。

快速开始

KCP 引擎只管「把字节流可靠送达对端」,不处理 socket、地址、加密。 两端各创建一个引擎,通过 UDP 数据报互相喂包即可:

require 'kcp'

CONV = 0x1122_3344  # 会话标识,两端必须一致

# ---- 发送端 ----
engine = Kcp::Engine.new(CONV)

engine.send("hello kcp")       # 数据进入发送队列
engine.flush                   # 立即刷出

# 从引擎取出编码后的 KCP 包,交给 UDP sendto
while (pkt = engine.output)
  udp_socket.send(pkt, 0, peer_host, peer_port)
end

# ---- 接收端(收到 UDP 数据报后)----
engine.input(recv_datagram)    # 喂入收到的包

# 从引擎取出按序送达的字节流
data = engine.recv             # => "hello kcp"(没有数据时返回 nil)

# 周期性驱动(重传 / 窗口更新 / 心跳)
engine.update(Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000)

接口说明

Kcp::Engine.new(conv)

创建引擎。conv 是 32 位会话标识(uint32),同一条连接的两端必须使用相同的 conv, 用于区分不同的会话。多个并发连接时应为每个连接分配不同的 conv

engine = Kcp::Engine.new(0x1234)

send(data)

把数据排入发送队列,返回排入的字节数,出错返回负数。数据不会立即发出, 需要调用 flush(立即)或 update(按间隔)才会生成编码包。

engine.send("some bytes")

flush

立即把发送队列里的数据刷成编码包(不受 update 的间隔节流)。刷完后用 output 取出。

engine.send(data)
engine.flush
pkt = engine.output

setmtu(mtu)

设置单个 KCP 包的最大尺寸(mtu),默认 1300。当上层把每个 KCP 包再套一层头作为单个 UDP 数据报发送时,应把它设为「链路 MTU - 上层头开销」,否则整包会超过 MTU 触发 IP 分片 (移动网络/VPN 下分片极易被丢弃)。

engine.setmtu(1200)   # 让 KCP 包 + 上层头 <= 链路 MTU

mss

返回当前分段载荷大小(= mtu - 24 字节 KCP 头)。单次 send 最多接受 IKCP_WND_RCV(128) 个分段,超出会返回 -2 丢弃;上层按 mss * 127 切块喂入可避免大块写被丢。

engine.mss   # => 1176(setmtu(1200) 之后)

output

取出待发送的编码包,每次一个(不超过 mtu),直接交给 UDP send。没有待发送数据时 返回 nil取出的字节必须原样发给对端,对端收到后原样 input

while (pkt = engine.output)
  socket.send(pkt, 0, host, port)
end

input(data)

喂入一个收到的编码包(低层 UDP 数据报的内容)。KCP 会解析、处理 ACK/重传/数据, 随后可用 recv 取数据、用 output 取要回发的包(例如 ACK)。

socket.recvfrom(65_536) do |data, addr|
  engine.input(data)
end

recv(maxlen = 65536)

取出对端已送达的、按序的字节流(最多 maxlen 字节)。没有数据时返回 nil

if (chunk = engine.recv)
  # 处理收到的数据
end

update(now_ms)

驱动 KCP 时钟:触发按间隔的重传、ACK、窗口探测等。now_ms单调毫秒时间戳 (建议用 Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000)。

应在主循环里周期性调用,例如每 10~100ms 一次:

loop do
  engine.update((Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i)
  sleep 0.01
end

close

释放对 C 对象的引用(底层 C 内存由 Ruby GC 通过 TypedData_Wrap_Struct 自动回收)。

engine.close

关于 updatenow_ms

KCP 内部没有计时器,不会自己读系统时间。你必须周期性地喂给它当前时间(now_ms), 它才能驱动所有和时间相关的逻辑:

  • 超时重传:每个已发出的段带一个 resendts(下次重传时刻),current 推进到它之后才会重传
  • RTT 测量:用 current - 段.ts 算往返时间,进而算出 RTO
  • 窗口探测:对端窗口为 0 时,定时发送 WINS 探测
  • 死链检测:超时无响应则判死

now_ms 必须是单调毫秒时间戳Process.clock_gettime(Process::CLOCK_MONOTONIC)), 因为单调时钟只前进、不受系统时间跳变(NTP 校时、手动改时间)干扰。它是 32 位毫秒,约 49 天回绕一次, KCP 内部用有符号差值处理回绕,属正常现象。

update 会怎样

  1. ikcp_flush 第一行是 if (kcp->updated == 0) return; —— 而 updated 只有 update 才置位, 所以不 update 时,连包都发不出去(本 gem 的 flush 内部会先兜底调一次 update,故单独 send + flush 仍可用)。
  2. 即便绕过第 1 点,重传依赖 current 推进:丢了一个包后若不 update,resendts 永远到不了, 这一个丢包就会永久卡死;ACK 也不会按间隔刷出,对端窗口不前进,双向一起僵住。

结论:update 就是 KCP 的心跳send + flush 是「立即发」(低延迟),update 是「周期性驱动」 (重传 / ACK / 窗口 / 保活)。每 10~100ms 调一次;缺了它,只要发生一次丢包连接就死。

完整示例(UDP 回显)

仓库内 examples/ 下有可直接运行的服务端和客户端:

ruby -I lib examples/server.rb 9000              # 终端 1:起服务端
ruby -I lib examples/client.rb 127.0.0.1 9000    # 终端 2:连上后输入即回显,/quit 退出

下面是对应的完整代码:

require 'socket'
require 'kcp'

CONV = 0x1234

def now_ms
  (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
end

# ---- 服务端 ----
server = UDPSocket.new
server.bind('0.0.0.0', 9000)
engine = Kcp::Engine.new(CONV)

loop do
  data, addr = server.recvfrom(65_536)
  engine.input(data)

  while (chunk = engine.recv)
    engine.send(chunk)  # 回显
  end
  engine.flush
  while (pkt = engine.output)
    server.send(pkt, 0, addr[3], addr[1])
  end
  engine.update(now_ms)
end

注意事项

  1. KCP 只管可靠传输,网络 I/O、对端地址管理、加密、会话保活都由上层负责 (本仓库的 mnet gem 在其上实现了「移动 IP 不中断」的漫游传输层)。
  2. conv 两端必须一致;并发连接请用不同 conv(或在上层协议里用会话 ID 复用)。
  3. flush 是立即刷出(低延迟);update 是按间隔节流(省 CPU)。高频交互场景两者配合使用。
  4. KCP 使用 32 位毫秒时间戳,约 49 天回绕一次,属正常现象(协议内部处理了回绕)。
  5. 默认 mtu 为 1300;上层再包一层头时请用 setmtu 把 KCP 包缩小到链路 MTU 以内,避免整包 超过 MTU 被 IP 分片。

许可

MIT。KCP 源码版权归 skywind3000。