Один канал, full-duplex и фреймы

После рукопожатия обе стороны могут говорить одновременно по одному каналу — разбираемся, как.

Full-duplex — режим связи, при котором обе стороны передают данные одновременно и независимо, как при телефонном разговоре, а не по очереди.

Один канал на всё

В polling и long-polling под каждое сообщение открывался новый HTTP-запрос с кучей заголовков. WebSocket держит одно TCP-соединение всё время жизни. Открыли в начале — и гоняем по нему данные туда-сюда без повторных рукопожатий и без заголовков на каждое сообщение. Это резко снижает накладные расходы и задержку.

Half-duplex против full-duplex

РежимАналогияКто где
Half-duplexрация: «приём»HTTP — клиент спросил, ждёт ответ
Full-duplexтелефонWebSocket — оба говорят разом

В WebSocket сервер может прислать уведомление в ту же секунду, когда браузер отправляет своё сообщение — они не мешают друг другу.

Фреймы вместо запросов

Данные передаются не запросами, а маленькими пакетами — фреймами. У каждого фрейма есть крошечный заголовок (несколько байт): тип (текст, бинарные данные, ping, pong, close), флаг «это последний фрагмент?» и длина. Никаких HTTP-заголовков. Поэтому одно короткое сообщение «привет» весит почти столько же, сколько сам текст — против сотен байт заголовков в HTTP.

Структура фрейма (упрощённо):
+------+--------+------------------+
| тип  | длина  |   полезные данные |
| 1 б  | 1-9 б  |   ...текст/байты  |
+------+--------+------------------+

Типы: text, binary, ping, pong, close

Как работает под капотом

Большое сообщение может разбиваться на несколько фреймов с флагом «продолжение». Фреймы от браузера к серверу дополнительно маскируются (XOR со случайным ключом) — это защита от атак на кеширующие прокси, которые могли бы принять данные за HTTP-запрос. От сервера к браузеру маскировки нет. Всё это скрыто внутри протокола: разработчик просто вызывает send() и получает onmessage, а сборкой и маскировкой занимается реализация WebSocket.

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

  • Слать тысячи крошечных фреймов в секунду. Накладные расходы малы, но не нулевые; объединяйте мелкие события (батчинг), если их слишком много.
  • Считать, что одно send() = один сетевой пакет. Соответствие фрейма и TCP-пакета не гарантировано; на это нельзя опираться.
  • Слать гигантское сообщение целиком. Лучше дробить большие данные самостоятельно и показывать прогресс.

Итоги

  • WebSocket держит один TCP-канал на всё время жизни соединения.
  • Связь full-duplex: обе стороны передают данные одновременно.
  • Данные идут лёгкими фреймами с крошечным заголовком — без HTTP-заголовков на каждое сообщение.
  • Маскировка и фрагментация скрыты внутри протокола.
Проверьте себя
1. Что означает full-duplex применительно к WebSocket?
AДанные шифруются дважды
BОбе стороны могут передавать одновременно и независимо
CСервер отвечает только по очереди
DСоединение использует два TCP-порта
2. Чем фрейм WebSocket выгоднее обычного HTTP-запроса для коротких сообщений?
AУ фрейма крошечный заголовок без HTTP-заголовков
BФрейм всегда зашифрован
CФрейм гарантированно доходит
DФрейм передаётся по UDP