Docs/Networking

WebSockets

Two-way messages between a client and a server.

On this page

A WebSocket keeps one connection open so the client and the server can both send messages at any time. Both sides use the same http.WebSocket type.

Accept WebSockets on a server#

A handler upgrades the request and passes a function that runs on the connection. The connection closes when that function returns:

Valk
use valk.http

fn handle(req: http.Request) http.Response {
    if req.path == "/chat" {
        return http.WebSocket.upgrade(req, fn(ws: http.WebSocket) {
            while true {
                let msg = ws.read() ! break // fails with `closed` once the client is gone
                ws.write("echo: " + msg.data) ! break
            }
        })
    }
    return http.Response.html("<script>new WebSocket('ws://' + location.host + '/chat')</script>")
}

fn main() {
    let server = http.Server.new("127.0.0.1", 9000, handle)
    server.start() ! return
}

A request that is not a WebSocket upgrade gets a 400. A fast handler uses res.send_websocket(ctx, fn) instead.

Connect as a client#

Valk
use valk.http

fn main() {
    let ws = http.WebSocket.connect("ws://127.0.0.1:9000/chat") ! {
        println("Could not connect")
        return
    }
    ws.write("hello") ! return
    let reply = ws.read() ! return
    println(reply.data) // echo: hello
    ws.close() ! return
}

connect takes ws:// and wss:// URLs, and an http.Options for extra headers, TLS settings and timeouts.

Messages#

Method Does
read() Waits for the next whole message; msg.data, msg.is_text
write(text) Sends a text message (UTF-8)
write_binary(data) Sends a binary message
ping() Sends a ping; pings from the other side are answered by read
close(code, reason) Runs the closing handshake
set_timeouts(read_ms, write_ms) Socket timeouts; 0 waits forever
set_cancel(token) Makes a waiting read or write fail with cancelled

After a close from either side, read and write throw closed, and close_code / close_reason say why (1006 when the connection dropped). max_message_size (16 MB) limits one message.

Read and write at the same time#

A read and a write may run on different coroutines, so one waits for messages while another pushes updates:

Valk
use valk.http
use valk.time

fn push_time(ws: http.WebSocket) {
    while !ws.is_closed {
        ws.write("tick") ! return
        time.sleep_ms(1000)
    }
}

fn handle(req: http.Request) http.Response {
    return http.WebSocket.upgrade(req, fn(ws: http.WebSocket) {
        co push_time(ws)
        while true {
            let msg = ws.read() ! break
            println("client said: " + msg.data)
        }
    })
}

fn main() {
    http.serve("127.0.0.1", 9000, handle) ! return
}

Send one message to many clients#

Prepare a message once with http.WebSocketFrame.text(data) or .binary(data) and send it with write_frame. A frame can be shared between threads:

Valk
use valk.http

fn broadcast(sockets: Array[http.WebSocket], text: String) {
    let frame = http.WebSocketFrame.text(text)
    each sockets as ws {
        ws.write_frame(frame) ! continue
    }
}

Subprotocols, headers and compression#

upgrade(req, fn, protocol: "chat.v2") accepts a subprotocol the client offered, and ws.protocol tells both sides what was agreed. headers: adds fields to the 101 response, such as cookies.

Messages can be compressed (permessage-deflate). The client offers it by default; a server accepts it when server.websocket_compression = true, or per endpoint with upgrade(req, fn, true). Turn the client's offer off with http.Options { websocket_compression: false }.

WebSockets work over HTTP/1.1 only, not over HTTP/2. Shutting down the server closes open WebSockets.