flare_mlake · API · ws

flare.ws.server

WebSocket server: upgrades HTTP connections to WebSocket (RFC 6455).

Server-to-client frames MUST NOT be masked (RFC 6455 §5.3). Client-to-server frames MUST be masked; WsConnection.recv un-masks them automatically.

The upgrade handshake (§4.2): 1. Accept TCP connection. 2. Read the HTTP GET request and locate Sec-WebSocket-Key. 3. Compute Sec-WebSocket-Accept = base64(SHA-1(key + GUID)). 4. Send 101 Switching Protocols. 5. Hand off to WsConnection.

Structs

WsConnection

struct WsConnection

An accepted WebSocket connection (server side).

Server-side frames MUST NOT be masked (RFC 6455 §5.3). Client-side frames MUST be masked; recv unmasks them automatically.

This type is Movable but not Copyable.

A CLOSE received by :meth:recv is answered here (RFC 6455 sec 5.5.1), and once a CLOSE has been sent, by that reply or by :meth:close, every send_* raises: an endpoint MUST NOT send data after its CLOSE.

Fields: _stream: The underlying TCP stream. _peer: The remote socket address. origin: The handshake Origin header (empty when absent).

Example:

def on_connect(conn: WsConnection) raises:
    var frame = conn.recv()
    conn.send_text(frame.text_payload()) # echo back

var srv = WsServer.bind(SocketAddr.localhost(9001))
srv.serve(on_connect)

Implements Movable

Fields
origin: String: The Origin header sent with the upgrade handshake, or empty when the client sent none.
max_frame_size: Int: Largest frame payload :meth:recv accepts. A client frame that declares more is answered with CLOSE 1009 (MESSAGE_TOO_BIG) and :meth:recv raises, before any of the payload is buffered. Defaults to :data:DEFAULT_MAX_FRAME_BYTES; set it in the handler before the first recv to change it.

__init__

def __init__(out self, var stream: TcpStream, peer: SocketAddr, var origin: String = "")
def __init__(out self, var stream: TcpStream, peer: SocketAddr, var prebuf: List[UInt8], var origin: String = "")

Construct a WsConnection seeded with already-buffered post-handshake bytes.

Used by the HttpServer WebSocket-upgrade seam to hand off any frame bytes the HTTP reactor had already read past the upgrade request. prebuf is consumed by the first recv/_recv_one before any socket read.

Args
stream: The upgraded connection, already in blocking mode.
peer: Remote address, for :attr:peer.
prebuf: Bytes the reactor read past the upgrade request.
origin: The handshake's Origin, for :attr:origin.

__deinit__

def __deinit__(deinit self)

send_text

def send_text(self, msg: String)

Send a UTF-8 text message to the client.

Server-to-client frames are NOT masked (RFC 6455 §5.3).

Args
msg: The UTF-8 string to send.

Raises NetworkError: On I/O failure, or if a CLOSE was already sent.

send_binary

def send_binary(self, data: List[UInt8])

Send a binary message to the client.

Server-to-client frames are NOT masked (RFC 6455 §5.3).

Args
data: The raw binary payload.

Raises NetworkError: On I/O failure, or if a CLOSE was already sent.

send_frame

def send_frame(mut self, frame: WsFrame)

Send an already-constructed frame (server, no masking).

Sending a CLOSE frame marks the closing handshake as started, the same as :meth:close; a second CLOSE is dropped.

Args
frame: Frame to send. The mask bit is always False.

Raises NetworkError: On I/O failure, or if a CLOSE was already sent.

recv

def recv(mut self) -> WsFrame

Receive the next data frame from the client.

Automatically replies to PING frames with an unmasked PONG and continues reading. Returns TEXT or BINARY frames. Client frames are unmasked by WsFrame.decode_one automatically.

A CLOSE frame is answered before it is returned (RFC 6455 sec 5.5.1): with the received status code, with an empty CLOSE when the body is empty, or with 1002 when the body is invalid (a 1-byte body, a code that may not be sent, a reason that is not UTF-8). The reply is skipped if :meth:close already sent a CLOSE. The frame is still returned, so a handler can leave its loop; later send_* calls raise.

A frame that violates the protocol fails the connection after a best-effort CLOSE: an unmasked client frame with 1002 (RFC 6455 sec 5.1), a final TEXT frame whose payload is not valid UTF-8 with 1007 (sec 8.1), an oversized frame with 1009. recv raises in each case and later send_* calls raise too.

Raises WsProtocolError: If the client sends an unmasked frame, or a final TEXT frame that is not valid UTF-8. NetworkError: On I/O failure.

recv_message

def recv_message(mut self) -> WsMessage

Receive the next complete message, reassembled.

A fragmented message (a TEXT or BINARY frame without FIN, then CONTINUATION frames up to one with FIN, RFC 6455 sec 5.4) comes back whole; :meth:recv returns its pieces one by one. PING is answered and PONG skipped, between fragments too. A text message is checked for UTF-8 as a whole, so a code point may be split across fragments. The reassembled payload is bounded by max_frame_size.

Raises NetworkError: If the client sent CLOSE (already answered by :meth:recv), or on I/O failure. WsProtocolError: After failing the connection with 1002 for a CONTINUATION with no message to continue or a new data frame inside a fragmented one, 1007 for a text message that is not UTF-8, 1009 for a message over max_frame_size.

