0
# Raw WebSocket Operations (CIO Engine)
1
2
Ktor Client WebSockets provides raw WebSocket operations for the CIO engine that bypass automatic ping-pong processing and provide direct, low-level frame access.
3
4
**Note**: Raw WebSocket operations are only available with the CIO client engine and provide no automatic ping-pong or service message handling.
5
6
## Raw Connection Functions
7
8
### webSocketRawSession() - Create Raw Session
9
10
Creates a raw WebSocket session without automatic ping-pong processing:
11
12
```kotlin { .api }
13
suspend fun HttpClient.webSocketRawSession(
14
method: HttpMethod = HttpMethod.Get,
15
host: String? = null,
16
port: Int? = null,
17
path: String? = null,
18
block: HttpRequestBuilder.() -> Unit = {}
19
): ClientWebSocketSession
20
```
21
22
**Parameters:**
23
- **method**: HTTP method for WebSocket handshake (typically GET)
24
- **host**: Target host for WebSocket connection
25
- **port**: Target port (optional, defaults based on protocol)
26
- **path**: WebSocket endpoint path
27
- **block**: HTTP request configuration block
28
29
**Returns:**
30
- ClientWebSocketSession without automatic ping-pong handling
31
32
**Usage:**
33
```kotlin
34
import io.ktor.client.*
35
import io.ktor.client.engine.cio.*
36
import io.ktor.client.plugins.websocket.*
37
import io.ktor.client.plugins.websocket.cio.*
38
39
val client = HttpClient(CIO) {
40
install(WebSockets)
41
}
42
43
val rawSession = client.webSocketRawSession(
44
host = "echo.websocket.org",
45
port = 80,
46
path = "/"
47
)
48
49
try {
50
// Manual ping-pong control
51
rawSession.send(Frame.Ping("manual-ping".toByteArray()))
52
53
// Direct frame access
54
val frame = rawSession.incoming.receive()
55
println("Raw frame: $frame")
56
} finally {
57
rawSession.close()
58
}
59
```
60
61
### webSocketRaw() - Raw Session with Block
62
63
Establishes a raw WebSocket connection and executes a block with the session:
64
65
```kotlin { .api }
66
suspend fun HttpClient.webSocketRaw(
67
method: HttpMethod = HttpMethod.Get,
68
host: String? = null,
69
port: Int? = null,
70
path: String? = null,
71
request: HttpRequestBuilder.() -> Unit = {},
72
block: suspend ClientWebSocketSession.() -> Unit
73
)
74
```
75
76
**Usage:**
77
```kotlin
78
client.webSocketRaw(
79
host = "echo.websocket.org",
80
port = 80,
81
path = "/"
82
) {
83
// No automatic ping-pong - full control
84
send(Frame.Text("Raw WebSocket message"))
85
86
// Handle all frames manually
87
for (frame in incoming) {
88
when (frame) {
89
is Frame.Text -> println("Text: ${frame.readText()}")
90
is Frame.Binary -> println("Binary: ${frame.data.size} bytes")
91
is Frame.Ping -> {
92
println("Ping received - sending pong manually")
93
send(Frame.Pong(frame.data))
94
}
95
is Frame.Pong -> println("Pong received")
96
is Frame.Close -> {
97
println("Close: ${frame.readReason()}")
98
break
99
}
100
}
101
}
102
}
103
```
104
105
### wsRaw() - Short Alias for Raw WebSocket
106
107
Convenience alias for `webSocketRaw()`:
108
109
```kotlin { .api }
110
suspend fun HttpClient.wsRaw(
111
method: HttpMethod = HttpMethod.Get,
112
host: String? = null,
113
port: Int? = null,
114
path: String? = null,
115
request: HttpRequestBuilder.() -> Unit = {},
116
block: suspend ClientWebSocketSession.() -> Unit
117
)
118
```
119
120
**Usage:**
121
```kotlin
122
client.wsRaw("ws://echo.websocket.org/") {
123
send("Raw message via short alias")
124
val response = incoming.receive()
125
println("Raw response: $response")
126
}
127
```
128
129
### wssRaw() - Secure Raw WebSocket
130
131
Establishes secure raw WebSocket connections over TLS/SSL:
132
133
```kotlin { .api }
134
suspend fun HttpClient.wssRaw(
135
method: HttpMethod = HttpMethod.Get,
136
host: String? = null,
137
port: Int? = null,
138
path: String? = null,
139
request: HttpRequestBuilder.() -> Unit = {},
140
block: suspend ClientWebSocketSession.() -> Unit
141
)
142
```
143
144
**Usage:**
145
```kotlin
146
client.wssRaw(
147
host = "secure.websocket.example.com",
148
port = 443,
149
path = "/raw-endpoint"
150
) {
151
send("Secure raw WebSocket message")
152
153
// Manual ping-pong on secure connection
154
send(Frame.Ping("secure-ping".toByteArray()))
155
156
for (frame in incoming) {
157
when (frame) {
158
is Frame.Pong -> println("Secure pong received")
159
is Frame.Text -> println("Secure text: ${frame.readText()}")
160
is Frame.Close -> break
161
else -> println("Other frame: $frame")
162
}
163
}
164
}
165
```
166
167
## Raw WebSocket Characteristics
168
169
### No Automatic Ping-Pong
170
171
Raw WebSocket sessions do not send automatic ping frames or respond to ping frames:
172
173
```kotlin
174
client.webSocketRaw("ws://example.com/") {
175
// Must handle ping manually
176
launch {
177
while (isActive) {
178
send(Frame.Ping("manual-heartbeat".toByteArray()))
179
delay(30_000) // 30 second interval
180
}
181
}
182
183
// Must respond to pings manually
184
for (frame in incoming) {
185
when (frame) {
186
is Frame.Ping -> {
187
// Manual pong response required
188
send(Frame.Pong(frame.data))
189
}
190
is Frame.Text -> {
191
processMessage(frame.readText())
192
}
193
is Frame.Close -> break
194
else -> { /* Handle other frames */ }
195
}
196
}
197
}
198
```
199
200
### Direct Frame Control
201
202
Full control over all WebSocket frame types:
203
204
```kotlin
205
client.webSocketRaw("ws://example.com/raw") {
206
// Send custom ping with payload
207
send(Frame.Ping("custom-ping-data".toByteArray()))
208
209
// Send fragmented message
210
send(Frame.Text(fin = false, data = "Part 1 ".toByteArray()))
211
send(Frame.Text(fin = false, data = "Part 2 ".toByteArray()))
212
send(Frame.Text(fin = true, data = "Part 3".toByteArray()))
213
214
// Send binary frame with specific flags
215
val binaryData = byteArrayOf(1, 2, 3, 4, 5)
216
send(Frame.Binary(fin = true, data = binaryData))
217
218
// Custom close with specific code
219
send(Frame.Close(CloseReason(CloseReason.Codes.NORMAL, "Custom close")))
220
}
221
```
222
223
### Performance Benefits
224
225
Raw WebSocket operations can provide performance benefits for specific use cases:
226
227
```kotlin
228
client.webSocketRaw("ws://high-frequency.example.com/") {
229
// High-frequency trading or gaming scenarios
230
// No overhead from automatic ping-pong
231
val startTime = System.currentTimeMillis()
232
233
repeat(1000) { i ->
234
send(Frame.Text("Message $i"))
235
val response = incoming.receive()
236
// Process response immediately
237
}
238
239
val elapsed = System.currentTimeMillis() - startTime
240
println("1000 messages in ${elapsed}ms")
241
}
242
```
243
244
## Raw WebSocket Use Cases
245
246
### Custom Heartbeat Implementation
247
248
Implement custom heartbeat logic:
249
250
```kotlin
251
client.webSocketRaw("ws://example.com/custom-heartbeat") {
252
val heartbeatJob = launch {
253
var pingCounter = 0
254
while (isActive) {
255
val pingData = "ping-${++pingCounter}".toByteArray()
256
send(Frame.Ping(pingData))
257
258
// Wait for pong with timeout
259
withTimeoutOrNull(5000) {
260
while (true) {
261
val frame = incoming.receive()
262
if (frame is Frame.Pong && frame.data.contentEquals(pingData)) {
263
break // Pong received
264
}
265
// Handle other frames while waiting for pong
266
handleFrame(frame)
267
}
268
} ?: run {
269
println("Ping timeout - connection may be dead")
270
close(CloseReason(CloseReason.Codes.GOING_AWAY, "Ping timeout"))
271
return@launch
272
}
273
274
delay(10_000) // 10 second heartbeat
275
}
276
}
277
278
try {
279
// Main message processing
280
for (frame in incoming) {
281
when (frame) {
282
is Frame.Text -> processMessage(frame.readText())
283
is Frame.Close -> break
284
else -> { /* Handle in heartbeat logic */ }
285
}
286
}
287
} finally {
288
heartbeatJob.cancel()
289
}
290
}
291
```
292
293
### Protocol Implementation
294
295
Implement custom WebSocket-based protocols:
296
297
```kotlin
298
// Custom protocol with raw frames
299
client.webSocketRaw("ws://protocol.example.com/v1") {
300
// Send protocol version negotiation
301
send(Frame.Binary(true, byteArrayOf(0x01, 0x00))) // Version 1.0
302
303
// Wait for version response
304
val versionFrame = incoming.receive()
305
if (versionFrame is Frame.Binary && versionFrame.data[0] == 0x01.toByte()) {
306
println("Protocol v1.0 negotiated")
307
}
308
309
// Custom frame format: [type:1][length:4][payload:length]
310
fun sendCustomFrame(type: Byte, payload: ByteArray) {
311
val frame = ByteArray(5 + payload.size)
312
frame[0] = type
313
// Write length (big-endian)
314
frame[1] = (payload.size shr 24).toByte()
315
frame[2] = (payload.size shr 16).toByte()
316
frame[3] = (payload.size shr 8).toByte()
317
frame[4] = payload.size.toByte()
318
// Copy payload
319
payload.copyInto(frame, 5)
320
321
send(Frame.Binary(true, frame))
322
}
323
324
// Send custom message
325
sendCustomFrame(0x10, "Hello Custom Protocol".toByteArray())
326
}
327
```
328
329
### Low-Level Debugging
330
331
Debug WebSocket protocol issues:
332
333
```kotlin
334
client.webSocketRaw("ws://debug.example.com/") {
335
// Log all frames
336
launch {
337
for (frame in incoming) {
338
when (frame) {
339
is Frame.Text -> {
340
println("RX Text: ${frame.readText()}")
341
println("RX Text fin=${frame.fin}, len=${frame.data.size}")
342
}
343
is Frame.Binary -> {
344
println("RX Binary: fin=${frame.fin}, len=${frame.data.size}")
345
println("RX Binary data: ${frame.data.joinToString(" ") { "%02x".format(it) }}")
346
}
347
is Frame.Ping -> {
348
println("RX Ping: ${frame.data.joinToString(" ") { "%02x".format(it) }}")
349
// Send pong manually for debugging
350
send(Frame.Pong(frame.data))
351
println("TX Pong: ${frame.data.joinToString(" ") { "%02x".format(it) }}")
352
}
353
is Frame.Pong -> {
354
println("RX Pong: ${frame.data.joinToString(" ") { "%02x".format(it) }}")
355
}
356
is Frame.Close -> {
357
val reason = frame.readReason()
358
println("RX Close: code=${reason?.code}, message='${reason?.message}'")
359
break
360
}
361
}
362
}
363
}
364
365
// Send test frames
366
println("TX Text: Hello")
367
send(Frame.Text("Hello"))
368
369
println("TX Binary: [1,2,3,4]")
370
send(Frame.Binary(true, byteArrayOf(1, 2, 3, 4)))
371
372
println("TX Ping: ping-data")
373
send(Frame.Ping("ping-data".toByteArray()))
374
375
delay(5000) // Let frames exchange
376
377
println("TX Close: Normal")
378
send(Frame.Close(CloseReason(CloseReason.Codes.NORMAL, "Debug complete")))
379
}
380
```
381
382
## Engine Requirements
383
384
Raw WebSocket operations are CIO engine specific:
385
386
```kotlin
387
// Correct - CIO engine supports raw operations
388
val cioClient = HttpClient(CIO) {
389
install(WebSockets)
390
}
391
cioClient.webSocketRaw("ws://example.com/") { /* Works */ }
392
393
// Incorrect - Other engines don't support raw operations
394
val jsClient = HttpClient(Js) {
395
install(WebSockets)
396
}
397
// jsClient.webSocketRaw() // Compilation error - function not available
398
```
399
400
**Engine compatibility:**
401
- **CIO**: Full raw WebSocket support
402
- **JavaScript**: No raw operations available
403
- **Native engines**: No raw operations available
404
- **Java**: No raw operations available (use CIO instead)
405
406
## Error Handling in Raw Mode
407
408
Handle errors without automatic ping-pong safety net:
409
410
```kotlin
411
client.webSocketRaw("ws://unreliable.example.com/") {
412
try {
413
// Manual connection health monitoring
414
var lastFrameTime = System.currentTimeMillis()
415
416
withTimeout(60_000) { // 60 second total timeout
417
for (frame in incoming) {
418
lastFrameTime = System.currentTimeMillis()
419
420
when (frame) {
421
is Frame.Text -> processMessage(frame.readText())
422
is Frame.Ping -> send(Frame.Pong(frame.data))
423
is Frame.Close -> {
424
val reason = frame.readReason()
425
println("Server closed: ${reason?.code} - ${reason?.message}")
426
break
427
}
428
else -> { /* Handle other frames */ }
429
}
430
431
// Check for connection timeout
432
if (System.currentTimeMillis() - lastFrameTime > 30_000) {
433
println("No frames received for 30 seconds")
434
close(CloseReason(CloseReason.Codes.GOING_AWAY, "Timeout"))
435
break
436
}
437
}
438
}
439
} catch (e: TimeoutCancellationException) {
440
println("Raw WebSocket operation timed out")
441
close(CloseReason(CloseReason.Codes.GOING_AWAY, "Client timeout"))
442
} catch (e: Exception) {
443
println("Raw WebSocket error: ${e.message}")
444
close(CloseReason(CloseReason.Codes.INTERNAL_ERROR, "Client error"))
445
}
446
}
447
```