
Привет, Хабр. Меня зовут Владимир Бурмистров, я главный системный аналитик холдинга Т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.1Host: example.comConnection: UpgradeUpgrade: websocketSec‑WebSocket‑Version: 13Sec‑WebSocket‑Key: dGhlIHNhbXBsZSBub25jZQ==
Ответ сервера:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec‑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‑технологиям.
|
Характеристика |
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 не подходят. |
Как выбирать на уровне требований
-
Нужна двусторонняя связь и частые события (чаты, доски, игры, трейдинг). Выбор почти всегда WebSocket.
-
Нужно только пушить данные на клиент (уведомления, ленты, мониторинг) с умеренной частотой. Рассматриваем SSE:
-
простой API EventSource в браузере;
-
обычный HTTP, проще ИБ и сетевикам;
-
автоматическое переподключение и события
open,error,message.
-
-
Очень ограниченная инфраструктура, корпоративная сеть режет WebSocket и SSE, есть только HTTP/1. Используем long‑poll’инг:
-
сервер держит запрос до появления данных или таймаута;
-
больше overhead’а по заголовкам, хуже масштабируется, но работает «везде».
-
-
Гибридный подход. Нормальная архитектура может комбинировать:
-
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.newdata: {"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 перестают быть «высоким искусством разработчиков» и становятся обычным, пусть и более сложным, инструментом в наборе системного аналитика и архитектора.
ссылка на оригинал статьи https://habr.com/ru/articles/1052342/