std.ws
WebSocket client.
import std.ws
conn = ws.connect("wss://example.com/socket")
ws.send(conn, "hello")
msg = ws.recv(conn) # blocks
ws.close(conn)
Needs net.
Receiving
recv blocks until a message arrives, and returns null when the connection closes. So the idiomatic loop ends on close:
loop {
msg = ws.recv(conn)
if msg == null { break }
handle(msg)
}
Server side
The upgrade lives in std.http, which can turn a request into a WebSocket connection. ECKO_MAX_WS_CONNS (default 1024) bounds concurrent connections - a bound that matters, since each one holds resources for as long as the client keeps it open.
What the client asked for
ws.request(c) gives you the request the socket was opened with - the same map an HTTP handler receives, so one piece of code can read either:
http.serve(8080, handler, on_ws: fn(c) {
r = ws.request(c)
if not starts_with(r.path, "/rooms/") {
ws.close(c)
return
}
if not session_ok(r.headers.cookie) {
ws.close(c)
return
}
join_room(r.path, c)
})
method, path, params and headers, carrying whatever the browser sent - its cookies included, which is how a socket opened from a page identifies itself.
A client connection answers null. It was not upgraded from anything, so there is no request to give; asking is reasonable, and code holding either kind should not have to know which.
on_ws runs after the handshake, so closing from there is a close code rather than a status. To answer an unwanted upgrade with a real HTTP status, use on_upgrade.
Refusing before the handshake
on_upgrade runs before the 101 with the same request map. Return null to accept; return a response to refuse with it:
http.serve(8080, handler,
on_upgrade: fn(r) {
if r.path != "/live" { return http.not_found() }
if get(r.headers, "cookie") == null { return http.response(403, "sign in") }
null # null accepts: send the 101
},
on_ws: fn(c) { ws.send(c, "welcome") })
The client sees a 404 or a 403 the way it would from any other route, instead of a socket that opens and immediately closes. A refused upgrade costs no connection slot, so refusing is not a way to exhaust ECKO_MAX_WS_CONNS.
It is a second callback rather than a return value on on_ws because the two have different lifetimes: on_upgrade answers one question and returns, while on_ws serves the socket for as long as it lives. A hook that raises refuses with a 500 - it has not said yes, and an upgrade is not something to grant on a maybe.
Which origins may connect
An upgrade whose Origin does not match the server's own Host is refused with 403. Upgrades bypass the router, so this is the only place such a request can be turned away before it is accepted - and without it any page on any site could open your socket from a visitor's browser, that visitor's cookies attached.
# same-origin only - the default, nothing to configure
http.serve(8080, handler, on_ws: fn(c) ws.recv(c))
# plus a named front end
http.serve(8080, handler, on_ws: fn(c) ws.recv(c),
origins: ["https://app.example"])
# public socket, opted into explicitly
http.serve(8080, handler, on_ws: fn(c) ws.recv(c), origins: ["*"])
Clients that send no Origin at all - everything that is not a browser - are always allowed: without a browser there are no ambient cookies to ride on, and refusing them would break every CLI and service-to-service client.
Concurrency
A connection is not a value you copy - do not use one from several tasks at once. The usual arrangement is one task reading and a channel carrying messages to whatever processes them, which also gives you backpressure.
Errors
Failures raise { kind: "net" }. Network connections drop, so a long-lived client needs reconnection logic - ws.recv returning null is where you notice.
When SSE is the better choice
For one-directional streaming from server to client, server-sent events over plain HTTP are simpler: no upgrade, no framing, no separate protocol for proxies to handle, and browsers reconnect automatically. See Streaming responses & SSE.
Use WebSockets when the client genuinely needs to send messages mid-stream - a chat, a collaborative editor, a game.
Higher level
For a Redis-style protocol client written over raw sockets, see std.net and the client packages built on it.