Ориентация главы

После завершения обновления (глава 94) по соединению бегут кадры WebSocket — эта глава рассказывает всё байтового слоя.Заголовки кадров: xrtWsFrameWrite пишет описание кадра в проволочные байты, xrtWsFrameParse разбирает заголовок в трёх состояниях (инкрементная модель, изоморфная кадрированию главы 72);маскирование: кадры клиент→сервер обязаны быть замаскированы, xrtWsMask делает циклическое «исключающее-или» по «индекс нагрузки mod 4» и продвигается по сегментам (сетевая фрагментация не ждёт целого кадра);пересборка фрагментов: одно сообщение может разбиться на кадры (флаг FIN завершает), контрольные кадры разрешено вплетать — машина состояний xrtWsMessageInit/FrameBegin/Payload/FrameEnd несёт инкрементальную проверку UTF-8 (китайский иероглиф, разрубленный границей кадра, всё равно судится по закону). Образец маскирования использует канонический пример RFC 6455 §5.7 — снова привязка к стандартному вектору.

95.1引入

Зачем WebSocket маскирует? Исторический ответ: против отравления кешей — были устройства, кеширующие TCP-поток как HTTP; атакующий мастерил данные нагрузки «под HTTP-заголовки ответа» и травил кеш. Маскирование делает исходящие от клиента данные непредсказуемыми (случайный 4-байтовый ключ на кадр), и промежуточные устройства не могут принять полезную нагрузку кадра за протокольный текст. Спецификация потому постановляет: клиент→сервер обязан маскировать, сервер→клиент обязан не маскировать — асимметрия направлений это жёсткое правило протокола, и режим Mask конфигурации xwsframeconfig (REQUIRED/FORBIDDEN) принудительно исполняет его при разборе.

Зачем сообщения фрагментируются? Сообщение на 100 МБ одним кадром заставляет приёмник сперва увидеть заголовок (общая длина неизвестна до FIN) и лишь потом решить, как обрабатывать; фрагментация позволяет отправителю производить потоком (не зная объёма), а приёмнику потреблять потоком (не копя целое сообщение) — та же мотивация, что у HTTP chunked (глава 91). Протокол фрагментации: первый кадр несёт opcode (TEXT/BINARY), последующие — CONTINUATION, последний — FIN=1;контрольные кадры (Ping/Pong/Close) разрешено и только разрешено вплетать между фрагментами — сердцебиение не блокируется передачей длинного сообщения.

95.2概念

Структура кадра и разбор заголовка

Кадр = заголовок + нагрузка. Заголовок от двух байтов: FIN + RSV(3) + Opcode (4 бита) + MASK + длина (7 бит; 126 → далее 16-битная, 127 → 64-битная расширенная); при MASK=1 следуют ещё 4 байта маски. Структура xwsframe описывает кадр: Flags (XWS_FRAME_FIN/XWS_FRAME_MASKED/биты RSV — ненулевой RSV означает согласованные расширения, deflate главы 97), Opcode (TEXT/BINARY/CONTINUATION/PING/PONG/CLOSE), PayloadSize, Mask.

  • xrtWsFrameWrite(帧, 配置, 输出, 容量, &头长) (кадр, конфигурация, вывод, ёмкость, &длина заголовка): пишет только заголовок (нагрузка отправляется следом сама) — большая нагрузка в паре с Vec-отправкой (глава 68).
  • xrtWsFrameParse(输入, &解析, 配置, NULL) (вход, &разбор, конфигурация, NULL): три состояния XWS_FRAME_READY/MORE/ERROR — нехватка заголовка ждёт данных (инкрементно); режим Mask конфигурации принуждает правило направления (парсер клиентского направления отвергает незамаскированные кадры).

Маскирование: циклическое «исключающее-или», продвигаемое по сегментам

