Коммуникация и realtime: что выбрать в API
Протоколы ты уже знаешь из урока про realtime в разделе Network: как устроен upgrade до WebSocket, чем SSE отличается от long-polling на уровне байтов. Этот урок про другое: проектный выбор. Какой паттерн общения взять под конкретную фичу, когда WebSocket оправдан, а когда это лишний stateful-груз, и почему socket.io в 2026 чаще наследие, чем выбор.
Карта паттернов коммуникации
Прежде чем выбирать транспорт, назови паттерн. Любое общение двух частей системы раскладывается по двум осям: кто инициирует обмен и сколько получателей у сообщения.
один получатель много получателей
┌──────────────────────────┬─────────────────────────┐
клиент ждёт │ request-reply │ request + fan-out │
ответ │ (REST, gRPC unary) │ (gateway, агрегация) │
├──────────────────────────┼─────────────────────────┤
отправил │ fire-and-forget, │ pub-sub │
и пошёл дальше │ competing consumers │ (события, подписки) │
├──────────────────────────┼─────────────────────────┤
сервер толкает │ стрим одному │ broadcast │
сам │ (SSE, WS, gRPC stream) │ (комнаты, каналы) │
└──────────────────────────┴─────────────────────────┘
- Request-reply. Синхрон: спросил, дождался, продолжил. REST и gRPC unary. Просто, понятно, но вызывающий блокируется на время ответа и падает вместе с отвечающим.
- Fire-and-forget. Отправил команду в очередь и пошёл дальше: «отправь письмо», «пересчитай рейтинг». Никто не ждёт ответа, зато появляется вопрос «а дошло ли», и с ним идемпотентность и повторы.
- Competing consumers: fire-and-forget плюс пул обработчиков. Каждое сообщение получает ровно один воркер.
- Pub-sub. Издатель публикует событие, не зная подписчиков; каждый подписчик получает свою копию. Это уже не «сделай», а «случилось»: разница между командой и событием станет важной темой в волне про распределёнку.
- Стримы. Сервер толкает поток сообщений по открытому соединению: одному клиенту (прогресс джобы) или комнате (чат).
Сквозная тема всех асинхронных клеток: сообщение может прийти дважды или не прийти. Поэтому идемпотентность, с которой ты познакомился в уроке про REST, здесь не опция, а обязательный фон: любой обработчик события пишется в расчёте на повтор.
Когда применять WebSocket
WebSocket это самый дорогой инструмент карты: постоянное соединение в обе стороны. Он уместен, когда выполняются оба условия: нужен push с сервера с низкой задержкой и нужна обратная связь от клиента по тому же каналу. Чат, курсоры коллег в совместном редактировании, отслеживание курьера, multiplayer.
Цена прячется в слове «постоянное». Открытый WebSocket превращает твой stateless-сервис из урока про структуру обратно в stateful:
- Соединение живёт на конкретной ноде. Балансировать его нельзя, только прибить клиента к ноде (sticky). Послать сообщение пользователю теперь означает найти ноду, где висит его сокет: появляется реестр соединений или pub-sub между нодами (Redis, NATS), через который ноды пересылают события друг другу.
- Память и лимиты. Десять тысяч молчащих соединений это десять тысяч открытых сокетов, буферов и heartbeat-таймеров на ноде.
- Graceful restart больно. Деплой рвёт все соединения разом, и тысячи клиентов одновременно начинают reconnect: thundering herd на ровном месте. Нужен джиттер переподключений и докатка пропущенного.
- Гарантии скромнее, чем кажется. TCP гарантирует порядок внутри одного соединения, но между разрывом и reconnect сообщения теряются. «WebSocket надёжный» это иллюзия: надёжность доставки всё равно строишь сам, поверх (номера сообщений, подтверждения, докатка).
Отсюда правило: для запрос-ответа WebSocket не нужен никогда, а для одностороннего push почти всегда хватает SSE. WebSocket бери, когда двунаправленность реальная, а не «вдруг пригодится».
Подписки в реальном времени: проектируем поток
Большинство realtime-фич односторонние: лента, нотификации, прогресс
джобы, обновление цен. Для них есть
SSE,
и он недооценён: это обычный HTTP-ответ, который не заканчивается.
Никакого upgrade-рукопожатия, проходит через прокси и корпоративные
файрволы, кеш-слою и балансировщику выглядит знакомым, а браузерный
EventSource из коробки умеет главное, что в WebSocket пишут руками:
автоматический reconnect с докаткой.
Механика докатки заслуживает внимания, потому что это готовый шаблон надёжной подписки:
сервер шлёт события с id: клиент упал и переподключился:
id: 41 GET /events
data: {"ticket":"A12"} Last-Event-ID: 42
id: 42 сервер отдаёт всё после 42,
data: {"ticket":"B3"} клиент не потерял ни события
Чтобы это работало, серверу нужно где-то хранить хвост событий (буфер в Redis, таблица с курсором), и вот он, главный проектный вопрос любого realtime: какое окно офлайна мы переживаем без потерь? Минута, час, сутки? Ответ определяет размер буфера. А что делать с клиентом, который отсутствовал дольше окна? Универсальный ответ: полный refetch состояния через обычный REST, и только потом подписка на дельты. Подписка доставляет изменения, а истиной владеет запрос.
Тот же шаблон работает и для WebSocket, только всё руками: нумеруй события (или используй курсор), при reconnect клиент присылает последний увиденный номер, сервер докатывает. Без этого «realtime» фича молча теряет события при каждом обрыве Wi-Fi.
Два паттерна по краям, о которых спросят на собеседовании:
- Long-polling как fallback: клиент делает запрос, сервер держит его открытым до события или таймаута, клиент тут же запрашивает снова. Работает везде, где работает HTTP, ценой задержки и лишних запросов. В 2026 нужен только для экзотических legacy-окружений.
- Authn для WebSocket: браузерный
WebSocketне умеет выставлять заголовки на upgrade-запрос, поэтому токен передают либо в query (попадает в логи прокси, осторожно), либо первым сообщением после открытия с серверным таймаутом «не представился за секунду, до свидания». И не забудь обратную сторону: revoke сессии должен закрывать уже открытые сокеты, иначе разлогиненный пользователь продолжает получать события. Значит, нужен канал от auth-системы к реестру соединений.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.