Формирование фрейма SEND

Фрейм SEND используется в протоколе STOMP для передачи сообщений от клиента брокеру сообщений. Через него отправляются данные в очереди, топики, пользовательские каналы, обработчики серверных приложений и промежуточные маршрутизаторы.

В библиотеке STOMP.js фрейм SEND формируется автоматически при вызове метода publish(). Несмотря на автоматизацию, понимание внутренней структуры фрейма имеет принципиальное значение при разработке высоконагруженных realtime-приложений, систем уведомлений, чатов, брокеров событий и интеграционных шлюзов.


Общая структура фрейма SEND

STOMP-фрейм представляет собой текстовую структуру, состоящую из:

  1. Команды
  2. Заголовков
  3. Пустой строки
  4. Тела сообщения
  5. NULL-байта \0

Базовый вид:

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

Hello World^@

Символ ^@ условно обозначает NULL-терминатор.


Формирование SEND через метод publish()

В современных версиях STOMP.js отправка сообщения выполняется через метод publish().

Простейший пример:

client.publish({
    destination: '/queue/chat',
    body: 'Привет'
});

Внутри библиотеки будет сформирован полноценный STOMP-фрейм:

SEND
destination:/queue/chat
content-length:12

Привет^@

Поля объекта publish()

Метод publish() принимает объект конфигурации.

Основные параметры:

client.publish({
    destination: '/topic/news',
    body: 'Новость',
    headers: {
        priority: '9'
    },
    binaryBody: binaryData,
    skipContentLengthHeader: false
});

Параметр destination

Заголовок destination является обязательным.

Он определяет конечную точку маршрутизации сообщения.

Пример:

client.publish({
    destination: '/queue/orders',
    body: 'Заказ создан'
});

Формируемый фрейм:

SEND
destination:/queue/orders

Заказ создан^@

Использование очередей и топиков

Разные брокеры используют разные соглашения маршрутизации.

Наиболее распространённые варианты:

/queue/tasks
/topic/events
/exchange/logs
/app/messages
/user/queue/private

Пример:

client.publish({
    destination: '/topic/system',
    body: 'Сервер запущен'
});

Тело сообщения body

Поле body содержит полезную нагрузку.

STOMP.js преобразует строку в текстовую часть фрейма.

Пример:

client.publish({
    destination: '/queue/logs',
    body: 'INFO: Application started'
});

Передача JSON

Наиболее распространённый сценарий — отправка JSON.

const payload = {
    id: 15,
    status: 'created',
    amount: 1200
};

client.publish({
    destination: '/topic/orders',
    body: JSON.stringify(payload)
});

Фрейм:

SEND
destination:/topic/orders

{"id":15,"status":"created","amount":1200}^@

Заголовок content-type

При отправке структурированных данных желательно явно указывать MIME-тип.

client.publish({
    destination: '/topic/orders',
    headers: {
        'content-type': 'application/json'
    },
    body: JSON.stringify({
        id: 10
    })
});

Формируемый фрейм:

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

{"id":10}^@

Автоматический content-length

STOMP.js может автоматически добавлять заголовок content-length.

Пример:

client.publish({
    destination: '/queue/test',
    body: 'Hello'
});

Результат:

SEND
destination:/queue/test
content-length:5

Hello^@

Отключение content-length

Некоторые брокеры работают корректнее без content-length.

Для этого используется параметр skipContentLengthHeader.

client.publish({
    destination: '/queue/test',
    body: 'Hello',
    skipContentLengthHeader: true
});

Фрейм:

SEND
destination:/queue/test

Hello^@

Пользовательские заголовки

SEND-фрейм поддерживает произвольные заголовки.

client.publish({
    destination: '/queue/payments',
    headers: {
        priority: '10',
        type: 'invoice',
        source: 'crm'
    },
    body: 'Оплата подтверждена'
});

Фрейм:

SEND
destination:/queue/payments
priority:10
type:invoice
source:crm

Оплата подтверждена^@

Назначение пользовательских заголовков

Дополнительные заголовки используются для:

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

Передача идентификаторов сообщений