xrtWsMask(数据, 长度, 掩码, 起始下标) (данные, длина, маска, стартовый индекс): data[i] ^= mask[(起始+i) % 4] — на месте; маскирование и демаскирование одна функция (операция самообратна). Четвёртый параметр — тонкость: данные приходят сегментами — первый с 0, следующий со «длины предыдущего» — смещение продолжается, ждать целого кадра не нужно. XWS_MASK_SIZE всегда 4.

Пересборка фрагментов и обход контрольных кадров

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

xrtWsMessageInit(状态, 可选配置) (состояние, опциональная конфигурация) строит состояние; каждый кадр проходит три шага FrameBegin(帧头) → Payload(负载) → FrameEnd. Машина состояний делает четыре дела: сшивка фрагментов (легальность opcode CONTINUATION);обход контрольных кадров (вплетённые Ping/Pong/Close игнорируются — вызывающий обрабатывает их вне машины сообщений);инкрементальная проверка UTF-8 (легальность TEXT-сообщений судится сквозь кадры — когда многобайтовый символ разрублен границей, передняя часть висит в состоянии и дожидается задней для вердикта);обратный вызов завершения (Info по целому сообщению: тип/общая длина). Конфигурация сообщения ограничивает максимальную длину сообщения — оборона против злой бесконечной фрагментации.

Трёхуровневое сопоставление: глава 72, глава 91, эта глава

Глава 72 — универсальное кадрирование с префиксом длины/по строкам (пользовательские протоколы); глава 91 — кадрирование тела HTTP (три ограничения + оборона от контрабанды); эта глава — кадрирование WebSocket (маскирование + правила направления + вплетение контрольных кадров + пересборка фрагментов). Три уровня делят один проектный словарь: трёхзначный возврат, отказ не продвигает, инкрементальное поглощение слипшихся и недоехавших пакетов — выучив первые два уровня, здесь новым остаётся только специфика протокола.

95.3示例

Первая полная программа: круг заголовка и сегментное демаскирование

Программа ниже из examples/websocket/frame/main.c — разбор заголовка в серверном направлении и образец маскирования RFC:

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

Что только что произошло. (1) Конструируется кадр: FIN+MASKED, TEXT, нагрузка 5 байтов, маска 37 FA 21 3D (канонический образец RFC 6455 §5.7); Config.Mask = XWS_MASK_REQUIRED — серверное направление (принятое обязано быть замаскировано). (2) FrameWrite даёт только 7-байтовый заголовок; xrtWsMask маскирует 5 байтов нагрузки на месте. (3) FrameParse разбирает заголовок в трёх состояниях (READY) — Parsed.Opcode=1 (TEXT), длина нагрузки 5, маска восстановлена. (4) Сегментное демаскирование: первые 2 байта со стартом 0, последние 3 со стартом 2 — продолжающееся смещение декодирует Hello. Вот и доказательство «сетевой фрагментации без ожидания целого кадра»: позиция потока маски определяется накопленным смещением и не зависит от границ кусков.

Вторая полная программа: иероглиф разрублен и контрольные кадры вплетены

Вторая программа из examples/websocket/message/main.c — граничный случай пересборки фрагментов:

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

Что только что произошло. (1) Точка разреза нарочно коварная: первый кадр {A, 0xE4, 0xB8}, последний {0xAD, B} — три UTF-8-байта E4 B8 AD китайского иероглифа разрублены границей кадров пополам. (2) Первый кадр opcode=TEXT (сообщение началось), последний opcode=CONTINUATION + FIN (сообщение завершено) — два цикла FrameBegin/Payload/FrameEnd. (3) Здесь проявляется инкрементальная проверка UTF-8 машины состояний: в конце первого кадра 0xE4 0xB8 — незавершённая последовательность; ошибки нет — состояние висит и ждёт следующего куска; последний кадр приносит 0xAD, всё целиком легально, сообщение пропускается (собирается A中B). Реализация с покадровой независимой проверкой дала бы здесь ложную тревогу — вот зачем существует «сквозь кадровое состояние». (4) Семантика обхода вплетённых контрольных кадров покрыта тестовыми векторами (tests) — машина сообщений напрямую игнорирует вплетённый кадр Ping, не трогая пересборку TEXT.

