Привет, радостная новость, у меня вышла статья на Хабре. Хабр конечно дело хорошее, но у себя статью я тоже сохраню!

В этой статье хочу посмотреть на WebSocket глазами системного аналитика и архитектора: от конкретики протокола HTTP 101 и фреймов до архитектурных решений с API Gateway, sticky‑sessions и формата постановок задач.

Материал основан на реальном опыте из высоконагруженной системы, где живут в одном «зоопарке»:

  • REST‑API;
  • WebSocket;
  • GraphQL;
  • gRPC;
  • Kafka;
  • Redis (кеш и pub/sub);
  • WebRTC для видео.

С таким набором очень быстро становится понятно: WebSocket — не модная игрушка, а инструмент для узкого, но важного класса задач.

Зачем системному аналитику думать о WebSocket

Во многих проектах системный аналитик живёт в уютном мире REST: ресурсы, методы, CRUD, contract‑first и прочий знакомый набор. API реального времени и WebSocket кажутся чем‑то «для финтеха, трейдинга и игр».

Но стоит появиться хотя бы одной из подобных задач:

  • групповые чаты с «живыми» индикаторами набора и доставкой сообщений без перезагрузки;
  • совместное редактирование документов (Confluence, Google Docs);
  • совместные доски (Miro‑подобные);
  • realtime‑уведомления и статусы;
  • онлайн‑мониторинги, где задержка критична.

…как REST начинает тянуть архитектуру вниз: polling, long‑polling, костыли вокруг частых запросов и растущей нагрузки.

WebSocket как раз и закрывает класс задач, в которых:

  • важна двусторонняя связь (client ↔ server);
  • нужны минимальные задержки;
  • нужно сократить сетевой overhead от повторных HTTP‑заголовков;
  • много одновременно подключённых пользователей.

Как WebSocket живёт поверх HTTP и TCP

Upgrade: переход с HTTP на WebSocket

WebSocket не возникает «магически» сам по себе — он запускается с обычного HTTP‑запроса, в котором клиент просит сервер сменить протокол.

Пример HTTP‑handshake.

Запрос клиента:

GET /ws/chat HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Ответ сервера:

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

Код HTTP 101 Switching Protocols означает, что сервер согласился перейти с HTTP на другой протокол, в нашем случае — WebSocket.

Ключевой момент для аналитика: WebSocket‑канал создаётся после успешного HTTP‑апгрейда, и до этого момента у вас самый обычный HTTP‑запрос с заголовками и всеми ограничениями прокси и шлюзов.

ws:// и wss://: схемы URI

После установления соединения с точки зрения клиента мы имеем адреса такого вида:

  • ws://example.com/ws/chat — незашифрованный WebSocket;
  • wss://example.com/ws/chat — WebSocket поверх TLS (аналог HTTPS).

Это важно, потому что:

  • на схемах интеграций вы сразу видите, где REST (https://), а где realtime‑канал (wss://);
  • для ИБ и DevOps это разные потоки трафика с разными правилами.

Внутренности WebSocket: фреймы, payload и дельты

Структура фрейма

WebSocket передаёт данные не строками, а фреймами. Упрощённо:

  • FIN — флаг, последний ли это фрейм сообщения;
  • OPCODE — тип фрейма (текст, бинарный, ping, pong, close);
  • MASK + MASKING-KEY — маскирование данных (клиент → сервер обязателен);
  • PAYLOAD-LENGTH — длина тела;
  • PAYLOAD — полезная нагрузка (текст, бинарные данные).

Для аналитика важны выводы:

  • сообщение может быть разбито на несколько фреймов (важно для больших бинарников);
  • есть отдельные служебные фреймы ping / pong для heartbeat’а;
  • в общем случае мы оперируем на уровне «сообщения» (message), а не отдельных фреймов, но для высоконагруженных сценариев знание про фреймы помогает объяснить странные баги.

Пример текстового payload (чат)

Обычное событие «новое сообщение в чате» может выглядеть так:

{
  "type": "chat.message.new",
  "chatId": "c_12345",
  "messageId": "m_67890",
  "senderId": "u_100500",
  "createdAt": "2026-02-09T13:45:12.123Z",
  "text": "Всем привет!",
  "attachments": [
    {
      "id": "att_1",
      "type": "image",
      "url": "https://cdn.example.com/att_1.png"
    }
  ]
}

Ключевое для постановки:

  • type — тип события внутри одного WebSocket‑канала;
  • идентификаторы сущностей (chatId, messageId, senderId);
  • поля для состояния интерфейса (наличие вложений, статусы прочтения и прочее).

Пример дельта‑payload (whiteboard)

Для совместной доски нет смысла гонять весь документ:

{
  "type": "board.elements.updated",
  "boardId": "b_42",
  "version": 157,
  "authorId": "u_100500",
  "changes": [
    {
      "elementId": "el_10",
      "op": "move",
      "from": { "x": 100, "y": 200 },
      "to":   { "x": 130, "y": 210 }
    },
    {
      "elementId": "el_11",
      "op": "text.update",
      "prev": "Hello",
      "next": "Hello, world"
    }
  ],
  "timestamp": "2026-02-09T13:45:12.123Z"
}

Здесь важно:

  • наличие версии (version) для разрешения конфликтов;
  • список изменений (changes), а не полное состояние доски;
  • операции (op) явно описаны и могут быть расширяемыми.

Heartbeat, мёртвые сессии и SLA на задержку

Ping/pong и heartbeat на прикладном уровне

Протокол WebSocket поддерживает ping/pong‑фреймы, но в браузерных API они не всегда доступны. Поэтому часто используют прикладочный heartbeat — обычные сообщения ping / pong с JSON.

Типовой контракт:

// Пинг от клиента
{
  "type": "ping",
  "timestamp": 1760000000000
}

// Понг от сервера
{
  "type": "pong",
  "timestamp": 1760000000000,
  "serverTime": 1760000000100,
  "latency": 100
}

На сервере дополнительно ведут метрики по соединениям:

  • connectedAt;
  • lastPingTime;
  • lastPongTime;
  • latencyHistory;
  • missedHeartbeats.

Дальше по таймеру проверяют:

  • если timeSinceLastPing > HEARTBEAT_INTERVAL * MAX_MISSED_HEARTBEATS — соединение закрыть;
  • при закрытии чистят состояние (карты соединений, внутренние subscription’ы).

SLA на задержки: как это формализовать в требованиях

Аналитик может и должен задавать рамки. Примеры:

  • чаты:
    • целевой SLA задержки доставки сообщения — до 100 мс для 95‑го перцентиля;
    • максимально допустимая задержка для 99‑го перцентиля — до 500 мс;
  • уведомления: задержка до 5 секунд считается нормальной, дальше пользователь может считать уведомление «запоздавшим»;
  • whiteboard: для плавного UX при перемещении фигур задержка не должна превышать 50–100 мс.

Такие числа помогают бэкенду и архитектуре:

  • заложить нужную конфигурацию брокеров и кешей;
  • понять, нужен ли отдельный WebSocket‑кластер под определённую функциональность;
  • рассчитать нагрузку и необходимость горизонтального масштабирования.

WebSocket в микросервисной архитектуре: схемы и паттерны

Базовая картина: выделенный WebSocket‑сервис

Типовой набор контейнеров (уровень C4‑Container):

  • Client (Web/App) устанавливает соединение wss://ws.example.com/ws;
  • API Gateway принимает WebSocket‑handshake, пробрасывает Upgrade / Connection в бэкенд;
  • WebSocket Service хранит сессии пользователей и маршрутизирует события (чаты, доски, уведомления) подписчикам;
  • Business Services:
    • Chat Service — владеет логикой чатов и хранением сообщений;
    • Board Service — отвечает за whiteboard;
    • Notification Service — моделирует жизненный цикл уведомлений;
  • Transport Layer:
    • Kafka/NATS/RabbitMQ — событийная шина;
    • Redis (pub/sub, кеш) для быстрых fan‑out‑рассылок.

Основная идея в том, что WebSocket‑сервис — это транспортный слой, который не содержит тематическую бизнес‑логику. Он выполняет роль «концентратора» соединений и «коннектора» между бизнес‑событиями (Kafka/Redis) и конкретными пользователями.

API Gateway и Upgrade‑заголовки

Чтобы WebSocket работал через API Gateway (например, nginx), нужно не забыть пробросить необходимые заголовки.

Пример конфигурации nginx:

location /ws/ {
    proxy_pass http://ws-backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

Без этого апгрейд не произойдёт: шлюз будет видеть обычный HTTP‑запрос и не создаст WebSocket‑туннель, в результате клиент останется в состоянии «101 Switching Protocols» или получит ошибку.

Sticky‑sessions и несколько экземпляров WebSocket

При горизонтальном масштабировании WebSocket‑сервиса возникают проблемы:

  • у одного пользователя может быть несколько устройств, а значит и несколько WebSocket‑сессий;
  • разные пользователи, участвующие в одном чате, могут оказаться на разных экземплярах.

Типовые подходы к решению:

  • Sticky‑sessions на балансировщике. Пользователя по cookie, IP или хешу userId всегда отправляют на один и тот же экземпляр (до смены конфигурации). Это уменьшает хаос, но не решает проблему fan‑out между экземплярами.
  • Внутренний pub/sub (Redis, Kafka). Любое событие, пришедшее в Chat Service, публикуется в топик вида chat.{chatId} в Kafka или Redis pub/sub. Все экземпляры WebSocket‑сервиса подписаны и доставляют сообщения только тем подключённым пользователям, которые висят у них в памяти.
  • Распределённые карты сессий. Хранилище вида userId -> [connectionId, instanceId...] помогает:
    • быстро находить, где сидит пользователь;
    • корректно закрывать все его сессии при logout;
    • реализовать бизнес‑ограничения: «не более N сессий на пользователя».

Ограничения браузеров и конкуренция за соединения

Исторически браузеры ограничивали количество одновременных HTTP‑соединений к одному домену (часто 6). Для WebSocket лимиты другие и обычно больше, но «бесконечными» они не являются. Практическое следствие:

  • если вы держите несколько WebSocket‑подключений к одному домену, нужно закладываться на ограничение;
  • в сложных случаях используют разные поддомены (ws1.example.com, ws2.example.com), чтобы распределить нагрузку.

Поэтому в реальных проектах чаще делают один WebSocket‑канал под чаты и второй WebSocket‑канал под остальные realtime‑функции. Всё остальное решается мультиплексированием по типу события внутри одного соединения (type в payload’е).

WebSocket, SSE и long‑polling: когда что выбирать

Чтобы системный аналитик и архитектор говорили с разработчиками на одном языке, полезно иметь простую «матрицу решений» по realtime‑технологиям.

Матрица технологий реального времени

ХарактеристикаWebSocketSSE (Server‑Sent Events)Long‑polling
Направление данныхДвустороннее (клиент ↔ сервер).Одностороннее (сервер → клиент).Клиент инициирует запрос, сервер отвечает при данных
ПротоколОтдельный протокол поверх TCP, старт через UpgradeЧистый HTTP (поток текстовых событий).Чистый HTTP (длинный запрос + повтор).
Формат данныхТекст и бинарные фреймы.Только текст (UTF‑8), чаще JSON.Любые данные в HTTP‑ответе (обычно JSON).
Сложность реализацииНаивысшая (инфра, масштабирование, отладка).Средняя, проще WebSocket.Самая простая, «просто HTTP».
Поддержка браузерамиВсе современные браузеры.Все современные, кроме очень старых IE.Везде, где есть HTTP.
АвтопереподключениеНужна реализация (или библиотеки).Встроенный EventSource + перезапуск.Реализуется циклом запросов на клиенте.
Работа через проксиТребует поддержки Upgrade, иногда блокируется.Легче, обычный HTTP‑поток.Максимально совместимо (HTTP).
Типичные сценарииЧаты, игры, совместное редактирование, трейдинг.Ленты событий, уведомления, метрики.Уведомления и обновления при ограниченной инфраструктуре.

Как выбирать на уровне требований

  • Нужна двусторонняя связь и частые события (чаты, доски, игры, трейдинг). Выбор почти всегда WebSocket.
  • Нужно только пушить данные на клиент (уведомления, ленты, мониторинг) с умеренной частотой. Рассматриваем SSE:
    • простой API EventSource в браузере;
    • обычный HTTP, проще ИБ и сетевикам;
    • автоматическое переподключение и события open, error, message.
  • Очень ограниченная инфраструктура, корпоративная сеть режет WebSocket и SSE, есть только HTTP/1. Используем long‑poll’инг:
    • сервер держит запрос до появления данных или таймаута;
    • больше overhead’а по заголовкам, хуже масштабируется, но работает «везде».

Гибридный подход: нормальная архитектура может комбинировать:

  • WebSocket для интерактивных фич (чаты, доски, курсоры);
  • SSE и long‑polling для «мягких» уведомлений и аналитических лент.

Усиленные шаблоны постановки задач

Whiteboard (совместная доска)

Событие: изменение элементов доски.

  • Событие: board.elements.updated.
  • Транспорт: WebSocket (wss://ws.example.com/ws).
  • Инициатор: клиент (пользователь двигает фигуру или правит текст), изменение подтверждается Board Service.
  • Получатели: все активные участники доски boardId, кроме (опционально) инициатора.

Направление:

  • Клиент → WebSocket‑сервис → Board Service (через Kafka/REST/gRPC);
  • Board Service проверяет и сохраняет, публикует событие board.elements.updated;
  • WebSocket‑сервис рассылает событие всем подписчикам доски.

JSON‑схема payload:

{
  "type": "board.elements.updated",
  "boardId": "string",
  "version": "integer",
  "authorId": "string",
  "changes": [
    {
      "elementId": "string",
      "op": "create|update|delete|move|resize",
      "prev": { "nullable": true },
      "next": { "nullable": true }
    }
  ],
  "timestamp": "string (ISO-8601)"
}

Требования:

  • version — монотонно растущая версия доски, используется для разрешения конфликтов;
  • prev и next содержат минимально необходимое состояние элемента (например, координаты, размеры, текст);
  • размер changes в одном событии — не более 100 элементов (остальное батчится).

Нагрузка и SLA:

  • пиковое количество активных пользователей на одной доске: до 50;
  • максимум событий board.elements.updated на доску: до 200/сек;
  • задержка между фиксацией в Board Service и доставкой в WebSocket‑клиент:
    • 95‑й перцентиль — до 100 мс;
    • 99‑й перцентиль — до 250 мс.

Надёжность:

  • потеря одного события допустима, так как клиент при переподключении обязан запросить полное состояние доски: GET /boards/{boardId}/state?version={clientVersion}.

Безопасность:

  • пользователь должен иметь право доступа к boardId (ACL);
  • подписка на события доски оформляется отдельным сообщением:
{
  "type": "board.subscribe",
  "boardId": "b_42"
}

Индикатор набора текста («user is typing…»)

Это типичный пример события, которое не сохраняется в БД и чисто realtime.

  • Событие: chat.typing.
  • Транспорт: WebSocket (общий канал чатов).
  • Инициатор: клиент (пользователь начинает или заканчивает набор).
  • Получатели: все участники чата chatId, кроме инициатора.

JSON‑схема payload:

{
  "type": "chat.typing",
  "chatId": "string",
  "userId": "string",
  "state": "started|stopped",
  "timestamp": "string (ISO-8601)"
}

Правила отправки с клиента:

  • state = started не чаще одного раза в две секунды при непрерывном наборе;
  • state = stopped при потере фокуса поля ввода или отсутствии ввода дольше N секунд.

Нагрузка и SLA:

  • до 20 активных набирающих пользователей в одном групповом чате;
  • максимум 10 событий/сек на чат по chat.typing;
  • задержка не критична, но желательно до 500 мс.

Надёжность:

  • потеря событий chat.typing допустима, они не влияют на бизнес‑данные;
  • не требуется повтора при переподключении.

Безопасность:

  • те же права, что и на chat.message.*: видеть typing‑event можно только в чате, где состоит пользователь.

Уведомления: WebSocket, SSE и long‑polling

Общая модель уведомления:

{
  "id": "string",
  "userId": "string",
  "type": "task.assigned|comment.added|system.alert|...",
  "title": "string",
  "body": "string",
  "createdAt": "string (ISO-8601)",
  "read": "boolean",
  "data": {
    "...": "..."
  }
}

WebSocket‑событие notification.new

Когда использовать:

  • пользователь уже в «толстом» клиенте с WebSocket;
  • нужны мгновенные реакции в интерфейсе (бейджи, всплывашки).

Событие: notification.new:

{
  "type": "notification.new",
  "notification": {
    "id": "n_123",
    "userId": "u_42",
    "type": "task.assigned",
    "title": "Новая задача",
    "body": "Вам назначена задача #12345",
    "createdAt": "2026-02-09T13:45:12.123Z",
    "read": false,
    "data": {
      "taskId": "12345"
    }
  }
}

Нагрузка и SLA:

  • до 5 000 уведомлений/сек по системе;
  • задержка: до 2 секунд.

Надёжность:

  • если событие по WebSocket потеряно, клиент при подключении должен сделать REST‑запрос: GET /notifications?since=lastSeenId.

SSE‑канал GET /sse/notifications

Когда использовать:

  • нужен только поток сервер → клиент;
  • интерфейс не держит WebSocket по другим причинам;
  • нужно более «мягкое» решение для инфраструктуры.

Клиент (псевдокод):

const evtSource = new EventSource("/sse/notifications");

evtSource.onmessage = (event) => {
  const payload = JSON.parse(event.data);
  // payload = notification.new, как в WebSocket-примере
};

evtSource.onerror = (err) => {
  // лог, UI-индикация, возможный fallback на long-polling
};

Формат серверного ответа (SSE):

event: notification.new
data: {"id":"n_123","userId":"u_42","type":"task.assigned", ...}

Переподключение и позиционирование по Last-Event-ID помогают восстановить пропущенные события.

Long‑polling GET /notifications/stream

Когда использовать:

  • в старых клиентах;
  • когда WebSocket и SSE заблокированы корпоративной сетью;
  • в очень простой архитектуре (минимум инфраструктурных изменений).

Протокол:

  • клиент отправляет GET /notifications/stream?since=lastSeenId;
  • сервер держит соединение до появления новых уведомлений или таймаута (например, 30 секунд);
  • сервер отвечает списком новых уведомлений (может быть пустым);
  • клиент сразу отправляет следующий запрос.

Достоинства:

  • работает поверх обычного HTTP;
  • дружелюбен к прокси и фаерволам.

Недостатки:

  • у каждого ответа — полный HTTP‑overhead (заголовки и так далее);
  • хуже масштабируется при большом количестве пользователей и частых событиях.

Как аналитикам и архитекторам прокачаться в WebSocket

Чтобы WebSocket перестал быть «чёрной коробкой», полезно сделать несколько шагов:

  • Разобрать руками HTTP‑handshake:
    • увидеть реальный GET с Upgrade / Connection;
    • изучить ответ 101 Switching Protocols.
  • Через Postman и Insomnia подключиться к тестовому WebSocket‑серверу (echo‑эндпоинты, аналоги websocket.org).
  • Составить свой небольшой шаблон постановки задачи на WebSocket‑событие, содержащий:
    • тип события;
    • инициатора и получателя;
    • JSON‑схему;
    • SLA и нагрузку;
    • требования по надёжности и безопасности.
  • Нарисовать (хотя бы текстом) C4‑схему, где:
    • WebSocket вынесен в отдельный микросервис;
    • ходит через API Gateway;
    • события проходят через брокер (Kafka/Redis) между бизнес‑сервисами и WebSocket‑слоем.

После этого разговоры про realtime‑API перестают быть «высоким искусством разработчиков» и становятся обычным, пусть и более сложным, инструментом в наборе системного аналитика и архитектора.