WebSocket как транспорт

WebSocket представляет собой полнодуплексный протокол связи поверх TCP, обеспечивающий постоянное соединение между клиентом и сервером. В отличие от HTTP, где каждое взаимодействие инициируется отдельным запросом, WebSocket создаёт устойчивый канал, по которому данные могут передаваться в обе стороны без повторного установления соединения.

Основные свойства WebSocket, критичные для STOMP.js:

  • постоянное соединение с минимальными накладными расходами;
  • двусторонняя передача сообщений в реальном времени;
  • низкая задержка доставки данных;
  • поддержка текстовых и бинарных фреймов;
  • работа поверх ws:// и wss:// (шифрованный канал).

STOMP.js использует WebSocket как транспортный слой, поверх которого реализуется текстовый протокол STOMP (Simple Text Oriented Messaging Protocol). WebSocket в этой архитектуре отвечает только за доставку байтового потока, не интерпретируя содержимое сообщений.

Роль WebSocket в архитектуре STOMP.js

STOMP.js не заменяет WebSocket, а работает поверх него, добавляя семантику сообщений:

  • очереди (queues);
  • топики (topics);
  • подписки (subscriptions);
  • команды протокола STOMP (CONNECT, SEND, SUBSCRIBE, DISCONNECT).

WebSocket выполняет роль канала передачи кадров STOMP, где каждый STOMP-фрейм передаётся как текстовое сообщение WebSocket.

Таким образом разделяются уровни:

  • WebSocket — транспорт;
  • STOMP — протокол сообщений;
  • приложение — бизнес-логика.

Формирование соединения STOMP через WebSocket

Подключение STOMP.js к серверу начинается с создания WebSocket-соединения. В современных версиях библиотеки используется параметр brokerURL, который указывает адрес WebSocket endpoint.

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

const client = new Client({
  brokerURL: 'ws://localhost:8080/ws',
  reconnectDelay: 5000,
  heartbeatIncoming: 4000,
  heartbeatOutgoing: 4000
});

В этом случае STOMP.js самостоятельно создаёт WebSocket:

new WebSocket('ws://localhost:8080/ws');

Если требуется более тонкий контроль над созданием соединения, используется фабрика:

const client = new Client({
  webSocketFactory: () => new WebSocket('ws://localhost:8080/ws')
});

Этот подход применяется при интеграции с кастомными транспортами или дополнительной логикой инициализации соединения.

Фазы установления соединения

WebSocket-соединение в контексте STOMP проходит несколько этапов.

1. TCP и HTTP Upgrade

Первичный запрос выполняется как HTTP с заголовком:

Upgrade: websocket
Connection: Upgrade

Сервер подтверждает переход на WebSocket-протокол, после чего соединение перестаёт быть HTTP.

2. Создание WebSocket канала

После успешного handshake создаётся постоянный канал передачи данных. На этом этапе STOMP ещё не активен.

3. STOMP CONNECT

STOMP.js отправляет первый протокольный кадр:

CONNECT
accept-version:1.2
host:localhost
heart-beat:4000,4000

WebSocket передаёт этот фрейм как текстовое сообщение.

4. Подтверждение соединения

Сервер отвечает:

CONNECTED
version:1.2
heart-beat:4000,4000

После этого канал считается полностью активным.

Передача STOMP-фреймов через WebSocket

Каждое сообщение STOMP инкапсулируется в WebSocket frame. WebSocket не анализирует содержимое, поэтому STOMP остаётся независимым уровнем.

Пример отправки сообщения:

client.publish({
  destination: '/queue/messages',
  body: JSON.stringify({
    text: 'hello'
  })
});

На уровне WebSocket это превращается в:

SEND
destination:/queue/messages

{"text":"hello"}\0

Особенности:

  • STOMP-фрейм всегда завершается null-символом \0;
  • WebSocket передаёт это как строку без изменений;
  • сервер парсит STOMP отдельно.

Подписка и получение сообщений

WebSocket сам по себе не знает о подписках. Эта логика реализуется в STOMP.

client.subscribe('/topic/chat', (message) => {
  const body = JSON.parse(message.body);
  console.log(body);
});

При подписке отправляется STOMP-фрейм:

SUBSCRIBE
id:sub-0
destination:/topic/chat

Далее сервер маршрутизирует сообщения и отправляет их через WebSocket:

MESSAGE
destination:/topic/chat
subscription:sub-0
message-id:001

{"text":"hi"}\0

WebSocket лишь доставляет этот поток клиенту, не интерпретируя заголовки.

Heartbeat поверх WebSocket

Несмотря на то что WebSocket уже поддерживает постоянное соединение, STOMP добавляет механизм heartbeat для контроля живости соединения.

Конфигурация:

heartbeatIncoming: 4000,
heartbeatOutgoing: 4000

Формат:

  • клиент отправляет пустые байты или \n;
  • сервер отвечает аналогично;
  • отсутствие heartbeat приводит к разрыву соединения.

Важно, что WebSocket не заменяет heartbeat STOMP, так как:

  • WebSocket не гарантирует доставку keep-alive на уровне приложения;
  • NAT и прокси могут разрывать idle-соединения;
  • STOMP heartbeat обеспечивает прикладной контроль.

Поведение при разрыве WebSocket

При разрыве TCP или WebSocket соединения STOMP.js переходит в состояние DISCONNECTED.

Основные сценарии:

  • потеря сети;
  • закрытие сервером;
  • таймаут heartbeat;
  • ошибка handshake.

В конфигурации:

reconnectDelay: 5000

STOMP.js автоматически инициирует новый WebSocket:

webSocketFactory: () => new WebSocket('ws://localhost:8080/ws')

и повторяет цикл:

  1. создание WebSocket;
  2. CONNECT;
  3. восстановление подписок;
  4. восстановление очередей сообщений.

Формат WebSocket сообщений в STOMP

WebSocket передаёт данные как текстовые фреймы. В контексте STOMP это всегда строка следующего вида:

COMMAND\n
header:value\n
header:value\n
\n
body\0

Особенности:

  • заголовки отделяются \n;
  • между заголовками и телом всегда пустая строка;
  • тело может быть JSON, XML или бинарная строка;
  • завершение фрейма — \0.

Binary WebSocket и ограничения STOMP

Хотя WebSocket поддерживает бинарные данные, классический STOMP поверх STOMP.js работает только с текстовыми фреймами.

Причины:

  • спецификация STOMP ориентирована на текст;
  • упрощённый парсинг серверной части;
  • совместимость с брокерами сообщений (RabbitMQ, ActiveMQ, Spring).

Передача бинарных данных возможна только через кодирование:

  • Base64;
  • ArrayBuffer → строка;
  • специализированные расширения протокола.

WebSocket subprotocol и STOMP

WebSocket поддерживает указание subprotocol при инициализации:

new WebSocket('ws://localhost:8080/ws', ['v10.stomp', 'v11.stomp', 'v12.stomp']);

Это позволяет серверу определить, что поверх WebSocket будет использоваться STOMP и его версия.

В STOMP.js этот параметр может задаваться через фабрику:

webSocketFactory: () =>
  new WebSocket('ws://localhost:8080/ws', ['v12.stomp'])

Интеграция с брокерами сообщений

WebSocket в связке со STOMP обычно подключается к серверному брокеру:

  • RabbitMQ (STOMP plugin);
  • ActiveMQ;
  • Spring WebSocket STOMP broker;
  • custom Node.js STOMP server.

Архитектура выглядит следующим образом:

  • клиент STOMP.js → WebSocket;
  • сервер WebSocket endpoint;
  • STOMP broker routing layer;
  • очереди и топики.

WebSocket при этом остаётся транспортом без логики маршрутизации.

Поведение заголовков при передаче через WebSocket

STOMP-фреймы передаются как plain text, но WebSocket гарантирует:

  • сохранение порядка сообщений;
  • атомарность фрейма;
  • отсутствие частичной доставки внутри одного сообщения.

Однако WebSocket не гарантирует:

  • доставку сообщений при обрыве;
  • повторную отправку;
  • подтверждение на уровне приложения.

Все эти функции реализуются STOMP или брокером.

Производительность WebSocket в STOMP.js

Использование WebSocket даёт следующие характеристики:

  • минимальные накладные расходы после handshake;
  • отсутствие HTTP overhead на каждое сообщение;
  • высокая пропускная способность;
  • стабильная задержка при постоянном соединении.

Ограничения:

  • чувствительность к нестабильным сетям;
  • необходимость heartbeat;
  • зависимость от серверной реализации WebSocket endpoint.

Поведение нескольких соединений

STOMP.js допускает создание нескольких независимых клиентов:

const chatClient = new Client({ brokerURL: 'ws://localhost:8080/chat' });
const notifClient = new Client({ brokerURL: 'ws://localhost:8080/notifications' });

Каждый клиент:

  • создаёт собственный WebSocket;
  • имеет собственный набор подписок;
  • управляет отдельным жизненным циклом соединения.

WebSocket не предоставляет мультиплексирование на уровне протокола, поэтому разделение происходит на уровне STOMP-клиентов.