websocket
1.1.1indexedRFC 6455-compliant WebSocket client with permessage-deflate compression, suspend-friendly I/O and Flow-based messages, zero-copy frame pipeline, SIMD-optimized masking, and Autobahn compliance.
RFC 6455-compliant WebSocket client with permessage-deflate compression, suspend-friendly I/O and Flow-based messages, zero-copy frame pipeline, SIMD-optimized masking, and Autobahn compliance.
See the project website for documentation and APIs.
A composable, RFC 6455 WebSocket client for Kotlin Multiplatform — one API on JVM, Android, iOS, macOS, tvOS, watchOS, Linux, Node.js, and the browser, each delegating to the best native transport.
WebSocket gives you a single connect* call that returns a typed Connection<WebSocketMessage<B>>: send and receive messages as a Kotlin Flow, decode binary frames into your own types with a pluggable codec, and — because it composes on top of com.ditchoom:socket — drop WebSocket in as the universal floor of a transport fallback chain.
Pick the module for how you want to connect — all pull in buffer and (for the socket-backed modules) socket transitively.
dependencies {
// Raw-socket platforms (JVM, Android, iOS/macOS, Linux, Node.js): TCP + TLS + WebSocket in one call
implementation("com.ditchoom:websocket-tcp:<latest-version>")
// Apple-only, backed by NSURLSessionWebSocketTask (system TLS/proxy/deflate, no socket dependency)
implementation("com.ditchoom:websocket-apple:<latest-version>")
// Core engine only — bring your own pre-connected ByteStream (advanced / browser lives here too)
implementation("com.ditchoom:websocket:<latest-version>")
}
Find the latest version on Maven Central.
import com.ditchoom.websocket.WebSocketConnectionOptions
import com.ditchoom.websocket.WebSocketMessage
import com.ditchoom.websocket.use
import com.ditchoom.websocket.tcp.connectTcpWebSocket
import kotlinx.coroutines.flow.first
val options = WebSocketConnectionOptions(
name = ,
port = ,
tls = ,
websocketEndpoint = ,
)
connectTcpWebSocket(options).use { ws ->
ws.send(WebSocketMessage.Text())
reply = ws.receive().first()
println((reply WebSocketMessage.Text).payload)
}
connect* suspends until the socket is connected, the HTTP upgrade completes, and permessage-deflate is negotiated — the returned Connection is live. send/receive are not thread-safe; confine each to its own coroutine.
WebSocketMessage<B> is a sealed interface — exhaustive when, no casting on the receive path:
ws.receive().collect { message ->
when (message) {
is WebSocketMessage.Text -> println("text: ${message.payload}") // always a UTF-8 String
is WebSocketMessage.Binary -> handle(message.payload) // payload: B (your codec's type)
WebSocketMessage.Ping -> println()
WebSocketMessage.Pong -> println()
WebSocketMessage.Close -> println()
}
}
Text is always a String (RFC 6455 §5.6). Binary frames run a Codec<B> you supply, so receive() hands you your own decoded type instead of raw bytes — the generic threads through as Connection<WebSocketMessage<B>>.
// Given a Codec<SensorReading> (e.g. generated by buffer-codec):
val ws: Connection<WebSocketMessage<SensorReading>> =
connectTcpWebSocket(options, binaryCodec = SensorReadingCodec)
ws.send(WebSocketMessage.Binary(SensorReading(id = 7u, celsius = 21)))
ws.receive().collect { msg ->
if (msg is WebSocketMessage.Binary) update(msg.payload) // payload: SensorReading, already decoded
}
Built-in codecs (com.ditchoom.websocket.codecs):
Decode runs after fragment reassembly, and the wire buffer is freed before your value is emitted, so decoded values never alias library buffers. A custom codec can allocate from the same pool the wire path uses via the WsBufferFactoryKey injected into its DecodeContext.
connectWebSocket takes a ByteStream and gives you WebSocket messages. is the inverse: it a that a whose bytes ride binary WebSocket frames — so WebSocket can be the always-reachable floor of a fallback chain (QUIC → WebTransport → TCP → ) when hostile middleboxes block everything but .
import com.ditchoom.websocket.tcp.WebSocketTransport
val transport = WebSocketTransport(
websocketEndpoint = "/tunnel",
underlying = TcpTransport(), // the WS handshake rides this transport
)
// Slots into any socket API that takes a Transport (e.g. a FallbackTransport chain):
val stream: ByteStream = transport.connect(host, port, config)
stream.write(payload) // one binary WebSocket frame per write
Request permessage-deflate (RFC 7692) — negotiated during the handshake, transparent thereafter:
val options = WebSocketConnectionOptions(
name = "example.com",
requestCompression = true,
compressionOptions = CompressionOptions(
clientNoContextTakeover = false, // keep the LZ77 window across messages (better ratio)
serverNoContextTakeover = false,
clientMaxWindowBits = 15, // 0 = don't request (platform default)
),
)
connect* and send throw the sealed WebSocketException:
try {
connectTcpWebSocket(options).use { ws -> /* ... */ }
} catch (e: WebSocketException.HandshakeRejected) {
println("server rejected the upgrade: HTTP ${e.statusCode}")
} catch (e: WebSocketException.TransportFailed) {
println("network/TLS failure: ${e.cause?.message}")
} catch (e: WebSocketException.ProtocolViolation) {
println("peer broke RFC 6455: ${e.message}")
} catch (e: WebSocketException.ConnectionClosed) {
println()
}
WebSocket sits on top of socket (TCP + TLS + transport fallback) and buffer (zero-copy ReadBuffer/WriteBuffer + codecs), and composes both ways — consuming a ByteStream to speak WebSocket, or exposing one as a fallback Transport.
┌─────────────────────────────┐
│ Your app / protocol │
├─────────────────────────────┤
│ websocket (+ your Codec<B>) │ ← com.ditchoom:websocket(-tcp / -apple)
├─────────────────────────────┤
│ socket (TCP + TLS + fallback) ← com.ditchoom:socket (WebSocketTransport plugs in here)
├─────────────────────────────┤
│ buffer (+ buffer-codec) │ ← com.ditchoom:buffer
└─────────────────────────────┘
Copyright 2022 DitchOoM
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:
Unless applicable law agreed to writing, software
distributed under the License distributed an BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express implied.
See the License the specific language governing permissions
limitations under the License.
| Concern | Platform WebSocket / OkHttp / Ktor | This library |
|---|
| Multiplatform | JVM-only (OkHttp), or a lowest-common-denominator API (Ktor) | One API; each platform delegates to its native transport (NIO2, Network.framework, io_uring, browser WebSocket) |
| Binary messages | Hand off a ByteArray/ByteBuffer, parse it yourself | Supply a Codec<B> — binary frames arrive as your decoded type (WebSocketMessage.Binary<B>) |
| Composition | An opaque client you can only talk WebSocket to | WebSocketTransport is a socket Transport — use it as a QUIC→WebTransport→TCP→WebSocket fallback rung |
| Allocations | Copies through intermediate ByteArrays | Zero-copy ReadBuffer/WriteBuffer pipeline with SIMD-optimized masking |
| Compression | Varies / unavailable | permessage-deflate (RFC 7692) everywhere, with context takeover + window bits where the platform allows |
| Conformance | Varies | Passes all 517 Autobahn test cases |
| Module | Entry point | Use when |
|---|
websocket | connectWebSocket(byteStream, options) | You already have a connected ByteStream, or you're on the browser (connectBrowserWebSocket) |
websocket-tcp | connectTcpWebSocket(options) and WebSocketTransport | JVM/Android/Apple/Linux/Node.js over raw TCP+TLS; or composing WebSocket into a socket transport chain |
websocket-apple | connectAppleNativeWebSocket(options) | Apple platforms wanting the smallest binary and system-managed TLS/proxy |
| Codec | B | Behavior |
|---|
EmptyCodec | Unit | Default — discards binary payloads |
BinaryPassThroughCodec | ReadBuffer | Raw bytes copied into a consumer-owned buffer |
RejectingCodec | Nothing | Throws on any binary frame — fail loudly when binary isn't expected |
WebSocketTransportTransportByteStream:443| You have… | Call | Module |
|---|
| A host + port, on JVM/Android/Apple/Linux/Node.js | connectTcpWebSocket(options) | websocket-tcp |
| An Apple app wanting system-managed TLS/proxy | connectAppleNativeWebSocket(options) | websocket-apple |
| Browser JS | connectBrowserWebSocket(options) | websocket (jsMain) |
A pre-connected ByteStream (custom transport, QUIC, test double) | connectWebSocket(stream, options) | websocket |
| Platform | Transport | Compression |
|---|
| JVM 1.8+ / Android | NIO2 AsynchronousSocketChannel | context takeover, max_window_bits=15 |
| iOS / macOS / tvOS / watchOS | NWConnection (Network.framework) or NSURLSessionWebSocketTask | context takeover |
| Linux x64 / ARM64 | io_uring or epoll, direct zlib | context takeover + custom window bits |
| Node.js | raw socket, RFC 6455 engine | no context takeover |
| Browser | native WebSocket | handled by the browser |
Surfaced from shared tags and platforms — no rankings paid for.