close

def close(mut self, code: UInt16 = UInt16(1000), reason: String = "")

Send a CLOSE frame, once.

Starts the closing handshake (RFC 6455 sec 7.1.2) and does not wait for the peer's CLOSE: call :meth:recv to read it. A second call, or a call after :meth:recv has already answered the client's CLOSE, sends nothing. After this every send_* raises.

Args
code: Close status code (see WsCloseCode.*).
reason: Optional UTF-8 reason phrase (≤123 bytes).

peer_addr

def peer_addr(self) -> SocketAddr

Return the remote socket address.

WsServer

struct WsServer

A WebSocket server that upgrades incoming HTTP connections.

Accepts TCP connections, performs the HTTP Upgrade handshake, and calls handler once per established WebSocket connection.

This type is Movable but not Copyable.

Fields: _listener: The bound TCP listener.

Example:

def handle(conn: WsConnection) raises:
    while True:
        var frame = conn.recv()
        if frame.opcode == WsOpcode.CLOSE:
            break  # recv() has already sent the CLOSE reply
        conn.send_text(frame.text_payload())

var srv = WsServer.bind(SocketAddr.localhost(9001))
srv.serve(handle)

Implements Movable

__init__

def __init__(out self, var listener: TcpListener)

__deinit__

def __deinit__(deinit self)

bind

def bind(addr: SocketAddr) -> Self

Bind a WebSocket server on addr.

Args
addr: Local address to accept connections on.

Raises AddressInUse: If the port is already bound. NetworkError: For any other OS error.

serve

def serve(self, handler: def(mut WsConnection) raises thin -> None)

Accept WebSocket connections in a single-threaded loop.

For each accepted TCP connection: 1. Read the HTTP Upgrade request. 2. Compute Sec-WebSocket-Accept. 3. Send 101 Switching Protocols. 4. Call handler(conn).

Upgrade errors for individual connections are silently skipped; only fatal accept-loop errors propagate. For the multi-worker variant (num_workers >= 2), see :meth:serve_multicore.

Args
handler: Callback invoked once per successfully upgraded connection.

Raises NetworkError: On fatal accept-loop errors.

def serve(mut self, handler: def(mut WsConnection) raises thin -> None, num_workers: Int)

Accept WebSocket connections across num_workers threads.

num_workers <= 1 falls back to the single-threaded :meth:serve shape (one worker on the current thread). num_workers >= 2 binds num_workers SO_REUSEPORT listeners on the same port (kernel-level load balancing across worker threads, matching the :class:flare.http.HttpServer multi-worker shape) and spawns one pthread per worker. Each worker runs its own single-threaded accept loop, so the per-connection upgrade + handler dispatch is unchanged.

The original :attr:_listener (whose port the SO_REUSEPORT listeners bind to) is closed so its backlog doesn't accept connections that would never be served. Workers are joined before this method returns, which today means it never returns: the workers run forever (no drain machinery yet). Use Ctrl-C / kill to terminate.

Args
handler: Per-connection callback (same shape as the single-threaded variant). Function pointers are trivially copyable so the same value is shared across all workers without per-worker context packaging.
num_workers: Worker count. <= 0 is coerced to 1. Values > 256 are rejected.

Raises NetworkError: On listener bind failure for any worker. Error: On pthread_create failure when num_workers >= 2.

def serve[H: WsHandler](mut self, var handler: H)

Accept WebSocket connections, dispatching each to a stateful :trait:WsHandler struct.

The struct-handler variant of :meth:serve: one handler instance serves every connection via mut self, so it can accumulate state (connection count, a shared room registry, etc.) across connections. Single-threaded; a multi-worker struct-handler path (per-worker handler copies over the SO_REUSEPORT listeners) is a follow-up -- the def handler already has num_workers.

Upgrade errors for individual connections are logged and skipped; only fatal accept-loop errors propagate.

local_addr

def local_addr(self) -> SocketAddr

Return the local address the server is bound to.

close

def close(mut self)

Stop accepting connections. Idempotent.

Traits

WsHandler

Stateful per-connection WebSocket handler.

The struct-handler counterpart to the def(mut WsConnection) callback: because :meth:on_connection takes mut self, the handler can carry state (a shared registry, a counter, config) that persists across the connections one server instance serves. Mount via :meth:WsServer.serve (the [H: WsHandler] overload).

The trait is protocol-agnostic -- it drives a :class:WsConnection, which is the h1 carrier today; the same handler will run over the WS-over-h2 carrier once that bridge lands (carrier abstraction).

Implements Copyable, Movable

on_connection

def on_connection(mut self, mut conn: WsConnection)

Run one established WebSocket connection to completion.

Functions

ws_offload_try_acquire

def ws_offload_try_acquire(limit: Int) -> Bool

Claim one of limit detached WebSocket threads, process-wide. False when all are held: the upgrade is then refused rather than letting clients spawn threads until the process hits its limit.

ws_offload_release

def ws_offload_release()

Give back a thread claimed by ws_offload_try_acquire.

ws_offloaded

def ws_offloaded() -> Int

Detached WebSocket threads currently running.

Aliases

comptime DEFAULT_MAX_OFFLOADED_WS = Int(4096)

Detached WebSocket threads a process runs at once by default.