A pure-Kotlin Multiplatform implementation of the KCP reliable-ARQ protocol. The wire format and protocol state machine are byte-for-byte identical to the original implementation, so a Kotlin endpoint interoperates with any other KCP implementation sharing the same conversation id.
KCP trades a little bandwidth for substantially lower latency than TCP-style ARQ, which makes it a good fit for real-time traffic (games, voice, remote input) running over UDP.
jvm, linuxX64, linuxArm64, mingwX64, macosArm64, iosArm64, iosSimulatorArm64.
The library is published with the coordinates dev.jokelbaf:kcp:1.0.0. Add it to a Kotlin
Multiplatform project:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.jokelbaf:kcp:1.0.0")
}
}
}KCP is driven by four operations: send to queue outgoing messages, input to feed received
packets, receive to read reassembled messages, and update to advance the clock. Whenever KCP has
something to transmit it calls your output callback.
import dev.jokelbaf.kcp.Kcp
// Both endpoints of a connection must use the same conversation id.
val kcp = Kcp(conv = 0x11223344u) { buffer, size ->
// Transmit the lower-level packet over your real transport.
// Only the first `size` bytes are valid; do not retain `buffer`.
socket.send(buffer, size)
}
// Optional tuning. "Fast" mode: nodelay on, 10 ms tick, fast resend after 2 dup-acks,
// congestion window disabled.
kcp.noDelay(nodelay = 1, interval = 10, resend = 2, nc = 1)
kcp.setWndSize(sndWnd = 128, rcvWnd = 128)
// Queue an application message (split into fragments automatically when needed).
kcp.send("hello, world".encodeToByteArray())
// Run this loop on a timer and whenever a datagram arrives:
fun tick(nowMillis: UInt, incoming: ByteArray?) {
incoming?.let { kcp.input(it) } // hand received datagrams to KCP
kcp.update(nowMillis) // drive retransmits / acks / flushing
while (true) { // drain fully received messages
val message = kcp.receive() ?: break
println(message.decodeToString())
}
}The current timestamp passed to [update] and [check] is a 32-bit millisecond clock
(UInt). Derive it from any monotonic source, e.g. on the JVM:
val now: UInt = System.currentTimeMillis().toUInt()Instead of calling update on a fixed timer, you can ask KCP when it next needs attention:
val next: UInt = kcp.check(now) // timestamp of the next required update
// sleep until `next`, then call kcp.update(...) again| Member | Description |
|---|---|
send(data, offset, length) |
Queue an application message for reliable delivery. |
receive() / recv(buffer, …) |
Read the next complete message (allocating, or into your buffer). |
input(data, offset, size) |
Feed a received lower-level packet into KCP. |
update(current) |
Advance the clock and flush due acks/data/probes. |
check(current) |
Timestamp at which update should next be called. |
flush() |
Force an immediate flush. |
peekSize() |
Size of the next complete message, or -1. |
waitSnd() |
Number of packets queued plus in flight. |
noDelay(nodelay, interval, resend, nc) |
Latency/retransmit tuning. |
setWndSize(sndWnd, rcvWnd) |
Send/receive window sizes in packets. |
setMtu(mtu) |
Change the MTU (default 1400). |
setCongestionControl(ops) |
Install a custom algorithm, or null for the built-in one. |
Set kcp.stream = true on both peers to treat the connection as an unframed byte stream (message
boundaries are not preserved), instead of the default datagram mode.
./gradlew build # build all targets
./gradlew jvmTest # run the test suite on the JVM
./gradlew linuxX64Test # run the test suite on Kotlin/NativeThe test suite covers codec round-trips, fragmentation, stream mode, flow control, pluggable congestion control and a lossy-network simulation reproducing the reference project's three modes.