Переподключение при обрыве связи

В реальных сетевых условиях WebSocket-соединение, поверх которого работает STOMP.js, не является стабильным по умолчанию. Разрыв может произойти по множеству причин: кратковременная потеря интернет-соединения, переход устройства между сетями, перезапуск брокера сообщений, таймауты прокси или балансировщика нагрузки, а также закрытие соединения сервером из-за политики heartbeat.

STOMP.js не гарантирует автоматическое восстановление соединения в базовой конфигурации клиента, поэтому механизм переподключения реализуется на уровне прикладного кода. Отсутствие стратегии восстановления приводит к потере подписок, сообщений и состояния канала, что критично для приложений реального времени: чатов, уведомлений, торговых интерфейсов, мониторинга.


Базовая модель жизненного цикла соединения STOMP.js

Клиент STOMP.js проходит несколько состояний:

  • инициализация клиента
  • установление соединения (CONNECT)
  • активная работа (SUBSCRIBE / SEND)
  • разрыв соединения (DISCONNECT / network failure)
  • повторное подключение (reconnect loop)

Ключевой момент заключается в том, что после разрыва соединения все подписки теряются, поскольку они привязаны к конкретной сессии брокера. Это означает, что после восстановления соединения необходимо заново подписываться на все топики.


Стратегия переподключения: базовый подход

Наиболее распространённая стратегия — цикл с задержкой между попытками подключения. В STOMP.js клиент создаётся один раз, а попытки повторного подключения выполняются через повторный вызов activate().

import { Client } from '@stomp/stompjs';

const client = new Client({
  brokerURL: 'wss://example.com/ws',
  reconnectDelay: 0, // отключаем встроенный авто-reconnect
  heartbeatIncoming: 10000,
  heartbeatOutgoing: 10000,
});

let reconnectAttempts = 0;

function connect() {
  client.onConn ect = () => {
    reconnectAttempts = 0;

    client.subscribe('/topic/updates', (message) => {
      console.log('Получено сообщение:', message.body);
    });
  };

  client.onWebSocketCl ose = () => {
    scheduleReconnect();
  };

  client.onStompEr ror = () => {
    scheduleReconnect();
  };

  client.activate();
}

Реализация логики повторных попыток

Переподключение без задержки создаёт риск перегрузки сервера. Поэтому используется экспоненциальная задержка (exponential backoff), при которой каждая следующая попытка увеличивает интервал ожидания.

let reconnectAttempts = 0;
let reconnectTimer = null;

function scheduleReconnect() {
  if (reconnectTimer) return;

  const delay = Math.min(30000, 1000 * Math.pow(2, reconnectAttempts));

  reconnectTimer = setTimeout(() => {
    reconnectTimer = null;
    reconnectAttempts += 1;
    client.activate();
  }, delay);
}

Такой подход позволяет:

  • снизить нагрузку на сервер при массовых реконнектах
  • учитывать временные сетевые сбои
  • стабилизировать поведение клиента в нестабильных сетях

Сброс состояния при переподключении

После успешного восстановления соединения необходимо учитывать, что:

  • подписки не сохраняются
  • очереди сообщений не восстанавливаются автоматически
  • состояние приложения может быть устаревшим

Поэтому рекомендуется централизованно описывать все подписки, чтобы их можно было восстановить после onConnect.

const subscriptions = [];

function addSubscription(destination, callback) {
  subscriptions.push({ destination, callback });

  if (client.connected) {
    client.subscribe(destination, callback);
  }
}

function restoreSubscriptions() {
  subscriptions.forEach(({ destination, callback }) => {
    client.subscribe(destination, callback);
  });
}

Интеграция восстановления подписок с onConnect

STOMP.js предоставляет событие onConnect, которое вызывается при каждом успешном подключении, включая повторные.

client.onConn ect = () => {
  reconnectAttempts = 0;

  restoreSubscriptions();
};

Этот механизм является центральной точкой восстановления состояния клиента.


Обработка частичных обрывов связи

Не все сбои приводят к немедленному закрытию WebSocket. Иногда соединение “зависает” и не передаёт данные. Для таких случаев используется heartbeat.

const client = new Client({
  brokerURL: 'wss://example.com/ws',
  heartbeatIncoming: 5000,
  heartbeatOutgoing: 5000,
});

Если heartbeat не подтверждается, STOMP.js инициирует закрытие соединения, что позволяет триггерить механизм переподключения.


Разделение ответственности: транспорт и логика приложения

При проектировании устойчивого клиента важно разделять:

  • транспортный слой (STOMP.js, WebSocket)
  • слой управления подписками
  • бизнес-логику обработки сообщений

Это позволяет избежать ситуации, когда логика подписок «размазана» по коду и теряется при реконнекте.

Пример архитектурного разделения:

class StompService {
  constructor(client) {
    this.client = client;
    this.subscriptions = [];
  }

  connect() {
    this.client.onConn ect = () => {
      this.restore();
    };

    this.client.onWebSocketCl ose = () => {
      this.reconnect();
    };

    this.client.activate();
  }

  subscribe(destination, handler) {
    this.subscriptions.push({ destination, handler });

    if (this.client.connected) {
      this.client.subscribe(destination, handler);
    }
  }

  restore() {
    this.subscriptions.forEach(s =>
      this.client.subscribe(s.destination, s.handler)
    );
  }

  reconnect() {
    setTimeout(() => this.client.activate(), 2000);
  }
}

Избежание дублирующих подписок

При переподключении существует риск повторной подписки на один и тот же канал, если не контролировать состояние. Это приводит к дублированию сообщений.

Решение заключается в хранении идентификаторов подписок и проверке их наличия перед созданием новой.

const activeSubscriptions = new Map();

function safeSubscribe(destination, callback) {
  if (activeSubscriptions.has(destination)) return;

  const sub = client.subscribe(destination, callback);
  activeSubscriptions.set(destination, sub);
}

При разрыве соединения карта должна очищаться:

client.onDisconn ect = () => {
  activeSubscriptions.clear();
};

Обработка ошибок подключения

STOMP.js предоставляет событие onStompError, которое вызывается при ошибках протокола, например:

  • неправильные credentials
  • отказ брокера
  • некорректные команды
client.onStompEr ror = (frame) => {
  console.error('STOMP ошибка:', frame.headers['message']);
  scheduleReconnect();
};

Важно различать:

  • транспортные ошибки (WebSocket закрыт)
  • протокольные ошибки (STOMP error frame)

Обе категории могут требовать переподключения, но с разной логикой задержек.


Управление жизненным циклом реконнекта

В сложных приложениях необходимо вводить ограничения:

  • максимальное число попыток
  • сброс состояния после длительных неудач
  • переход в режим degraded mode
if (reconnectAttempts > 10) {
  console.error('Превышено число попыток подключения');
  return;
}

Это предотвращает бесконечный цикл реконнектов в условиях полной недоступности сервера.


Поведение при смене сети

На мобильных и нестабильных соединениях важно учитывать событие navigator.onLine.

window.addEventListener('offline', () => {
  client.deactivate();
});

window.addEventListener('online', () => {
  client.activate();
});

Однако navigator.onLine не всегда точно отражает состояние соединения, поэтому он используется только как дополнительный сигнал.


Согласованность данных после восстановления соединения

После переподключения клиент может получить устаревшее состояние системы. Для синхронизации обычно используется один из подходов:

  • запрос актуального состояния через HTTP API после onConnect
  • получение snapshot-сообщения через отдельный STOMP-топик
  • использование versioning сообщений на стороне сервера
client.onConn ect = () => {
  restoreSubscriptions();

  fetch('/api/state')
    .then(res => res.json())
    .then(state => {
      console.log('Актуальное состояние:', state);
    });
};

Устойчивость к множественным разрывам

В условиях плохой сети возможны каскадные разрывы соединения. В таких сценариях критично:

  • избегать параллельных попыток reconnect
  • централизовать управление таймерами
  • блокировать повторный activate во время активного reconnect
let isReconnecting = false;

function reconnect() {
  if (isReconnecting) return;

  isReconnecting = true;

  setTimeout(() => {
    client.activate();
    isReconnecting = false;
  }, 2000);
}

Итоговая модель поведения клиента

Устойчивый STOMP.js клиент при обрыве соединения должен:

  • фиксировать факт разрыва через события WebSocket/STOMP
  • запускать контролируемый цикл переподключения
  • восстанавливать подписки после успешного соединения
  • предотвращать дублирование подписок
  • синхронизировать состояние приложения после реконнекта
  • учитывать heartbeat и сетевые изменения
  • ограничивать число попыток при фатальных ошибках