Привет, Хабр. Меня зовут Владимир Бурмистров, я главный системный аналитик холдинга Т1. В этой статье хочу посмотреть на 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‑канала;

  • идентификаторы сущностей (chatIdmessageIdsenderId);

  • поля для состояния интерфейса (наличие вложений, статусы прочтения и прочее).

Пример дельта‑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‑сессий;

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

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

  1. Sticky‑sessions на балансировщике. Пользователя по cookie, IP или хешу userId всегда отправляют на один и тот же экземпляр (до смены конфигурации). Это уменьшает хаос, но не решает проблему fan‑out между экземплярами.

  2. Внутренний pub/sub (Redis, Kafka). Любое событие, пришедшее в Chat Service, публикуется в топик вида chat.{chatId} в Kafka или Redis pub/sub. Все экземпляры WebSocket‑сервиса подписаны и доставляют сообщения только тем подключённым пользователям, которые висят у них в памяти.

  3. Распределённые карты сессий. Хранилище вида userId -> [connectionId, instanceId...] помогает:

    • быстро находить, где сидит пользователь;

    • корректно закрывать все его сессии при logout;

    • реализовать бизнес‑ограничения: «не более N сессий на пользователя».

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

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

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

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

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

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

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

Характеристика

WebSocket

SSE (Server‑Sent Events)

Long‑polling

Направление данных

Двустороннее (клиент ↔ сервер).

Одностороннее (сервер → клиент).

Клиент инициирует запрос, сервер отвечает при появлении данных.

Протокол

Отдельный протокол поверх TCP, старт через HTTP Upgrade.

Чистый HTTP (поток текстовых событий).

Чистый HTTP (длинный запрос + немедленный повтор)

Формат данных

Текст и бинарные фреймы.

Только текст (UTF‑8), чаще всего JSON.

Любые данные в HTTP‑ответе (обычно JSON).

Сложность реализации

Наиболее высокая (инфраструктура, масштабирование, отладка).

Средняя, проще WebSocket.

Самая простая, «просто HTTP».

Поддержка браузерами

Все современные браузеры.

Все современные браузеры, кроме очень старых IE.

Везде, где есть HTTP.

Автоматическое переподключение

Нужно реализовывать самому (или библиотеками).

Есть встроенный механизм EventSource + перезапуск.

Реализуется на клиенте циклом запросов.

Поддержка через прокси и балансировщики

Требует поддержки Upgrade, иногда блокируется сетевой инфраструктурой

Легче проходит, так как это обычный HTTP‑поток.

Максимально совместимо (обычный HTTP).

Типичные сценарии использования

Чаты, игры, совместное редактирование, управление устройствами, трейдинг.

Ленты событий, уведомления, поток журналов и метрик.

Уведомления и обновления в системах, не являющихся realtime, когда WebSocket/SSE не подходят.

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

  1. Нужна двусторонняя связь и частые события (чаты, доски, игры, трейдинг). Выбор почти всегда WebSocket.

  2. Нужно только пушить данные на клиент (уведомления, ленты, мониторинг) с умеренной частотой. Рассматриваем SSE:

    • простой API EventSource в браузере;

    • обычный HTTP, проще ИБ и сетевикам;

    • автоматическое переподключение и события open, error, message.

  3. Очень ограниченная инфраструктура, корпоративная сеть режет WebSocket и SSE, есть только HTTP/1. Используем long‑poll’инг:

    • сервер держит запрос до появления данных или таймаута;

    • больше overhead’а по заголовкам, хуже масштабируется, но работает «везде».

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

    • WebSocket для интерактивных фич (чаты, доски, курсоры);

    • SSE и long‑polling для «мягких» уведомлений и аналитических лент.

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

Ниже описаны три более подробных шаблона: для whiteboard, индикаторов набора текста и уведомлений.

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 — монотонно растущая версия доски, используется для разрешения конфликтов;

  • rev и next содержат минимально необходимое состояние элемента (например, координаты, размеры, текст);

  • размер changes в одном событии — не более 100 элементов (остальное батчится).

Нагрузка и SLA

  • Пиковое количество активных пользователей на одной доске: до 50.

  • Максимальное количество событий board.elements.updated на доску: до 200/сек.

  • SLA задержки между фиксацией изменений в Board Service и доставкой в WebSocket‑клиент:

    • 95‑й перцентиль — до 100 мс;

    • 99‑й перцентиль — до 250 мс.

Надёжность

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

  • В случае расхождений Board Service возвращает дельты или полное состояние.

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

  • Пользователь должен иметь право доступа к 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.

  • SLA: задержка не критична, но желательно до 500 мс для комфортного UX.

Надёжность

  • Потеря событий 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 уведомлений/сек по системе;

  • SLA задержки: до 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 заблокированы корпоративной сетью;

  • в очень простой архитектуре (минимум инфраструктурных изменений).

Протокол:

  1. Клиент отправляет GET /notifications/stream?since=lastSeenId.

  2. Сервер держит соединение до появления новых уведомлений или истечения таймаута (например, 30 секунд).

  3. Сервер отвечает списком новых уведомлений (может быть пустым).

  4. Клиент сразу отправляет следующий запрос.

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

  • работает поверх обычного HTTP;

  • дружелюбен к прокси и фаерволам.

Недостатки:

  • у каждого ответа — полный HTTP overhead (заголовки и так далее);

  • хуже масштабируется при большом количестве пользователей и частых событиях.

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

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

  1. Разобрать руками HTTP‑handshake:

    • увидеть реальный GET с Upgrade/Connection;

    • изучить ответ 101 Switching Protocols.

  2. Через Postman и Insomnia подключиться к тестовому WebSocket‑серверу (echo‑эндпоинты, аналоги websocket.org).

  3. Составить свой небольшой шаблон постановки задачи на WebSocket‑событие, содержащий:

    • тип события;

    • инициатора и получателя;

    • JSON‑схему;

    • SLA и нагрузку;

    • требования по надёжности и безопасности.

  4. Нарисовать (хотя бы текстом) C4‑схему, где:

    • WebSocket вынесен в отдельный микросервис;

    • ходит через API Gateway;

    • события проходят через брокер (Kafka/Redis) между бизнес‑сервисами и WebSocket‑слоем.

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