Часто передаются correlation-id и request-id.

client.publish({
    destination: '/queue/rpc',
    headers: {
        'correlation-id': 'req-1001',
        'reply-to': '/queue/replies'
    },
    body: 'Запрос'
});

Приоритет сообщений

Некоторые брокеры поддерживают приоритеты.

client.publish({
    destination: '/queue/tasks',
    headers: {
        priority: '9'
    },
    body: 'Критическая задача'
});

Использование receipt

Заголовок receipt позволяет запросить подтверждение от брокера.

client.publish({
    destination: '/queue/orders',
    headers: {
        receipt: 'msg-001'
    },
    body: 'Создать заказ'
});

После обработки брокер вернёт фрейм RECEIPT.


Отправка бинарных данных

STOMP.js поддерживает бинарные сообщения через binaryBody.

Пример с Uint8Array:

const bytes = new Uint8Array([10, 20, 30, 40]);

client.publish({
    destination: '/queue/binary',
    binaryBody: bytes
});

Отличие body и binaryBody

body:

  • работает со строками;
  • используется для текста и JSON;
  • автоматически кодируется.

binaryBody:

  • работает с бинарными массивами;
  • применяется для файлов и бинарных протоколов;
  • не преобразуется в строку.

Передача файлов

Пример отправки файла:

const file = input.files[0];

const arrayBuffer = await file.arrayBuffer();

client.publish({
    destination: '/queue/files',
    binaryBody: new Uint8Array(arrayBuffer),
    headers: {
        filename: file.name
    }
});

Кодировка сообщений

По умолчанию используется UTF-8.

Пример Unicode-сообщения:

client.publish({
    destination: '/topic/chat',
    body: 'Привет мир'
});

STOMP.js корректно рассчитывает размер UTF-8 строки при формировании content-length.


Экранирование заголовков

В STOMP специальные символы внутри заголовков экранируются автоматически.

Экранируются:

\r
\n
:
\

Пример:

client.publish({
    destination: '/queue/test',
    headers: {
        description: 'line1\nline2'
    },
    body: 'data'
});

Формирование фрейма вручную

Внутри библиотеки формирование фрейма происходит через сериализацию объекта команды.

Упрощённая схема:

const frame = [
    'SEND',
    'destination:/queue/test',
    '',
    'Hello'
].join('\n');

Реальная реализация дополнительно:

  • экранирует заголовки;
  • добавляет content-length;
  • кодирует бинарные данные;
  • завершает пакет NULL-байтом.

Последовательность отправки

Процесс передачи включает:

  1. Формирование объекта publish
  2. Создание STOMP-фрейма
  3. Сериализацию
  4. Кодирование
  5. Передачу через WebSocket
  6. Обработку брокером

Взаимодействие с WebSocket

После формирования фрейм передаётся через WebSocket-соединение.

webSocket.send(serializedFrame);

STOMP.js выступает надстройкой над WebSocket и реализует протокольный уровень обмена.


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

client.publish({
    destination: '/topic/notifications',
    headers: {
        'content-type': 'application/json',
        priority: '5',
        receipt: 'notify-77'
    },
    body: JSON.stringify({
        type: 'email',
        userId: 15,
        message: 'Подтверждение'
    })
});

Результирующий фрейм:

SEND
destination:/topic/notifications
content-type:application/json
priority:5
receipt:notify-77
content-length:62

{"type":"email","userId":15,"message":"Подтверждение"}^@

Ошибки при формировании SEND

Отсутствие destination

client.publish({
    body: 'Test'
});

Проблема:

  • брокер не сможет определить маршрут сообщения.

Одновременное использование body и binaryBody

client.publish({
    body: 'text',
    binaryBody: bytes
});

Подобная конструкция считается ошибочной архитектурой.


Некорректный JSON

body: "{name:'test'}"

Ошибка:

  • невалидный JSON;
  • возможны проблемы десериализации на сервере.

Неверный content-type

headers: {
    'content-type': 'text/plain'
}

при фактической передаче JSON приводит к:

  • ошибкам обработки;
  • неверному парсингу;
  • проблемам интеграции.

