Привет, радостная новость, у меня вышла статья на Хабре. Хабр конечно дело хорошее, но у себя статью я тоже сохраню!
В этой статье хочу посмотреть на 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‑технологиям.
Матрица технологий реального времени
| Характеристика | WebSocket | SSE (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.
- простой API
- Очень ограниченная инфраструктура, корпоративная сеть режет 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 перестают быть «высоким искусством разработчиков» и становятся обычным, пусть и более сложным, инструментом в наборе системного аналитика и архитектора.
