7.6.6. HV-06 — WebSockets
This tutorial covers bidirectional WebSocket communication — creating
a WebSocket server with HvWebServer, connecting clients with
HvWebSocketClient, sending and receiving messages, broadcasting,
and mixing HTTP routes with WebSocket endpoints.
Prerequisites: HV-03 — HTTP Server Basics.
7.6.6.1. WebSocket Server
HvWebServer supports WebSocket connections out of the box.
Override three callbacks:
onWsOpen(channel, url)— a client connectedonWsClose(channel)— a client disconnectedonWsMessage(channel, msg)— a text message arrived
class ChatServer : HvWebServer {
clients : table<WebSocketChannel?; string>
next_id : int = 0
def override onWsOpen(channel : WebSocketChannel?; url : string#) {
next_id++
let nickname = "user_{next_id}"
clients |> insert_clone(channel, nickname)
send(channel, "welcome {nickname}", ws_opcode.WS_OPCODE_TEXT, true)
broadcast("{nickname} joined", channel)
}
def override onWsClose(channel : WebSocketChannel?) {
let nickname = clients?[channel] ?? "unknown"
clients |> erase(channel)
broadcast("{nickname} left", null)
}
def override onWsMessage(channel : WebSocketChannel?; msg : string#) {
let nickname = clients?[channel] ?? "unknown"
send(channel, "echo: {msg}", ws_opcode.WS_OPCODE_TEXT, true)
broadcast("{nickname}: {msg}", channel)
}
def broadcast(msg : string; exclude : WebSocketChannel?) {
for (ch in keys(clients)) {
if (ch != exclude) {
send(ch, msg, ws_opcode.WS_OPCODE_TEXT, true)
}
}
}
}
The send function takes a channel, a message string, an opcode
(ws_opcode.WS_OPCODE_TEXT or WS_OPCODE_BINARY), and a
fin flag (true for a complete frame).
7.6.6.2. WebSocket Client
Subclass HvWebSocketClient and override:
onOpen()— connection establishedonClose()— connection lostonMessage(msg)— text message received
class ChatClient : HvWebSocketClient {
name : string
received : array<string>
def override onOpen {
print("[{name}] connected\n")
}
def override onClose {
print("[{name}] disconnected\n")
}
def override onMessage(msg : string#) {
received |> push_clone(string(msg))
}
}
7.6.6.3. Connecting and Receiving
Create a client, call init(url) to connect, then pump the event
queue with process_event_que() to receive callbacks:
var alice = new ChatClient()
alice.name = "alice"
alice->init("{base_url}/chat")
// Wait for the welcome message
wait_for_messages(alice, 1)
The init URL uses the ws:// scheme. The path (/chat)
is passed to the server’s onWsOpen as the url parameter.
7.6.6.4. Sending Messages
Call send(text) on the client to send a text frame:
alice->send("hello server!")
wait_for_messages(alice, 2) // welcome + echo
The server receives the message in onWsMessage and can respond
with send(channel, ...) or broadcast to all clients.
7.6.6.5. Multiple Clients
Multiple clients can connect to the same server. Each gets its own
onWsOpen callback and a unique channel:
var bob = new ChatClient()
bob.name = "bob"
bob->init("{base_url}/chat")
wait_for_messages(bob, 1) // bob gets welcome
7.6.6.6. Broadcasting
The broadcast method in the example server sends a message to
every connected client except the sender. When Bob sends a message,
Alice receives the broadcast and Bob receives the echo:
bob->send("hi everyone!")
wait_for_messages(bob, 2) // welcome + echo
wait_for_messages(alice, length(alice.received) + 1) // broadcast
7.6.6.7. HTTP Alongside WebSocket
HvWebServer supports HTTP routes and WebSocket on the same port.
Register routes in onInit as usual — they work independently of
WebSocket callbacks:
def override onInit {
GET("/ping") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status {
return resp |> TEXT_PLAIN("pong")
}
GET("/clients") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status {
return resp |> TEXT_PLAIN("{length(self.clients)}")
}
}
HTTP requests work normally while WebSocket clients are connected:
GET("http://127.0.0.1:{SERVER_PORT}/clients") <| $(resp) {
print("connected clients: {resp.body}\n")
}
7.6.6.8. Graceful Close
Call close() on the client to send a WebSocket close frame. This
triggers onClose on both the client and server sides. Pump the
event queue until is_connected() returns false:
alice->close()
bob->close()
var elapsed = 0
while ((alice->is_connected() || bob->is_connected()) && elapsed < 2000) {
alice->process_event_que()
bob->process_event_que()
sleep(20u)
elapsed += 20
}
7.6.6.9. Server Lifecycle
The server runs on a background thread. Use with_job_status for
synchronization and with_atomic32 for the stop signal:
def with_ws_server(port : int; blk : block<(base_url : string) : void>) {
with_job_status(1) $(started) {
with_job_status(1) $(finished) {
with_atomic32() $(stop_flag) {
new_thread() @() {
var server = new ChatServer()
server->init(port)
server->start()
started |> notify_and_release
while (stop_flag |> get == 0) {
server->tick()
sleep(10u)
}
server->stop()
unsafe {
delete server
}
finished |> notify_and_release
}
started |> join
invoke(blk, "ws://127.0.0.1:{port}")
stop_flag |> set(1)
finished |> join
}
}
}
}
7.6.6.10. Quick Reference
Function / Override |
Description |
|---|---|
|
Server: client connected |
|
Server: client disconnected |
|
Server: text message received |
|
Server: send to one client |
|
Client: connection established |
|
Client: connection lost |
|
Client: text message received |
|
Client: connect to WebSocket server |
|
Client: send a text message |
|
Client: graceful disconnect (close frame) |
|
Client: check connection state |
|
Client: pump event queue (call regularly) |
|
Text frame opcode |
|
Binary frame opcode |
See also
Full source: tutorials/dasHV/06_websockets.das
Previous tutorial: HV-05 — Cookies and Form / File Upload
Next tutorial: HV-07 — SSE and Streaming