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

У HTTP/1.1 есть механизм «перевоплощения»: заголовок Upgrade предлагает сменить протокол, сервер отвечает 101 Switching Protocols, и в том же TCP-соединении начинается новый язык. WebSocket — самый знаменитый пользователь механизма: кадры, сообщения и потоки глав 95~97 всё начинаются с одного успешного обновления. Глава разбирает обе стороны: сторону предложений (xrtHttpUpgradeFieldCursorInit/FieldNext итерируют клиентские предложения, xrtHttpUpgradeWrite пишет «поддержанное мной подмножество» как канонический ответ — не только для WebSocket; обновления вроде h2c употребляют то же); сторону рукопожатия WebSocket (построение и сверка запроса/ответа в websocket/upgrade, каноническое по RFC вычисление xrtWsAccept, согласование подпротоколов xrtWsProtocolSelect). У значения Accept есть стандартный тестовый вектор RFC 6455 — совпадение с ним и есть интероперабельное доказательство правильности реализации.

94.1引入

Почему WebSocket начинается с HTTP? Инженерный ответ: порты 80/443 уже открыты, прокси и межсетевые экраны понимают HTTP, аутентификация и маршрутизация могут переиспользовать инфраструктуру HTTP. Протокольный ответ: обновление позволяет провести «переговоры» на языке, который знают обе стороны, — клиент предлагает (хочу говорить websocket, подпротоколы вот эти), сервер выбирает (101 + принимаемые ключевые параметры), и лишь по завершении переговоров язык меняется; любая сторона недовольна — продолжают говорить HTTP (обычный ответ, без переключения).

Три детали семантики обновления стоит установить заранее: предложения могут повторяться (несколько полей Upgrade, списки через запятую — правила семейств полей главы 92); имена протоколов регистронезависимы (WebSocket/websocket — одно и то же); версия может прилагаться (форма protocol/version вроде HTTP/2.0). Всё это обрабатывают итераторы слоя обновления — опять история «не режьте руками».

94.2概念

Сторона предложений: итерация и ответ

1КлиентUpgrade: h2c, websocket (повторяемые поля, списки через запятую, регистронезависимость)
2Итерация сервераFieldCursorInit + FieldNext -> по одному выдаёт предложения protocol/version
3Решение серверапересечение с локальным списком поддержки (порядок по предпочтениям клиента)
4ОтветUpgradeWrite пишет "поддержанное подмножество" как каноническое значение Upgrade + ответ 101
5Переключениепосле отправки 101 соединение говорит на новом протоколе; без совпадения - обычный HTTP-ответ (без переключения)

Выход xrtHttpUpgradeFieldNext — xhttpupgradeitem: представление Protocol + опциональное представление Version — итератор объединил повторяемые поля, разрезал запятые, нормализовал регистр (при сравнении). xrtHttpUpgradeWrite(支持列表, 数量, 输出, 容量, &长度) (список поддержки, количество, вывод, ёмкость, &длина) обратно порождает каноническое значение — атомарность ёмкости общебиблиотечная.Семантика ответа: ответ обновления пишет только «выбранные протоколы» (не весь список поддержки); ответ 101 Switching Protocols обязан ещё и повторить Connection: Upgrade — построение ответа выполняется WebSocket-вариантом слоя обновления (следующий раздел) или собирается вручную.

Рукопожатие WebSocket: запрос, ключ и сверка ответа

WebSocket-обновление накладывает на общий механизм специализированные поля: клиент шлёт Upgrade: websocket + Connection: Upgrade + Sec-WebSocket-Key (Base64 от 16 случайных байтов) + опционально Sec-WebSocket-Protocol (список подпротоколов); сервер обязан ответить Sec-WebSocket-Accept — вычисленным по канону RFC 6455: Base64(SHA1(Key + 魔法串)) (ключ + магическая строка). Это вычисление — не «проверка секрета» (в ключе нет тайны), а доказательство, что сервер действительно знает протокол: реализация, вычислившая верный Accept, неизбежно реализовала спецификацию. xrtWsAccept(Key, 输出, 容量) (Key, вывод, ёмкость) делает это за один шаг, выводя 28 символов Base64.

Построение запроса (демонстрирует examples/websocket/upgrade): полный заголовок запроса GET 路径 HTTP/1.1 + Host + Upgrade/Connection + Key/Version(+Protocol); сверка ответа (response check binds key+protocol из upgrade_tour): сверка обязана быть привязана к ключу этой сессии — повторенный противоположной стороной Accept сравнивается со значением, вычисленным из вашего отправленного ключа (против подмены сессии посредником), а выбранный подпротокол обязан быть в вашем списке предложений.

