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

Четыре главы xws завершаются. Первые три дали соединения (109), отправку (110), группировку (111); эта глава подключает их к HTTP-серверу: фиксированная конечная точка WebSocket (xrtWsServerRoute — одна регистрация вешается на HTTP Router главы 105; ws и wss определяются способом старта, вторая регистрация не нужна); безопасные умолчания (политика Origin — первый шлюз против междоменных атак браузера; обратный вызов Authorize — точка бизнес-авторизации); низкоуровневый Upgrade (xrtWsServerCheck/Reply/Reject/Upgrade ступенчатые API — когда фиксированной конечной точки мало, собирайте прямо в обратном вызове HTTP-маршрута); форма сессии (session_channel_coroutine — соединение + Channel + корутина превращают бизнес каждого соединения в последовательный код; точка встречи главы 57 и этой главы). Автоответы конечной точки (405/403/400/426/500) покрывают полную классификацию протокольных ошибок.

113.1引入

«Инженерия входа» сервера WebSocket больше, чем кажется: прибывший запрос сперва проходит HTTP-слой (метод обязан быть GET; объявление тела недопустимо); затем валидацию Origin (серверное исполнение политики одинакового происхождения браузера — междоменные соединения с отсутствующим или подделанным Origin суть WebSocket-вариант CSRF); затем бизнес-авторизацию (можно ли этому пользователю в этот канал); затем согласование подпротоколов (глава 94); затем рукопожатие обновления; и лишь тогда ваш Open-обратный вызов получает xwsconn. Фиксированная конечная точка стандартизирует этот конвейер — безопасно по умолчанию (политика одинакового происхождения из коробки), ошибки автоматны (у каждого класса отказа канонический код ответа), жизненный цикл ясен (Open заимствует соединение; чтобы сохранить — обязательно xrtWsConnRef — восходящий источник Add-держит-ссылку главы 112).

Когда опускаться на низкоуровневые API? Список контракта практичен: авторизация с нестандартными ответами (формы помимо 403), асинхронная авторизация, состояние на соединение по параметрам пути, собственные поля рукопожатия — эти «каркасные» потребности фиксированная конечная точка не делает (избегая второй протокольной машины состояний); собирайте их ступенчато через ServerCheck/Reply/Reject/Upgrade в обычном обратном вызове HTTP-маршрута.

113.2概念

Фиксированная конечная точка: регистрация и автоматика

1РегистрацияWsServerRouteConfigInit (лимиты по умолчанию + пустая таблица событий + политика одинакового происхождения)
2Запрос прибылстадия Header отвергает неверный метод и объявление тела (нелегальное тело не читается)
3Валидацияполитика Origin -> обратный вызов Authorize (опционально) -> протокол/версия рукопожатия
4Автоответыне GET -> 405+Allow / авторизация не прошла -> 403 / протокольная ошибка -> 400 / версия -> 426+13 / внутренняя -> 500
5Успех обновленияOpen заимствует xwsconn - для сохранения обязателен ConnRef; после возврата обратного вызова адаптер освобождает переданную ссылку

Регистрация копирует: WsServerRoute читает фиксированную конфигурацию один раз до регистрации, копируя конфигурацию, события и список подпротоколов — после регистрации изменение или освобождение конфигурации и строк вызывающим на конечную точку не влияет (семантика заморозки главы 99, регистрационная версия). Открытый текст и TLS как одно: когда Router стартует через StartTls, тот же маршрут автоматически принимает TLS-потоки, строя конечную точку wss — ноль правок регистрационного кода.

Политика Origin: три ступени по умолчанию

XWS_SERVER_ORIGIN_SAME_HOST_OR_ABSENT (по умолчанию): нативные клиенты могут опускать Origin; когда браузер его присылает, он обязан совпадать со scheme/host/действующим портом текущего запроса — прагматичное умолчание снисходительное к нативным, строгое к браузерам. SAME_HOST: отвергает даже отсутствие — строгие сценарии, доверяющие только браузерам. ANY: полностью открыто — годится лишь для композиций, где валидация источника уже выполнена внешним слоем (шлюз проверил, изоляция интрасети). Origin — WebSocket-линия обороны от CSRF — «безопасно по умолчанию» главы 85, исполненное на слое конечной точки.

Авторизация и жизненный цикл

Authorize исполняется после прохождения протокольного рукопожатия и Origin: читает полный запрос, параметры маршрута и результат согласования — возврат false заставляет маршрутизатор единообразно ответить 403 (нестандартная форма отказа — через низкоуровневые API). Три обратных вызова жизненного цикла: Open (заимствует соединение — сохранение требует ConnRef), Error (лишь наблюдает ошибки синхронного рукопожатия и асинхронного обновления, и не должен фиксировать здесь ответ — это уже стадия «после ответа»), Release (единственная точка освобождения Data: исполняется ровно один раз, когда Router и все обновлённые соединения перестают пользоваться им — не при уничтожении Router! Идущие соединения продлевают жизнь Data). Домены ошибок используют XWS_SERVER_ROUTER_ERROR_AUTHORIZATION, чтобы отличать отказ Origin от отказа бизнес-авторизации — аудит видит, «кто отказал».

Ступенчатый низкий уровень: Check/Reply/Reject/Upgrade

Под фиксированной конечной точкой четыре независимых входа: xrtWsServerCheck (проверяет обновимость запроса — Origin/версия/протокол), Reply (строит ответ 101), Reject (строит отказ — форма настраиваема), Upgrade (исполняет обновление и выдаёт соединение). Они свободно комбинируются в обычных обратных вызовах HTTP-маршрутов — авторизация по базе (асинхронно), затем Upgrade; состояние на соединение по параметру пути; свой ответ 426… луковичная модель главы 105 со специализированным WebSocket-слоем.

Форма сессии: Channel + корутина

Образец session_channel_coroutine показывает последовательный стиль бизнеса на соединение: события соединения постятся в Channel (глава 57); корутина-потребитель берёт события из Channel и обрабатывает по порядку — ад обратных вызовов превращается в прямолинейный код. Порядок финала — шаблон: закрыть Channel → остановить планировщик → Run для осушения → уничтожить корутину/планировщик → ChannelDrain для очистки остатков — структурированная конкурентность главы 60 в форме WebSocket-сессии.

113.3示例

Первая полная программа: фиксированная конечная точка одной регистрацией

Программа ниже из examples/websocket/server_router — минимальная компилируемая регистрация:

extlibs/xws/examples/websocket/server_router/main.c源码 ↗
#include <stdio.h>

#include <xws.h>



/* 固定端点建立后发送一条欢迎消息;Connection 只在当前回调内借用。 */
static void onOpen(
	xhttpconn* pHttp,
	xwsconn* pConnection,
	ptr pData
)
{
	(void)pHttp;
	(void)pData;
	(void)xrtWsConnText(
		pConnection,
		XRT_STR_LITERAL("connected")
	);
}



/* 注册失败时输出当前结构化错误的可读消息。 */
static int fail(cstr sOperation)
{
	const xerror* pError = xrtGetError();

	fprintf(
		stderr,
		"%s: %s\n",
		sOperation,
		pError != NULL ?
			xrtErrorMessage(pError) : "unknown error"
	);
	return 1;
}



