Фреймы и их структура

STOMP (Simple Text Oriented Messaging Protocol) поверх WebSocket представляет обмен данными в виде текстовых фреймов. Каждый фрейм — это атомарная единица коммуникации между клиентом и брокером сообщений. Вся логика протокола строится вокруг строго определённой структуры, в которой выделяются три ключевых компонента: команда, заголовки и тело сообщения.

Фрейм всегда интерпретируется последовательно: сначала читается строка команды, затем набор заголовков, затем тело до нулевого байта. Такая линейная модель делает протокол простым для парсинга и реализации в JavaScript.


Общая форма фрейма

Фрейм STOMP в текстовом представлении имеет следующий формат:

COMMAND
header1:value1
header2:value2

body^@

Где:

  • COMMAND — управляющая инструкция (например, CONNECT, SEND, SUBSCRIBE)
  • headers — набор пар ключ-значение
  • пустая строка отделяет заголовки от тела
  • body — полезная нагрузка сообщения
  • ^@ — нулевой символ (NULL, \x00), обозначающий конец фрейма

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


Команда (Command)

Команда — первый элемент фрейма. Она определяет тип операции, которую необходимо выполнить.

На уровне STOMP.js команды можно разделить на несколько категорий:

Клиентские команды

  • CONNECT — установление соединения
  • SEND — отправка сообщения в destination
  • SUBSCRIBE — подписка на канал
  • UNSUBSCRIBE — отмена подписки
  • DISCONNECT — закрытие соединения

Серверные команды

  • CONNECTED — подтверждение соединения
  • MESSAGE — доставка сообщения подписчику
  • RECEIPT — подтверждение выполнения операции
  • ERROR — сообщение об ошибке

Команда всегда находится в первой строке и не содержит дополнительных символов или префиксов.


Заголовки (Headers)

Заголовки представляют собой набор метаданных, влияющих на поведение доставки сообщений и обработки фрейма.

Формат заголовка:

key:value

Каждый заголовок занимает отдельную строку. Порядок заголовков не имеет семантического значения.

Основные заголовки STOMP

  • destination — маршрут сообщения (очередь или топик)
  • content-type — тип содержимого (например, application/json)
  • content-length — длина тела сообщения
  • id — идентификатор подписки
  • receipt — запрос подтверждения выполнения операции
  • authorization — токен авторизации (в некоторых реализациях)

Пример набора заголовков

destination:/topic/chat
content-type:application/json

Заголовки часто используются брокерами (RabbitMQ, ActiveMQ, Spring STOMP) для маршрутизации и контроля доставки.


Тело сообщения (Body)

Тело фрейма содержит полезную нагрузку. Это может быть:

  • JSON-объект
  • строка
  • бинарные данные в текстовом представлении (например, Base64)
  • произвольный текст

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

Пример JSON-данных:

{"user":"alex","message":"hello"}

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


Завершение фрейма (NULL byte)

Каждый фрейм обязательно завершается нулевым символом:

\x00

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

Без этого символа невозможно корректно разделить последовательность фреймов в одном TCP-потоке.


Пример полного STOMP-фрейма

SEND
destination:/topic/chat
content-type:application/json

{"user":"alex","message":"hello"}\x00

Этот фрейм означает отправку сообщения в топик /topic/chat.


Фреймы подключения

При установлении соединения используется специальный набор фреймов, определяющих параметры сессии.

CONNECT

CONNECT
accept-version:1.2
host:localhost
login:user
passcode:password

\x00

Заголовки:

  • accept-version — поддерживаемая версия протокола
  • host — виртуальный хост брокера
  • login и passcode — аутентификация

CONNECTED (ответ сервера)

CONNECTED
version:1.2
session:session-id-123

\x00

Этот фрейм подтверждает успешное установление соединения и создание сессии.


Фреймы подписки

Подписка на канал осуществляется через SUBSCRIBE:

SUBSCRIBE
id:sub-1
destination:/topic/news

\x00

Здесь:

  • id используется для управления подпиской
  • destination определяет канал сообщений

Ответные сообщения приходят в виде MESSAGE-фреймов.


MESSAGE-фреймы

Когда брокер доставляет данные подписчику, используется структура:

MESSAGE
subscription:sub-1
message-id:msg-001
destination:/topic/news
content-type:application/json

{"title":"update","body":"text"}\x00

Заголовки включают идентификаторы доставки, позволяющие отслеживать маршрут сообщения и подтверждать получение.


Фреймы отправки данных

Отправка данных через STOMP.js реализуется через SEND-фреймы:

SEND
destination:/queue/task
content-type:text/plain

task payload\x00

Маршрутизация определяется заголовком destination. Брокер интерпретирует его как очередь или topic в зависимости от конфигурации.


ACK и подтверждения

В режиме ручного подтверждения доставки используются ACK-фреймы:

ACK
id:msg-001

\x00

И отрицательное подтверждение:

NACK
id:msg-001

\x00

Эти механизмы обеспечивают контроль доставки сообщений в надёжных системах.


ERROR-фреймы

При возникновении ошибок сервер отправляет:

ERROR
message:Invalid subscription

The subscription id was not found\x00

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

  • заголовок message содержит краткое описание
  • тело может содержать расширенную информацию
  • соединение может быть разорвано в зависимости от критичности ошибки

Особенности кодирования

STOMP-фреймы передаются поверх WebSocket как текстовые строки. Важные нюансы:

  • используется UTF-8
  • символы перевода строк — LF (\n)
  • CRLF допустим, но чаще нормализуется
  • NULL (\x00) обязателен
  • заголовки чувствительны к формату key:value без пробелов вокруг двоеточия

Любое отклонение от формата может привести к некорректному разбору фрейма на стороне брокера.


Парсинг фреймов в STOMP.js

В STOMP.js фрейм обрабатывается как поток символов. Общая логика:

  1. Чтение команды до \n
  2. Построчное чтение заголовков до пустой строки
  3. Сбор тела до \x00
  4. Преобразование в объект сообщения

Упрощённая модель объекта:

{
  command: "MESSAGE",
  headers: {
    subscription: "sub-1",
    destination: "/topic/news"
  },
  body: "{\"title\":\"update\"}"
}

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


Фрагментация и потоковая передача

STOMP не гарантирует, что один WebSocket message соответствует одному фрейму. Возможны три ситуации:

  • один пакет содержит несколько фреймов
  • один фрейм разделён между пакетами
  • частичное получение тела

Поэтому STOMP.js использует буферизацию входящего потока и анализ на наличие \x00.


Экранирование и ограничения

Некоторые символы требуют специальной обработки:

  • \n — конец строки
  • \r — может игнорироваться
  • : — разделитель заголовков
  • \x00 — конец фрейма

В теле сообщения допустим любой контент, но бинарные данные обычно сериализуются в Base64, поскольку STOMP изначально текстовый.


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

Фреймы являются базовым уровнем абстракции, на котором строится вся логика STOMP.js:

  • управление соединением через CONNECT/CONNECTED
  • маршрутизация через destination
  • подписка через SUBSCRIBE
  • доставка через MESSAGE
  • контроль надёжности через ACK/NACK
  • обработка ошибок через ERROR

Каждый фрейм представляет независимое событие, что упрощает масштабирование и интеграцию с брокерами сообщений.


Поведение при высокой нагрузке

При интенсивной передаче сообщений структура фрейма остаётся неизменной, но увеличивается частота обработки:

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

Эффективность STOMP-системы напрямую зависит от оптимизации размера фреймов и частоты их отправки.