Изоляция каналов в STOMP.js применяется для разделения потоков сообщений между различными группами клиентов, сервисов и типов данных. Такой подход предотвращает пересечение логики обработки, снижает вероятность ошибок маршрутизации и повышает безопасность системы обмена сообщениями.
В системах реального времени один брокер сообщений может одновременно обслуживать:
Без изоляции сообщения разных подсистем начинают смешиваться, что приводит к:
STOMP.js предоставляет механизмы, позволяющие строить полностью разделённые каналы обмена сообщениями.
В STOMP основными сущностями являются:
Примеры каналов:
/topic/chat
/topic/notifications
/queue/orders
/queue/logs
Каждый destination фактически становится изолированным каналом передачи данных.
Пример подписки:
client.subscribe('/topic/chat', message => {
console.log(message.body);
});
Пример отправки:
client.publish({
destination: '/topic/chat',
body: JSON.stringify({
text: 'Hello'
})
});
Наиболее распространённый способ разделения — использование различных пространств имён.
/topic/public/*
/topic/private/*
/topic/system/*
/topic/admin/*
Пример:
client.subscribe('/topic/public/news', handler);
client.subscribe('/topic/private/messages', handler);
Преимущества:
Одним из важнейших механизмов является разделение доступа по ролям.
client.subscribe('/topic/admin/events', eventHandler);
client.subscribe('/topic/user/notifications', notificationHandler);
На стороне брокера доступ ограничивается ACL-политиками.
Например:
ROLE_ADMIN -> /topic/admin/**
ROLE_USER -> /topic/user/**
Это предотвращает:
Для полной изоляции пользователей применяются индивидуальные destination.
/user/125/messages
/user/890/messages
Подписка:
client.subscribe('/user/125/messages', message => {
console.log(message.body);
});
Отправка:
client.publish({
destination: '/user/125/messages',
body: 'Private message'
});
В больших системах каналы часто создаются динамически.
const roomId = 15;
client.subscribe(`/topic/rooms/${roomId}`, onMessage);
client.subscribe(`/topic/game/${sessionId}`, onGameEvent);
client.subscribe(`/topic/device/${deviceId}`, onTelemetry);
Такой подход обеспечивает естественную изоляцию данных.
Крупные системы иногда разделяют каналы не только логически, но и физически.
| Брокер | Назначение |
|---|---|
| Broker A | чат |
| Broker B | аналитика |
| Broker C | системные события |
| Broker D | телеметрия |
Подключение к отдельному брокеру:
const client = new Client({
brokerURL: 'wss://chat.example.com/ws'
});
Другой канал:
const metricsClient = new Client({
brokerURL: 'wss://metrics.example.com/ws'
});
Преимущества:
Многие брокеры STOMP поддерживают virtual hosts.
Пример:
const client = new Client({
brokerURL: 'wss://broker.example.com/ws',
connectHeaders: {
host: 'finance-vhost'
}
});
Другой виртуальный хост:
connectHeaders: {
host: 'chat-vhost'
}
Виртуальные хосты позволяют:
Многопользовательские SaaS-системы часто используют tenant-based routing.
/topic/tenant/100/orders
/topic/tenant/200/orders
Формирование destination:
const tenantId = 100;
client.subscribe(
`/topic/tenant/${tenantId}/orders`,
processOrders
);
Преимущества:
Иногда требуется разделить каналы отправки и чтения.
/topic/inbound/orders
/topic/outbound/orders
Публикация:
client.publish({
destination: '/topic/inbound/orders',
body: orderData
});
Подписка:
client.subscribe(
'/topic/outbound/orders',
handleOrderResult
);
Подобная архитектура уменьшает риск циклических сообщений.
Системные события не должны смешиваться с пользовательскими данными.
/topic/messages
Внутри:
{
"type": "chat"
}
и
{
"type": "system"
}
/topic/chat/messages
/topic/system/events
/topic/system/errors
Это:
Каналы могут разделяться по характеру трафика.
/topic/telemetry
/topic/notifications
/topic/critical
Такой подход помогает:
Распространённая практика — использование стандартизированных префиксов.
/ws/chat/*
/ws/system/*
/ws/private/*
/ws/admin/*
Или:
/topic/*
/queue/*
/exchange/*
Преимущества:
При ручном подтверждении сообщений важно избегать смешивания различных потоков ACK.
client.subscribe('/queue/orders', message => {
processOrder(message);
message.ack();
}, {
ack: 'client'
});
Для критичных данных используют отдельные очереди:
/queue/payments
/queue/security
Это уменьшает вероятность неправильного подтверждения сообщений.
STOMP поддерживает транзакции.
Пример:
const tx = client.begin();
client.publish({
destination: '/queue/orders',
body: orderData,
transaction: tx.id
});
tx.commit();
Изоляция транзакционных каналов позволяет:
Heartbeat-пакеты не должны перегружать основные каналы.
Настройка heartbeat:
const client = new Client({
brokerURL: 'wss://broker.example.com/ws',
heartbeatIncoming: 10000,
heartbeatOutgoing: 10000
});
В высоконагруженных системах heartbeat может обслуживаться отдельным балансировщиком или кластером.
Даже WebSocket endpoint может быть разделён.
/ws/chat
/ws/admin
/ws/metrics
/ws/telemetry
Подключение:
const chatClient = new Client({
brokerURL: 'wss://example.com/ws/chat'
});
Административный канал:
const adminClient = new Client({
brokerURL: 'wss://example.com/ws/admin'
});
Иногда выгоднее использовать несколько экземпляров Client.
const notificationClient = new Client({
brokerURL: 'wss://broker/ws'
});
const analyticsClient = new Client({
brokerURL: 'wss://analytics/ws'
});
Преимущества:
При сбое одного канала не должна останавливаться вся система.
const metricsClient = new Client({
reconnectDelay: 10000
});
Для критичных каналов:
const criticalClient = new Client({
reconnectDelay: 1000
});
Это позволяет гибко управлять политикой восстановления соединений.
Некоторые брокеры поддерживают selectors.
client.subscribe('/topic/events', handler, {
selector: "type = 'system'"
});
Другой подписчик:
client.subscribe('/topic/events', handler, {
selector: "type = 'user'"
});
Хотя сообщения находятся в одном topic, логическая обработка остаётся изолированной.
При RPC-взаимодействии применяются correlation-id.
client.publish({
destination: '/queue/rpc',
headers: {
'correlation-id': 'req-100'
},
body: requestData
});
Обработка ответа:
client.subscribe('/queue/rpc-replies', message => {
const correlationId =
message.headers['correlation-id'];
if (correlationId === 'req-100') {
processResponse(message);
}
});
Это позволяет разделять независимые запросы внутри общего канала.
Для производительности иногда разделяют:
/topic/json/events
/topic/binary/telemetry
Такой подход уменьшает overhead сериализации.
Логи не должны попадать в пользовательские каналы.
/topic/logs/errors
/topic/logs/debug
/topic/logs/security
Разделение логов позволяет:
Изоляция напрямую связана с безопасностью.
| Угроза | Последствие |
|---|---|
| подписка на чужой topic | утечка данных |
| публикация в системный канал | нарушение логики |
| wildcard-подписки | массовый перехват |
| отсутствие ACL | полный компромисс |
ALLOW user -> /topic/user/**
DENY user -> /topic/admin/**
connectHeaders: {
Authorization: 'Bearer token'
}
На сервере необходимо проверять:
Изолированные каналы проще масштабируются.
| Канал | Узел |
|---|---|
| chat | node-1 |
| telemetry | node-2 |
| analytics | node-3 |
Это снижает:
/topic/all
Недостатки:
Плохой пример:
/orders
/messages
/events
Хороший пример:
/topic/orders
/topic/chat/messages
/topic/system/events
Опасный пример:
/topic/**
Это может привести к:
Пример структуры:
/ws/chat
/ws/admin
/ws/telemetry
/topic/chat/*
/topic/system/*
/topic/admin/*
/topic/tenant/*
/queue/payments
/queue/security
/queue/rpc
Дополнительно:
Такая архитектура обеспечивает: