awesome-everything EN
↑ Обратно к восхождению

Сети и протоколы

Формат WebSocket-фрейма: opcodes, маскирование, фрагментация

Суть 2-байтный заголовок, несущий каждое WebSocket-сообщение — что означают FIN, opcode, MASK и трёхуровневое кодирование длины, и почему клиентские фреймы обязаны маскироваться.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 12 min

После WebSocket-handshake HTTP-парсер исчезает. По проводу идёт компактный бинарный формат, несущий каждое сообщение — текст, бинарные данные, keepalive-пинги и graceful close — при накладных расходах всего от 2 байт. Понимание этого формата отделяет «как-то работает» от «точно знаю, что сломалось».

Анатомия заголовка фрейма

WebSocket-фрейм начинается с 2 обязательных байт, за которыми следуют опциональные поля расширения длины и masking-ключа, а затем сам payload:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)    |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Masking-key (если MASK=1, 4 байта)                        |
+---------------------------------------------------------------+
|                    Payload data                               |
+---------------------------------------------------------------+

Байт 1:

  • FIN (бит 7)1 означает, что это последний (или единственный) фрагмент сообщения.
  • RSV1-3 (биты 6-4) — зарезервированы для расширений (например, permessage-deflate выставляет RSV1=1).
  • Opcode (биты 3-0) — тип данных фрейма:
OpcodeЗначение
0x0Continuation-фрейм
0x1Текстовые данные (UTF-8)
0x2Бинарные данные
0x8Close
0x9Ping
0xAPong

Байт 2:

  • MASK (бит 7)1 = payload XOR-маскирован (клиент→сервер всегда; сервер→клиент никогда).
  • Длина payload (биты 6-0):
    • 0–125 — реальная длина.
    • 126 — следующие 2 байта (uint16) содержат реальную длину.
    • 127 — следующие 8 байт (uint64) содержат реальную длину.

Итоговые накладные расходы фрейма:

  • Маленький фрейм сервер→клиент: 2 байта только заголовок.
  • Маленький фрейм клиент→сервер: 2 байта заголовок + 4 байта masking-ключ = 6 байт.
Накладные расходы WebSocket-фрейма
Минимальный заголовок фрейма (без маски, payload ≤125 байт)
2 байта
Накладные расходы клиент→сервер (маска обязательна)
6 байт
Максимальный payload в 7-битном поле длины
125 байт
Расширение длины для payload 126–65535 байт
+2 байта (uint16)
Расширение длины для бо́льших payload
+8 байт (uint64)
Максимальный payload control-фреймов (ping/pong/close)
125 байт

Почему клиентские фреймы должны маскироваться

Маскирование — не шифрование, а защита от cache-poisoning. Вот атака, которую оно предотвращает:

Вредоносный JavaScript на site-a.com открывает WebSocket-соединение к промежуточному прокси. Затем отправляет байты, которые выглядят как корректный HTTP-ответ. Наивный stateless-прокси воспринимает эти байты как HTTP и возвращает их другим клиентам — отравляя кеш.

С маскированием клиент XOR-ирует каждый байт payload с 4-байтным случайным ключом из заголовка фрейма:

masked_byte[i] = payload[i] XOR mask_key[i % 4]

Получатель XOR-ирует обратно тем же ключом, восстанавливая исходный payload. Поскольку masking-ключ случаен для каждого фрейма, JavaScript на вредоносном сайте не может заранее сконструировать байты, которые одновременно выглядели бы как HTTP-ответ И корректно раскодировались под XOR. Атака становится нереализуемой.

Серверные фреймы не маскируются, потому что JavaScript на site-a.com всё равно не может прочитать сырые байты ответа с site-b.com (same-origin policy это блокирует).

Фрагментация и continuation-фреймы

Большое сообщение можно разбить на несколько фреймов. Правила:

  1. Первый фрагмент: реальный opcode (0x1 или 0x2), FIN=0.
  2. Средние фрагменты: opcode 0x0 (continuation), FIN=0.
  3. Последний фрагмент: opcode 0x0, FIN=1.

Получатель собирает фрагменты по порядку. Control-фреймы (ping, pong, close) не могут фрагментироваться и ограничены 125 байтами; они могут приходить между фрагментами данных.

Control-фреймы: ping, pong, close

