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 ...                |
+---------------------------------------------------------------+

Клиентский 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

КодЗначение
1000Normal closure
1001Going away (страница закрывается)
1002Protocol error
1003Unsupported data
1006Abnormal closure (не было close-фрейма). Никогда не отправляется, только логическое
1008Policy violation
1009Message too big
1011Internal error
4000-4999Application-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

WebSocketSSELong-polling
НаправлениеДвустороннееСервер→клиентКлиент→сервер (запросы)
Overhead на сообщение2-14 байт~5 байтПолный HTTP-заголовок
Auto-reconnectНет, реализуется рукамиДа, в браузереЧасть протокола
Прокси-совместимостьНекоторые режутОтличноОтлично

Серверы

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;
}

Соображения по масштабу

Использование как VPN-транспорт

WebSocket пробивается через любой HTTPS-прокси и файрвол — трафик выглядит как обычный веб. Отсюда WebSocket-транспорт для VLESS/Trojan (ws transport), HTTPUpgrade, XHTTP.

См. также