#WebSocket Parser
#define FIO_WEBSOCKET_PARSER
#include FIO_INCLUDE_FILESmall RFC 6455 frame parser and writer. It keeps 24 bytes of parser state, allocates nothing, unmasks incoming payload bytes in-place, and reports one parse event at a time.
Nearby context: [IO and HTTP overview](./400 io-overview.md), the neighboring [HTTP/1.x parser](./004 http1 parser.md), the higher-level [HTTP module](./439 http.md), and WebSocket compression support in [DEFLATE / Gzip](./162 deflate.md).
#What Gets Added
FIO_WEBSOCKET_PARSER exposes:
fio_websocket_s— incremental parser state.fio_websocket_event_s— one parse event returned byfio_websocket_parse.fio_websocket_init/fio_websocket_reset— parser lifecycle helpers.fio_websocket_parse— incremental parser entry point.- frame writer helpers for server and client data, ping, pong, and close frames.
- WebSocket close code, event type, opcode, RSV, parser state, flag, and error constants.
The implementation also defines helpers named with fio___websocket...; those
are private implementation details.
#Parser State
#fio_websocket_s
typedef struct fio_websocket_s {
uint64_t frame_remaining;
uint32_t mask;
uint32_t frame_consumed;
uint16_t close_code;
uint8_t state;
uint8_t flags;
uint8_t flags2;
uint8_t reserved;
} fio_websocket_s;The parser state is intentionally small and is asserted to fit in 24 bytes. Allocate it wherever the connection state lives, initialize it before use, and prefer the public helper macros below instead of editing fields by hand.
FIO_WEBSOCKET_GET_FIN(p)
FIO_WEBSOCKET_GET_MASKED(p)
FIO_WEBSOCKET_GET_OPCODE(p)
FIO_WEBSOCKET_GET_MSG_OPCODE(p)
FIO_WEBSOCKET_GET_PAUSED(p)
FIO_WEBSOCKET_GET_MSG_RSV(p)GET_OPCODE reports the current frame opcode. GET_MSG_OPCODE reports the
open message opcode (FIO_WEBSOCKET_OP_TEXT, FIO_WEBSOCKET_OP_BINARY, or
0 when no message is open). GET_MSG_RSV reports the RSV bits from the
opening data frame in the same 3-bit format used by the writer API.
#Lifecycle
FIO_IFUNC void fio_websocket_init(fio_websocket_s *p);
FIO_IFUNC void fio_websocket_reset(fio_websocket_s *p);Both helpers clear the struct and set the parser to the header-reading state. After a protocol error or after receiving a close frame, discard or reset the parser before reusing it.
#Events
#fio_websocket_event_s
typedef struct {
uint8_t type;
uint8_t opcode;
uint8_t is_text;
uint8_t rsv;
uint8_t is_first;
uint8_t is_last;
fio_buf_info_s payload;
uint16_t close_code;
} fio_websocket_event_s;payload points into the input buffer passed to fio_websocket_parse. If the
incoming frame is masked, that memory is unmasked in-place before the event is
returned. Keep the input bytes writable and alive until the event payload is no
longer needed.
#Event Types
FIO_WEBSOCKET_EV_NONE
FIO_WEBSOCKET_EV_DATA_CHUNK
FIO_WEBSOCKET_EV_CONTROL
FIO_WEBSOCKET_EV_MESSAGE_END
FIO_WEBSOCKET_EV_ERRORThe current parser reports message data with FIO_WEBSOCKET_EV_DATA_CHUNK and
marks message boundaries with is_first and is_last. Control frames are
reported whole with FIO_WEBSOCKET_EV_CONTROL. Protocol errors are reported as
FIO_WEBSOCKET_EV_ERROR when an event pointer is supplied.
FIO_WEBSOCKET_EV_MESSAGE_END is defined for API completeness, but the current
parse loop uses the is_last flag on data events instead of emitting a separate
message-end event.
#Event Fields
For data events:
opcodeis the current frame opcode. Continuation frames reportFIO_WEBSOCKET_OP_CONT.is_textis true when the open message started as a text message.rsvis copied from the opening data frame.is_firstis true for the first chunk of the opening data frame.is_lastis true for the final chunk of the message.payloadis the available payload chunk, possibly length0.
For control events:
opcodeisFIO_WEBSOCKET_OP_CLOSE,FIO_WEBSOCKET_OP_PING, orFIO_WEBSOCKET_OP_PONG.payloadis the complete control payload, never a partial control frame.- close frames set
close_codeto the on-wire close code, orFIO_WEBSOCKET_CLOSE_NO_STATUSfor an empty close payload.
#Parsing API
FIO_SFUNC size_t fio_websocket_parse(fio_websocket_s *p,
fio_buf_info_s buf,
fio_websocket_event_s *ev);Parses WebSocket bytes from buf and returns the number of bytes consumed, or
FIO_WEBSOCKET_PARSE_ERROR ((size_t)-1) on protocol error.
A successful call may consume fewer bytes than supplied. Feed the remaining
bytes to the next call, usually in a loop. If more bytes are needed before an
event can be produced, the function returns the bytes consumed so far, which can
be 0, and leaves ev->type as FIO_WEBSOCKET_EV_NONE.
The parser is pure state plus the caller-provided buffer:
- no heap allocation;
- no callbacks;
- no retained payload pointers;
- no internal message accumulator;
- no control-frame buffering beyond waiting until a complete control payload is available in the supplied input.
ev may be NULL if the caller only wants to advance or validate input, but
then event details and close/error codes must be read from parser state where
available.
#Parser Flow
header
├─ incomplete header -> wait for more bytes
├─ control frame -> wait for full payload -> CONTROL event
└─ data / continuation -> unmask available bytes -> DATA_CHUNK event
└─ is_last marks message completionThe parser returns after at most one event. For fragmented messages, each data frame may produce one or more data chunks depending on the input buffer splits. Control frames may appear between fragmented data frames and are reported as control events without closing the open message.
#Protocol Checks Performed
The parser rejects:
- unknown opcodes;
- fragmented control frames;
- continuation frames without an open message;
- nested text/binary messages while another data message is open;
- control payloads larger than 125 bytes;
- 64-bit lengths with the high bit set;
- frames larger than
FIO_WEBSOCKET_DEFAULT_MAX_FRAME; - close frames with a 1-byte payload;
- close frames carrying invalid on-wire close codes.
On rejection, the parser state becomes FIO_WEBSOCKET_STATE_ERROR,
p->close_code is set, and fio_websocket_parse returns
FIO_WEBSOCKET_PARSE_ERROR.
Caller policy still includes:
- enforcing client/server masking rules;
- interpreting RSV bits and applying extension transforms such as
permessage-deflate; - enforcing total message size limits;
- validating UTF-8 for text messages;
- accumulating message chunks if whole-message delivery is desired.
#Frame Writers
Server writers produce unmasked frames. Client writers produce masked frames;
passing mask == 0 asks the writer to generate a non-zero PRNG mask.
#Sizing
FIO_IFUNC uint64_t fio_websocket_write_len(uint64_t payload_len, _Bool masked);Returns the number of bytes required for one complete frame with the requested payload length and masking mode. Allocate at least this many bytes before calling a writer.
#Data Messages
FIO_IFUNC uint64_t fio_websocket_write_message_server(void *target,
fio_buf_info_s msg,
_Bool is_text,
uint8_t rsv);
FIO_IFUNC uint64_t fio_websocket_write_message_client(void *target,
fio_buf_info_s msg,
_Bool is_text,
uint32_t mask,
uint8_t rsv);Writes one complete FIN data message to target and returns the bytes written.
is_text selects text (1) or binary (0). rsv is the 3-bit RSV value;
usually pass 0, or FIO_WEBSOCKET_RSV1 for a compressed
permessage-deflate message after applying the extension transform.
#Ping / Pong
FIO_IFUNC uint64_t fio_websocket_write_ping_server(void *target,
fio_buf_info_s payload);
FIO_IFUNC uint64_t fio_websocket_write_ping_client(void *target,
fio_buf_info_s payload,
uint32_t mask);
FIO_IFUNC uint64_t fio_websocket_write_pong_server(void *target,
fio_buf_info_s payload);
FIO_IFUNC uint64_t fio_websocket_write_pong_client(void *target,
fio_buf_info_s payload,
uint32_t mask);Writes one FIN control frame. Keep ping and pong payloads at 125 bytes or less; the writer does not add a separate policy check for that RFC limit.
#Close
FIO_IFUNC uint64_t fio_websocket_write_close_server(void *target,
uint16_t code,
fio_buf_info_s reason);
FIO_IFUNC uint64_t fio_websocket_write_close_client(void *target,
uint16_t code,
fio_buf_info_s reason,
uint32_t mask);Writes a close frame containing the 2-byte close code followed by the optional reason. The reason is truncated so the whole close payload fits in the 125-byte control-frame limit. Choose a close code that is valid to send on the wire.
#Constants
#Opcodes
FIO_WEBSOCKET_OP_CONT
FIO_WEBSOCKET_OP_TEXT
FIO_WEBSOCKET_OP_BINARY
FIO_WEBSOCKET_OP_CLOSE
FIO_WEBSOCKET_OP_PING
FIO_WEBSOCKET_OP_PONG#Close Codes
FIO_WEBSOCKET_CLOSE_OK
FIO_WEBSOCKET_CLOSE_GOING_AWAY
FIO_WEBSOCKET_CLOSE_PROTOCOL_ERROR
FIO_WEBSOCKET_CLOSE_UNSUPPORTED_DATA
FIO_WEBSOCKET_CLOSE_NO_STATUS
FIO_WEBSOCKET_CLOSE_INVALID_PAYLOAD
FIO_WEBSOCKET_CLOSE_POLICY_VIOLATION
FIO_WEBSOCKET_CLOSE_MESSAGE_TOO_BIG
FIO_WEBSOCKET_CLOSE_MANDATORY_EXT
FIO_WEBSOCKET_CLOSE_INTERNAL_ERRORFIO_WEBSOCKET_CLOSE_NO_STATUS is synthesized for an empty received close
payload and must not be sent as an on-wire status code. The parser also accepts
valid registered wire codes such as 1012-1014 and application/library codes in
the 3000-4999 range.
#RSV Bits
FIO_WEBSOCKET_RSV1
FIO_WEBSOCKET_RSV2
FIO_WEBSOCKET_RSV3These are 3-bit values for the writer API and event rsv field. The writer
shifts them into byte-0 bits 4..6 on the wire.
#Parser States and Limits
FIO_WEBSOCKET_STATE_HEADER
FIO_WEBSOCKET_STATE_PAYLOAD
FIO_WEBSOCKET_STATE_CLOSED
FIO_WEBSOCKET_STATE_ERROR
FIO_WEBSOCKET_DEFAULT_MAX_FRAME
FIO_WEBSOCKET_PARSE_ERRORFIO_WEBSOCKET_DEFAULT_MAX_FRAME defaults to 1 GiB and may be overridden before
including the header. FIO_WEBSOCKET_PARSE_ERROR is the parse error sentinel.
The header also exposes flag bit masks used by the accessor macros:
FIO_WEBSOCKET_FLAG_FIN, FIO_WEBSOCKET_FLAG_MASKED,
FIO_WEBSOCKET_FLAG_OPCODE_MASK, FIO_WEBSOCKET_FLAG_OPCODE_SHIFT,
FIO_WEBSOCKET_FLAG_MSG_OPCODE_MASK, FIO_WEBSOCKET_FLAG2_PAUSED,
FIO_WEBSOCKET_FLAG2_MSG_RSV_MASK, and
FIO_WEBSOCKET_FLAG2_MSG_RSV_SHIFT.
#Minimal Parse Loop
#define FIO_WEBSOCKET_PARSER
#include FIO_INCLUDE_FILE
typedef struct {
fio_websocket_s ws;
} connection_s;
void websocket_consume(connection_s *c, char *data, size_t len) {
fio_buf_info_s buf = FIO_BUF_INFO2(data, len);
while (buf.len) {
fio_websocket_event_s ev;
size_t n = fio_websocket_parse(&c->ws, buf, &ev);
if (n == FIO_WEBSOCKET_PARSE_ERROR) {
/* Send/record c->ws.close_code and close the connection. */
return;
}
buf.buf += n;
buf.len -= n;
if (!n && ev.type == FIO_WEBSOCKET_EV_NONE)
break; /* wait for more network bytes */
switch (ev.type) {
case FIO_WEBSOCKET_EV_DATA_CHUNK:
/* ev.payload is already unmasked and points into data. */
if (ev.is_first) {
/* start a message accumulator, if needed */
}
if (ev.payload.len) {
/* copy/process this chunk before reusing data */
}
if (ev.is_last) {
/* finish the message */
}
break;
case FIO_WEBSOCKET_EV_CONTROL:
if (ev.opcode == FIO_WEBSOCKET_OP_PING) {
char out[128 + 14];
uint64_t written = fio_websocket_write_pong_server(out, ev.payload);
(void)written; /* write out to the socket */
} else if (ev.opcode == FIO_WEBSOCKET_OP_CLOSE) {
/* Echo/close according to local policy. */
return;
}
break;
default:
break;
}
}
}
void websocket_open(connection_s *c) {
fio_websocket_init(&c->ws);
}For real IO, preserve any unconsumed bytes and retry when more data arrives. Copy payload bytes before recycling the read buffer or applying asynchronous message handling.
#Minimal Write Example
char out[14 + 1024];
fio_buf_info_s msg = FIO_BUF_INFO2("hello", 5);
uint64_t len = fio_websocket_write_message_server(out, msg, 1, 0);
/* write `len` bytes from out */For client frames:
uint64_t len = fio_websocket_write_message_client(out, msg, 1, 0, 0);The fourth argument is an explicit mask; 0 lets the writer choose one.
#Ownership and Lifetime
- The parser owns only
fio_websocket_sstate supplied by the caller. - The parser never allocates, frees, stores callbacks, or retains payload pointers after returning.
- Input buffers passed to
fio_websocket_parsemust be writable because masked payloads are modified in-place. - Event payload slices are valid only while the input buffer remains valid and unchanged.
- Writer targets must be large enough for
fio_websocket_write_len(payload.len, masked)bytes. - Independent parser instances can be used concurrently by different threads; synchronize shared buffers and connection state in the caller.