Зачем WebSocket
HTTP - request/response, сервер не может «толкнуть» обновление сам. До WebSocket'а делали polling (запрос каждые 5 сек) или long-polling. Это тратит CPU и каналы. WebSocket даёт одно TCP-соединение, через которое и клиент, и сервер шлют сообщения когда захотят.
Применение в LinuxLab: WebSocket на /api/ws/sessions/{id} - PTY для
терминала + state-updates от агента в контейнере.
Handshake
Начинается как обычный [[http-protocol|HTTP/1.1]] запрос с upgrade:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Сервер отвечает 101 Switching Protocols:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept = base64(SHA1(client_key + magic_GUID)). Это
не безопасность, а защита от случайного апгрейда того, что не понимает
WebSocket.
После 101 - тот же TCP-сокет, но протокол поменялся: дальше идут бинарные WebSocket-frame'ы, не HTTP.
Frame format
0 1 2 3 4 5 6 7
+-+-+-+-+---+---+-+-+---+---+----+
|F|R|R|R| op|M| payload-len |...|
|I|S|S|S|cod|A| | |
|N|V|V|V|e |S| | |
| |1|2|3| |K| | |
+-+-+-+-+---+-+-+--------------+
- FIN - последний frame в логическом message? (можно фрагментировать)
- opcode - 0x1 text, 0x2 binary, 0x8 close, 0x9 ping, 0xA pong
- MASK - клиент → сервер обязан маскировать payload XOR-ом, сервер → клиент не маскирует
- payload-len - 7 / 16 / 64 бит (extended если > 125)
Маскирование с клиента нужно для защиты от cache-poisoning старых HTTP-прокси.
ws:// vs wss://
ws://- голый TCP, порт 80wss://- TLS-обёрнут, порт 443
В проде используем wss:// всегда. WebSocket-handshake живёт внутри
[[tls-handshake|TLS-сессии]] - с виду как HTTPS-запрос, апгрейд внутри
шифрованного канала.
Ping / Pong
Чтобы убедиться, что соединение живо (NAT не закрыл, peer не упал), стороны шлют ping-frame (opcode 0x9). Получатель обязан вернуть pong (0xA) с тем же payload. Если pong не приходит за N секунд - соединение протухло, реконнект.
Стандартный интервал: 30-60 секунд (короче чем NAT-timeout, обычно 5 минут на UDP-NAT и 2 часа на TCP-NAT).
Close
Корректное закрытие - close-frame (0x8) с 2-байтным status code:
| Код | Что |
|---|---|
| 1000 | normal closure |
| 1001 | going away (страница закрыта) |
| 1002 | protocol error |
| 1003 | unsupported data |
| 1006 | abnormal closure (не приходит явно, означает «соединение разорвано») |
| 1011 | server error |
| 4000+ | application-defined |
Хорошая клиентская реализация на 1006 делает экспоненциальный реконнект.
Backpressure
WebSocket не имеет нативного flow-control на уровне сообщений (только TCP-window). Если сервер шлёт быстрее чем клиент жуёт - буфер растёт. На больших нагрузках надо либо application-level backpressure (acks от клиента), либо drop-message политика.
Размер сообщения
Технически до 2^63 байт. Реально - сервера ставят лимит 1-16 MB. Большие файлы лучше через HTTP, не через WS.
Когда что-то пошло не так
- Connection closed before handshake - reverse-proxy не пропустил
Upgradeheader. В nginx нуженproxy_set_header Upgrade $http_upgrade; - 1006 каждые 60 сек - load balancer закрывает idle. Нужен ping или конфиг LB на больший timeout
- Stuck after 1 message - клиент забыл MASK, сервер дропнул
- Memory growth - нет backpressure, медленные клиенты копят buffer
- Echo обратный к серверу - забыли валидацию
Originheader, cross-site WebSocket hijacking