95.4契约

  • Правило направления: клиент→сервер маскируется обязательно, сервер→клиент не маскируется обязательно; конфигурация Mask (REQUIRED/FORBIDDEN) принуждается при разборе — кадр, нарушающий направление, отвергается сразу.
  • Три состояния заголовка: READY/MORE/ERROR; MORE ничего не потребляет и не сообщает об ошибке; отказ не продвигает (как в главах 71/90).
  • Write даёт только заголовок: нагрузку отправляет вызывающий (маскирование своё или продвижение по сегментам); атомарность ёмкости.
  • Продвижение маски: xrtWsMask на месте, стартовый индекс продолжается — независимо от границ кусков; XWS_MASK_SIZE=4.
  • Протокол фрагментации: первый кадр задаёт opcode, последующие CONTINUATION, последний FIN; контрольные кадры можно вплетать только между фрагментами (не внутрь сообщения как данные).
  • Обход контрольных кадров: машина сообщений игнорирует вплетённые контрольные кадры — вызывающий обрабатывает Ping/Pong/Close вне потока сообщений.
  • Инкрементальный UTF-8: легальность TEXT-сообщений судится сквозь кадры; многобайтовые последовательности на границе висят в состоянии до завершения; ложных тревог нет.
  • Длиновая оборона: конфигурация сообщения ограничивает максимум длины; PayloadSize сверх предела отвергается на уровне кадра (параметр конфигурации).
  • Биты RSV: ненулевой RSV означает расширение (permessage-deflate главы 97) — получение ненулевого RSV без согласованного расширения это протокольная ошибка.
  • Обрезка: WEBSOCKET_FRAME/WEBSOCKET_MESSAGE — слои кадра и сообщения независимо от слоя Stream (глава 96) — годятся и без транспорта.

Типичные ошибки

Ловушка 1: маскирование каждого сегмента с нуля:Ловушка 1: маскирование каждого сегмента с нуля

Симптом: при фрагментированном прибытии данные выходят «частично верными, частично перепутанными» — первый сегмент верен, последующие искажены, и узор искажения меняется с границами кусков.

Причина: фаза цикла маски — абсолютный индекс по всей нагрузке кадра. Второй сегмент с нуля смещает фазу — результат операции систематически неверен.

错误写法
recv(seg1); xrtWsMask(seg1, n1, Mask, 0);
recv(seg2); xrtWsMask(seg2, n2, Mask, 0);   /* сброс фазы: каша */
正确写法
size_t iOff = 0;
recv(seg1); xrtWsMask(seg1, n1, Mask, iOff); iOff += n1;
recv(seg2); xrtWsMask(seg2, n2, Mask, iOff); /* смещение продолжается */
Ловушка 2: машину сообщений перекормили при вплетении контрольных кадров:Ловушка 2: машину сообщений перекормили при вплетении контрольных кадров

Симптом: в середине передачи длинного фрагментированного сообщения приходит Ping, и пересборка на приёме сообщает «неожиданный opcode» — сообщение рвётся.

Причина: вплетение контрольных кадров — возможность протокола (сердцебиение не должно блокироваться большим сообщением). Машина сообщений проекционно обходит их — но вызывающий обязан правильно сортировать: opcode контрольного кадра — не кормить машину сообщений (или кормить и доверить обход — одно из двух, не смешивать).

错误写法
while ( parse_frame(&Frame) == READY ) {
	message_frame_begin(&State, &Frame);  /* и Ping уходит в машину: состояние перепутано */
	...
}
正确写法
while ( parse_frame(&Frame) == READY ) {
	if ( is_control(Frame.Opcode) ) {
		handle_control(&Frame);   /* Ping/Pong/Close отдельным путём */
		continue;
	}
	message_frame_begin(&State, &Frame);   /* в пересборку идут только кадры данных */
	...
}
Ловушка 3: покадровая независимая проверка UTF-8:Ловушка 3: покадровая независимая проверка UTF-8

