CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-ktor--ktor-client-websockets

Ktor client WebSocket plugin - provides WebSocket support for the Ktor HTTP client on multiple platforms including iOS x64

Pending
Overview
Eval results
Files

raw-websocket-operations.mddocs/

Raw WebSocket Operations (CIO Engine)

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.

Raw Connection Functions

webSocketRawSession() - Create Raw Session

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 = {}
): ClientWebSocketSession

Parameters:

  • method: HTTP method for WebSocket handshake (typically GET)
  • host: Target host for WebSocket connection
  • port: Target port (optional, defaults based on protocol)
  • path: WebSocket endpoint path
  • block: HTTP request configuration block

Returns:

  • ClientWebSocketSession without automatic ping-pong handling

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()
}

webSocketRaw() - Raw Session with Block

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
            }
        }
    }
}

wsRaw() - Short Alias for Raw WebSocket

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")
}

wssRaw() - Secure Raw WebSocket

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 Characteristics

No Automatic Ping-Pong

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 */ }
        }
    }
}

Direct Frame Control

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")))
}

Performance Benefits

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")
}

Raw WebSocket Use Cases

Custom Heartbeat Implementation

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()
    }
}

Protocol Implementation

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())
}

Low-Level Debugging

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")))
}

Engine Requirements

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 available

Engine compatibility:

  • CIO: Full raw WebSocket support
  • JavaScript: No raw operations available
  • Native engines: No raw operations available
  • Java: No raw operations available (use CIO instead)

Error Handling in Raw Mode

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

docs

content-serialization.md

index.md

plugin-configuration.md

raw-websocket-operations.md

session-operations.md

websocket-connections.md

tile.json