Chapter Orientation

Once the upgrade completes (Chapter 94), what runs on the connection is WebSocket frames — this chapter covers the byte layer's everything.Frame headers: xrtWsFrameWrite writes a frame description into wire bytes, xrtWsFrameParse parses a header back in three states (an incremental model isomorphic with Chapter 72's framing);masking: client-to-server frames must be masked, xrtWsMask does the cyclic exclusive-or by "payload index mod 4" and advances per segment (network fragmentation need not wait for a whole frame);fragment reassembly: one message may split across frames (the FIN flag ends it), control frames may interleave — the xrtWsMessageInit/FrameBegin/Payload/FrameEnd state machine carries incremental UTF-8 validation (a Chinese character split by a frame boundary still gets its legality judged). The masking sample uses RFC 6455 §5.7's canonical example — standard-vector anchoring again.

95.1引入

Why does WebSocket mask? The historical answer: against cache poisoning — back then, intermediate devices cached TCP streams as HTTP, and attackers crafted payload data "looking like HTTP response headers" to poison caches. Masking makes client-emitted data unpredictable (a random 4-byte key per frame), so intermediates cannot mistake frame payloads for protocol text. The spec accordingly rules: client→server must mask, server→client must not — the directional asymmetry is the protocol's hard rule, enforced at parse time by xwsframeconfig's Mask mode (REQUIRED/FORBIDDEN).

Why do messages fragment? Sending a 100 MB message in one frame forces the receiver to see the header first (total length unknown until FIN) before deciding how to process; fragmentation lets the sender produce as a stream (total unknown) and the receiver consume as a stream (no complete-message buffering) — the same motive as HTTP chunked (Chapter 91). The fragmentation protocol: the first frame carries the opcode (TEXT/BINARY), subsequent frames CONTINUATION, the last frame FIN=1;control frames (Ping/Pong/Close) may — and only may — interleave between fragments — heartbeats are not blocked during long-message transfer.

95.2概念

Frame structure and header parsing

One frame = header + payload. The header starts at two bytes: FIN + RSV(3) + Opcode (4 bits) + MASK + length (7 bits; 126 → a following 16-bit, 127 → a following 64-bit extended length); when MASK=1, a 4-byte mask follows. The xwsframe structure describes a frame: Flags (XWS_FRAME_FIN/XWS_FRAME_MASKED/RSV bits — nonzero RSV means an extension was negotiated, Chapter 97 deflate), Opcode (TEXT/BINARY/CONTINUATION/PING/PONG/CLOSE), PayloadSize, Mask.

  • xrtWsFrameWrite(帧, 配置, 输出, 容量, &头长) (frame, config, output, capacity, &header length): writes only the header (the payload is yours to send after) — large payloads pair with Vec sends (Chapter 68).
  • xrtWsFrameParse(输入, &解析, 配置, NULL) (input, &parsed, config, NULL): three states XWS_FRAME_READY/MORE/ERROR — an insufficient header waits for more (incremental); the configured Mask mode enforces the direction rule (a client-direction parser rejects unmasked frames).

Masking: a cyclic exclusive-or advancing per segment

xrtWsMask(数据, 长度, 掩码, 起始下标) (data, length, mask, start index): data[i] ^= mask[(起始+i) % 4] (start) — in place; masking and unmasking are the same function (the operation is self-inverse). The fourth parameter is the elegance: when data arrives in segments, the first runs from 0, the next from "previous segment's length" — the offset continues, no waiting for a whole frame. XWS_MASK_SIZE is always 4.

Fragment reassembly and control-frame bypass

消息开始首帧 FrameBegin(opcode 定类型) →消息中
消息中Payload × N + FrameEnd(该帧完) →消息中
消息中控制帧穿插 → 被状态机旁路(不当消息数据) →消息中
消息中FIN 帧的 FrameEnd(校验全过) →消息完成

xrtWsMessageInit(状态, 可选配置) (state, optional config) builds the state; every frame walks the three steps FrameBegin(帧头) → Payload(负载) → FrameEnd (header, payload). The state machine does four things: fragment chaining (the legality of CONTINUATION opcodes); control-frame bypass (interleaved Ping/Pong/Close are ignored — the caller handles them outside the message state machine); incremental UTF-8 validation (TEXT-message legality judged across frames — when a multi-byte character is split by a frame boundary, the leading part hangs in the state waiting for the trailing part to complete the judgment); the completion callback (the message-complete Info: type/total length). The message config can set a maximum message length — defense against malicious infinite fragmentation.