Симптом: легальное сообщение ложно отвергнуто — иероглиф или эмодзи пересёк границу кадра; или хуже — «починив это», проверку просто выключили (нелегальный текст проходит).

Причина: UTF-8 — многобайтовые последовательности, легальность судится по природе сквозь кадры. Покадровая проверка считает незавершённую последовательность нелегальной; выключение проверки принимает нелегальный текст (протокол требует: TEXT обязан быть легальным UTF-8).

错误写法
on_frame_payload(seg) {
	if ( !utf8_valid(seg) ) {   /* граница кадра разрубила иероглиф: ложная тревога */
		close_protocol_error();
	}
}
正确写法
/* инкрементальная проверка встроена в машину сообщений: пропускается только целое после сквозной сборки */
xrtWsMessagePayload(&State, Seg, NULL);   /* проверка - дело машины состояний */
/* если что-то было нелегально, пути FrameEnd/Init уже сообщили об ошибке по протоколу */

Практические задания

Базовое: матрица маскирования на четыре разреза

Образцовой маской RFC на Hello сделайте четыре разреза (1+4, 2+3, 3+2, 4+1): маскировать по сегментам и демаскировать по сегментам, проверяя полное восстановление. Критерии приёмки: все четыре разреза дают одинаковый вывод; параметр стартового смещения соответствует накопленной длине.

Продвинутое: потоковый отправитель большого сообщения

Реализуйте send_text_streamed(write_fn, 数据, 总长, 块大小) (write_fn, данные, общая длина, размер куска): первый кадр TEXT (без FIN) + N кадров CONTINUATION + последний FIN — каждый кадр FrameWrite заголовок + write_fn нагрузку, имитируя потоковое производство. Критерии приёмки: приёмная машина сообщений пересобирает полный оригинал (включая многобайтовый символ точно на границе); размеры куска 1 и 4096 дают одинаковый результат.

Вызов: ядро эхо-сервера на голом уровне кадров

Не пользуясь слоем Stream главы 96, постройте руками кадрировочный цикл на событийной плоскости TCP главы 67: приём → FrameParse в трёх состояниях → демаскирование (продвижение по сегментам) → сортировка контрольных кадров (Ping автоматически Pong) → кадры данных в пересборку сообщения → целое сообщение FrameWrite обратно (серверное направление без маски) + принуждение правила маскирования (незамаскированный кадр отвергается). Критерии приёмки: успешный обмен текстом с браузером/wscat; вплетённый Ping не мешает большому сообщению; незамаскированный кадр отвергается с Close 1002.

Итоги

ТемаКратко
Заголовок кадраFIN+RSV+Opcode+MASK+длина (7/16/64-битная); Write даёт только заголовок, атомарность ёмкости
Три состояния разбораREADY/MORE/ERROR; MORE ждёт данных без потребления; правило направления принуждает конфигурация Mask
Маскированиеклиент→сервер обязательно, обратное обязано не; цикл mod 4; на месте, самообратно
Сегментное маскированиестартовый индекс = накопленное смещение нагрузки - не зависит от границ кусков; с нуля на каждом сегменте - классическая ошибка
Протокол фрагментациипервый кадр задаёт opcode → CONTINUATION×N → FIN завершает; поток выгоден обеим сторонам
Вплетение контрольных кадровPing/Pong/Close можно вплетать между фрагментами; машина сообщений обходит, вызывающий сортирует
Инкрементальный UTF-8легальность TEXT судится сквозь кадры; последовательности на границе висят в состоянии - покадровая проверка ложно тревожит, отключение теряет затвор
Длиновая оборонамаксимум уровня сообщения + потолок уровня кадра; против атак бесконечной фрагментацией
Биты RSVненулевой = согласовано расширение (deflate главы 97); полученное несогласованным - протокольная ошибка
Трёхуровневое сопоставление71 универсально → 90 разделение границ HTTP → 94 маски/фрагменты WS: один инкрементный словарь

📖 Справочники API в этой главе:websocket (86 записей)http (72 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки