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

Заключительная глава тома 9. Три предыдущие дали обновление (93), кадры (94), потоки (95); эта добавляет последние два инженерных звена двунаправленного канала и сцепляет всю цепочку.Протокол закрытия: круг xrtWsCloseWrite/CloseParse, семантические домены кодов закрытия (1000 норма/1001 ухожу/1002 протокольная ошибка...), принуждение на стороне записи (причина обязана быть легальным UTF-8, общая длина в пределе, зарезервированные коды не выходят в сеть) — грациозное закрытие это рукопожатие «обе стороны знают, что конец».permessage-deflate: расширение сжатия WebSocket — слой согласования (строгое различение offer/response, window bits, context takeover), потоковые объекты xwsinflater/xwsdeflater (продвижение фрагментами без агрегации, двойные потолки, RSV1 только на первом кадре), путь сжатой отправки слоя Stream. Напоследок «полное путешествие одного сообщения» сцепляет девять глав тома от разбора HTTP до сжатых фрагментированных кадров — с главы 98 начинается высокоуровневый мир xhttp.

97.1引入

Два инженерных вопроса завершают том 9. Первый: как соединению «красиво попрощаться»? Разорвать TCP — противоположная сторона не отличит «нормальное завершение» от «сетевой аварии/обрезки промежуточным устройством»: мониторинг, тревоги и стратегия переподключения все откажут. Контрольный кадр Close несёт код состояния и причину, делая закрытие подтверждённым обеими сторонами протокольным событием: инициатор шлёт Close, противоположная сторона отвечает Close, каждая входит в финал — снимок CloseInfo сообщает «почему ушла» противоположная сторона. Второй: JSON-сообщения высоко повторяемы (имена ключей, структура); сжатие текста экономит 70-90% полосы — но подход Content-Encoding HTTP (глава 93) на двунаправленном потоке нужно перепроектировать: сжатие по сообщениям, а не по соединению; словарь переиспользуется через сообщения (context takeover); контрольные кадры не сжимаются никогда — это permessage-deflate, и согласование, и потоковая обработка которого суть инженерия протокольного слоя.

97.2概念

Рукопожатие закрытия и семантика кодов

1ИнициацияCloseWrite(код, причина) порождает нагрузку контрольного кадра -> Close отправлен (гарантия одноразовости)
2Противоположная сторонаполучила Close -> машина состояний автоматически отвечает Close (снимок CloseInfo уходит приложению)
3Финалобмен Close обеими сторонами завершён -> CLOSED; тайм-аут (нет ответа) принуждает завершение

Семантические домены кодов закрытия делятся на три полосы.1000-1003: семейство нормального закрытия (1000 норма, 1001 точка уходит, 1002 протокольная ошибка, 1003 неприемлемый тип данных);1008-1011: семейство ошибок (1008 нарушение политики, 1009 сообщение слишком велико, 1010 нет расширения, 1011 внутренняя ошибка);3000-4999: прикладные пользовательские (библиотека не толкует);зарезервированные коды (1005 «без кода», 1006 «аварийное закрытие», 1015 «провал рукопожатия TLS») — только выражение API, выход в сеть запрещён: они описывают «ситуации без кадра Close», и сторона записи отвергает их наотрез. Правила нагрузки: код 2 байта + причина (легальный UTF-8, всего ≤123 — XWS_CLOSE_PAYLOAD_MAX) — сторона письма отказывает на нарушении, сторона чтения отвергает уродливую нагрузку.

permessage-deflate: слой согласования

Имя расширения permessage-deflate, параметров четыре: server_max_window_bits/client_max_window_bits (окно сжатия 9-15 — меньше экономит память, степень сжатия чуть падает), server_no_context_takeover/client_no_context_takeover (сброс словаря на границе сообщений — экономия памяти, степень через сообщения падает). Правила согласования строго принуждаются слоем: offer и response валидируются раздельно, повтор параметров отвергается, нелегальные window bits отвергаются, response не вправе вводить возможности, которых не было в offer, context takeover един на обоих концах. Принцип ответа — минимальное соответствующее подмножество: возвращаются только необходимые параметры (offer предложил server_max_window_bits=10; приняв, отвечаем ровно это). xrtWsDeflateDirection переводит результат согласования в односторонние рабочие параметры — параметры приёма и отправки могут различаться.

Потоковые объекты сжатия

xwsinflater (распаковка)/xwsdeflater (упаковка) — потоковые объекты в той же позе, что декомпрессия главы 29 и декодирование главы 93:

  • Продвижение фрагментами: вход и выход по кускам, без агрегации целого сообщения — большое сообщение держит память постоянной.
  • Вывод в обратный вызов: распакованные данные идут прямо в буфер вызывающего или очередь отправки.
  • Двойной потолок: общий объём распаковки ограничен и потолком сообщения, и потолком Inflate — оборона от бомб сжатия (3 байта входа заявляют 4 ГБ выхода? потолок перехватывает первым).
  • no-context-takeover: при согласовании словарь сбрасывается на границе сообщений.
  • Планирование ёмкости: xrtWsDeflaterBound даёт верхнюю границу до записи — вход в бюджетирование SendLimit.
  • Пути отказа: отказ обратного вызова/OOM/ошибка данных с ясной семантикой reset/abort/destroy.

Правило RSV1: флаг сжатия RSV1 разрешён только на первом кадре данных сжатого сообщения — continuation его не повторяет, контрольные кадры не сжимаются никогда. Это исполнение «ненулевой RSV = согласовано расширение» из главы 95.

Полное путешествие одного сообщения (панорама тома 9)

1Запрос прибылTCP/TLS (главы 67/87) -> RequestParse (89) -> проверка обновления + рукопожатие (93)
2После 101StreamAttach перехватывает (95) - машина состояний кадров заработала (94)
3Сообщение прибылоFrameParse три состояния -> демаскирование (сегментная фаза) -> пересборка сообщения (инкрементальный UTF-8)
4Сжатое сообщениеRSV1 -> inflater потоково распаковывает (двойной потолок) -> представление MessageData
5Обработка приложениемпотребление внутри обратного вызова (пересылка/разбор/эхо)
6Обратный путьотправка Text/Ref/Compressed (единый SendLimit) -> фрагментированные кадры + маскирование (направление клиента)
7ПрощаниеClose(код, причина) -> противоположная сторона отвечает Close -> снимки финалов обеих сторон

API девяти глав по местам в одном сообщении — это и есть приёмочная форма «блочной структуры».

97.3示例

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

Программа ниже из examples/websocket/close/main.c — выписывание/разбор кода и причины туда-обратно:

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

#include <stdio.h>




















 */


/* 写出并解析一个带 UTF-8 原因的 Close 控制帧负载。 */
int main(void)
{
	uint8 Payload[XWS_CLOSE_PAYLOAD_MAX];
	xwsclose Close;
	xbytesview Input;
	size_t iSize;

	if ( !xrtWsCloseWrite(
		XWS_CLOSE_NORMAL,
		XRT_STR_LITERAL("shutdown"),
		Payload,
		sizeof(Payload),
		&iSize
	) ) {
		return 1;
	}
	Input.Data = Payload;
	Input.Size = iSize;
	if ( !xrtWsCloseParse(Input, &Close) ) {
		return 2;
	}
	printf(
		"code=%u reason=%.*s\n",
		(unsigned)Close.Code,
		(int)Close.Reason.Size,
		Close.Reason.Data
	);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/websocket/close/main.c -lws2_32 -liphlpapicode=1000 reason=shutdown

Что только что произошло. (1) CloseWrite(1000, "shutdown") даёт нагрузку 9 байтов (код 2 + причина 8) — тройное принуждение стороны письма на входе: причина легальный UTF-8, всего ≤123, код вне зарезервированной полосы. (2) CloseParse разбирает круг в xwsclose{Code, представление Reason} — код 1000 семантически «нормальное закрытие», причина для людей (журнал/аудит). (3) Эксперимент с зарезервированными кодами: попытка записать 1005/1006/1015 отказывает сразу — это «выразительные коды» уровня API (CloseInfo у Stream использует 1006 для «закрыто аварийно, кадра Close не получил»), а не отправляемые значения. Этот «отказ стороны письма» останавливает самую частую протокольную ошибку реализаторов. (4) Close слоя Stream (глава 96) внутри и есть эта пара функций — отношение слоёв: эта глава слой нагрузки, 95-я слой соединения.

Вторая полная программа: ответ согласования сжатия

Вторая программа из examples/websocket/deflate/main.c — минимальный соответствующий ответ от offer к response:

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

#include <xrt.h>























 */


/* 展示解析客户端 offer 并构造最小合规服务端响应。 */
int main(void)
{
	xstrview Text = XRT_STR_LITERAL(
		"permessage-deflate; server_max_window_bits=10; "
		"client_max_window_bits"
	);
	xwsextension Extension;
	xwsdeflate Offer;
	xwsdeflate Response;
	char Output[XWS_DEFLATE_MAX_SIZE + 1u];
	size_t iOffset = 0;
	size_t iSize;

	if ( xrtWsExtensionNext(
		Text,
		&iOffset,
		&Extension
	) != XHTTP_NEXT_ITEM ) {
		return 1;
	}
	if ( !xrtWsDeflateOfferParse(
		&Extension,
		&Offer
	) || !xrtWsDeflateAccept(
		&Offer,
		&Response
	) || !xrtWsDeflateResponseWrite(
		&Response,
		Output,
		XWS_DEFLATE_MAX_SIZE,
		&iSize
	) ) {
		return 2;
	}
	Output[iSize] = '\0';
	printf("response=%s\n", Output);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/websocket/deflate/main.c -lws2_32 -liphlpapiresponse=permessage-deflate; server_max_window_bits=10

Что только что произошло. (1) Offer предлагает server_max_window_bits=10 (окно сжатия сервера 1 КБ) и уведомляет, что клиент может принести client_max_window_bits. (2) Ответ принимает 10 и пишет минимальное соответствующее подмножество — только необходимый параметр: permessage-deflate; server_max_window_bits=10, без эха «уведомления возможности» клиентской стороны.Почему минимально: каждый параметр, отражённый в response, — обязательство: ответили client_max_window_bits — обязались ограничить окно; больше отзовали — больше обязались — меньше манёвра. Строгая валидация слоя согласования (повтор параметров/нелегальные значения/непредложенные возможности) встроена целиком. (3) Строка ответа ложится в поле ответа Sec-WebSocket-Extensions — в композиции с ответом рукопожатия главы 94. Сопроводительные образцы deflater/inflater демонстрируют двустороннюю работу потоковых объектов, extension_tour — полный обход слоя расширений.

97.4契约

  • Нагрузка закрытия: код 2 байта (сетевой порядок) + опциональная причина (UTF-8); всего ≤ XWS_CLOSE_PAYLOAD_MAX (125); сторона письма принуждает три правила, сторона чтения отвергает уродливое.
  • Семантические домены кодов: 1000-1003 нормальное семейство / 1008-1011 семейство ошибок / 3000-4999 прикладные / 1005/1006/1015 зарезервированы (только выражение API, выход запрещён).
  • Рукопожатие закрытия: Close одноразово, противоположная сторона отвечает автоматически, тайм-аут принуждает финал, CloseInfo снимает финал (гарантия слоя соединения главы 96).
  • Согласование сжатия: offer/response валидируются раздельно и строго; повтор/нелегальность/непредложенность — отказ; ответ — минимальное соответствующее подмножество; DeflateDirection даёт односторонние параметры.
  • Параметры сжатия: window bits 9-15; context takeover един на обоих концах; no-takeover сбрасывает словарь на границе сообщений.
  • Потоковые объекты: продвижение фрагментами без агрегации; вывод в обратный вызов; распаковка под двойным потолком сообщение+Inflate (оборона от бомб); DeflaterBound планирует ёмкость; ясные reset/abort/destroy.
  • Правило RSV1: флаг сжатия только на первом кадре данных; continuation не повторяет; контрольные кадры не сжимаются никогда.
  • Путь отправки: семейство TextCompressed создаёт Deflater по надобности (глава 96); бюджет включает верхнюю границу сжатого вывода.
  • Обрезка: WEBSOCKET_CLOSE/_DEFLATE/_DEFLATER/_INFLATER/_EXTENSION независимые макросы — несжатые соединения несут ноль кода сжатия.

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

Ловушка 1: в Close запихнули зарезервированный код или сверхдлинную причину:Ловушка 1: в Close запихнули зарезервированный код или сверхдлинную причину

Симптом: CloseWrite(1006, ...) отказывает сразу; или запихнули 200-байтовое «подробное объяснение» как причину — отказ тоже.

Причина: 1005/1006/1015 — значения API, выражающие «ситуации без кадра Close»; протокол запрещает их в сеть; потолок нагрузки 125 байтов (код 2 + причина 123). Сверхдлинная причина идёт прикладным сообщением; причина Close несёт только резюме.

错误写法
xrtWsCloseWrite(1006, XRT_STR_LITERAL("abnormal"), ...);  /* зарезервированный код: отказ */
xrtWsCloseWrite(1000, LongParagraph, ...);                 /* сверх предела: отказ */
正确写法
xrtWsCloseWrite(1000, XRT_STR_LITERAL("policy: quota"), ...);
/* код из семантического домена; причина одной фразой; детали - прикладным сообщением или в журнал */
Ловушка 2: ответ согласования отзывает непрошенные параметры:Ловушка 2: ответ согласования отзывает непрошенные параметры

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

Причина: каждый параметр response — обязательство. Минимальное соответствующее подмножество — безопасная поза протокола: отвечайте только то, что реально исполните.

错误写法
/* вернуть offer как есть */
response = offer_string;   /* отозваны не обещанные возможности */
正确写法
/* ответ слоем согласования: минимальное подмножество и все проверки встроены */
xrtWsDeflateInit(&Offer);
parse_offer(Fields, &Offer);
accept = deflate_accept(&Offer);   /* только необходимые параметры */
Ловушка 3: без потолка Inflate съедают бомбу сжатия:Ловушка 3: без потолка Inflate съедают бомбу сжатия

Симптом: злая противоположная сторона шлёт 3 байта сжатого куска, заявляя 4 ГБ на выходе — распаковщик честно производит, пока OOM не убьёт.

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

错误写法
xrtWsInflaterConfigInit(&Config);    /* строить без потолка? опасно */
xrtWsInflaterCreate(&Config, NULL);
feed_everything(&Infl, PeerData);     /* бомба доходит до памяти */
正确写法
/* потолок сообщения + потолок Inflate заданы явно (порядок по бизнесу: напр. 1МБ / 8МБ) */
MessageConfig.MaxMessage = 1024 * 1024;
InflateLimit = 8 * 1024 * 1024;
/* сверх предела - протокольная ошибка: ответить Close 1009 (сообщение слишком велико) и разорвать */

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

Базовое: матрица кодов закрытия

Пять экспериментов с CloseWrite/Parse: 1000 + короткая причина, 1001 + пустая причина, 1005 (зарезервированный), причина с нелегальным UTF-8, причина 130 байтов — первые три проверяют принуждение стороны письма, последние два — пути отказа. Критерии приёмки: легальные группы сходятся в обе стороны; ошибки нелегальных групп различают три причины.

Продвинутое: ручная сборка цепочки сжатия

Переделайте образцы deflater/inflater: сторона отправки сжимает JSON на 10 КБ в кадр RSV1 (первый кадр RSV1, фрагментация, FIN), сторона приёма распаковывает и пересобирает. Посчитайте байты до/после сжатия и пиковую память (глава 6). Критерии приёмки: круг туда-обратно даёт тот же оригинал; граница фрагментации посреди сжатых данных тоже корректна; пиковая память не зависит от размера сообщения (потоковость).

Вызов: полнофункциональная версия службы эха

Добавьте к службе эха главы 96 всё: согласование сжатия (минимальный ответ на offer), сжатые приём/отправку (правило RSV1), протокол закрытия (оба пути — нормальный Close 1000 и отказ по политике 1008), потолок сообщения + двойной потолок Inflate, Ping/Pong (сердцебиение в том числе под противодавлением). Сразитесь с WebSocket браузера (консоль разработчика). Критерии приёмки: большое сообщение (1 МБ текста) корректно проходит сжатый круг; злой вход (сверхпотолочные сообщения/уродливые кадры/Close с зарезервированным кодом) весь отвергается по протоколу с чистым закрытием; вся цепочка нигде не буферизует целое сообщение.

Итоги

ТемаКратко
Нагрузка закрытиякод (2B сетевой порядок) + причина UTF-8; ≤125 байтов; три принуждения стороны письма, сторона чтения отвергает уродливое
Домены кодов1000-1003 норма / 1008-1011 ошибки / 3000-4999 прикладные / 1005·1006·1015 зарезервированы, выход запрещён
Рукопожатие закрытияодноразовая отправка, автоответ, финал по тайм-ауту, снимок CloseInfo
Правила согласованияoffer/response раздельно и строго; повтор/нелегальность/непредложенность - отказ; ответ минимальным подмножеством
Параметры согласованияwindow bits 9-15; context takeover един; no-takeover сбрасывает по сообщениям
Потоковые объектыinflater/deflater продвигаются фрагментами, вывод в обратный вызов, ясные reset/abort/destroy
Оборона от бомбпотолок сообщения + потолок Inflate двойное ограничение; сверх предела ответ 1009
Правило RSV1флаг сжатия только на первом кадре данных; continuation не повторяет; контрольные кадры никогда не сжимаются
Планирование ёмкостиDeflaterBound считает верхнюю границу до входа в бюджет SendLimit
Панорамапутешествие одного сообщения сцепляет API девяти глав, 89->93->94->95->96

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

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