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