Chapter 21 / 51
Volume 3 · Protocol Development

WebSocket basics: two-way communication in six callbacks

RFC 6455's handshake, masking, fragmentation, control frames and compression all complete inside XServer and the xrt layer — your script is six functions at the end of that pipeline.

Overview

Positioning: the "basics" chapter already lets you write usable real-time services — because the host ate the protocol complexity. The advanced chapter (ch. 22) covers broadcasting, limits and subprotocols.

21.1 Intro

The pain list of hand-written WebSocket: Upgrade handshake validation (SHA-1+Base64 of Sec-WebSocket-Key), frame header parsing (mask bit, extended lengths), fragment reassembly, interleaved control frames, the close state machine, permessage-deflate negotiation. XServer's split of labor: the host does the handshake and validation (400 on failure), xrtWsStreamAttach does the frame layer — callbacks receive whole messages, and sending is one function call.

21.2 Handshake & callbacks

HTTP request → Upgrade validation (xrtWsUpgradeRequestCheck) fail → 400 ok → 101 Switching → xrtWsStreamAttach │ ┌──────────────────────────────────┴───────────────┐ ▼ ▼ ▼ ▼ ▼ WsOpen WsText WsBinary WsPing WsClose (ready) (whole text) (whole binary) (observe) (iCode+reason)
  • Handshakes route by vhost (Host/SNI like HTTP); routing failure 404
  • A host exporting neither WsText nor WsBinary → 503 (service exists, business not written)
  • Pong is answered automatically by the xrt layer — the WsPing/WsPong callbacks are for observation only (heartbeat stats)
  • Hard wire boundary during handshake: guards against slow-loris attacks that fill ReadLimit then wait forever

21.3 The send family & echo

An echo service (contract source style)
static void WsOpen(XS_HostInfo* pHost, xwsstream* pWs)
{
    (void)xrtWsStreamText(pWs, xrtStrView("connected"));
}

static void WsText(XS_HostInfo* pHost, xwsstream* pWs, xstrview tText)
{
    (void)xrtWsStreamText(pWs, tText);      /* echo */
}

static void WsBinary(XS_HostInfo* pHost, xwsstream* pWs, xbytesview tData)
{
    (void)xrtWsStreamBinary(pWs, tData);
}

What just happened: the send family is led by xrtWsStreamText / Binary (copying sends); the ...Ref (borrowed send — the buffer must not move before the call returns) and ...Take (ownership transfer, zero-copy for large messages) variants — choose by where the message comes from.

21.4 Connection context

Nearly all WS business needs "per-connection state" (user identity, subscription lists). The recommended approach: a connection registry (your own map: connection pointer → context), inserted in WsOpen, removed in WsClose:

Connection registry skeleton
static xmutex  G_WsLock;
static xmap*   G_WsConns;    /* xwsstream* → ConnCtx* (cross-callback borrowing is yours) */

typedef struct ConnCtx {
    char sUser[64];
    uint64 iLastSeen;
} ConnCtx;

static void WsOpen(XS_HostInfo* pHost, xwsstream* pWs)
{
    ConnCtx* pCtx = xrtCalloc(1, sizeof(ConnCtx));
    /* insert (under lock) ... */
}
static void WsClose(XS_HostInfo* pHost, xwsstream* pWs,
    uint16 iCode, xstrview tReason)
{
    /* remove and free (under lock); WsClose is guaranteed terminal */
}

Note: the host owns the connection's lifetime; you own your context's lifetime — the registry pattern keeps the two exactly in sync. This table is also ch. 22's broadcasting infrastructure.

21.5 Contract

  • Callbacks: the six run on worker threads, concurrently; message views (tText/tData) are valid inside the callback.
  • Terminal: WsClose is each connection's terminal callback — registry cleanup is most reliable here.
  • Sending: Text/Binary copy, Ref borrows, Take transfers — pick the variant by buffer ownership.

Common mistakes

Storing message views in the registry
Symptom
Later reads return garbled data.
Cause
tText/tData are borrowed views valid in the callback — copy before storing (the registry holds your own ConnCtx, never views).
Mutating the buffer right after a Ref send
Symptom
Sporadic garbled message content.
Cause
xrtWsStreamTextRef borrows your buffer — the function returning ≠ sending finished (under backpressure data still sits in the buffer). To reuse immediately, use the copying or Take variant.

Exercises

BasicThe echo triple test

After echo works, send text / binary / a large message (1MB) with any WS client.

Acceptance: all three echo identically; the 1MB message is rejected with 1009 (default limit, tuned in ch. 22).
AdvancedSessions with identity

Convention: the first message is "login:name"; the registry records identity, and every later echo is prefixed "[name] ".

Acceptance: two concurrent sessions never cross; no registry residue after disconnects.
ChallengeA heartbeat watchdog

Use WsPing to track RTT stats; proactively close connections silent for 60 seconds.

Acceptance: a zombie connection (unplugged cable) is swept from the registry within ~60s.

Summary

PointConclusion
Host's jobhandshake validation (400/404/503) + the Attach frame layer
Six callbacksOpen/Text/Binary/Ping/Pong/Close; Pong answered automatically
Send familyText/Binary (copy), Ref (borrow), Take (transfer)
Contextyour own registry: insert on Open, remove on Close (terminal guarantee)
API references in this chapter
xrt further reading