Согласование подпротоколов

Sec-WebSocket-Protocol — выбор протокола прикладного уровня (нечто вроде chat, superchat, binary): xrtWsProtocolSelect(客户端提议, 服务端支持, &选中) (предложения клиента, поддержка сервера, &выбранный) берёт первый по порядку клиентских предпочтений, поддержанный и сервером — порядок чувствителен (выражение приоритетов клиента). Без пересечения — отказ (рукопожатие продолжается без подпротокола либо приложение отказывает — стратегия ваша).

Место обновления в конвейере

1Прибытие HTTP-запросаRequestParse (глава 90)
2Проверка обновленияFieldTokenFind видит upgrade в Connection (глава 92) + наличие поля Upgrade
3Совпадение предложенийитерация UpgradeFieldNext ∩ локальная поддержка -> websocket попадает
4Обработка рукопожатиясверка ключа / вычисление Accept / выбор подпротокола (этот раздел)
5Ответ 101UpgradeWrite + Accept + выбранный протокол
6ПерехватWsStreamAttach принимает транспорт после 101 (глава 96)

Пять слоёв последовательно — каждый слой это API предыдущих глав. Такова блочная структура тома 9: обновление — «последнее дело HTTP и первое дело WebSocket».

94.3示例

Первая полная программа: итерация предложений и генерация ответа

Программа ниже из examples/http/upgrade/main.c — полная обработка повторяемых полей, списков через запятую и форм версий:

examples/http/upgrade/main.c源码 ↗
#include <stdio.h>

#include <xrt/http_upgrade.h>























 */


