WebSocket
WebSocket (RFC 6455, 2011) — протокол двунаправленной связи поверх одного TCP-соединения. Открывается через HTTP Upgrade, дальше это уже не HTTP, а свой бинарный формат кадров. Заменил Comet / long-polling. Универсально поддерживается везде — все браузеры, все языки.
Handshake
Клиент шлёт обычный HTTP-запрос с апгрейдом:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
Origin: https://example.com
Сервер отвечает:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Accept = base64(SHA-1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)). Магическая строка защищает от кэш-отравления и случайного апгрейда.
Дальше по этому же TCP-соединению — уже WebSocket-фреймы.
Формат фрейма
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 (if MASK set) | Payload |
+-------------------------------+-------------------------------+
| Payload Data continued ... |
+---------------------------------------------------------------+
- FIN — последний фрейм в сообщении (сообщение может быть фрагментировано).
- Opcode:
0x0 continuation 0x1 text (UTF-8) 0x2 binary 0x8 close 0x9 ping 0xA pong - MASK — обязательно 1 для клиент→сервер, 0 для сервер→клиент.
- Payload len: если ≤125 — прямо это. 126 → следующие 2 байта = длина. 127 → следующие 8 байт.
- Masking key — 4 байта. Клиент XOR'ит payload этим ключом. Защита от cache poisoning на прокси.
Клиентский API
const ws = new WebSocket('wss://example.com/chat', ['chat']);
ws.onopen = () => ws.send('hello');
ws.onmessage = (ev) => console.log(ev.data);
ws.onclose = (ev) => console.log('closed', ev.code, ev.reason);
ws.onerror = () => console.log('error');
// Бинарь
ws.binaryType = 'arraybuffer';
ws.send(new Uint8Array([1,2,3]).buffer);
Ping/Pong
Сторона может отправить ping — получатель ОБЯЗАН ответить pong с тем же payload. Используется для keepalive. Браузер отправляет ping'и автоматически при некоторых условиях, но JS сам их не видит — только автопонги.
Close codes
| Код | Значение |
|---|---|
| 1000 | Normal closure |
| 1001 | Going away (страница закрывается) |
| 1002 | Protocol error |
| 1003 | Unsupported data |
| 1006 | Abnormal closure (не было close-фрейма). Никогда не отправляется, только логическое |
| 1008 | Policy violation |
| 1009 | Message too big |
| 1011 | Internal error |
| 4000-4999 | Application-defined |
Subprotocols
Клиент указывает список желаемых подпротоколов в Sec-WebSocket-Protocol, сервер выбирает один. Пример: WAMP, MQTT-over-WebSocket, GraphQL Subscriptions, STOMP. Задают формат сообщений поверх бинарного канала.
permessage-deflate
Расширение RFC 7692 — сжатие сообщений через deflate. Договариваются в handshake через Sec-WebSocket-Extensions. Есть CRIME-подобная атака при передаче секретов вперемешку с user-input.
WebSocket vs SSE vs long-polling
| WebSocket | SSE | Long-polling | |
|---|---|---|---|
| Направление | Двустороннее | Сервер→клиент | Клиент→сервер (запросы) |
| Overhead на сообщение | 2-14 байт | ~5 байт | Полный HTTP-заголовок |
| Auto-reconnect | Нет, реализуется руками | Да, в браузере | Часть протокола |
| Прокси-совместимость | Некоторые режут | Отлично | Отлично |
Серверы
- Node.js —
ws,uWebSockets.js,socket.io. - Go —
gorilla/websocket,nhooyr/websocket,fasthttp/websocket. - Python —
websockets(async),aiohttp. - Rust —
tokio-tungstenite,axumс ws feature. - Nginx — умеет проксировать WebSocket через специальные заголовки:
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
Соображения по масштабу
- Одно соединение = один сокет = один файл-дескриптор. Ulimit важен.
- Broadcast N сообщений на M клиентов = N*M send'ов. При тысячах клиентов — pub/sub-архитектура нужна.
- Sticky sessions на балансере, потому что стейт per-connection.
- Тайм-аут NAT/прокси на idle-соединение — 30-120 сек. Keepalive обязателен.
Использование как VPN-транспорт
WebSocket пробивается через любой HTTPS-прокси и файрвол — трафик выглядит как обычный веб. Отсюда WebSocket-транспорт для VLESS/Trojan (ws transport), HTTPUpgrade, XHTTP.