std.http.websocket
import std.http.websocket speaks the WebSocket protocol of
RFC 6455: the opening handshake,
as a std.http.server exchange that switches protocols
or as a client, then framed text and binary messages, ping and pong, and
the closing handshake. std.http.wss adds wss clients through
std.tls. Both modules are checked Plenty in stdlib/std/http/
and add no runtime primitives; the handshake’s SHA-1 comes from
std.crypto. The tutorial lesson
Talk over WebSockets shows a
complete program.
import std.time
import std.bytes
import std.http.server
import std.http.websocket
import std.net
from std.http.server import Exchange
from std.http.websocket import Message, WebSocketError
from std.net import TcpStream
from std.sync import CancellationToken
from std.time import Deadline
type ChatError = WebSocketError | std.bytes.BytesError | RangeError | AllocError
def soon() -> Result[Deadline, RangeError]:
Ok(std.time.deadline_after(&std.time.monotonic_now(), &std.time.seconds(5u32))?)
pub enum Route:
Chat
pub class Shout:
pub def handle(self, route: &Route, exchange: &mut Exchange[TcpStream]) -> Result[(), ChatError]:
wait_cancel = exchange.cancel_token()
mut socket = std.http.websocket.accept(exchange, &std.http.websocket.config())?
mut message = std.bytes.empty()?
while True:
message.clear()
match socket.receive(&mut message, &soon()?, &wait_cancel)?:
case Message.Close(_):
return Ok(())
case _:
socket.send_text(&message.to_text()?.upper()?, &soon()?, &wait_cancel)?
Ok(())
def chat(port: u16, stop: CancellationToken) -> Result[(), ChatError | IoError]:
with stop.cancel_on_exit():
never = CancellationToken()?
url = f"ws://127.0.0.1:{port}/chat"?
mut socket = std.http.websocket.connect(&url, &std.http.websocket.config(), &soon()?, &never)?
socket.send_text("hello", &soon()?, &never)?
mut reply = std.bytes.empty()?
print(socket.receive(&mut reply, &soon()?, &never)?, reply.to_text()?)?
socket.close(1000, "bye", &soon()?, &never)?
Ok(())
def main() -> Result[(), SpawnError[()] | std.http.server.ServerError | std.net.NetError | ChatError | IoError]:
mut routes = std.http.server.router[Route]()?
routes.add("GET", "/chat", Route.Chat)?
listener = std.net.listen(&std.net.loopback_v4(0u16), 16)?
port = listener.local_address()?.port
stop = CancellationToken()?
with spawn(chat, port, stop.share()).drop_job()? as task:
drop(std.http.server.serve(listener, &routes, &Shout(), &std.http.server.config(), &stop)?)
task.join()?
Ok(())
std.http.websocket.Message.Text HELLO
Connections
WebSocket[T] is one open connection over a stream T that reads and
writes like a server Transport: TcpStream, or
TlsTransport for TLS.
| Item | Meaning |
|---|---|
socket.receive(&mut out, &deadline, &wait_cancel) | Result[Message, WebSocketError]: append the next whole message to out |
socket.send(kind, data: &[u8], &deadline, &wait_cancel) | Result[(), WebSocketError]: send data as one Kind.Text or Kind.Binary message |
socket.send_text(&text, &deadline, &wait_cancel) | send of text, copied into a buffer first |
socket.ping(data: &[u8], &deadline, &wait_cancel) | Send a ping with at most 125 bytes of payload |
socket.close(code, &reason, &deadline, &wait_cancel) | Run the closing handshake, then shut the stream down |
socket.is_open() | Whether messages can still be sent |
socket.subprotocol | Option[str]: the subprotocol both sides agreed on |
Message is Text, Binary, or Close(code). Text has been checked as
UTF-8. A message sent in several frames is joined before receive returns
it. Pings are answered with a pong and pongs are skipped while receiving.
When the peer closes, receive answers with the same code and returns
Close with the peer’s code, or 1005 when it gave none, and its reason
appended to out; the connection is then closed.
send splits a message longer than max_frame into several frames. A
Text message must be UTF-8 (InvalidText). Client frames are masked with
a fresh key from std.secrets for each frame, as RFC 6455
requires; server frames are not.
close accepts the codes a peer may send: 1000 to 1003, 1007 to 1014, and
3000 to 4999, with a reason of at most 123 bytes (OutOfRange otherwise).
It sends the close, skips any messages still arriving until the peer’s
close, and shuts the stream down. Closing a connection whose closing
handshake already finished returns Ok.
On an error, out keeps only what it held before the call. Every error
except Stopping and OutOfRange closes the connection; later calls fail
with Closed. A protocol fault first sends a close: 1002 for Protocol,
1009 for TooLarge and 1007 for InvalidText.
Each call has its own deadline. A connection holds one 4 KiB input buffer,
grown only to hold input that arrived with the handshake, and one 4 KiB
output buffer; a sent message is copied through the output buffer in
pieces, so send allocates nothing for any message size.
Configuration
config() returns the defaults.
| Field | Default | Meaning |
|---|---|---|
max_frame | 65536 | Largest frame payload received, and the size send splits messages into |
max_message | 1048576 | Largest message receive joins |
subprotocol | Nothing | The subprotocol a client offers, or a server accepts when the client offers it |
Both maxima are Positive[i64], so a zero or negative maximum is a type error.
They bound payload bytes, not the prefix already held by the caller’s buffer.
An unrepresentable combined buffer extent is
Allocation(AllocError.CapacityOverflow), not TooLarge; as with other receive
errors, the original prefix is retained and the connection is closed. Frame
splitting and mask positions stay within their source ranges even when the
configured maximum is i64.MAX.
Servers
accept(exchange: &mut Exchange[T], config: &Config) returns
Result[WebSocket[T], WebSocketError]. It checks that the request is a
handshake: GET over HTTP/1.1 with no body, Upgrade: websocket,
Connection: upgrade, Sec-WebSocket-Version: 13 and one
Sec-WebSocket-Key of 16 bytes in base64. Field names and the Upgrade
and Connection tokens are compared without case. A request that fails
these checks is answered and accept returns Refused(status):
| Request | Answer |
|---|---|
Missing websocket in Upgrade, or another version | 426 Upgrade Required with Upgrade: websocket and Sec-WebSocket-Version: 13 |
| Any other fault | 400 Bad Request |
Otherwise it answers 101 Switching Protocols with the accept value, and
the configured subprotocol when the client listed it, and takes the
connection with
switch_protocols. The server no
longer counts or closes the connection; dropping the WebSocket closes it.
The WebSocket keeps the server’s shutdown tokens. A receive waiting for
the next message ends with Stopping when shutdown begins, and the
connection stays open, so the handler can close with 1001. A wait in the
middle of a message, or in send or close, ends with Net(Cancelled)
when the drain deadline passes.
The handler keeps its worker for as long as the WebSocket is open, so each
open connection counts against workers. Route a WebSocket to a handler
that returns when the connection closes.
Clients
| Item | Meaning |
|---|---|
connect(url: &str, &config, &deadline, &cancel) | Result[WebSocket[TcpStream], WebSocketError]: dial and handshake a ws URL |
target(url: &str) | Result[Target, WebSocketError]: parse a ws or wss URL |
dial(&target, &deadline, &cancel) | Result[TcpStream, WebSocketError]: a TCP connection to the target |
handshake(stream, &target, &config, &deadline, &cancel) | Result[WebSocket[T], WebSocketError]: the client handshake over a connected stream |
Target.secure, host, port | wss or not; the host, lowercase without IPv6 brackets; the port, 80 or 443 by default |
target rejects URLs with another scheme (UnsupportedScheme), with
credentials or a fragment, without a host, or with port 0
(InvalidUrl). The request target is the URL’s path, / when empty, and
its query.
The handshake sends a random 16-byte key and checks the server’s answer: a
status other than 101 is Rejected(status), and a 101 without the
Upgrade and Connection tokens, with the wrong accept value, with
extensions, or with a subprotocol the client did not offer is
InvalidHandshake. The deadline bounds the handshake; later calls take
their own. cancel ends every later wait with Net(Cancelled).
connect returns UnsupportedScheme for wss URLs: use
std.http.wss.
WSS
import std.http.wss runs the client handshake over TLS.
| Item | Meaning |
|---|---|
connect(url: &str, &tls: ClientConfig, &config, &deadline, &cancel) | Result[WebSocket[TlsTransport], WebSocketError] for a wss URL |
The server’s certificate is verified against tls’s roots with the URL’s
host as the name, as for HTTPS. A TLS failure is
Connect(ClientError.Tls(...)). A ws URL is UnsupportedScheme. The
module is separate so that a program using only ws does not link TLS. On
the server side, accept takes exchanges from
std.http.tls.serve unchanged.
Every message operation accepts a caller cancellation token after its deadline.
Pre-cancellation returns Net(Cancelled) before changing the connection.
Cancellation while receiving or writing closes the connection, since a partial
frame cannot safely be resumed as a different operation. The retained server
shutdown tokens keep their existing behavior. Native waits keep their shutdown
wakeups and check the caller token between slices of at most 10 ms; scheduling
and lock contention can delay observation beyond that interval.
Errors
WebSocketError has these cases:
| Case | Meaning |
|---|---|
Net(NetError) | The stream failed, timed out, was cancelled, or ended early |
Allocation(AllocError) | A buffer could not grow |
Http(HttpError) | The handshake’s HTTP head could not be read or written |
Connect(ClientError) | The client could not connect, including TLS failures |
Entropy(StreamError) | No random bytes for a key or mask |
InvalidConfig, InvalidUrl, UnsupportedScheme | See above |
OutOfRange | A ping payload, close code or reason out of bounds |
Refused(status) | The server refused a request that was not a handshake |
Rejected(status) | The server answered the client with another status |
InvalidHandshake | A 101 answer that does not complete the handshake |
Protocol | The peer broke the framing rules |
TooLarge | A frame or message over the configured limits |
InvalidText | Text that is not UTF-8 |
Closed | The connection has closed |
Stopping | Server shutdown began while waiting for a message |
Limits
- No extensions:
permessage-deflateis neither offered nor accepted. - No proxy for clients;
connectdials the URL’s host directly. - One caller at a time:
receiveand the sending methods all borrow the connection exclusively, so one thread cannot wait for a message while another sends. - A server WebSocket holds a worker until its handler returns.
receiveplaces a control frame’s payload inoutwhile handling it, sooutbriefly grows by up to 125 bytes.- A close answer echoes the peer’s code without its reason.
send_textcopies its text;sendsends bytes the caller already holds.