Раздел 9 · Backend

Коммуникация и realtime: что выбрать в API

middle~30 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Коммуникация и 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.