kcp
English | 简体中文
KCP(A Fast and Reliable ARQ Protocol)的 Ruby 绑定, 以原生 C 扩展形式提供。KCP 是一个用 C 实现的快速可靠传输协议(ARQ), 相比 TCP 在丢包/弱网场景下有更低的延迟和更好的实时性。
特性
- 原生 C 扩展:直接编译 skywind3000/kcp 的 C 源码,性能与 C 一致
- 跨平台:通过
mkmf+ Ruby 工具链构建,支持 x86_64 / arm64,macOS / 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
关于 update 与 now_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 会怎样
ikcp_flush第一行是if (kcp->updated == 0) return;—— 而updated只有update才置位, 所以不 update 时,连包都发不出去(本 gem 的flush内部会先兜底调一次 update,故单独send + flush仍可用)。- 即便绕过第 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
注意事项
- KCP 只管可靠传输,网络 I/O、对端地址管理、加密、会话保活都由上层负责
(本仓库的
mnetgem 在其上实现了「移动 IP 不中断」的漫游传输层)。 conv两端必须一致;并发连接请用不同conv(或在上层协议里用会话 ID 复用)。flush是立即刷出(低延迟);update是按间隔节流(省 CPU)。高频交互场景两者配合使用。- KCP 使用 32 位毫秒时间戳,约 49 天回绕一次,属正常现象(协议内部处理了回绕)。
- 默认
mtu为 1300;上层再包一层头时请用setmtu把 KCP 包缩小到链路 MTU 以内,避免整包 超过 MTU 被 IP 分片。
许可
MIT。KCP 源码版权归 skywind3000。