Ktor client WebSocket plugin - provides WebSocket support for the Ktor HTTP client on multiple platforms including iOS x64
—
Ktor Client WebSockets provides raw WebSocket operations for the CIO engine that bypass automatic ping-pong processing and provide direct, low-level frame access.
Note: Raw WebSocket operations are only available with the CIO client engine and provide no automatic ping-pong or service message handling.
Creates a raw WebSocket session without automatic ping-pong processing:
suspend fun HttpClient.webSocketRawSession(
method: HttpMethod = HttpMethod.Get,
host: String? = null,
port: Int? = null,
path: String? = null,
block: HttpRequestBuilder.() -> Unit = {}
): ClientWebSocketSessionParameters:
Returns:
Usage:
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.websocket.*
import io.ktor.client.plugins.websocket.cio.*
val client = HttpClient(CIO) {
install(WebSockets)
}
val rawSession = client.webSocketRawSession(
host = "echo.websocket.org",
port = 80,
path = "/"
)
try {
// Manual ping-pong control
rawSession.send(Frame.Ping("manual-ping".toByteArray()))
// Direct frame access
val frame = rawSession.incoming.receive()
println("Raw frame: $frame")
} finally {
rawSession.close()
}Establishes a raw WebSocket connection and executes a block with the session:
suspend fun HttpClient.webSocketRaw(
method: HttpMethod = HttpMethod.Get,
host: String? = null,
port: Int? = null,
path: String? = null,
request: HttpRequestBuilder.() -> Unit = {},
block: suspend ClientWebSocketSession.() -> Unit
)Usage:
client.webSocketRaw(
host = "echo.websocket.org",
port = 80,
path = "/"
) {
// No automatic ping-pong - full control
send(Frame.Text("Raw WebSocket message"))
// Handle all frames manually
for (frame in incoming) {
when (frame) {
is Frame.Text -> println("Text: ${frame.readText()}")
is Frame.Binary -> println("Binary: ${frame.data.size} bytes")
is Frame.Ping -> {
println("Ping received - sending pong manually")
send(Frame.Pong(frame.data))
}
is Frame.Pong -> println("Pong received")
is Frame.Close -> {
println("Close: ${frame.readReason()}")
break
}
}
}
}Convenience alias for webSocketRaw():
suspend fun HttpClient.wsRaw(
method: HttpMethod = HttpMethod.Get,
host: String? = null,
port: Int? = null,
path: String? = null,
request: HttpRequestBuilder.() -> Unit = {},
block: suspend ClientWebSocketSession.() -> Unit
)Usage:
client.wsRaw("ws://echo.websocket.org/") {
send("Raw message via short alias")
val response = incoming.receive()
println("Raw response: $response")
}Establishes secure raw WebSocket connections over TLS/SSL:
suspend fun HttpClient.wssRaw(
method: HttpMethod = HttpMethod.Get,
host: String? = null,
port: Int? = null,
path: String? = null,
request: HttpRequestBuilder.() -> Unit = {},
block: suspend ClientWebSocketSession.() -> Unit
)Usage:
client.wssRaw(
host = "secure.websocket.example.com",
port = 443,
path = "/raw-endpoint"
) {
send("Secure raw WebSocket message")
// Manual ping-pong on secure connection
send(Frame.Ping("secure-ping".toByteArray()))
for (frame in incoming) {
when (frame) {
is Frame.Pong -> println("Secure pong received")
is Frame.Text -> println("Secure text: ${frame.readText()}")
is Frame.Close -> break
else -> println("Other frame: $frame")
}
}
}Raw WebSocket sessions do not send automatic ping frames or respond to ping frames:
client.webSocketRaw("ws://example.com/") {
// Must handle ping manually
launch {
while (isActive) {
send(Frame.Ping("manual-heartbeat".toByteArray()))
delay(30_000) // 30 second interval
}
}
// Must respond to pings manually
for (frame in incoming) {
when (frame) {
is Frame.Ping -> {
// Manual pong response required
send(Frame.Pong(frame.data))
}
is Frame.Text -> {
processMessage(frame.readText())
}
is Frame.Close -> break
else -> { /* Handle other frames */ }
}
}
}Full control over all WebSocket frame types:
client.webSocketRaw("ws://example.com/raw") {
// Send custom ping with payload
send(Frame.Ping("custom-ping-data".toByteArray()))
// Send fragmented message
send(Frame.Text(fin = false, data = "Part 1 ".toByteArray()))
send(Frame.Text(fin = false, data = "Part 2 ".toByteArray()))
send(Frame.Text(fin = true, data = "Part 3".toByteArray()))
// Send binary frame with specific flags
val binaryData = byteArrayOf(1, 2, 3, 4, 5)
send(Frame.Binary(fin = true, data = binaryData))
// Custom close with specific code
send(Frame.Close(CloseReason(CloseReason.Codes.NORMAL, "Custom close")))
}Raw WebSocket operations can provide performance benefits for specific use cases:
client.webSocketRaw("ws://high-frequency.example.com/") {
// High-frequency trading or gaming scenarios
// No overhead from automatic ping-pong
val startTime = System.currentTimeMillis()
repeat(1000) { i ->
send(Frame.Text("Message $i"))
val response = incoming.receive()
// Process response immediately
}
val elapsed = System.currentTimeMillis() - startTime
println("1000 messages in ${elapsed}ms")
}Implement custom heartbeat logic:
client.webSocketRaw("ws://example.com/custom-heartbeat") {
val heartbeatJob = launch {
var pingCounter = 0
while (isActive) {
val pingData = "ping-${++pingCounter}".toByteArray()
send(Frame.Ping(pingData))
// Wait for pong with timeout
withTimeoutOrNull(5000) {
while (true) {
val frame = incoming.receive()
if (frame is Frame.Pong && frame.data.contentEquals(pingData)) {
break // Pong received
}
// Handle other frames while waiting for pong
handleFrame(frame)
}
} ?: run {
println("Ping timeout - connection may be dead")
close(CloseReason(CloseReason.Codes.GOING_AWAY, "Ping timeout"))
return@launch
}
delay(10_000) // 10 second heartbeat
}
}
try {
// Main message processing
for (frame in incoming) {
when (frame) {
is Frame.Text -> processMessage(frame.readText())
is Frame.Close -> break
else -> { /* Handle in heartbeat logic */ }
}
}
} finally {
heartbeatJob.cancel()
}
}Implement custom WebSocket-based protocols:
// Custom protocol with raw frames
client.webSocketRaw("ws://protocol.example.com/v1") {
// Send protocol version negotiation
send(Frame.Binary(true, byteArrayOf(0x01, 0x00))) // Version 1.0
// Wait for version response
val versionFrame = incoming.receive()
if (versionFrame is Frame.Binary && versionFrame.data[0] == 0x01.toByte()) {
println("Protocol v1.0 negotiated")
}
// Custom frame format: [type:1][length:4][payload:length]
fun sendCustomFrame(type: Byte, payload: ByteArray) {
val frame = ByteArray(5 + payload.size)
frame[0] = type
// Write length (big-endian)
frame[1] = (payload.size shr 24).toByte()
frame[2] = (payload.size shr 16).toByte()
frame[3] = (payload.size shr 8).toByte()
frame[4] = payload.size.toByte()
// Copy payload
payload.copyInto(frame, 5)
send(Frame.Binary(true, frame))
}
// Send custom message
sendCustomFrame(0x10, "Hello Custom Protocol".toByteArray())
}Debug WebSocket protocol issues:
client.webSocketRaw("ws://debug.example.com/") {
// Log all frames
launch {
for (frame in incoming) {
when (frame) {
is Frame.Text -> {
println("RX Text: ${frame.readText()}")
println("RX Text fin=${frame.fin}, len=${frame.data.size}")
}
is Frame.Binary -> {
println("RX Binary: fin=${frame.fin}, len=${frame.data.size}")
println("RX Binary data: ${frame.data.joinToString(" ") { "%02x".format(it) }}")
}
is Frame.Ping -> {
println("RX Ping: ${frame.data.joinToString(" ") { "%02x".format(it) }}")
// Send pong manually for debugging
send(Frame.Pong(frame.data))
println("TX Pong: ${frame.data.joinToString(" ") { "%02x".format(it) }}")
}
is Frame.Pong -> {
println("RX Pong: ${frame.data.joinToString(" ") { "%02x".format(it) }}")
}
is Frame.Close -> {
val reason = frame.readReason()
println("RX Close: code=${reason?.code}, message='${reason?.message}'")
break
}
}
}
}
// Send test frames
println("TX Text: Hello")
send(Frame.Text("Hello"))
println("TX Binary: [1,2,3,4]")
send(Frame.Binary(true, byteArrayOf(1, 2, 3, 4)))
println("TX Ping: ping-data")
send(Frame.Ping("ping-data".toByteArray()))
delay(5000) // Let frames exchange
println("TX Close: Normal")
send(Frame.Close(CloseReason(CloseReason.Codes.NORMAL, "Debug complete")))
}Raw WebSocket operations are CIO engine specific:
// Correct - CIO engine supports raw operations
val cioClient = HttpClient(CIO) {
install(WebSockets)
}
cioClient.webSocketRaw("ws://example.com/") { /* Works */ }
// Incorrect - Other engines don't support raw operations
val jsClient = HttpClient(Js) {
install(WebSockets)
}
// jsClient.webSocketRaw() // Compilation error - function not availableEngine compatibility:
Handle errors without automatic ping-pong safety net:
client.webSocketRaw("ws://unreliable.example.com/") {
try {
// Manual connection health monitoring
var lastFrameTime = System.currentTimeMillis()
withTimeout(60_000) { // 60 second total timeout
for (frame in incoming) {
lastFrameTime = System.currentTimeMillis()
when (frame) {
is Frame.Text -> processMessage(frame.readText())
is Frame.Ping -> send(Frame.Pong(frame.data))
is Frame.Close -> {
val reason = frame.readReason()
println("Server closed: ${reason?.code} - ${reason?.message}")
break
}
else -> { /* Handle other frames */ }
}
// Check for connection timeout
if (System.currentTimeMillis() - lastFrameTime > 30_000) {
println("No frames received for 30 seconds")
close(CloseReason(CloseReason.Codes.GOING_AWAY, "Timeout"))
break
}
}
}
} catch (e: TimeoutCancellationException) {
println("Raw WebSocket operation timed out")
close(CloseReason(CloseReason.Codes.GOING_AWAY, "Client timeout"))
} catch (e: Exception) {
println("Raw WebSocket error: ${e.message}")
close(CloseReason(CloseReason.Codes.INTERNAL_ERROR, "Client error"))
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-ktor--ktor-client-websockets