/* 解析重复字段并规范生成一个 Upgrade 选择列表。 */
int main(void)
{
	static const xhttpfield Fields[] = {
		{ XRT_STR_INIT("Upgrade"), XRT_STR_INIT("h2c, websocket") },
		{ XRT_STR_INIT("upgrade"), XRT_STR_INIT("HTTP/2.0") }
	};
	static const xhttpupgradeitem Offered[] = {
		{ XRT_STR_INIT("websocket"), { NULL, 0 } },
		{ XRT_STR_INIT("HTTP"), XRT_STR_INIT("2.0") }
	};
	xhttpupgradefieldcursor Cursor;
	xhttpupgradeitem Upgrade;
	xhttpnext Next;
	char sOutput[64];
	size_t iSize;

	xrtHttpUpgradeFieldCursorInit(&Cursor);
	while ( (Next = xrtHttpUpgradeFieldNext(
		Fields, 2u, &Cursor, &Upgrade
	)) == XHTTP_NEXT_ITEM ) {
		printf(
			"protocol: %.*s",
			(int)Upgrade.Protocol.Size,
			Upgrade.Protocol.Data
		);
		if ( Upgrade.Version.Size != 0 ) {
			printf(
				"/%.*s",
				(int)Upgrade.Version.Size,
				Upgrade.Version.Data
			);
		}
		putchar('\n');
	}
	if ( (Next == XHTTP_NEXT_ERROR) ||
		!xrtHttpUpgradeWrite(
			Offered,
			2u,
			sOutput,
			sizeof(sOutput),
			&iSize
		) ) {
		return 1;
	}
	printf("field: %.*s\n", (int)iSize, sOutput);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/http/upgrade/main.c -lws2_32 -liphlpapiprotocol: h2cprotocol: websocketprotocol: HTTP/2.0field: websocket, HTTP/2.0

Что только что произошло. (1) Вход — два поля Upgrade (имена в разном регистре, одно со списком через запятую) — итератор объединяет их в три предложения: h2c, websocket, HTTP/2.0 (форма protocol/version показана раздельно).Никакой ручной резки — повторяемые поля, запятые, регистр целиком обрабатывают FieldCursorInit/FieldNext. (2) UpgradeWrite пишет «поддержанное подмножество» (websocket, HTTP/2.0) как каноническое значение websocket, HTTP/2.0 — ответ пишет только выбранное множество, порядок из предоставленного списка. (3) Этот образец — чисто полевой уровень (без сети), юнит-тестовая форма семантики обновления; вставленный между RequestParse и построением ответа 101, он и есть полный серверный путь обновления.

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

Вторая программа из examples/websocket/handshake/main.c — стандартные векторы RFC и согласование по предпочтениям:

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

#include <xrt.h>




















 */


/* 展示无需网络对象的 RFC Accept 计算与子协议协商。 */
int main(void)
{
	char Accept[XWS_ACCEPT_CAPACITY];
	xstrview Selected;

	if ( !xrtWsAccept(
		XRT_STR_LITERAL("dGhlIHNhbXBsZSBub25jZQ=="),
		Accept,
		sizeof(Accept)
	) ) {
		return 1;
	}
	if ( !xrtWsProtocolSelect(
		XRT_STR_LITERAL("chat, superchat"),
		XRT_STR_LITERAL("superchat, binary"),
		&Selected
	) ) {
		return 2;
	}
	printf(
		"accept=%s protocol=%.*s\n",
		Accept,
		(int)Selected.Size,
		Selected.Data != NULL ? Selected.Data : ""
	);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/websocket/handshake/main.c -lws2_32 -liphlpapiaccept=s3pPLMBiTxaQ9kYGzzhZRbK+xOo= protocol=superchat

Что только что произошло. (1) Входной nonce dGhlIHNhbXBsZSBub25jZQ== — канонический образец RFC 6455 §1.3; вычисленный xrtWsAccept-ом s3pPLMBiTxaQ9kYGzzhZRbK+xOo= посимвольно совпадает со стандартным вектором — совпадение и есть интероперабельное доказательство правильности реализации (векторы RFC главы 76, применённые вновь). (2) ProtocolSelect("chat, superchat", "superchat, binary") выбирает superchat — в порядке клиентских предпочтений (chat первым) сервер не знает chat; superchat — первый, поддержанный обеими сторонами. (3) Ноль сетевых объектов: вычислительный слой рукопожатия полностью автономен — тесты, прокси и отладчики употребляют его напрямую. Сопроводительные examples/websocket/upgrade (построение полного запроса рукопожатия) и upgrade_tour (четыре строки самопроверки: конфигурация/сверка запроса/привязка ответа/стыковка конфигурации потока) покрывают обе стороны полной API-поверхности рукопожатия.

94.4契约

  • Семантика предложений: поля Upgrade повторяемы, значения — списки через запятую, имена регистронезависимы; итератор выдаёт структурированные предложения protocol/version.
  • Семантика ответа: UpgradeWrite пишет только выбранное подмножество; ответ 101 обязан повторить Connection: Upgrade; атомарность ёмкости.
  • Вычисление Accept: Base64(SHA1(Key + RFC 魔法串)) (ключ + магическая строка RFC); ключ — Base64 от 16 случайных байтов (24 символа); само вычисление не секретно — это доказательство соответствия спецификации.
  • Привязка сверки ответа: клиент обязан проверить, что Accept соответствует отправленному им ключу, а выбранный протокол — из его предложений; против подмены.
  • Согласование подпротоколов: порядок клиентских предпочтений ∩ поддержка сервера, берётся первый по клиентскому порядку; обработка пустого пересечения — политика приложения.
  • Путь неудачного обновления: без совпадения — обычный HTTP-ответ (без переключения): обновление это предложение, а не требование.
  • Обрезка: HTTP_UPGRADE независимо; рукопожатие WebSocket зависит от CRYPTO_SHA1 (вычисление Accept) и Base64.
  • Граница композиции: вверх RequestParse/FieldTokenFind (главы 90/92); вниз WsStreamAttach (глава 96).

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

Ловушка 1: сверка ответа не привязана к собственному ключу:Ловушка 1: сверка ответа не привязана к собственному ключу

Симптом: клиент пропускает Accept, потому что «похоже на формат» (28 символов Base64) — промежуточное устройство пересоздаёт рукопожатие, и соединение принадлежит уже другой сессии.

Причина: сверка Accept — утверждение равенства: 服务端回的 Accept == 我用我的 Key 算的值 (возвращённый сервером Accept == значение, вычисленное из моего ключа). Проверка формата — не сверка.

错误写法
if ( looks_like_base64(AcceptField, 28) ) {
	proceed();   /* "формат похож" - и хватит: пара ключ+Accept от человека-в-середине тоже пройдёт */
}
正确写法
char Mine[XWS_ACCEPT_CAPACITY];
if ( !xrtWsAccept(MyKey, Mine, sizeof(Mine)) ||
		!equal(AcceptField, Mine) ) {
	abort_handshake();   /* утверждение равенства провалено: это не моя сессия */
}
Ловушка 2: согласование подпротоколов по порядку предпочтений сервера:Ловушка 2: согласование подпротоколов по порядку предпочтений сервера

Симптом: сервер выбирает binary по порядку своего списка поддержки, хотя клиент явно хотел chat — результат согласования нарушает клиентский приоритет, поведение расходится.

Причина: RFC 6455 требует уважать порядок предложений клиента — он встроен в ProtocolSelect; свой цикл «обойти серверный список в поисках первого общего» переворачивает смысл.

错误写法
for ( each server_proto ) {        /* серверский порядок: нарушение протокола */
	for ( each client_proto ) {
		if ( equal ) return server_proto;
	}
}
正确写法
xstrview Selected;
if ( xrtWsProtocolSelect(ClientOffered, ServerSupported,
		&Selected) ) {
	/* клиентский порядок ∩ поддержка сервера: семантика протокола встроена */
}
Ловушка 3: суждение об обновлении только по полю Upgrade без Connection:Ловушка 3: суждение об обновлении только по полю Upgrade без Connection

Симптом: запрос только с Upgrade: websocket, но Connection: keep-alive — соединение переключили; клиент не просил обновления, состояние соединения перепутано.

Причина: обновление — совместная семантика двух полей: Upgrade говорит, на что переключиться, Connection: Upgrade — «я прошу переключить». Глядеть только на одно — решать за клиента.

错误写法
if ( has_field(Fields, "Upgrade") ) {
	switch_protocol();   /* Connection не сказал upgrade: ошибочное переключение */
}
正确写法
if ( xrtHttpFieldTokenFind(Fields, iCount,
		XRT_STR_LITERAL("upgrade")) &&       /* Connection содержит upgrade */
		has_upgrade_websocket(Fields) ) {    /* Upgrade предлагает websocket */
	switch_protocol();
}

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

Базовое: прогнать построение запроса рукопожатия

Прогоните образцы websocket/upgrade и handshake; командной строкой OpenSSL посчитайте Accept для того же ключа и сверьте с образцом. Критерии приёмки: Accept совпадает с вектором RFC; построенный запрос содержит все обязательные поля.

Продвинутое: посредник определения обновления

На массиве полей реализуйте is_upgrade_request(Fields, 数量, &提议协议) (Fields, количество, &предложенный протокол): Connection содержит upgrade (поиск токена) + итерация предложений Upgrade, возвращает — обновление ли и первое предложение. Проверьте на четырёх входах: стандартное обновление, без Connection, пустой список Upgrade, предложение не-websocket. Критерии приёмки: четыре вердикта сходятся с ручной выкладкой; всюду API семейства главы 92, ноль ручной резки.

Вызов: ответчик рукопожатия

Реализуйте accept_handshake(请求字段, 支持的子协议, 输出, 容量, &长度) (поля запроса, поддержанные подпротоколы, вывод, ёмкость, &длина): извлечь ключ, вычислить Accept, согласовать подпротокол, сгенерировать полный ответ 101 (строка состояния + поля Upgrade/Connection/Accept/Protocol). Проверьте сквозным образом запрос, построенный websocket/upgrade. Критерии приёмки: ответ проходит сверку upgrade_tour; без подходящего подпротокола деградация по политике (без поля Protocol); нехватка ёмкости — ноль записей.

Итоги

ТемаКратко
Универсальное обновлениепредложения повторяемы/через запятую/регистронезависимы; итерация FieldCursorInit+FieldNext; ответ UpgradeWrite пишет только выбранное
Семантика 101ответ повторяет Connection: Upgrade; без совпадения - обычный HTTP-ответ (предложение, не требование)
Спецполя WSUpgrade: websocket + Connection + Sec-WebSocket-Key + Protocol (опционально)
Вычисление AcceptBase64(SHA1(Key+魔法)) (ключ+магия); вектор RFC s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Сверка ответаутверждение равенства с привязкой к собственному ключу + выбранный протокол в пределах предложений - проверка формата не сверка
Согласование подпротоколовклиентский порядок ∩ поддержка сервера, первый по клиенту; пустое пересечение - политика приложения
Совместное определениеConnection содержит upgrade И Upgrade содержит предложение - оба поля обязательны
Место в конвейерепоследнее дело HTTP, первое дело WebSocket; пять слоёв сцепляют главы 90->95
ОбрезкаHTTP_UPGRADE независимо; рукопожатие WS зависит от SHA1+Base64

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

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