Ping (0x9): keepalive-проба. Получатель обязан ответить pong с тем же payload. Прокси часто имеют idle timeout (обычно 60 секунд); ping каждые 25–30 секунд сбрасывает таймер прокси и поддерживает соединение живым.

Pong (0xA): обязательный ответ на ping. Можно слать без запроса как односторонний heartbeat.

Close (0x8): инициирует closing-handshake. Тело содержит опциональный 2-байтный код статуса и UTF-8 причину. Стандартные коды:

КодЗначение
1000Нормальное закрытие
1001Going away (перезапуск сервера, вкладка закрыта)
1006Аномальное закрытие (close-фрейм не получен; генерируется реализацией)
1008Нарушение политики
1011Непредвиденная ошибка
1013Попробуйте позже

После отправки close-фрейма каждая сторона должна дождаться ответного close-фрейма, прежде чем закрывать TCP-соединение.

Почему это работает

Почему RSV-биты важны для расширений. Расширение permessage-deflate (RFC 7692), согласованное в handshake, использует RSV1=1 для сигнализации, что payload сжат DEFLATE. Сервер, не согласовавший это расширение и увидевший RSV1=1, обязан закрыть соединение с кодом 1002 (ошибка протокола). Такая строгая проверка гарантирует, что расширения не могут незаметно повредить фреймы.

Разбор небольшого WebSocket-фрейма

1/3
# Сервер отправляет "OK" клиенту # Opcode 0x1 = текст, FIN=1, payload = "OK" (2 байта), без маски Байты фрейма (hex): 81 02 4F 4B Байт 1: 0x81 = 10000001 FIN=1 — полное сообщение, без фрагментов RSV=000 — расширения не активны Opcode=0001 — текстовые данные (UTF-8) Байт 2: 0x02 = 00000010 MASK=0 — сервер никогда не маскирует (верно) Длина payload=2 Байты 3-4: 0x4F 0x4B "O" (0x4F), "K" (0x4B) = "OK" Провод: 2 байта заголовок + 2 байта payload = 4 байта всего
Викторина

Почему Sec-WebSocket-Key клиента преобразуется в Sec-WebSocket-Accept через добавление фиксированного GUID, хеширование и base64-кодирование?

Викторина

Почему фреймы клиент→сервер должны маскироваться, а фреймы сервер→клиент — нет?

Расставь шаги по порядку

Упорядочите шаги closing-handshake WebSocket:

  1. 1 Одна сторона отправляет close-фрейм с кодом 1000
  2. 2 Другая сторона получает его и отвечает close-фреймом
  3. 3 Отправитель второго close-фрейма закрывает TCP-соединение
  4. 4 Обе стороны переходят в состояние closed
Вспомните перед уходом
  1. 01
    Объясните, почему маскирование защищает от cache-poisoning, даже несмотря на то, что masking-ключ передаётся открытым текстом внутри фрейма.
  2. 02
    Чат-сервер рассылает сообщение 10 000 подключённых клиентов. Рассылка занимает 100 мс, хотя RTT сети всего 5 мс. Откуда берутся остальные 95 мс?
  3. 03
    Для чего нужен FIN-бит в WebSocket-фрейме и как он взаимодействует с opcode?
Итог

Каждое WebSocket-сообщение передаётся в одном или нескольких фреймах. 2-байтный заголовок кодирует FIN-бит (флаг последнего фрагмента), opcode (текст, бинарные данные, ping, pong, close), флаг MASK и длину payload. Клиентские фреймы обязаны XOR-ировать payload с случайным 4-байтным masking-ключом для защиты от cache-poisoning, где вредоносный JavaScript конструирует байты, похожие на HTTP-ответы; серверные фреймы никогда не маскируются, потому что same-origin policy и так блокирует чтение JavaScript cross-origin сырых байт. Большие сообщения могут фрагментироваться через opcode 0x0 (continuation) с FIN=0 на всех фрагментах кроме последнего. Control-фреймы (ping, pong, close) несут не более 125 байт и не могут фрагментироваться. Close-фреймы несут 2-байтный код статуса; 1000 — нормальное закрытие, 1006 генерируется когда close-фрейм не был получен.

Связанные уроки
встречается в152
Продолжить восхождение ↑WebSocket vs SSE vs long-polling: выбор правильного транспорта
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.