The three-level comparison: Chapter 72 framing, Chapter 91 HTTP framing, this chapter

Chapter 72 is generic length-prefix/line framing (custom protocols); Chapter 91 is HTTP body framing (three delimittings + smuggling defense); this chapter is WebSocket framing (masking + direction rules + control-frame interleaving + fragment reassembly). The three levels share one design vocabulary: three-state returns, failure-does-not-advance, incremental absorption of sticky and partial packets — having learned the first two levels, this chapter's only new things are the protocol-specific rules.

95.3示例

First complete program: header round trip and segmented unmasking

The program below is from examples/websocket/frame/main.c — server-direction header parsing and the RFC masking sample:

examples/websocket/frame/main.c源码 ↗
#include <stdio.h>
#include <string.h>

#include <xrt.h>






















 */


/* 展示服务端方向的帧头解析和可分片负载解掩码。 */
int main(void)
{
	static const uint8 Mask[XWS_MASK_SIZE] = {
		0x37, 0xFA, 0x21, 0x3D
	};
	xwsframeconfig Config;
	xwsframe Parsed;
	xwsframe Frame;
	xbytesview Input;
	uint8 Head[XWS_FRAME_HEAD_MAX];
	uint8 Payload[] = { 'H', 'e', 'l', 'l', 'o' };
	size_t iHeadSize;

	xrtWsFrameConfigInit(&Config);
	Config.Mask = XWS_MASK_REQUIRED;

	xrtWsFrameInit(&Frame);
	Frame.Flags =
		(uint32)XWS_FRAME_FIN | (uint32)XWS_FRAME_MASKED;
	Frame.Opcode = (uint8)XWS_OPCODE_TEXT;
	Frame.PayloadSize = sizeof(Payload);
	memcpy(Frame.Mask, Mask, sizeof(Mask));

	if ( !xrtWsFrameWrite(
		&Frame, &Config, Head, sizeof(Head), &iHeadSize
	) ) {
		return 1;
	}
	if ( !xrtWsMask(Payload, sizeof(Payload), Mask, 0) ) {
		return 2;
	}

	Input.Data = Head;
	Input.Size = iHeadSize;
	if ( xrtWsFrameParse(
		Input, &Parsed, &Config, NULL
	) != XWS_FRAME_READY ) {
		return 3;
	}
	if ( !xrtWsMask(Payload, 2, Parsed.Mask, 0) ||
		!xrtWsMask(Payload + 2, 3, Parsed.Mask, 2) ) {
		return 4;
	}

	printf(
		"opcode=%u payload=%llu text=%.*s\n",
		(unsigned int)Parsed.Opcode,
		(unsigned long long)Parsed.PayloadSize,
		(int)sizeof(Payload),
		(const char*)Payload
	);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/websocket/frame/main.c -lws2_32 -liphlpapiopcode=1 payload=5 text=Hello

What just happened. (1) A frame is constructed: FIN+MASKED, TEXT, 5-byte payload, mask 37 FA 21 3D (RFC 6455 §5.7's canonical sample); Config.Mask = XWS_MASK_REQUIRED is the server direction (received frames must be masked). (2) FrameWrite produces only the 7-byte header; xrtWsMask masks the 5-byte payload in place. (3) FrameParse parses the header back in three states (READY) — Parsed.Opcode=1 (TEXT), payload length 5, mask restored. (4) Segmented unmasking: the first 2 bytes start from 0, the last 3 from 2 — the continuing offset decodes Hello. This is the proof that "network fragmentation need not wait for a whole frame": the mask stream's position is decided by the cumulative offset, independent of chunk boundaries.

Second complete program: a Chinese character split and control-frame interleaving

The second program is from examples/websocket/message/main.c — fragmentation reassembly's edge case:

examples/websocket/message/main.c源码 ↗
#include <xrt.h>

#include <stdio.h>






















 */


/* 以控制帧可穿插的方式流式接收一个分片文本消息。 */
int main(void)
{
	static const uint8 First[] = { 'A', 0xE4, 0xB8 };
	static const uint8 Last[] = { 0xAD, 'B' };
	xwsmessagestate State;
	xwsmessageinfo Info;
	xwsframe Frame;

	if ( !xrtWsMessageInit(&State, NULL) ) {
		return 1;
	}

	xrtWsFrameInit(&Frame);
	Frame.Opcode = XWS_OPCODE_TEXT;
	Frame.PayloadSize = sizeof(First);
	if ( !xrtWsMessageFrameBegin(&State, &Frame, &Info, NULL) ||
		!xrtWsMessagePayload(
			&State,
			(xbytesview) { First, sizeof(First) },
			NULL
		) ||
		!xrtWsMessageFrameEnd(&State, NULL) ) {
		return 2;
	}

	xrtWsFrameInit(&Frame);
	Frame.Flags = XWS_FRAME_FIN;
	Frame.Opcode = XWS_OPCODE_CONTINUATION;
	Frame.PayloadSize = sizeof(Last);
	if ( !xrtWsMessageFrameBegin(&State, &Frame, &Info, NULL) ||
		!xrtWsMessagePayload(
			&State,
			(xbytesview) { Last, sizeof(Last) },
			NULL
		) ||
		!xrtWsMessageFrameEnd(&State, NULL) ) {
		return 3;
	}
	printf("message complete\n");
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/websocket/message/main.c -lws2_32 -liphlpapimessage complete

What just happened. (1) The split point is deliberately wicked: first frame {A, 0xE4, 0xB8}, last frame {0xAD, B} — the three UTF-8 bytes E4 B8 AD of the sample Chinese character are split in half by the frame boundary. (2) First frame opcode=TEXT (message begins), last frame opcode=CONTINUATION + FIN (message ends) — two FrameBegin/Payload/FrameEnd cycles. (3) The state machine's incremental UTF-8 validation shows itself: at the first frame's end, 0xE4 0xB8 is an incomplete sequence — no error, the state hangs waiting for the next stretch; the last frame supplies 0xAD and it is complete and legal, the message passes (assembling A中B). A per-frame independent validation would misreport here — that is why "cross-frame state" exists. (4) The control-frame interleaving bypass semantics are covered in the test vectors (tests) — the message state machine directly ignores an interleaved Ping frame, leaving TEXT reassembly untouched.

95.4契约

  • Direction rule: client→server must mask, server→client must not; the Mask config (REQUIRED/FORBIDDEN) is enforced at parse time — a direction-violating frame is rejected outright.
  • Header three states: READY/MORE/ERROR; MORE consumes nothing and reports no error; failure does not advance (as Chapters 72/91).
  • Write produces only the header: the payload is sent by the caller (masking your own or advancing per segment); capacity atomic.
  • Mask advancing: xrtWsMask transforms in place, the start index continues — independent of chunk boundaries; XWS_MASK_SIZE=4.
  • Fragmentation protocol: the first frame sets the opcode, subsequent frames CONTINUATION, the last frame FIN; control frames may only interleave between fragments (not embed mid-message as data).
  • Control-frame bypass: the message state machine ignores interleaved control frames — the caller handles Ping/Pong/Close outside the message stream.
  • Incremental UTF-8: TEXT-message legality is judged across frames — multi-byte sequences crossing frame boundaries hang in state awaiting completion; no misreporting.
  • Length defense: the message config caps maximum message length; frame-level PayloadSize over the cap is rejected (a config item).
  • RSV bits: nonzero RSV means an extension (Chapter 97's permessage-deflate) — receiving nonzero RSV without a negotiated extension is a protocol error.
  • Trimming: WEBSOCKET_FRAME/WEBSOCKET_MESSAGE frame and message layers are independent of the Stream layer (Chapter 96) — usable without a transport.

Common Mistakes

Pitfall 1: masking each segment from 0:Pitfall 1: masking each segment from 0

Symptom: with fragmented arrival, the decoded data is "partly right, partly garbled" — the first segment correct, the later ones scrambled, and the scrambling pattern shifts with chunk boundaries.

Cause: the mask cycle's phase is the absolute index into the whole frame's payload. Restarting the second segment from 0 misplaces the phase — the result is systematically wrong.

错误写法
recv(seg1); xrtWsMask(seg1, n1, Mask, 0);
recv(seg2); xrtWsMask(seg2, n2, Mask, 0);   /* phase reset: scrambled */
正确写法
size_t iOff = 0;
recv(seg1); xrtWsMask(seg1, n1, Mask, iOff); iOff += n1;
recv(seg2); xrtWsMask(seg2, n2, Mask, iOff); /* offset continues */
Pitfall 2: feeding the message state machine awry when control frames interleave:Pitfall 2: feeding the message state machine awry when control frames interleave

Symptom: mid-transfer of a long fragmented message a Ping arrives, and the receiver's reassembly reports "unexpected opcode" — the message breaks.

Cause: control-frame interleaving is a protocol feature (heartbeats must not be blocked by big messages). The message state machine is designed to bypass them — but the caller must triage correctly: if the opcode is a control frame, don't feed it to the message state machine (or feed it and trust the bypass — pick one, don't mix).

错误写法
while ( parse_frame(&Frame) == READY ) {
	message_frame_begin(&State, &Frame);  /* Ping fed too: state scrambled */
	...
}
正确写法
while ( parse_frame(&Frame) == READY ) {
	if ( is_control(Frame.Opcode) ) {
		handle_control(&Frame);   /* Ping/Pong/Close on a separate path */
		continue;
	}
	message_frame_begin(&State, &Frame);   /* only data frames enter reassembly */
	...
}
Pitfall 3: per-frame independent UTF-8 validation:Pitfall 3: per-frame independent UTF-8 validation

Symptom: legal messages misreported illegal — precisely when a Chinese character/emoji crosses a frame boundary; or worse, "fixing it" by turning validation off (letting illegal text through).

Cause: UTF-8 is a multi-byte sequence; legality judgment is natively cross-frame. Per-frame validation treats incomplete sequences as illegal; disabling validation accepts illegal text (the protocol requires TEXT to be legal UTF-8).

错误写法
on_frame_payload(seg) {
	if ( !utf8_valid(seg) ) {   /* a frame boundary splits a character: misreport */
		close_protocol_error();
	}
}
正确写法
/* the message state machine has incremental validation built in: legal only when whole after cross-frame assembly */
xrtWsMessagePayload(&State, Seg, NULL);   /* validation is the state machine's business */
/* if anything was illegal, the FrameEnd/Init paths have already errored per protocol */

Programming Exercises

Basic: the four-direction mask matrix

Using the RFC sample mask on Hello, do four splits (1+4, 2+3, 3+2, 4+1): mask per segment then unmask per segment, verifying full restoration. Acceptance criteria: all four splits produce identical output; the start-offset parameters correspond to cumulative lengths.

Advanced: a large-message streaming sender

Implement send_text_streamed(write_fn, 数据, 总长, 块大小) (write_fn, data, total, chunk size): first frame TEXT (no FIN) + N CONTINUATION frames + last frame FIN — each frame FrameWrites the header + write_fn the payload, mocking streaming production. Acceptance criteria: the receiver's message state machine reassembles the complete original (including a multi-byte character crossing frames precisely); chunk sizes 1 and 4096 give identical results.

Challenge: a bare frame-level echo-server core

Without Chapter 96's Stream layer, hand-build the frame loop on Chapter 67's TCP event surface: receive → FrameParse three states → unmask (advancing per segment) → control-frame triage (Ping auto-Ponged) → data frames into message reassembly → complete messages FrameWrite-n back (server direction, unmasked) + the mask rule enforced (unmasked frames rejected). Acceptance criteria: successful text exchange against a browser/wscat; interleaved Pings don't disturb big messages; unmasked frames rejected with Close 1002.

Summary

TopicQuick reference
Frame headerFIN+RSV+Opcode+MASK+length (7/16/64-bit extended); Write produces only the header, capacity atomic
Three-state parsingREADY/MORE/ERROR; MORE waits for data without consuming; the direction rule enforced by the Mask config
Maskingclient→server mandatory, the reverse must not; cyclic exclusive-or mod 4; in-place, self-inverse
Segmented maskingstart index = cumulative payload offset - independent of chunk boundaries; restarting each segment from 0 is the classic error
Fragmentation protocolfirst frame sets the opcode → CONTINUATION×N → FIN terminates; streaming favors both ends
Control-frame interleavingPing/Pong/Close may interleave between fragments; the message state machine bypasses, the caller triages
Incremental UTF-8TEXT legality judged across frames; sequences crossing frames hang in state - per-frame validation misreports, disabling loses the gate
Length defensemessage-level maximum + frame-level cap; against infinite-fragmentation attacks
RSV bitsnonzero = an extension negotiated (Chapter 97 deflate); receiving it un-negotiated is a protocol error
Three-level comparison71 generic -> 90 HTTP delimiting -> 94 WS masking/fragmentation: one incremental vocabulary

📖 API references in this chapter:websocket (86 entries)http (72 entries)

Click for full signatures, parameter constraints, return values and error notes