Глава 21 / 51
Том 3 · Разработка протоколов

Основы WebSocket: двусторонняя связь в шести колбэках

Рукопожатие, маскирование, фрагментация, управляющие кадры и сжатие RFC 6455 целиком завершаются в XServer и слое xrt — ваш скрипт это шесть функций в конце конвейера.

Обзор

Позиционирование: «базовой» главы уже хватает на работающий real-time сервис — сложность протокола съел хост. Продвинутая глава (гл. 22) займётся широковещанием, лимитами и подпротоколами.

21.1 Введение

Список боли рукописного WebSocket: валидация Upgrade-рукопожатия (SHA-1+Base64 от Sec-WebSocket-Key), разбор заголовков кадров (бит маски, расширенные длины), пересборка фрагментов, вплетение управляющих кадров, автомат состояний close, согласование permessage-deflate. Разделение труда в XServer: хост делает рукопожатие и проверку (провал — 400), xrtWsStreamAttach — кадровый слой; колбэк получает «сообщение целиком», отправка — один вызов функции.

21.2 Рукопожатие и колбэки

HTTP-запрос → валидация Upgrade (xrtWsUpgradeRequestCheck) провал → 400 успех → 101 Switching → xrtWsStreamAttach │ ┌──────────────────────────────────┴───────────────┐ ▼ ▼ ▼ ▼ ▼ WsOpen WsText WsBinary WsPing WsClose (готово) (текст целиком)(бинарное целиком)(наблюдение)(iCode+причина)
  • Рукопожатие маршрутизируется по vhost (Host/SNI как у HTTP); неудача маршрутизации — 404
  • Host без экспортированных WsText/WsBinary → 503 (сервис есть, бизнеса нет)
  • Pong отвечает автоматически на уровне xrt — колбэки WsPing/WsPong только наблюдают (статистика пульса)
  • Жёсткая граница линии в рукопожатии: защита от медленных атак «заполнить ReadLimit и ждать вечно»

21.3 Семейство отправки и эхо

Эхо-сервис (в стиле контрактных исходников)
static void WsOpen(XS_HostInfo* pHost, xwsstream* pWs)
{
    (void)xrtWsStreamText(pWs, xrtStrView("connected"));
}

static void WsText(XS_HostInfo* pHost, xwsstream* pWs, xstrview tText)
{
    (void)xrtWsStreamText(pWs, tText);      /* echo */
}

static void WsBinary(XS_HostInfo* pHost, xwsstream* pWs, xbytesview tData)
{
    (void)xrtWsStreamBinary(pWs, tData);
}

Что только что произошло: во главе семейства отправки xrtWsStreamText / Binary (копирующая отправка); есть варианты ...Ref (заимствующая — буфер нельзя трогать до возврата) и ...Take (передача владения, ноль копий для больших сообщений) — выбирайте по происхождению сообщения.

21.4 Контекст соединения

WS-бизнесу почти всегда нужно «состояние на каждое соединение» (личность пользователя, список подписок). Рекомендуемый подход — реестр соединений (собственный map: указатель соединения → контекст), вставка в WsOpen, удаление в WsClose:

Скелет реестра соединений
static xmutex  G_WsLock;
static xmap*   G_WsConns;    /* xwsstream* → ConnCtx* (межколбэковое заимствование — ваша забота) */

typedef struct ConnCtx {
    char sUser[64];
    uint64 iLastSeen;
} ConnCtx;

static void WsOpen(XS_HostInfo* pHost, xwsstream* pWs)
{
    ConnCtx* pCtx = xrtCalloc(1, sizeof(ConnCtx));
    /* вставка (под блокировкой) ... */
}
static void WsClose(XS_HostInfo* pHost, xwsstream* pWs,
    uint16 iCode, xstrview tReason)
{
    /* удаление и освобождение (под блокировкой); WsClose гарантированно терминальный */
}

Обратите внимание: жизненным циклом соединения владеет хост, контекста — вы; паттерн реестра держит их в точной синхронизации. Эта таблица же — инфраструктура широковещания из гл. 22.

21.5 Контракт

  • Колбэки: шесть исполняются на worker-потоках конкурентно; представления сообщений (tText/tData) действительны в колбэке.
  • Терминальность: WsClose — терминальный колбэк каждого соединения; чистка реестра здесь надёжнее всего.
  • Отправка: Text/Binary копирует, Ref заимствует, Take передаёт — вариант выбирается по владению буфером.

Частые ошибки

Представления сообщений положили в реестр
Симптом
Последующие чтения дают перепутанные данные.
Причина
tText/tData — заимствованные представления колбэка; перед закладкой в таблицу копируйте (реестр хранит ваш ConnCtx, не представления).
Буфер правят сразу после Ref-отправки
Симптом
Случайная порча содержимого сообщений.
Причина
xrtWsStreamTextRef заимствует ваш буфер — возврат функции ≠ завершение отправки (при противодавлении данные ещё в буфере). Нужно переиспользовать немедленно — берите копирующий или Take-вариант.

Упражнения

БазовыйЭхо-тройной тест

Запустив эхо, отправьте любым WS-клиентом текст / бинарные данные / большое сообщение (1 МБ).

Приёмка: все три класса эхом совпадают; 1 МБ отвергается с 1009 (лимит по умолчанию, настраивается в гл. 22).
ПродвинутыйСеансы с личностью

Договоритесь, что первое сообщение — "login:имя"; реестр фиксирует личность, дальнейшее эхо с префиксом "[имя] ".

Приёмка: два конкурентных сеанса не пересекаются; после разрыва в реестре нет остатков.
ВызовСторож пульса

Через WsPing ведите статистику RTT; соединения без единого сообщения 60 секунд закрывайте сами.

Приёмка: заклинившие соединения без сети вычищаются из реестра за десятки секунд.

Итоги

ТезисВывод
Задача хоставалидация рукопожатия (400/404/503) + кадровый слой Attach
Шесть колбэковOpen/Text/Binary/Ping/Pong/Close; Pong отвечает сам
Семейство отправкиText/Binary (копия), Ref (заимствование), Take (передача)
Контекстсвой реестр: вставка в Open, удаление в Close (терминальная гарантия)
API, затронутые в главе
Что дальше по xrt