/* 创建一个可供明文或 TLS HTTP Router 共用的固定 WebSocket 端点。 */
int main(void)
{
	xhttpserverrouter* pRouter;
	xwsserverrouteconfig Config;

	pRouter = xrtHttpServerRouterCreate(NULL);
	if ( pRouter == NULL ) {
		return fail("create router");
	}
	xrtWsServerRouteConfigInit(&Config);
	Config.Server.Protocols =
		XRT_STR_LITERAL("chat.v2, chat.v1");
	Config.Open = onOpen;
	if ( !xrtWsServerRoute(
		pRouter,
		XRT_STR_LITERAL("/chat"),
		&Config
	) || !xrtHttpServerRouterFreeze(pRouter) ) {
		xrtHttpServerRouterDestroy(pRouter);
		return fail("register websocket route");
	}

	printf("WebSocket route /chat is ready\n");
	xrtHttpServerRouterDestroy(pRouter);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXHTTP_MODULE_ALL -DXWS_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xhttp.h -include xws.h impl.c extlibs/xws/examples/websocket/server_router/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapiWebSocket route /chat is ready

Что только что произошло. (1) xhttpserverrouter главы 105 — носитель: конечная точка WebSocket — это конечная точка пути, повешенная на него: она сосуществует с HTTP-маршрутами (один Router с HTTP-точкой /api и WS-точкой /chat — норма). (2) WsServerRouteConfigInit берёт умолчания (лимиты + политика одинакового происхождения) → заполнить Protocols ("chat.v2, chat.v1" — согласование главы 94), Open, (образец опускает Data/Release) → WsServerRoute регистрирует — регистрация копирует; стековая Config отбрасывается после использования. (3) После заморозки RouterFreeze конечная точка готова — однострочный комментарий, где Open-обратный вызов получает соединение (Add группы главы 112 пишется ровно здесь). Реальное кольцо, TLS-варианты и откат OOM покрыты отдельными тестами — образец сфокусирован на форме регистрации.

Вторая полная программа: полный жизненный цикл обратного вызова обновления

Вторая программа из examples/websocket/http_server — стандартная форма обратного вызова завершения Upgrade:

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

#include <xws.h>



/* Upgrade 完成后,回调取得 WebSocket Connection 的调用方引用。 */
static void onWebSocket(
	xhttpconn* pHttp,
	xnetresult Result,
	xwsconn* pConnection,
	const xerror* pError,
	ptr pData
)
{
	(void)pHttp;
	(void)pError;
	(void)pData;

	if ( (Result != XNET_RESULT_OK) ||
		(pConnection == NULL) ) {
		return;
	}
		#if defined(XWS_FEATURE_WEBSOCKET_SERVER_TLS)
		if ( xrtWsConnTls(pConnection) == NULL ) {
			xrtWsConnDestroy(pConnection);
			return;
		}
		#endif
	(void)xrtWsConnText(
		pConnection,
		XRT_STR_LITERAL("connected")
	);
	(void)xrtWsConnClose(
		pConnection,
		XWS_CLOSE_NORMAL,
		(xstrview) { 0 }
	);
	xrtWsConnDestroy(pConnection);
}



/* HTTP Request 事件可以先完成路由和鉴权,再把当前连接升级为 WebSocket。 */
static void onRequest(
	xhttpserver* pServer,
	xhttpconn* pHttp,
	const xhttpserverrequest* pRequest,
	ptr pData
)
{
	xwsserverconfig Config;
	xwsconnevents Events;

	(void)pServer;
	(void)pRequest;
	xrtWsServerConfigInit(&Config);
	memset(&Events, 0, sizeof(Events));
	Config.Protocols =
		XRT_STR_LITERAL("chat.v2, chat.v1");
		#if defined(XWS_FEATURE_WEBSOCKET_SERVER_DEFLATE)
		Config.EnableDeflate = true;
		#endif
	(void)xrtWsUpgrade(
		pHttp,
		&Config,
		&Events,
		pData,
		onWebSocket,
		pData
	);
}



/* 真实服务器把 onRequest 安装到 xhttpserverevents.Request。 */
int main(void)
{
	(void)onRequest;
	printf("WebSocket server Upgrade handler is ready\n");
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXHTTP_MODULE_ALL -DXWS_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xhttp.h -include xws.h impl.c extlibs/xws/examples/websocket/http_server/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(升级回调演示完成:连接发送-关闭-销毁路径执行后正常退出)

Что только что произошло. (1) Трёхшаговый шаблон финала обратного вызова обновления: ConnText (приветственное сообщение — форма copy главы 111) → ConnClose (нормальное закрытие с кодом NORMAL) → ConnDestroy (освободить свою ссылку) — здесь закрывают немедленно, поэтому три шага подряд; долгоживущее соединение сперва Ref-сохраняет (в группу/в сессию) и позже идёт тем же путём. (2) Заголовочный комментарий формулирует архитектуру: «событие HTTP Request может сперва завершить маршрутизацию и авторизацию, затем обновить текущее соединение» — это и есть сценарий ступенчатых низкоуровневых API (авторизация на HTTP-слое, затем Upgrade). (3) Форма onRequest полностью идентична главам 103/104 — обновление WebSocket есть один из видов «ответа» на обработку HTTP-запроса.

113.4契约

  • Регистрация копирует: конфигурация/события/подпротоколы копируются один раз при регистрации; дальнейшие изменения вызывающего на конечную точку не влияют; невыровненные адреса допустимы, но должны быть полными без заворота.
  • Открытый текст и TLS как одно: один и тот же маршрут строит ws под RouterStart и wss под RouterStartTls — без второй регистрации.
  • Три ступени Origin: SAME_HOST_OR_ABSENT по умолчанию (нативные могут опускать / браузеры обязаны совпадать) / SAME_HOST строго / ANY только когда внешний слой уже проверил.
  • Автоответы: не GET → 405+Allow; Origin/авторизация → 403; протокол → 400; версия → 426 + Sec-WebSocket-Version: 13; внутренняя → 500 (аварийное закрытие, если фиксация невозможна).
  • Отказ на стадии Header: неверный метод и объявление тела отвергаются на стадии Header без чтения нелегального тела; запросы без тела обновляются на стадии полного запроса.
  • Момент Authorize: после рукопожатия и Origin; читает запрос/параметры/результат согласования; false → единый 403; нестандартное/асинхронное — вниз, на низкий уровень.
  • Open заимствует: после возврата обратного вызова адаптер освобождает переданную ссылку — сохранение требует ConnRef.
  • Семантика Release: ровно один раз после выхода Router и всех обновлённых соединений — идущие соединения продлевают жизнь Data.
  • Границы Error: только наблюдать, не отвечать — оно уведомитель отказа рукопожатия, не обработчик.
  • Четыре низкоуровневых входа: Check/Reply/Reject/Upgrade комбинируются независимо — аварийный выход для собственной авторизации/ответов/состояния.

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

Ловушка 1: сохранение соединения в Open без Ref:Ловушка 1: сохранение соединения в Open без Ref

Симптом: в какой-то момент после Open обращение к соединению падает — адаптер освободил переданную ссылку после возврата обратного вызова.

Причина: Open — семантика заимствования. Для долгого удержания (в группу, в таблицу сессий) обязательно xrtWsConnRef внутри обратного вызова — Add главы 113 внутри делает именно это.

错误写法
static void onOpen(xwsconn* pConn, ptr pData) {
	g_Session = pConn;   /* указатель сохранён без Ref: висит после возврата */
}
正确写法
static void onOpen(xwsconn* pConn, ptr pData) {
	if ( xrtWsConnRef(pConn) != NULL ) {
		g_Session = pConn;      /* сохранить после наращивания */
		xrtWsGroupAdd(g_Group, pConn);  /* группа держит ещё долю */
	}
}
Ловушка 2: политика ANY ради удобства:Ловушка 2: политика ANY ради удобства

Симптом: сторонняя страница устанавливает WebSocket-соединение с браузерными учётными данными пользователя — межсайтовый перехват WebSocket (WS-версия CSRF).

Причина: браузер автоматически прикладывает Cookie/учётные данные; ORIGIN_ANY не проверяет источник — если внешний слой (шлюз/изоляция интрасети) уже не проверил, ANY — открытая дверь. Умолчание уже балансирует нативных (можно опускать) и браузеры (обязаны совпадать) — причин опускаться до ANY нет.

错误写法
Config.Origin = XWS_SERVER_ORIGIN_ANY;  /* "сперва запустим" */
正确写法
/* держать умолчание SAME_HOST_OR_ABSENT; для строгих сценариев поднять до SAME_HOST */
Config.Origin = XWS_SERVER_ORIGIN_SAME_HOST;
Config.Authorize = check_session;       /* бизнес-авторизация ещё одним шлюзом */
Ловушка 3: ожидание Release при уничтожении Router:Ловушка 3: ожидание Release при уничтожении Router

Симптом: после Destroy Router Data ещё используется идущими соединениями — преждевременное освобождение роняет; или наоборот: Release не приходит и течёт.

Причина: контракт Release — «ровно один раз после выхода Router и всех обновлённых соединений» — идущие соединения продлевают жизнь Data. Порядок очистки: Router уничтожен → соединения закрываются по одному → финальный Release.

错误写法
xrtHttpServerRouterDestroy(pRouter);
free(my_data);   /* идущие соединения ещё пользуются: слишком рано */
正确写法
/* освобождение внутри обратного вызова Release - момент гарантируется контрактом */
static void releaseState(ptr pData) {
	free(pData);   /* исполняется ровно один раз после выхода Router и всех соединений */
}
Ws.Release = releaseState;
/* порядок уничтожения: Drain сервера -> финал соединений -> Destroy Router -> Release приходит сам */

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

Базовое: матрица регистрации и автоответов

Прогоните server_router; curl-ом отправьте POST на /chat (405) и запрос без Origin с неверной версией (426) — сверьте с таблицей автоответов. Критерии приёмки: все пять классов автоответов воспроизведены; присутствует заголовок Allow: GET.

Продвинутое: авторизующая конечная точка

Добавьте обратный вызов Authorize: проверка токена (имитация) решает пропуск/403. Тесты в форме браузера: совпадающий Origin проходит; чужой домен отвергается (403 с доменом ошибки AUTHORIZATION). Критерии приёмки: отказ Origin и бизнес-отказ различимы по домену ошибки; легитимные соединения обновляются нормально.

Вызов: полный стек канального сервиса

Приземление вызова главы 112: Router вешает конечную точку /chat/{channel} (параметр пути выбирает группу), Open делает Ref + вход в группу, сообщения рассылаются в группу канала, kick и порядок остановки (Seal → осушение → закрытие по каналам → уничтожение → Release завершает). Критерии приёмки: множество каналов под конкурентной нагрузкой без хаоса; после остановки Release исполняется ровно один раз (проверка счётчиком); ноль утечек по всему пути.

Итоги

ТемаКратко
Фиксированная конечная точкаWsServerRoute на HTTP Router; регистрация копирует; открытый текст/TLS как одно
Три ступени OriginSAME_HOST_OR_ABSENT по умолчанию / SAME_HOST строго / ANY только при внешней проверке
Автоответы405+Allow / 403 / 400 / 426+13 / 500 — пять классов полностью
Отказ на стадии Headerметод/объявление тела отвергаются без чтения тела; обновление на стадии полного запроса
Authorizeпосле рукопожатия и Origin; false → единый 403; нестандартное — на низкий уровень
Open заимствуетсохранение требует ConnRef; после возврата адаптер освобождает переданную ссылку
Releaseровно один раз после выхода Router и всех соединений — идущие продлевают жизнь Data
Четыре низкоуровневых входаCheck/Reply/Reject/Upgrade — аварийный выход для своей авторизации/ответов/состояния
Форма сессииChannel + корутина секвенируют бизнес соединения; финал по порядку: закрыть-осушить-уничтожить
Главы-компаньоны109 соединения / 110 отправка / 111 группировка / 112 вход — четыре куска пазла соединены

📖 Справочники API в этой главе:xws-websocket_server_router (2 записей)xws-websocket_http (14 записей)xhttp-http_server_router (17 записей)

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