Ориентация главы
После завершения обновления (глава 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.
Пересборка фрагментов и обход контрольных кадров
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:
#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; }
Что только что произошло. (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 — граничный случай пересборки фрагментов:
#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; }
Что только что произошло. (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) — годятся и без транспорта.
Типичные ошибки
Симптом: при фрагментированном прибытии данные выходят «частично верными, частично перепутанными» — первый сегмент верен, последующие искажены, и узор искажения меняется с границами кусков.
Причина: фаза цикла маски — абсолютный индекс по всей нагрузке кадра. Второй сегмент с нуля смещает фазу — результат операции систематически неверен.
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); /* смещение продолжается */
Симптом: в середине передачи длинного фрагментированного сообщения приходит 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); /* в пересборку идут только кадры данных */ ... }
Симптом: легальное сообщение ложно отвергнуто — иероглиф или эмодзи пересёк границу кадра; или хуже — «починив это», проверку просто выключили (нелегальный текст проходит).
Причина: 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки