Learning Platform
Глоссарий Troubleshooting
Урок 12.03 · 35 мин
Начальный
WebSocketRFC 6455Full-DuplexHTTP UpgradewebsocketsScaling

WebSocket: full-duplex поверх одного TCP

SSE даёт server push, но только в одну сторону. gRPC bidirectional требует HTTP/2 и сложного toolchain-а. Когда нужен симметричный двусторонний канал между браузером и сервером — стандартом остаётся WebSocket, описанный в RFC 6455 с 2011 года.

WebSocket — это протокол, который начинается как обычный HTTP-запрос, но после handshake-а превращает TCP-соединение в двунаправленный поток frame-ов. Дальше HTTP-логики нет — только бинарный протокол с очень простым форматом.

Зачем понадобился WebSocket

До WebSocket в браузерах были два workaround-а для real-time:

  1. Long polling. Клиент делает GET, сервер не отвечает, пока не появится событие; тогда возвращает ответ и клиент сразу делает следующий GET. Latency приемлемая, но overhead на каждый цикл.
  2. Comet / forever frame / SSE. Сервер пишет в долгий ответ. В одну сторону работает, для двусторонней связи нужно отдельное соединение для отправки.

WebSocket убрал эти костыли: один TCP, обе стороны равноправны. Идеально для chat-ов, multiplayer-игр, collaborative editing-а (Figma, Notion, Google Docs), trading dashboard-ов.

TCP сокеты: клиент-сервер на уровне BSD sockets

Upgrade handshake

WebSocket «маскируется» под HTTP, чтобы пройти через файрволы. Запрос:

GET /ws/chat HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat.v1, chat.v2
Origin: https://app.example.com

Сервер отвечает:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat.v2

Магия в Sec-WebSocket-Accept: сервер берёт клиентский Sec-WebSocket-Key, конкатенирует с фиксированным GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, вычисляет SHA-1 и кодирует в base64. Этот алгоритм описан в RFC и нужен, чтобы сервер случайно не поверил, что клиент — это WebSocket, когда на самом деле это просто кеш-ответ старого прокси.

После 101 Switching Protocols TCP-соединение перестаёт быть HTTP. Дальше летят WebSocket frame-ы.

Жизненный цикл WebSocket-соединения
Браузер
WS-сервер
GET /ws Upgrade: websocket101 Switching ProtocolsFrame text 'hello'Frame text 'world'Frame pingFrame pongFrame binary 0x01 0x02 0x03Frame close (1000 'bye')Frame close (1000)

Анатомия frame-а

Frame состоит из 2-14 байт заголовка + 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|             |                               |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
  • FIN — последний frame в логическом сообщении (можно фрагментировать большое сообщение на несколько frame-ов).
  • Opcode — тип frame-а: 0x1 text, 0x2 binary, 0x8 close, 0x9 ping, 0xA pong.
  • MASK — клиент к серверу обязан XOR-ить payload случайной 4-байтовой маской (защита от cache poisoning старых прокси). Сервер к клиенту маску не использует.
  • Payload length — 7 бит (до 125), либо 16 бит (до 65535), либо 64 бита (до 2^63).

В реальности с этим работают библиотеки — никто frame-ы руками не парсит.

Subprotocols

Заголовок Sec-WebSocket-Protocol позволяет клиенту и серверу договориться о формате сообщений поверх WebSocket. Это просто строка-имя: клиент шлёт список приемлемых протоколов, сервер выбирает один.

Известные subprotocols:

  • mqtt — MQTT 3.1/5 поверх WebSocket для IoT.
  • graphql-ws — спецификация GraphQL subscriptions.
  • wamp.2.json — Web Application Messaging Protocol для RPC.
  • Кастомный — большинство приложений придумывает свой chat.v1.

Это просто соглашение о формате payload-а; WebSocket frame-ы остаются прежними.

Heartbeats

В отличие от SSE, WebSocket имеет встроенные ping/pong frame-ы. Сервер периодически шлёт 0x9 — клиент обязан ответить 0xA. Это позволяет:

  • Засечь мёртвое соединение (NAT забыл mapping, мобильная сеть оборвалась).
  • Поддерживать соединение через прокси с idle-таймаутом.

Хорошие библиотеки делают ping каждые 20-30 секунд автоматически. В Python websockets это ping_interval=20, ping_timeout=20.

Python: библиотека websockets

websockets — каноническая Python-библиотека (asyncio-нативная). Сервер:

import asyncio
import websockets
from websockets.server import WebSocketServerProtocol

connections: set[WebSocketServerProtocol] = set()

async def chat_handler(ws: WebSocketServerProtocol):
    connections.add(ws)
    try:
        async for message in ws:
            # Broadcast: рассылка всем подключённым
            for peer in connections:
                if peer is not ws and peer.open:
                    await peer.send(message)
    finally:
        connections.remove(ws)

async def main():
    async with websockets.serve(
        chat_handler,
        "0.0.0.0",
        8765,
        ping_interval=20,
        ping_timeout=20,
        max_size=1_000_000,  # лимит на сообщение, защита от DoS
    ):
        await asyncio.Future()  # run forever

asyncio.run(main())

Клиент:

import asyncio
import websockets

async def chat_client():
    uri = "wss://api.example.com/ws/chat"
    async with websockets.connect(uri, extra_headers={"Authorization": "Bearer ..."}) as ws:
        await ws.send("hello server")
        async for message in ws:
            print("received:", message)

asyncio.run(chat_client())

Несколько важных моментов production-кода:

  • wss:// обязательно в production (зашифрованный WebSocket поверх TLS).
  • Авторизация — заголовок Authorization доступен только при handshake-е. После него ничего не передаётся, нужна ваша protocol-level аутентификация (первое сообщение с токеном).
  • Лимит размера сообщений (max_size) — иначе один клиент может прислать 1 GB payload и положить сервер.

Scaling: stateful-coupling

Главная боль WebSocket-ов — stateful. Каждое соединение привязано к конкретному серверу. Если у вас 10 нод и 100 000 пользователей в чате, и Alice сидит на ноде 3, а Bob на ноде 7, нужен механизм межсерверного broadcast-а.

Типовые решения:

  1. Sticky sessions на load balancer-е — каждый клиент всегда попадает на ту же ноду. Решает проблему reconnect-а на ту же ноду, но не межсерверный broadcast.
  2. Pub/sub backbone — Redis pub/sub, NATS, Kafka. При получении сообщения нода публикует в канал, остальные получают и рассылают своим клиентам.
  3. Managed WebSocket gateway — Pusher, Ably, AWS API Gateway WebSocket. Они держат TCP-coupling, ваш backend публикует через REST.

В serverless-среде (Lambda, Cloud Run scale-to-zero) WebSocket работает плохо — функция не может удерживать долгое соединение. Поэтому AWS придумал API Gateway WebSocket: gateway держит соединение, а Lambda вызывается на каждое сообщение.

Backpressure

Если клиент медленнее сервера (мобильный 3G), сообщения копятся в буфере отправки на сервере. Без контроля — память течёт.

Решения:

  • Drop old messages — отбрасывать устаревшие данные (для тиковых данных биржи это норма).
  • Pause producer — приостанавливать генерацию данных, пока буфер не опустеет.
  • Coalescing — заменять серию обновлений одним актуальным состоянием.

В Python websockets можно проверять ws.transport.get_write_buffer_size() или ловить websockets.exceptions.ConnectionClosed при переполнении.

WARNING

WebSocket — мощный, но избыточный для server -> client scenario. Если у вас нет сообщений от клиента к серверу (кроме periodic heartbeat-ов) — SSE даст ту же функциональность с в 5 раз меньшим объёмом серверного кода. Не используйте WS только потому, что «он круче».

WebSocket в DE-сценариях

  • Live аналитика — биржевые тиковые данные (Binance, Coinbase публикуют WS-фиды), real-time дашборды.
  • Collaborative editing — Jupyter notebook collaboration, Mode Analytics.
  • Streaming source ingestion — pipeline принимает WS-фид, парсит сообщения, кладёт в Kafka.
  • Notifications между data tools — Airflow -> UI, dbt cloud -> IDE.

Junior DE чаще всего сталкивается с WS как клиент: подключиться к Binance, парсить тики, складывать в локальный буфер. Серверная сторона WS — типичная задача backend-команды, но базовое понимание помогает в дискуссиях.

Когда выбрать WebSocket

СценарийПодходит?
Двусторонний chatДа
Multiplayer gameДа
Collaborative editingДа
Биржевые тикиДа (биржи отдают через WS)
Прогресс операцииЛучше SSE
LLM streamingЛучше SSE
Push-уведомленияЛучше SSE / FCM
Webhook от внешнего сервисаЛучше HTTPS webhook
Проверка знанийKnowledge check
Команда планирует чат на 50 000 одновременных пользователей через WebSocket. Архитектура: 5 серверных инстансов за load balancer-ом, sticky sessions включены. На тестах при 1 пользователе всё работает. При нагрузочном тесте обнаруживается, что Alice на ноде 1 не видит сообщения от Bob на ноде 3. В чём проблема и какое решение?
ОтветAnswer
Проблема -- отсутствие межсерверного broadcast-а. Sticky sessions решают только то, что Alice всегда переподключается на ноду 1 (и её state не теряется). Но WebSocket-соединение Bob-а живёт на ноде 3, и нода 3 ничего не знает о существовании Alice на ноде 1 -- у каждой ноды свой in-memory set активных connection-ов. Когда Bob отправляет сообщение, нода 3 рассылает его только своим connection-ам и не уведомляет ноду 1. Решение -- pub/sub backbone между нодами. Самый простой вариант -- Redis Pub/Sub: при получении сообщения от любого клиента нода публикует в общий канал 'chat:room:42'; все ноды подписаны на этот канал и при получении пересылают своим локальным connection-ам. Альтернативы: NATS (легче и быстрее Redis для pub/sub), Kafka (если нужна персистентность сообщений), managed решения вроде Ably/Pusher/AWS API Gateway WebSocket. Дополнительные соображения: 1) sticky sessions всё равно полезны для уменьшения reconnect-shuffling; 2) нужен механизм чистки 'мёртвых' connection-ов через ping/pong heartbeats; 3) при больших объёмах sharding по chat-room-ам помогает не публиковать всё во все ноды.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чем WebSocket принципиально отличается от SSE?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 3