facil.io

#WebSocket Parser

c
#define FIO_WEBSOCKET_PARSER
#include FIO_INCLUDE_FILE

Small 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 by fio_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

c
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.

c
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

c
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

c
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

c
FIO_WEBSOCKET_EV_NONE
FIO_WEBSOCKET_EV_DATA_CHUNK
FIO_WEBSOCKET_EV_CONTROL
FIO_WEBSOCKET_EV_MESSAGE_END
FIO_WEBSOCKET_EV_ERROR

The 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:

  • opcode is the current frame opcode. Continuation frames report FIO_WEBSOCKET_OP_CONT.
  • is_text is true when the open message started as a text message.
  • rsv is copied from the opening data frame.
  • is_first is true for the first chunk of the opening data frame.
  • is_last is true for the final chunk of the message.
  • payload is the available payload chunk, possibly length 0.

For control events:

  • opcode is FIO_WEBSOCKET_OP_CLOSE, FIO_WEBSOCKET_OP_PING, or FIO_WEBSOCKET_OP_PONG.
  • payload is the complete control payload, never a partial control frame.
  • close frames set close_code to the on-wire close code, or FIO_WEBSOCKET_CLOSE_NO_STATUS for an empty close payload.

#Parsing API

c
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

text
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 completion

The 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

c
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

c
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

c
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

c
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

c
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

c
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_ERROR

FIO_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

c
FIO_WEBSOCKET_RSV1
FIO_WEBSOCKET_RSV2
FIO_WEBSOCKET_RSV3

These 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

c
FIO_WEBSOCKET_STATE_HEADER
FIO_WEBSOCKET_STATE_PAYLOAD
FIO_WEBSOCKET_STATE_CLOSED
FIO_WEBSOCKET_STATE_ERROR

FIO_WEBSOCKET_DEFAULT_MAX_FRAME
FIO_WEBSOCKET_PARSE_ERROR

FIO_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

c
#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

c
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:

c
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_s state supplied by the caller.
  • The parser never allocates, frees, stores callbacks, or retains payload pointers after returning.
  • Input buffers passed to fio_websocket_parse must 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.