Потеря соединения в приложениях на основе STOMP.js — одна из ключевых проблем при разработке систем реального времени. Даже при стабильной серверной инфраструктуре WebSocket-соединение может быть разорвано из-за сетевых сбоев, ограничений браузера, таймаутов балансировщиков нагрузки, перезапуска брокера сообщений или ошибок в клиентской логике.
Наиболее распространённые причины:
STOMP.js предоставляет встроенные механизмы для обработки подобных ситуаций, включая автоматическое переподключение, heartbeat-пакеты и события жизненного цикла соединения.
Библиотека использует несколько механизмов:
При разрыве WebSocket вызывается callback:
client.onWebSocketCl ose = (event) => {
console.log('Соединение закрыто');
console.log(event);
};
Объект event содержит:
{
code: 1006,
reason: "",
wasClean: false
}
Основные коды закрытия:
| Код | Описание |
|---|---|
| 1000 | Нормальное закрытие |
| 1001 | Сервер завершил работу |
| 1006 | Аварийное отключение |
| 1008 | Ошибка политики безопасности |
| 1011 | Внутренняя ошибка сервера |
STOMP-протокол поддерживает heartbeat-пакеты.
Если сервер или клиент перестают получать heartbeat в течение заданного времени, соединение считается потерянным.
Настройка heartbeat:
client.heartbeatIncoming = 10000;
client.heartbeatOutgoing = 10000;
Значения указываются в миллисекундах.
В данном примере:
Если heartbeat не приходит — соединение разрывается.
Минимальная конфигурация:
import { Client } from '@stomp/stompjs';
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
reconnectDelay: 5000
});
client.onConn ect = () => {
console.log('Соединение установлено');
};
client.onDisconn ect = () => {
console.log('Соединение закрыто');
};
client.onStompEr ror = (frame) => {
console.error('STOMP ошибка');
console.error(frame.headers['message']);
};
client.onWebSocketCl ose = () => {
console.log('WebSocket закрыт');
};
client.activate();
STOMP.js поддерживает встроенный reconnect-механизм.
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
reconnectDelay: 5000
});
Параметр:
reconnectDelay: 5000
означает:
Типичный сценарий:
onWebSocketClose.onConnect.После переподключения старые subscriptions становятся недействительными.
Это критически важный момент.
Неправильный вариант:
client.subscribe('/topic/messages', callback);
После reconnect подписка исчезнет.
Правильный подход:
client.onConn ect = () => {
client.subscribe('/topic/messages', (message) => {
console.log(message.body);
});
};
Теперь каждая новая сессия автоматически восстанавливает подписки.
В больших приложениях подписок может быть много.
Удобно хранить их в массиве:
const subscriptions = [
{
destination: '/topic/chat'
},
{
destination: '/topic/notifications'
},
{
destination: '/topic/system'
}
];
Повторное подключение:
client.onConn ect = () => {
subscriptions.forEach((item) => {
client.subscribe(item.destination, (message) => {
console.log(item.destination);
console.log(message.body);
});
});
};
Фиксированная задержка не всегда эффективна.
Иногда сервер недоступен длительное время, и постоянные reconnect-попытки создают лишнюю нагрузку.
Можно реализовать экспоненциальную стратегию:
let reconnectDelay = 1000;
function createClient() {
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
reconnectDelay: reconnectDelay
});
client.onConn ect = () => {
reconnectDelay = 1000;
console.log('Подключение восстановлено');
};
client.onWebSocketCl ose = () => {
reconnectDelay = Math.min(
reconnectDelay * 2,
30000
);
console.log(
'Новая задержка:',
reconnectDelay
);
};
return client;
}
Reconnect storm — ситуация, когда тысячи клиентов одновременно пытаются восстановить соединение после сбоя сервера.
Для защиты используется случайная задержка:
const reconnectDelay =
3000 + Math.random() * 5000;
Теперь клиенты будут подключаться в разное время.
На мобильных устройствах сеть может переключаться:
Браузер предоставляет события:
window.addEventListener('offline', () => {
console.log('Интернет отключен');
});
window.addEventListener('online', () => {
console.log('Интернет восстановлен');
});
Можно инициировать reconnect:
window.addEventListener('online', () => {
if (!client.active) {
client.activate();
}
});
STOMP.js предоставляет свойства:
console.log(client.active);
console.log(client.connected);
Разница:
| Свойство | Значение |
|---|---|
| active | Клиент активирован |
| connected | Соединение установлено |
Пример:
if (client.connected) {
client.publish({
destination: '/app/chat',
body: 'Сообщение'
});
}
Возможна ситуация:
publish;Простейшая защита:
function safePublish(destination, body) {
if (!client.connected) {
console.log('Нет соединения');
return false;
}
client.publish({
destination,
body
});
return true;
}
Более надёжный подход — локальная очередь.
const pendingMessages = [];
Отправка:
function send(destination, body) {
if (!client.connected) {
pendingMessages.push({
destination,
body
});
return;
}
client.publish({
destination,
body
});
}
После reconnect:
client.onConn ect = () => {
while (pendingMessages.length > 0) {
const message =
pendingMessages.shift();
client.publish({
destination: message.destination,
body: message.body
});
}
};
При повторной отправке возможны дубликаты.
Особенно это актуально для:
Решение — уникальный messageId.
const message = {
id: crypto.randomUUID(),
text: 'Привет'
};
Сервер хранит уже обработанные идентификаторы.
Pub/Sub архитектура не гарантирует доставку сообщений отключённому клиенту.
Если пользователь потерял соединение:
Решения:
/queue/messages
После reconnect клиент запрашивает:
GET /api/messages?after=timestamp
Сервер хранит журнал событий.
Слишком маленький heartbeat приводит к ложным reconnect.
Проблемный пример:
client.heartbeatIncoming = 1000;
client.heartbeatOutgoing = 1000;
Даже кратковременная задержка сети может вызвать disconnect.
Рекомендуемые значения:
| Тип сети | Heartbeat |
|---|---|
| LAN | 5000 |
| Интернет | 10000 |
| Мобильная сеть | 20000–30000 |
Иногда TCP-соединение остаётся открытым, но данные больше не передаются.
Heartbeat помогает выявлять подобные ситуации.
Дополнительная проверка:
let lastMessageTime = Date.now();
client.onConn ect = () => {
client.subscribe('/topic/data', () => {
lastMessageTime = Date.now();
});
};
Периодическая проверка:
setInterval(() => {
const now = Date.now();
if (now - lastMessageTime > 30000) {
console.log('Соединение зависло');
client.deactivate();
client.activate();
}
}, 5000);
Иногда требуется принудительный reconnect:
async function reconnect() {
await client.deactivate();
client.activate();
}
Применяется:
Частая проблема — истечение access token.
Решение:
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
connectHeaders: {
Authorization: `Bearer ${getToken()}`
},
reconnectDelay: 5000
});
Но reconnect использует старый token.
Правильный подход:
beforeConnect: async () => {
client.connectHeaders = {
Authorization: `Bearer ${await refreshToken()}`
};
}
STOMP-ошибки не всегда приводят к закрытию WebSocket.
client.onStompEr ror = (frame) => {
console.error(
'Broker error:',
frame.headers['message']
);
console.error(
'Details:',
frame.body
);
};
Причины:
Полезно включать debug:
client.debug = (message) => {
console.log(message);
};
Лог покажет:
Удобно централизовать события:
function logState(state) {
console.log(
`[STOMP STATE]: ${state}`
);
}
Использование:
client.onConn ect = () => {
logState('CONNECTED');
};
client.onDisconn ect = () => {
logState('DISCONNECTED');
};
client.onWebSocketCl ose = () => {
logState('SOCKET CLOSED');
};
Современные браузеры ограничивают таймеры фоновых вкладок.
Последствия:
Особенно заметно:
В распределённой архитектуре reconnect может подключить клиента к другому серверу.
Проблемы:
Решения:
При закрытии вкладки желательно корректно завершать соединение.
window.addEventListener('beforeunload', () => {
client.deactivate();
});
Это уменьшает количество “висячих” соединений на сервере.
const client = new Client({
brokerURL: 'wss://api.example.com/ws',
reconnectDelay: 5000,
heartbeatIncoming: 10000,
heartbeatOutgoing: 10000,
beforeConnect: async () => {
client.connectHeaders = {
Authorization: `Bearer ${await refreshToken()}`
};
},
debug: (message) => {
console.log(message);
}
});
client.onConn ect = () => {
console.log('STOMP connected');
client.subscribe('/topic/events', (message) => {
console.log(message.body);
});
};
client.onStompEr ror = (frame) => {
console.error(
frame.headers['message']
);
};
client.onWebSocketCl ose = () => {
console.log(
'WebSocket disconnected'
);
};
client.activate();