ws
Run WebSocket servers and connect to them as a client, sending text, binary and JSON messages in both directions.
import "std:ws"std:ws speaks WebSocket: a connection that stays open so either side can send a message at any time. Reach for it for chat, live dashboards, notifications and games, anywhere polling over plain HTTP would be wasteful. A server is usually mounted on a serve router, and a client connects with ws.connect.
Every example below runs a server and a client in the one program on 127.0.0.1. Messages arrive on another thread, so the examples collect them in a channel from thread and read them in order.
An echo server and a client#
ws.new() makes a server. server.on("message", fn) registers a handler that receives the connection and the text. Mount the server on a route with app.ws(path, server), then connect with ws.connect:
import "std:ws"
import "std:serve"
import "std:thread"
auto app = serve.new()
auto echo = ws.new()
echo.on("message", def(*ws.Connection conn, string message) (
conn.send("echo: " ++ message).unwrap()
))
app.ws("/echo", echo)
app.start("127.0.0.1:8780").unwrap()
auto client = ws.connect("ws://127.0.0.1:8780/echo").unwrap()
auto inbox = thread.channel(8)
client.on("message", def(*ws.Connection conn, string message) (
inbox.send(message).unwrap()
))
client.send("hello").unwrap()
log inbox.recv()
client.send("again").unwrap()
log inbox.recv()
client.close({wait: true})
app.stop().unwrap()Send a third message, or an empty string, and run it again.
send returns a result because it can fail: the connection may be closed, or the peer too slow to read. unwrap() is fine in an example, and real code checks it. Handlers run on the connection's own thread, which is why the client hands what it receives to a channel. inbox.recv() waits until a message is there.
Events#
A server has three events: connect and disconnect call fn(conn), and message calls fn(conn, message). A client connection has message, close, and open (after a reconnect). Add as many handlers as you like. off removes one.
import "std:ws"
import "std:serve"
import "std:thread"
auto app = serve.new()
auto server = ws.new()
auto events = thread.channel(8)
server.on("connect", def(*ws.Connection conn) (
events.send("connected").unwrap()
conn.send("welcome").unwrap()
))
server.on("disconnect", def(*ws.Connection conn) (
events.send("disconnected").unwrap()
))
app.ws("/", server)
app.start("127.0.0.1:8781").unwrap()
auto client = ws.connect("ws://127.0.0.1:8781/").unwrap()
auto inbox = thread.channel(8)
client.on("message", def(*ws.Connection conn, string message) (
inbox.send(message).unwrap()
))
log events.recv()
log inbox.recv()
log client.isConnected()
client.close({wait: true})
log events.recv()
log client.isConnected()
app.stop().unwrap()The server calls the connect handlers before it delivers the connection's first message, so a handler there can set things up without racing the client.
Messages: text, bytes and JSON#
send picks the format from the value. A string goes as a text message, byte[] as a binary message, and an object or array is encoded as JSON text. On the receiving side every message arrives as a string, so parse JSON with json:
import "std:ws"
import "std:serve"
import "std:json"
import "std:thread"
auto app = serve.new()
auto server = ws.new()
server.on("message", def(*ws.Connection conn, string message) (
auto request = json.parse<object>(message).unwrap()
conn.send({sum: request["a"] + request["b"], received: message.len}).unwrap()
))
app.ws("/add", server)
app.start("127.0.0.1:8782").unwrap()
auto client = ws.connect("ws://127.0.0.1:8782/add").unwrap()
auto inbox = thread.channel(4)
client.on("message", def(*ws.Connection conn, string message) (
inbox.send(message).unwrap()
))
client.send({a: 2, b: 40}).unwrap()
log inbox.recv()
client.close({wait: true})
app.stop().unwrap()Messages over 1 MiB are refused by default. Raise the limit with the maxMessageBytes option of ws.new and ws.connect when you need larger ones.
Broadcast and rooms#
server.broadcast(message) sends to every connected client. To send to a subset, tag connections with conn.set(key, value) and call server.broadcastWhere(key, value, message). Here two clients join different rooms from a header, and a message reaches only the sender's room:
import "std:ws"
import "std:serve"
import "std:thread"
auto app = serve.new()
auto chat = ws.new()
chat.on("connect", def(*ws.Connection conn) (
conn.set("room", conn.header("X-Room") ?? "lobby")
))
chat.on("message", def(*ws.Connection conn, string message) (
string room = conn.get("room")
chat.broadcastWhere("room", room, room ++ ": " ++ message)
))
app.ws("/chat", chat)
app.start("127.0.0.1:8783").unwrap()
auto red = ws.connect("ws://127.0.0.1:8783/chat", {headers: {"X-Room": "red"}}).unwrap()
auto blue = ws.connect("ws://127.0.0.1:8783/chat", {headers: {"X-Room": "blue"}}).unwrap()
auto redInbox = thread.channel(4)
auto blueInbox = thread.channel(4)
red.on("message", def(*ws.Connection conn, string message) (
redInbox.send(message).unwrap()
))
blue.on("message", def(*ws.Connection conn, string message) (
blueInbox.send(message).unwrap()
))
red.send("hello red").unwrap()
blue.send("hello blue").unwrap()
log redInbox.recv()
log blueInbox.recv()
log chat.connections().len
red.close({wait: true})
blue.close({wait: true})
app.stop().unwrap()conn.header(name) reads a header of the upgrade request, so the same trick carries a token or a user name. Browsers can't set custom headers on a WebSocket, so for those clients put the value in the URL and read it from the serve context in middleware instead.
Serving on a path of your own#
Without serve, a server can listen by itself. Give ws.new an address and path and call run(), which blocks until stop(), so run it in an async function:
import "std:ws"
import "std:thread"
auto server = ws.new({address: "127.0.0.1:8784", path: "/live"})
server.on("message", def(*ws.Connection conn, string message) (
conn.send(message.toUpper()).unwrap()
))
async listen() result<unit, error> (
return server.run()
)
auto running = listen()
thread.sleep(300)
auto client = ws.connect("ws://127.0.0.1:8784/live").unwrap()
auto inbox = thread.channel(4)
client.on("message", def(*ws.Connection conn, string message) (
inbox.send(message).unwrap()
))
client.send("quiet please").unwrap()
log inbox.recv()
client.close({wait: true})
server.stop().unwrap()
log running.wait().isOk()Prefer mounting on serve when the program has other routes: one port, shared middleware and one place to stop.
Closing#
conn.close() returns at once and shuts the connection down in the background: queued messages go first, then a close message. Options set a code and reason the other side sees, and wait: true returns only when the socket is closed. After closing, send fails with a StateError:
import "std:ws"
import "std:serve"
import "std:thread"
auto app = serve.new()
auto server = ws.new()
server.on("message", def(*ws.Connection conn, string message) (
conn.send("ok: " ++ message).unwrap()
if message == "bye" (
conn.close({code: 1000, reason: "finished"})
)
))
app.ws("/", server)
app.start("127.0.0.1:8785").unwrap()
auto client = ws.connect("ws://127.0.0.1:8785/").unwrap()
auto inbox = thread.channel(8)
client.on("message", def(*ws.Connection conn, string message) (
inbox.send(message).unwrap()
))
client.on("close", def(*ws.Connection conn) (
inbox.send("closed").unwrap()
))
client.send("bye").unwrap()
log inbox.recv()
log inbox.recv()
log client.send("too late").unwrapErr().kind
app.stop().unwrap()Failing to connect#
ws.connect returns a result. The error kind says what went wrong: NetworkError for a refused connection, TimeoutError when the handshake takes longer than the timeout option (10 seconds by default), UnsupportedError for a URL that isn't ws:// or wss://, ParseError for a malformed URL and NotFoundError when the host doesn't exist:
import "std:ws"
auto refused = ws.connect("ws://127.0.0.1:1/nothing")
log refused.isErr() refused.unwrapErr().kind
auto wrong = ws.connect("http://example.com")
log wrong.unwrapErr().kind
switch ws.connect("ws://127.0.0.1:1/") (
case ok(conn)
log "connected"
case err(e)
log "could not connect:" e.kind
)Reconnecting#
A client connection can reopen itself after it drops. conn.enableReconnect() retries with a delay that doubles from one second to thirty, and fires open each time it succeeds. conn.close() stops it for good:
import "std:ws"
auto client = ws.connect("wss://example.com/feed").unwrap()
client.enableReconnect()
client.on("open", def(*ws.Connection conn) (
log "reconnected, resubscribing"
conn.send({subscribe: "prices"}).unwrap()
))
client.on("message", def(*ws.Connection conn, string message) (
log message
))After a reconnect the server sees a brand new connection, so anything it knew about the old one (rooms, a user name) has to be sent again from the open handler.
Securing a server#
A server accepts upgrades only from pages on its own origin unless you call server.allowAllOrigins(). Do that only for a public API. Because the upgrade is an ordinary HTTP request, middleware on the route applies to it, so serve.requireBearer or your own check can refuse a socket before it opens. For wss://, use server.runTLS(certFile, keyFile) or app.runTLS(...), or put a proxy that terminates TLS in front.
A client handler runs on the connection's own thread. How does the example above pass the messages to the main program?
Shout service
Make the server answer each message with the message upper-cased and its length, as JSON: {"shout": "HI", "length": 2}. The client part is written for you.
{"length":2,"shout":"HI"}
{"length":3,"shout":"OSL"}Visitor numbers
Tell each client who it is. When a client connects, the server sends you are visitor N, counting from 1. Two clients connect, and each prints its greeting.
you are visitor 1 you are visitor 2
API reference#
39 members, generated from the std:ws source of OSL 9.3.0.
Functions
ws.closeConn(any conn) booleanDeprecated. Deprecated: use conn.close() instead.
closeConn closes conn and reports whether it was a connection.
ws.connect(string url, any ...opts) result<*ws.Connection, error>connect opens a WebSocket connection and starts receiving messages; register handlers with conn.on. Options: protocols, headers, timeout (milliseconds, default 10000), maxMessageBytes (default 1 MiB), sendQueue (default 256) and compression. Fails with ParseError, UnsupportedError, NotFoundError, TimeoutError or NetworkError. Deprecated positional protocols from argument 2 are still accepted.
Deprecated positional arguments from 2: pass {protocols: [...]} instead.
ws.new(any ...opts) *ws.Servernew creates a WebSocket server for app.ws(path, server) or standalone server.run(). Options: address (default ":8080"), path (default "/"), maxMessageBytes (default 1 MiB), sendQueue (default 256) and compression. Deprecated positional address/path arguments are still accepted.
Deprecated positional arguments from 1: pass {address, path} instead.
ws.newServer(string addr, string path) *ws.ServerDeprecated. Deprecated: use ws.new({address: addr, path: path}) instead.
newServer creates a standalone server on addr and path.
ws.send(any conn, any message) booleanDeprecated. Deprecated: use conn.send(message) instead, which reports why sending failed.
send queues a message on conn and reports whether it was queued; false for anything that isn't a connection.
*ws.Connection
connection.close(object? options)close closes the connection for good and cancels reconnecting, and returns at once: from then on send fails and no more messages arrive. In the background, messages already passed to send go first, then a close message with code (default 1000, a normal closure) and reason (default ""), and the socket stays open until the peer answers with its own close message, so the peer sees a clean close and every message sent before it. Options: code (1000-1003, 1007-1014 or 3000-4999), reason (at most 123 bytes), timeout, the most milliseconds to wait for the peer before closing the socket anyway (default 2000; 0 closes it as soon as the close message is out), and wait, true to return only once the socket is closed. The queued messages and the close message get the same 10 s write limit as any send. "close" fires after. Closing again, even while the first close is still running, is a no-op that returns at once, or with wait, once the socket is closed. Throws ValueError for an invalid code or reason.
connection.connected() booleanDeprecated. Deprecated: use conn.isConnected() instead.
connected reports whether the connection is open.
connection.data() objectdata returns every stored value as a new object.
connection.delete(string key)delete removes the value stored under key.
connection.enableReconnect()enableReconnect makes a client connection reopen itself after it drops, retrying with a delay that doubles from 1 s up to 30 s, and firing "open" each time it reconnects. close() stops it.
connection.get(string key) anyget returns the value stored under key, or null.
connection.getAll() objectDeprecated. Deprecated: use conn.data() instead.
getAll returns every stored value.
connection.getHeader(string key) stringDeprecated. Deprecated: use conn.header(name) instead, which returns null for a missing header.
getHeader returns an upgrade request header, or "" when it wasn't sent.
connection.getHeaders() objectDeprecated. Deprecated: use conn.headers() instead.
getHeaders returns the upgrade request's headers.
connection.header(string name) string?header returns a header of the upgrade request that opened a server-side connection, matched case-insensitively, or null when it wasn't sent. The "Remote-Addr" entry holds the client's address. Client connections have no request headers.
connection.headers() string[string]headers returns the upgrade request's headers as an object, with the first value of each; empty for client connections.
connection.isConnected() booleanisConnected reports whether the connection is currently open.
connection.off(string event, any fn) booleanoff removes a handler added with on and reports whether it was registered.
connection.on(string event, any fn)on adds a handler for a connection event: "message" calls fn(conn, message) for every message received (binary messages arrive as strings), "close" calls fn(conn) when the connection closes from either side, and "open" calls fn(conn) after a reconnect. Handlers run on the connection's own thread; one that throws has its error printed and the connection keeps running. Throws ValueError for an unknown event.
connection.onClose(any handler)Deprecated. Deprecated: use conn.on("close", fn) instead, which adds a handler.
onClose replaces the connection's close handler.
connection.onMessage(any handler)Deprecated. Deprecated: use conn.on("message", fn) instead, which adds a handler.
onMessage replaces the connection's message handler.
connection.send(any message) result<unit, error>send queues message for delivery: a string is sent as a text message, byte[] as a binary message, and an object or array as JSON text. Fails with StateError when the connection is closed and IOError when its send queue is full because the peer isn't reading. Throws TypeError for other message types.
connection.set(string key, any value)set stores a value on the connection under key, for the program's own bookkeeping (a user name, a room). broadcastWhere matches on it.
connection.shutdown()Deprecated. Deprecated: use conn.close() instead, which now also cancels reconnecting.
shutdown closes the connection and cancels reconnecting.
*ws.Server
server.allowAllOrigins()allowAllOrigins accepts upgrades from pages on any origin. By default only same-origin browser pages may connect.
server.broadcast(any message)broadcast sends message to every connected client, skipping any whose connection has closed or whose send queue is full. message is a string (text), byte[] (binary) or an object or array (JSON text). Throws TypeError for other message types.
server.broadcastWhere(string key, any value, any message)broadcastWhere sends message to every client whose stored value for key (see conn.set) equals value, skipping closed connections. message takes the same types as broadcast.
server.connections() *ws.Connection[]connections returns the currently connected clients.
server.getConnections() *ws.Connection[]Deprecated. Deprecated: use server.connections() instead.
getConnections returns the connected clients.
server.off(string event, any fn) booleanoff removes a handler added with on and reports whether it was registered.
server.on(string event, any fn)on adds a handler for a server event: "connect" and "disconnect" call fn(conn), and "message" calls fn(conn, message) for every message from any client. Handlers run on the connection's own thread; one that throws has its error printed and the server keeps running. Throws ValueError for an unknown event.
server.onConnect(any handler)Deprecated. Deprecated: use server.on("connect", fn) instead, which adds a handler.
onConnect replaces the server's connect handler.
server.onDisconnect(any handler)Deprecated. Deprecated: use server.on("disconnect", fn) instead, which adds a handler.
onDisconnect replaces the server's disconnect handler.
server.onMessage(any handler)Deprecated. Deprecated: use server.on("message", fn) instead, which adds a handler.
onMessage replaces the server's message handler.
server.run() result<unit, error>run serves WebSocket upgrades on the server's address and path and blocks until stop() is called; run it in a thread to keep going. Fails with NetworkError when the address can't be bound. Throws StateError when the server is already running.
server.runTLS(string certFile, string keyFile) result<unit, error>runTLS is run over TLS (wss://) with the certificate and key in the given PEM files. Fails with NotFoundError or IOError when they can't be read.
server.start() result<unit, error>Deprecated. Deprecated: use server.run() instead.
start serves until stopped.
server.startTLS(string certFile, string keyFile) result<unit, error>Deprecated. Deprecated: use server.runTLS(certFile, keyFile) instead.
startTLS serves over TLS until stopped.
server.stop() result<unit, error>stop closes the server and every connection, firing "disconnect" for each. A run() call then returns. Stopping a server that isn't running only closes its connections.