В системах реального времени соединение между клиентом и брокером сообщений должно постоянно поддерживаться в активном состоянии. Однако WebSocket-соединения подвержены различным проблемам:
Таймауты в STOMP.js позволяют обнаруживать подобные ситуации и автоматически реагировать на них:
В STOMP.js используются несколько категорий таймаутов.
Контролирует максимальное время ожидания успешного подключения к брокеру.
Если соединение не установлено за указанный период:
Используется для проверки активности соединения.
Если heartbeat-сообщения перестают приходить:
Применяется на уровне бизнес-логики.
Например:
Определяет задержку между повторными попытками подключения.
Контролирует длительность жизни подписки или ожидания сообщения внутри неё.
Heartbeat — это механизм периодической отправки служебных пакетов для проверки активности соединения.
В STOMP.js используются два параметра:
heartbeatIncoming
heartbeatOutgoing
Пример настройки:
import { Client } from '@stomp/stompjs';
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
heartbeatIncoming: 10000,
heartbeatOutgoing: 10000
});
Значения задаются в миллисекундах.
В данном примере:
Алгоритм работы выглядит следующим образом:
heartbeatOutgoing миллисекунд отправляется
heartbeat.heartbeatIncoming, соединение признаётся невалидным.Heartbeat представляет собой минимальный пакет:
\n
STOMP-протокол допускает использование пустой строки как сигнала активности.
Это снижает нагрузку на сеть и брокер.
Для критичных realtime-систем heartbeat делают короткими.
Пример:
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
heartbeatIncoming: 3000,
heartbeatOutgoing: 3000
});
Подобная конфигурация используется:
Недостатки:
Для обычных корпоративных приложений heartbeat делают реже:
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
heartbeatIncoming: 30000,
heartbeatOutgoing: 30000
});
Преимущества:
Недостаток — медленное обнаружение разрыва соединения.
Heartbeat можно полностью отключить:
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
heartbeatIncoming: 0,
heartbeatOutgoing: 0
});
Подобный режим опасен, поскольку:
STOMP.js поддерживает автоматическое переподключение.
Главный параметр:
reconnectDelay
Пример:
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
reconnectDelay: 5000
});
В этом случае:
клиент подождёт 5 секунд и попытается подключиться снова.
Без ограничений reconnect может создавать серьёзные проблемы:
Плохой пример:
reconnectDelay: 10
Подобная конфигурация может инициировать сотни подключений в секунду.
Наиболее безопасный подход — постепенное увеличение таймаута реконнекта.
Пример:
let reconnectDelay = 1000;
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
reconnectDelay
});
client.onWebSocketCl ose = () => {
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
client.reconnectDelay = reconnectDelay;
};
Алгоритм:
| Попытка | Задержка |
|---|---|
| 1 | 1 сек |
| 2 | 2 сек |
| 3 | 4 сек |
| 4 | 8 сек |
| 5 | 16 сек |
| 6 | 30 сек |
Если тысячи клиентов одновременно переподключаются, возникает эффект thundering herd.
Для предотвращения используется jitter.
Пример:
function getReconnectDelay(baseDelay) {
const jitter = Math.random() * 1000;
return baseDelay + jitter;
}
client.reconnectDelay = getReconnectDelay(5000);
Это распределяет нагрузку по времени.
В некоторых сценариях необходимо ограничивать время ожидания ответа.
Например:
Пример:
function waitResponse(timeout = 5000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Timeout'));
}, timeout);
client.subscribe('/topic/response', message => {
clearTimeout(timer);
resolve(JSON.parse(message.body));
});
});
}
Типичная ошибка — забытый clearTimeout.
Плохой пример:
setTimeout(() => {
console.log('timeout');
}, 5000);
Если операция завершилась раньше, таймер остаётся в памяти.
Это приводит к:
Правильный вариант:
const timer = setTimeout(() => {
reject(new Error('Timeout'));
}, 5000);
subscription.unsubscribe();
clearTimeout(timer);
Иногда подписка должна существовать ограниченное время.
Пример:
const subscription = client.subscribe(
'/topic/events',
message => {
console.log(message.body);
}
);
setTimeout(() => {
subscription.unsubscribe();
}, 10000);
Через 10 секунд подписка будет удалена.
В режиме manual acknowledgement клиент обязан подтверждать получение сообщений.
Пример:
client.subscribe(
'/queue/tasks',
message => {
processTask(message);
message.ack();
},
{
ack: 'client'
}
);
Если ACK не отправлен вовремя:
Пример:
client.subscribe(
'/queue/tasks',
async message => {
const timer = setTimeout(() => {
console.error('ACK timeout');
message.nack();
}, 5000);
try {
await processTask(message);
clearTimeout(timer);
message.ack();
} catch (error) {
clearTimeout(timer);
message.nack();
}
},
{
ack: 'client'
}
);
Иногда WebSocket зависает во время подключения.
Решение:
function connectWithTimeout(timeout = 5000) {
return Promise.race([
new Promise(resolve => {
client.onConn ect = resolve;
client.activate();
}),
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Connect timeout'));
}, timeout);
})
]);
}
Современный подход — использование AbortController.
Пример:
function waitMessage(timeout = 5000) {
const controller = new AbortController();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
controller.abort();
reject(new Error('Timeout'));
}, timeout);
const subscription = client.subscribe(
'/topic/data',
message => {
clearTimeout(timer);
subscription.unsubscribe();
resolve(message.body);
}
);
controller.signal.addEventListener('abort', () => {
subscription.unsubscribe();
});
});
}
Во многих браузерах background-вкладки ограничивают работу таймеров.
Это влияет на:
Последствия:
Для оптимизации таймаутов используют Visibility API.
Пример:
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
client.deactivate();
} else {
client.activate();
}
});
Мобильные устройства агрессивно ограничивают фоновые процессы.
Особенности:
Поэтому мобильные приложения требуют:
Многие reverse proxy автоматически закрывают idle WebSocket-соединения.
Типичные значения:
| Система | Idle timeout |
|---|---|
| Nginx | 60 сек |
| AWS ALB | 60 сек |
| Cloudflare | 100 сек |
| HAProxy | 50 сек |
Heartbeat должен быть меньше proxy-timeout.
Например:
heartbeatOutgoing: 30000
В RabbitMQ heartbeat координируется между клиентом и сервером.
Если значения несовместимы:
Для анализа используют debug-режим:
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
debug(str) {
console.log(str);
}
});
В логах отображаются:
heartbeatIncoming: 500
Проблемы:
reconnectDelay: 600000
Недостаток:
Плохой код:
setTimeout(() => {
doSomething();
}, 5000);
Без:
clearTimeout(timer);
Ошибка:
client.activate();
client.activate();
client.activate();
Это создаёт параллельные циклы подключения.
Хорошая практика — вынесение всех значений в конфигурацию.
Пример:
const STOMP_TIMEOUTS = {
heartbeatIncoming: 10000,
heartbeatOutgoing: 10000,
reconnectDelay: 5000,
requestTimeout: 15000,
ackTimeout: 30000
};
В сложных системах таймауты меняются динамически.
Например:
В production-системах отслеживаются:
Типичные метрики:
| Метрика | Назначение |
|---|---|
| reconnect_total | количество реконнектов |
| heartbeat_missed | пропущенные heartbeat |
| websocket_disconnects | разрывы соединения |
| ack_timeout_total | timeout ACK |
| request_timeout_total | timeout запросов |
| reconnect_duration | длительность восстановления |
Надёжная реализация обычно включает:
const client = new Client({
brokerURL: 'wss://broker.example/ws',
reconnectDelay: 5000,
heartbeatIncoming: 10000,
heartbeatOutgoing: 10000,
debug(str) {
console.log(str);
},
onConnect() {
console.log('Connected');
},
onDisconnect() {
console.log('Disconnected');
},
onWebSocketClose() {
console.log('Socket closed');
},
onStompError(frame) {
console.error(frame.headers.message);
}
});
Подобная конфигурация обеспечивает: