DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
websocket.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file websocket.cpp
6 * @brief WebSocket frame parser and connection pool implementation.
7 *
8 * Handles RFC 6455 framing. Control frames (ping/pong/close) are handled
9 * automatically here; data frames (text/binary) are surfaced to the
10 * application layer via WsParseState::WS_FRAME_READY.
11 *
12 * **Automatic control frame handling**
13 * - Ping -> sends Pong with the same payload immediately.
14 * - Close -> sends echoed Close frame, marks slot WsParseState::WS_CLOSED.
15 * - Pong -> silently discarded (keepalive response, no action needed).
16 */
17
18#include "websocket.h"
21#include <string.h>
22
23#if DETWS_ENABLE_WS_DEFLATE
27#endif
28
30
31void ws_init()
32{
33 for (int i = 0; i < MAX_WS_CONNS; i++)
34 {
35 ws_pool[i] = {};
36 ws_pool[i].ws_id = (uint8_t)i;
37 }
38}
39
40bool ws_active(uint8_t ws_id)
41{
42 return ws_id < MAX_WS_CONNS && ws_pool[ws_id].active;
43}
44
45const char *ws_payload(uint8_t ws_id)
46{
47 return (ws_id < MAX_WS_CONNS && ws_pool[ws_id].active) ? (const char *)ws_pool[ws_id].buf : nullptr;
48}
49
50WsConn *ws_alloc(uint8_t slot_id)
51{
52 for (int i = 0; i < MAX_WS_CONNS; i++)
53 {
54 if (!ws_pool[i].active)
55 {
56 ws_pool[i] = {};
57 ws_pool[i].ws_id = (uint8_t)i;
58 ws_pool[i].slot_id = slot_id;
59 ws_pool[i].active = true;
61 return &ws_pool[i];
62 }
63 }
64 return nullptr;
65}
66
67WsConn *ws_find(uint8_t slot_id)
68{
69 for (int i = 0; i < MAX_WS_CONNS; i++)
70 {
71 if (ws_pool[i].active && ws_pool[i].slot_id == slot_id)
72 return &ws_pool[i];
73 }
74 return nullptr;
75}
76
77void ws_free(uint8_t slot_id)
78{
79 for (int i = 0; i < MAX_WS_CONNS; i++)
80 {
81 if (ws_pool[i].active && ws_pool[i].slot_id == slot_id)
82 {
83 ws_pool[i] = {};
84 ws_pool[i].ws_id = (uint8_t)i;
85 return;
86 }
87 }
88}
89
90// Reset only the per-frame parser fields, preserving any in-progress
91// fragmented-message state (msg_len/msg_opcode/fragmenting/buf). Used to
92// resume reading the next frame after handling an interleaved control frame.
93static void ws_reset_perframe(WsConn *ws)
94{
97 ws->fin = false;
98 ws->masked = false;
99 ws->payload_len = 0;
100 ws->payload_idx = 0;
101 ws->len64_count = 0;
102 ws->mask_key[0] = ws->mask_key[1] = ws->mask_key[2] = ws->mask_key[3] = 0;
103}
104
106{
107 ws_reset_perframe(ws);
108 // Also clear reassembly state - a full reset between messages.
109 ws->fragmenting = false;
111 ws->msg_len = 0;
112 ws->buf[0] = '\0';
113 ws->ctl_buf[0] = '\0';
114}
115
116// ---------------------------------------------------------------------------
117// Frame send helpers
118// ---------------------------------------------------------------------------
119
120// WebSocket presentation config, owned by one instance (internal linkage): the outbound
121// fragmentation size (RFC 6455 sec 5.4), payload bytes; 0 = one frame per message (default).
122// One named owner, unreachable cross-TU. (The ws_pool[] table is the shared substrate.)
123struct WsCtx
124{
126};
127static WsCtx s_ws;
128void ws_set_frag_size(uint16_t bytes)
129{
130 s_ws.frag_size = bytes;
131}
132
133// Emit one WebSocket frame. b0 is the finished first header byte (FIN | RSV1 | opcode). Server frames
134// are never masked (RFC 6455 sec 5.1). Returns false if a transport send fails.
135static bool ws_emit_one(TcpConn *conn, uint8_t b0, const uint8_t *payload, uint16_t len)
136{
137 uint8_t header[4];
138 uint8_t hlen;
139 header[0] = b0;
140 if (len <= 125)
141 {
142 header[1] = (uint8_t)len;
143 hlen = 2;
144 }
145 else
146 {
147 header[1] = 126;
148 header[2] = (uint8_t)(len >> 8);
149 header[3] = (uint8_t)len;
150 hlen = 4;
151 }
152 if (!det_conn_send(conn->id, header, hlen))
153 return false;
154 if (len > 0 && payload && !det_conn_send(conn->id, payload, len))
155 return false;
156 return true;
157}
158
159bool ws_send_frame(WsConn *ws, WsOpcode opcode, const uint8_t *payload, uint16_t len)
160{
161 TcpConn *conn = &conn_pool[ws->slot_id];
162 if (!det_conn_active(ws->slot_id))
163 return false;
164
165 uint8_t rsv1 = 0; // permessage-deflate per-message "compressed" flag (RFC 7692)
166
167#if DETWS_ENABLE_WS_DEFLATE
168 // Compress data frames when permessage-deflate is negotiated. Control frames
169 // (close/ping/pong) are never compressed (RFC 7692 sec 5.1). Scratch + output
170 // are borrowed from the per-dispatch arena and released when this scope exits;
171 // det_conn_send copies (TCP_WRITE_FLAG_COPY) so the buffer can go immediately.
172 ScratchScope scope;
173 if (ws->pmd && len > 0 && (opcode == WsOpcode::WS_OP_TEXT || opcode == WsOpcode::WS_OP_BINARY))
174 {
175 size_t cap = (size_t)len + len / 8 + 16; // static-Huffman worst-case headroom
176 void *scr = scratch_alloc(DEFLATE_SCRATCH_SIZE, 16);
177 uint8_t *cbuf = (uint8_t *)scratch_alloc(cap, 1);
178 if (scr && cbuf)
179 {
180 size_t clen = 0;
181 DeflateResult rc = deflate_raw(payload, len, cbuf, cap, &clen, scr, DEFLATE_SCRATCH_SIZE);
182 // Only adopt it if it actually shrank the message; otherwise send it
183 // uncompressed (the per-message RSV1 flag makes that legal).
184 if (rc == DeflateResult::DEFLATE_OK && clen < len)
185 {
186 payload = cbuf;
187 len = (uint16_t)clen;
188 rsv1 = 0x40;
189 }
190 }
191 }
192#endif
193
194 // Fragment only data frames (RFC 6455 §5.4: control frames MUST NOT be fragmented, and are small
195 // anyway). frag == 0, a non-data frame, or a message that already fits -> a single FIN frame (the
196 // default, unchanged). Server-to-client frames are never masked (§5.1).
197 bool data = (opcode == WsOpcode::WS_OP_TEXT || opcode == WsOpcode::WS_OP_BINARY);
198 uint16_t frag = s_ws.frag_size;
199 if (!data || frag == 0 || len <= frag)
200 return ws_emit_one(conn, (uint8_t)(0x80 | rsv1 | (uint8_t)opcode), payload, len);
201
202 // Split into <= frag-byte frames: the opcode (+ RSV1) rides the first frame, the rest are
203 // CONTINUATION, and FIN marks the last. The compressed bytes (RFC 7692) are split as-is - the peer
204 // concatenates the fragment payloads back into one stream before inflating.
205 uint16_t off = 0;
206 bool first = true;
207 while (off < len)
208 {
209 uint16_t chunk = (uint16_t)(len - off) < frag ? (uint16_t)(len - off) : frag;
210 bool last = (uint16_t)(off + chunk) >= len;
211 uint8_t b0 = (uint8_t)((last ? 0x80 : 0x00) |
212 (first ? (rsv1 | (uint8_t)opcode) : (uint8_t)WsOpcode::WS_OP_CONTINUATION));
213 if (!ws_emit_one(conn, b0, payload + off, chunk))
214 return false;
215 off = (uint16_t)(off + chunk);
216 first = false;
217 }
218 return true;
219}
220
222{
223 // Send Close frame with 2-byte status code payload
224 uint8_t payload[2] = {(uint8_t)((uint16_t)code >> 8), (uint8_t)code};
225 ws_send_frame(ws, WsOpcode::WS_OP_CLOSE, payload, 2);
226
227 if (det_conn_active(ws->slot_id))
229
231}
232
233// ---------------------------------------------------------------------------
234// Frame parser
235// ---------------------------------------------------------------------------
236
237// RFC 6455 §5.5: opcodes 0x8 (close), 0x9 (ping), 0xA (pong) are control frames.
238static inline bool ws_is_control(WsOpcode op)
239{
240 return ((uint8_t)op & 0x08) != 0;
241}
242
243// Called once a frame's full payload has been received (payload_idx ==
244// payload_len, also true immediately for zero-length frames once the masking
245// key is consumed). Control frames are handled in place; data frames are
246// reassembled and delivered as WsParseState::WS_FRAME_READY only when the FIN frame arrives.
247static void ws_finish_frame(WsConn *ws, TcpConn *conn)
248{
249 // ---- Control frames (ping/pong/close): use the separate ctl_buf ----
250 if (ws_is_control(ws->opcode))
251 {
252 size_t n = ws->payload_idx < sizeof(ws->ctl_buf) - 1 ? ws->payload_idx : sizeof(ws->ctl_buf) - 1;
253 ws->ctl_buf[n] = '\0';
254
255 if (ws->opcode == WsOpcode::WS_OP_PING)
256 {
257 ws_send_frame(ws, WsOpcode::WS_OP_PONG, ws->ctl_buf, (uint16_t)ws->payload_idx);
258 if (det_conn_active(conn->id))
259 det_conn_flush(conn->id);
260 }
261 else if (ws->opcode == WsOpcode::WS_OP_CLOSE)
262 {
264 return;
265 }
266 // PONG: silently ignored.
267
268 // Resume reading the next frame, keeping any partial data message.
269 ws_reset_perframe(ws);
270 return;
271 }
272
273 // ---- Data frames (text/binary/continuation): reassemble into buf ----
274 ws->msg_len += ws->payload_idx;
275
276 if (ws->fin)
277 {
278#if DETWS_ENABLE_WS_DEFLATE
279 // permessage-deflate: decompress the reassembled message before delivery.
280 // The compressed bytes are in ws->buf; append the RFC 7692 00 00 ff ff
281 // marker, INFLATE into an arena buffer, and copy the result back. All
282 // scratch is borrowed per-dispatch and released when this scope exits.
283 if (ws->msg_compressed)
284 {
285 ScratchScope scope;
286 size_t comp_len = ws->msg_len;
287 uint8_t *in = (uint8_t *)scratch_alloc(comp_len + 4, 1);
288 uint8_t *out = (uint8_t *)scratch_alloc(WS_FRAME_SIZE, 1);
289 uint8_t *tbl = (uint8_t *)scratch_alloc(INFLATE_SCRATCH_SIZE, 16);
290 if (!in || !out || !tbl)
291 {
292 ws_close(ws, WsCloseCode::WS_CLOSE_PROTOCOL); // arena exhausted: fail closed
294 return;
295 }
296 memcpy(in, ws->buf, comp_len);
297 in[comp_len] = 0x00;
298 in[comp_len + 1] = 0x00;
299 in[comp_len + 2] = 0xff;
300 in[comp_len + 3] = 0xff;
301 size_t dlen = 0;
302 InflateResult rc = inflate_raw(in, comp_len + 4, out, WS_FRAME_SIZE, &dlen, tbl, INFLATE_SCRATCH_SIZE);
303 if (rc == InflateResult::INFLATE_ERR_OVERFLOW)
304 {
307 return;
308 }
309 if (rc != InflateResult::INFLATE_OK)
310 {
313 return;
314 }
315 memcpy(ws->buf, out, dlen);
316 ws->msg_len = dlen;
317 ws->msg_compressed = false;
318 }
319#endif
320 // Whole message received - surface it to the application.
321 size_t n = ws->msg_len < WS_FRAME_SIZE ? ws->msg_len : WS_FRAME_SIZE;
322 // RFC 6455 8.1: a TEXT message MUST be valid UTF-8 (checked on the fully
323 // reassembled + decompressed message); otherwise fail the connection with 1007.
324 if (ws->msg_opcode == WsOpcode::WS_OP_TEXT && !det_utf8_valid(ws->buf, n))
325 {
328 return;
329 }
330 ws->buf[n] = '\0';
331 ws->opcode = ws->msg_opcode; // report the original TEXT/BINARY opcode
332 ws->payload_len = ws->msg_len; // app reads payload_len / payload_idx
333 ws->payload_idx = ws->msg_len;
334 ws->fragmenting = false;
336 }
337 else
338 {
339 // More fragments to come; keep buf and msg_len, read the next frame.
340 ws->fragmenting = true;
341 ws_reset_perframe(ws);
342 }
343}
344
346{
347 if (!det_conn_active(ws->slot_id))
348 return;
349
350 while (det_conn_available(ws->slot_id) > 0)
351 {
352 // Stop if we hit a terminal state (leave the rest in the ring)
355 return;
356
357 uint8_t byte = 0;
358 if (!det_conn_read_byte(ws->slot_id, &byte)) // ring drained between available() and here
359 break;
360 ws_feed_byte(ws, byte);
361 }
362}
363
364void ws_feed_byte(WsConn *ws, uint8_t byte)
365{
366 TcpConn *conn = &conn_pool[ws->slot_id];
367 {
368 switch (ws->parse_state)
369 {
371 ws->fin = (byte & 0x80) != 0;
372 // RSV bits are validated below, once the opcode / message position is
373 // known (RSV1 is permessage-deflate's per-message "compressed" flag).
374 uint8_t rsv = byte & 0x70;
375 ws->opcode = static_cast<WsOpcode>(byte & 0x0F);
376 // RFC 6455 §5.2: only opcodes 0x0/0x1/0x2 (data) and 0x8/0x9/0xA
377 // (control) are defined; everything else MUST fail the connection.
378 switch (ws->opcode)
379 {
386 break;
387 default:
390 return;
391 }
392 // RFC 6455 §5.5: control frames MUST NOT be fragmented (FIN set).
393 if (ws_is_control(ws->opcode) && !ws->fin)
394 {
397 return;
398 }
399 // RFC 6455 §5.4: fragmentation sequencing for data frames.
400 if (!ws_is_control(ws->opcode))
401 {
403 {
404 // A continuation with no message in progress is illegal.
405 if (!ws->fragmenting)
406 {
409 return;
410 }
411 }
412 else
413 {
414 // A new text/binary frame while a message is still open is
415 // illegal - the previous message must finish first.
416 if (ws->fragmenting)
417 {
420 return;
421 }
422 // Start of a new data message.
423 ws->msg_opcode = ws->opcode;
424 ws->msg_len = 0;
425#if DETWS_ENABLE_WS_DEFLATE
426 // RSV1 on the first frame of a data message marks it compressed
427 // (RFC 7692); only honored when permessage-deflate was negotiated.
428 ws->msg_compressed = ws->pmd && (rsv & 0x40);
429#endif
430 }
431 }
432 // Validate reserved bits. RSV2/RSV3 are never legal; RSV1 is legal only
433 // as the per-message compression flag set above (pmd + new data frame).
434#if DETWS_ENABLE_WS_DEFLATE
435 {
436 bool new_data = !ws_is_control(ws->opcode) && ws->opcode != WsOpcode::WS_OP_CONTINUATION;
437 if ((rsv & 0x30) || ((rsv & 0x40) && !(ws->pmd && new_data)))
438 {
441 return;
442 }
443 }
444#else
445 if (rsv)
446 {
449 return;
450 }
451#endif
453 break;
454 }
455
457 ws->masked = (byte & 0x80) != 0;
458 // RFC 6455 §5.1: every client-to-server frame MUST be masked.
459 if (!ws->masked)
460 {
463 return;
464 }
465 {
466 uint8_t len7 = byte & 0x7F;
467 // RFC 6455 §5.5: control frames MUST have payload length <= 125.
468 if (ws_is_control(ws->opcode) && len7 > 125)
469 {
472 return;
473 }
474 if (len7 <= 125)
475 {
476 // Masking is mandatory, so always consume the 4 mask bytes
477 // next - even for zero-length frames (WsParseState::WS_MASK3 finishes them).
478 ws->payload_len = len7;
479 // Reassembled data message must fit in WS_FRAME_SIZE.
480 if (!ws_is_control(ws->opcode) && ws->msg_len + ws->payload_len > WS_FRAME_SIZE)
481 {
484 return;
485 }
487 }
488 else if (len7 == 126)
489 {
490 ws->payload_len = 0;
492 }
493 else
494 {
495 // 64-bit length -- always too large
496 ws->len64_count = 0;
498 }
499 }
500 break;
501
503 ws->payload_len = (uint32_t)byte << 8;
505 break;
506
508 ws->payload_len |= byte;
509 // 16-bit length only occurs on data frames (control frames are
510 // capped at 125); the reassembled message must fit WS_FRAME_SIZE.
511 if (ws->msg_len + ws->payload_len > WS_FRAME_SIZE)
512 {
515 return;
516 }
517 // Masking is mandatory; consume the 4 mask bytes next.
519 break;
520
522 // Consume all 8 bytes then reject
523 if (++ws->len64_count == 8)
524 {
527 return;
528 }
529 break;
530
532 ws->mask_key[0] = byte;
534 break;
536 ws->mask_key[1] = byte;
538 break;
540 ws->mask_key[2] = byte;
542 break;
544 ws->mask_key[3] = byte;
545 if (ws->payload_len > 0)
547 else
548 ws_finish_frame(ws, conn); // zero-length frame is complete now
549 break;
550
552 // Mask is applied per frame, so the keystream index is the
553 // within-frame position.
554 uint8_t unmasked = byte ^ ws->mask_key[ws->payload_idx % 4];
555 if (ws_is_control(ws->opcode))
556 {
557 // Control payload goes to its own buffer so it never disturbs
558 // a partially-assembled data message.
559 if (ws->payload_idx < sizeof(ws->ctl_buf) - 1)
560 ws->ctl_buf[ws->payload_idx] = unmasked;
561 }
562 else
563 {
564 // Data payload appends after any earlier fragments.
565 uint32_t pos = ws->msg_len + ws->payload_idx;
566 if (pos < WS_FRAME_SIZE)
567 ws->buf[pos] = unmasked;
568 }
569 ws->payload_idx++;
570
571 if (ws->payload_idx >= ws->payload_len)
572 ws_finish_frame(ws, conn);
573 break;
574 }
575
576 default:
577 break;
578 }
579 }
580}
#define DETWS_WS_FRAG_SIZE
WebSocket outbound fragmentation size (RFC 6455 sec 5.4), in payload bytes. 0 = off.
#define WS_FRAME_SIZE
Maximum WebSocket frame payload in bytes.
#define MAX_WS_CONNS
Maximum simultaneous WebSocket connections.
RAII scope guard for transient scratch borrows.
Definition scratch.h:110
Bounded RFC 1951 DEFLATE compressor (DEFLATE) - no heap.
Bounded RFC 1951 DEFLATE decompressor (INFLATE) - no heap.
void * scratch_alloc(size_t n, size_t align)
Borrow n bytes of scratch, aligned to align.
Definition scratch.cpp:85
Shared per-dispatch scratch arena (Layer 5, session-scoped memory).
A single TCP connection context.
Definition tcp.h:72
uint8_t id
Fixed slot index (0 … MAX_CONNS-1).
Definition tcp.h:73
WebSocket connection state stored in ws_pool[].
Definition websocket.h:125
bool active
True when this entry is in use.
Definition websocket.h:128
uint8_t len64_count
Bytes consumed from 64-bit length.
Definition websocket.h:138
uint8_t mask_key[4]
Client masking key.
Definition websocket.h:135
uint32_t payload_len
Expected payload byte count (current frame).
Definition websocket.h:136
uint32_t msg_len
Bytes assembled so far across all fragments.
Definition websocket.h:147
uint8_t ws_id
Index into ws_pool[] (set at init).
Definition websocket.h:126
bool fin
FIN bit of the frame being parsed.
Definition websocket.h:132
bool masked
True if client sent a masking key.
Definition websocket.h:133
uint8_t slot_id
Owning TCP slot in conn_pool[].
Definition websocket.h:127
WsOpcode msg_opcode
Opcode of the data message being assembled.
Definition websocket.h:146
uint32_t payload_idx
Bytes received so far (current frame).
Definition websocket.h:137
WsParseState parse_state
Current frame parser state.
Definition websocket.h:130
WsOpcode opcode
Opcode of the frame being parsed.
Definition websocket.h:131
bool fragmenting
True between a non-FIN data frame and its FIN.
Definition websocket.h:145
uint8_t buf[WS_FRAME_SIZE+1]
Reassembled message payload, null-terminated.
Definition websocket.h:139
uint8_t ctl_buf[125+1]
Control-frame payload (ping/pong/close), null-terminated.
Definition websocket.h:148
uint16_t frag_size
bool det_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:362
void det_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
Definition tcp.cpp:413
TcpConn conn_pool[CONN_POOL_SLOTS]
Static pool of connection contexts. Defined in tcp.cpp. Sized CONN_POOL_SLOTS: MAX_CONNS TCP slots pl...
Definition tcp.cpp:347
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
Strict UTF-8 validation (RFC 3629), one shared copy.
bool det_utf8_valid(const uint8_t *s, size_t n)
True if [s, s+n) is well-formed UTF-8.
Definition utf8.h:29
void ws_free(uint8_t slot_id)
Free the WsConn associated with a TCP slot.
Definition websocket.cpp:77
bool ws_send_frame(WsConn *ws, WsOpcode opcode, const uint8_t *payload, uint16_t len)
Send a WebSocket frame to the client.
const char * ws_payload(uint8_t ws_id)
The NUL-terminated reassembled message payload for ws_id, or nullptr if the slot is out of range / in...
Definition websocket.cpp:45
void ws_feed_byte(WsConn *ws, uint8_t byte)
Feed one already-plaintext byte through the WS frame state machine.
void ws_close(WsConn *ws, WsCloseCode code)
Send a Close frame and mark the slot WsParseState::WS_CLOSED.
void ws_parse(WsConn *ws)
Drain the ring buffer for slot_id and feed bytes to the WS parser.
WsConn * ws_find(uint8_t slot_id)
Find the WsConn for a given TCP slot, or nullptr if none.
Definition websocket.cpp:67
void ws_reset_frame(WsConn *ws)
Reset the frame parser back to WsParseState::WS_HEADER1, ready for the next frame.
void ws_set_frag_size(uint16_t bytes)
Set the outbound fragmentation size (RFC 6455 sec 5.4), in payload bytes; 0 = off.
bool ws_active(uint8_t ws_id)
True if ws_id is a valid, in-use WebSocket slot. Use this instead of reaching into ws_pool[ws_id]....
Definition websocket.cpp:40
WsConn * ws_alloc(uint8_t slot_id)
Allocate a WsConn slot and bind it to a TCP slot.
Definition websocket.cpp:50
WsConn ws_pool[MAX_WS_CONNS]
Pool of WebSocket connection state, one per MAX_WS_CONNS.
Definition websocket.cpp:29
void ws_init()
Initialize all WebSocket pool slots to inactive.
Definition websocket.cpp:31
Layer 6 (Presentation) – WebSocket frame parser and connection pool.
WsCloseCode
WebSocket close status codes (RFC 6455 §7.4.1).
Definition websocket.h:83
@ WS_CLOSE_INVALID_PAYLOAD
Text message that is not valid UTF-8 (RFC 6455 8.1).
@ WS_CLOSE_PROTOCOL
Protocol error.
@ WS_CLOSE_NORMAL
Normal closure.
@ WS_CLOSE_TOO_BIG
Payload too large for WS_FRAME_SIZE.
@ WS_MASK3
Reading masking key byte 3.
@ WS_PAYLOAD
Accumulating payload bytes.
@ WS_FRAME_READY
Complete frame ready for dispatch.
@ WS_LEN16_LO
Reading extended 16-bit length, low byte.
@ WS_ERROR
Protocol error; close frame has been queued.
@ WS_HEADER2
Awaiting second header byte (MASK, 7-bit length).
@ WS_MASK2
Reading masking key byte 2.
@ WS_LEN64
Consuming 8-byte 64-bit length (always rejected).
@ WS_LEN16_HI
Reading extended 16-bit length, high byte.
@ WS_MASK0
Reading masking key byte 0.
@ WS_CLOSED
Connection closed; slot may be recycled.
@ WS_MASK1
Reading masking key byte 1.
@ WS_HEADER1
Awaiting first header byte (FIN, RSV, opcode).
WsOpcode
WebSocket frame opcodes.
Definition websocket.h:72
@ WS_OP_CLOSE
Connection close.
@ WS_OP_PING
Ping (auto-ponged by the library).
@ WS_OP_CONTINUATION
Continuation frame (data-message fragment; reassembled into buf).
@ WS_OP_PONG
Pong (echoed ping; ignored by library).
@ WS_OP_TEXT
UTF-8 text payload.
@ WS_OP_BINARY
Binary payload.