Оптимизация SEND-фреймов

В высоконагруженных системах важно:

  • минимизировать размер заголовков;
  • избегать избыточных данных;
  • использовать бинарный формат при больших объёмах;
  • сокращать размер JSON;
  • избегать лишнего receipt;
  • уменьшать частоту мелких сообщений.

Массовая отправка сообщений

Пример серии SEND-фреймов:

for (let i = 0; i < 1000; i++) {
    client.publish({
        destination: '/queue/bulk',
        body: `Message ${i}`
    });
}

При высокой нагрузке это создаёт:

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

Батчинг сообщений

Иногда эффективнее передавать массив объектов:

client.publish({
    destination: '/queue/bulk',
    headers: {
        'content-type': 'application/json'
    },
    body: JSON.stringify([
        { id: 1 },
        { id: 2 },
        { id: 3 }
    ])
});

SEND внутри транзакций

Фрейм SEND может быть частью транзакции.

client.publish({
    destination: '/queue/orders',
    headers: {
        transaction: 'tx-001'
    },
    body: 'Создание заказа'
});

Сообщение будет окончательно обработано только после COMMIT.


Использование persistent-сообщений

Некоторые брокеры поддерживают долговременное хранение сообщений.

Пример:

client.publish({
    destination: '/queue/tasks',
    headers: {
        persistent: 'true'
    },
    body: 'Важная задача'
});

Диагностика SEND-фреймов

Для анализа полезно включать debug-режим.

client.debug = (message) => {
    console.log(message);
};

Это позволяет видеть:

  • сериализованные фреймы;
  • заголовки;
  • ошибки протокола;
  • данные heartbeat;
  • состояние соединения.

Анализ сетевого трафика

STOMP-фреймы можно исследовать через:

  • DevTools браузера;
  • вкладку Network;
  • WebSocket Frames;
  • прокси-анализаторы;
  • Wireshark.

Безопасность SEND-сообщений

При передаче данных необходимо учитывать:

  • валидацию payload;
  • фильтрацию пользовательских данных;
  • защиту от oversized messages;
  • ограничения брокера;
  • контроль типов контента;
  • защиту от injection-атак;
  • ограничение частоты отправки.

SEND и heartbeat

Heartbeat не влияет напрямую на структуру SEND-фрейма, однако поддерживает стабильность транспортного соединения во время активного обмена сообщениями.


Поведение брокеров

Разные брокеры по-разному интерпретируют SEND-фреймы.

Особенности могут касаться:

  • обязательности content-length;
  • обработки бинарных данных;
  • поддержки priority;
  • persistent-заголовков;
  • пользовательских headers;
  • максимального размера сообщения.

Совместимость версий STOMP

Формирование SEND может отличаться в версиях:

  • STOMP 1.0
  • STOMP 1.1
  • STOMP 1.2

Наиболее заметные изменения:

  • правила экранирования;
  • heartbeat;
  • обработка content-length;
  • кодировка заголовков.

Внутренний жизненный цикл SEND в STOMP.js

Последовательность внутренних операций библиотеки:

  1. Получение параметров publish
  2. Проверка destination
  3. Подготовка headers
  4. Формирование body
  5. Расчёт content-length
  6. Экранирование заголовков
  7. Сериализация STOMP-фрейма
  8. Передача через WebSocket
  9. Ожидание receipt при необходимости

Практический пример сложного SEND

const payload = {
    event: 'payment.created',
    paymentId: 501,
    amount: 1200,
    currency: 'USD',
    createdAt: Date.now()
};

client.publish({
    destination: '/topic/payments',
    headers: {
        'content-type': 'application/json',
        priority: '8',
        persistent: 'true',
        receipt: 'payment-501',
        source: 'billing-service'
    },
    body: JSON.stringify(payload)
});

Формируемый STOMP-фрейм:

SEND
destination:/topic/payments
content-type:application/json
priority:8
persistent:true
receipt:payment-501
source:billing-service
content-length:97

{"event":"payment.created","paymentId":501,"amount":1200,"currency":"USD","createdAt":1710000000}^@