Handshake и генерация ключа
Установление соединения начинается с HTTP‑запроса Upgrade, в котором клиент отправляет заголовок Sec-WebSocket-Key. Значение – 16‑байтовый случайный набор, закодированный в Base64. Сервер получает этот ключ, добавляет к нему GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, вычисляет SHA‑1‑хеш и снова кодирует результат в Base64. Полученный токен помещается в заголовок Sec-WebSocket-Accept ответа.
import os, base64, hashlib
def client_key():
# 16 случайных байт → Base64
return base64.b64encode(os.urandom(16)).decode()
def server_accept(key):
GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
sha1 = hashlib.sha1((key + GUID).encode()).digest()
return base64.b64encode(sha1).decode()
Если клиент получает Sec-WebSocket-Accept, совпадающий с результатом вычислений, handshake считается успешным и обе стороны переходят в режим полного дуплекса.
Структура фрейма
Каждый пакет данных (фрейм) состоит из фиксированного заголовка и необязательного полезного payload. Заголовок начинается с двух байтов:
| Бит | Описание |
|---|---|
| FIN (1) | Флаг завершения сообщения. Если 1 – текущий фрейм последний в последовательности. |
| RSV1‑3 (3) | Зарезервированы для будущих расширений, обычно 0. |
| Opcode (4) | Тип фрейма: 0x0 – продолжение, 0x1 – текст, 0x2 – бинарный, 0x8 – закрытие, 0x9 – ping, 0xA – pong. |
| Mask (1) | Указывает, маскируются ли данные (обязательно для клиента). |
| Payload length (7) | Длина полезных данных. При значениях 126 и 127 следуют расширенные поля длины (2 байта или 8 байт). |
После этих двух байтов, если Mask установлен, идут 4 байта маскирования, а затем – сами данные, при необходимости обработанные маской.
Маскирование данных
Клиент обязан маскировать каждый фрейм, чтобы предотвратить атаки типа «протокол-спуфинг». Маска – случайные 4 байта, применяемые к каждому байту полезного поля по формуле:
masked_byte[i] = payload_byte[i] XOR mask[i % 4]
Сервер, получив фрейм, просто повторяет XOR‑операцию, используя ту же маску, и восстанавливает оригинальные данные. Маскирование не добавляет криптографической защиты, но гарантирует, что в сети не будет «чистого» текста, который может быть ошибочно интерпретирован как HTTP‑запрос.
def mask_payload(payload, mask):
return bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
def unmask_payload(masked, mask):
return mask_payload(masked, mask) # XOR обратим
Фрагментация и переупорядочивание
Протокол позволяет разбивать крупные сообщения на несколько фреймов. Первый фрейм содержит реальный Opcode (текст или бинарный), последующие – только Opcode = 0x0 (Continuation). Финальный фрейм помечается битом FIN = 1. При получении фрагментированных сообщений клиент обязан собрать payload в том порядке, в котором они пришли, без переупорядочивания.
Если сервер получает фрагментированное сообщение, где один из фреймов имеет FIN = 1, но предшествующее количество фреймов не соответствует ожидаемому (например, пропущен Continuation), он обязан закрыть соединение с кодом 1002 (протокольная ошибка).
Процедура закрытия соединения
Закрытие инициируется отправкой фрейма с Opcode = 0x8. Тело может содержать 2‑байтовый код причины (по стандарту – любой из 1000‑4999) и необязательное текстовое сообщение. После отправки закрывающего фрейма сторона должна ждать аналогичный фрейм от партнёра, после чего закрывает TCP‑соединение.
def close_frame(code=1000, reason=''):
payload = code.to_bytes(2, 'big') + reason.encode('utf-8')
# клиент маскирует, сервер – нет
mask = os.urandom(4)
masked = mask_payload(payload, mask)
# FIN=1, Opcode=0x8, Mask=1, Payload length
header = bytes([0b10001000, 0b10000000 | len(masked)])
return header + mask + masked
Если соединение прерывается без корректного закрывающего фрейма, получатель обычно генерирует код 1006 – «неполучено закрытие», что часто указывает на проблемы в балансировщике, фаерволе или ошибку в реализации.
Практические нюансы реализации
-
Размеры payload: При длине ≤ 125 байт длина указывается непосредственно в 7‑битном поле. Для 126 ≤ len ≤ 65535 используется 2‑байтовый расширенный размер, а для более крупных сообщений – 8 байтовый. Реализация должна корректно обрабатывать оба случая.
-
Проверка RSV‑битов: Если сервер не поддерживает расширения (например, permessage‑deflate), любые установленные RSV‑биты должны привести к закрытию с кодом 1002.
-
Ping/Pong: Фреймы
Opcode = 0x9(ping) и0xA(pong) работают без маскирования от сервера, но клиент обязан маскировать ping. При получении ping сервер обязан немедленно ответить pong с тем же payload. -
Тайм‑ауты: RFC рекомендует отправлять ping‑фреймы периодически (например, каждые 30 сек) для обнаружения «молчаливых» разрывов.
-
Бинарные данные: При работе с бинарными сообщениями важно помнить, что маскирование применяется к каждому байту, независимо от его смысловой нагрузки.
Эти детали позволяют построить надёжный WebSocket‑клиент или сервер, который будет корректно взаимодействовать со всеми современными браузерами и балансировщиками, избегая типичных ошибок, связанных с неожиданным разрывом соединения, неверной фрагментацией или неправильным закрытием.