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

Протокол STOMP изначально ориентирован на передачу текстовых сообщений. Большинство примеров используют JSON, строки или текстовые команды. Однако современные приложения регулярно работают с бинарными форматами:

  • изображениями;
  • аудиофайлами;
  • видеофрагментами;
  • архивами;
  • protobuf-сообщениями;
  • MessagePack;
  • сериализованными структурами;
  • массивами байтов;
  • файлами произвольного формата.

Библиотека STOMP.js поддерживает передачу бинарных данных через свойство binaryBody.


Текстовые и бинарные сообщения

В STOMP.js существуют два основных варианта отправки содержимого:

client.publish({
    destination: '/topic/messages',
    body: 'Текстовое сообщение'
});

и бинарный вариант:

client.publish({
    destination: '/topic/files',
    binaryBody: binaryData
});

Ключевое различие:

Свойство Тип
body строка
binaryBody Uint8Array

Для бинарной передачи используется исключительно Uint8Array.


Uint8Array в Javascript

Uint8Array представляет массив беззнаковых 8-битных целых чисел.

Пример создания:

const bytes = new Uint8Array([72, 101, 108, 108, 111]);

Каждый элемент массива — отдельный байт.

Такой формат идеально подходит для:

  • сетевой передачи;
  • работы с WebSocket;
  • потоковой обработки;
  • файловых операций;
  • сериализации бинарных протоколов.

Отправка бинарного сообщения

Базовый пример:

const data = new Uint8Array([1, 2, 3, 4, 5]);

client.publish({
    destination: '/topic/binary',
    binaryBody: data
});

После отправки STOMP.js формирует бинарный WebSocket-фрейм.


Преобразование строки в бинарный формат

Для кодирования текста используется TextEncoder.

const encoder = new TextEncoder();

const binaryData = encoder.encode('Привет мир');

client.publish({
    destination: '/topic/chat',
    binaryBody: binaryData
});

encode() возвращает Uint8Array.


Декодирование бинарных данных

На стороне подписчика выполняется обратное преобразование через TextDecoder.

client.subscribe('/topic/chat', (message) => {

    const decoder = new TextDecoder();

    const text = decoder.decode(message.binaryBody);

    console.log(text);

});

binaryBody в объекте Message

При получении бинарного сообщения STOMP.js предоставляет:

message.binaryBody

Тип:

Uint8Array

Текстовое поле message.body в бинарных сообщениях обычно не используется.


Отправка JSON в бинарном виде

Иногда JSON передают не как строку, а как бинарный поток.

Причины

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

Пример:

const payload = {
    id: 15,
    name: 'Alex',
    online: true
};

const encoder = new TextEncoder();

const binaryData = encoder.encode(
    JSON.stringify(payload)
);

client.publish({
    destination: '/topic/users',
    binaryBody: binaryData
});

Получение:

client.subscribe('/topic/users', (message) => {

    const decoder = new TextDecoder();

    const json = decoder.decode(message.binaryBody);

    const data = JSON.parse(json);

    console.log(data);

});

Отправка файлов

Получение файла из input

<input type="file" id="fileInput">
const input = document.getElementById('fileInput');

input.addEventListener('change', async () => {

    const file = input.files[0];

    const arrayBuffer = await file.arrayBuffer();

    const binaryData = new Uint8Array(arrayBuffer);

    client.publish({
        destination: '/topic/upload',
        binaryBody: binaryData
    });

});

ArrayBuffer и Uint8Array

Между этими структурами существует важное различие.

ArrayBuffer

Содержит сырые бинарные данные.

Uint8Array

Предоставляет интерфейс доступа к байтам.

Пример:

const buffer = new ArrayBuffer(8);

const bytes = new Uint8Array(buffer);

STOMP.js ожидает именно Uint8Array.


Передача изображений

const response = await fetch('/image.png');

const buffer = await response.arrayBuffer();

client.publish({
    destination: '/topic/images',
    binaryBody: new Uint8Array(buffer)
});

Передача аудио

const response = await fetch('/audio.mp3');

const buffer = await response.arrayBuffer();

client.publish({
    destination: '/topic/audio',
    binaryBody: new Uint8Array(buffer)
});

Передача protobuf-сообщений

Многие высоконагруженные системы используют Protocol Buffers вместо JSON.

Пример:

const encodedMessage = Message.encode(payload).finish();

client.publish({
    destination: '/topic/protobuf',
    binaryBody: encodedMessage
});

Метод finish() возвращает Uint8Array.


content-type для бинарных данных

При бинарной отправке желательно явно задавать MIME-тип.

Пример:

client.publish({
    destination: '/topic/files',
    binaryBody: fileData,
    headers: {
        'content-type': 'application/octet-stream'
    }
});

Популярные MIME-типы

Тип данных MIME
Бинарный поток application/octet-stream
JSON application/json
PNG image/png
JPEG image/jpeg
MP3 audio/mpeg
PDF application/pdf
ZIP application/zip

Передача PNG-файла

client.publish({
    destination: '/topic/images',
    binaryBody: imageBytes,
    headers: {
        'content-type': 'image/png'
    }
});

Передача PDF

client.publish({
    destination: '/topic/docs',
    binaryBody: pdfBytes,
    headers: {
        'content-type': 'application/pdf'
    }
});

content-length и бинарные данные

Для бинарных сообщений заголовок content-length особенно важен.

Он позволяет:

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

STOMP.js обычно рассчитывает его автоматически.


Принудительное указание content-length

client.publish({
    destination: '/topic/data',
    binaryBody: binaryData,
    headers: {
        'content-length': binaryData.length
    }
});

Отключение расчёта content-length

Иногда брокеры работают нестандартно.

client.publish({
    destination: '/topic/data',
    binaryBody: binaryData,
    skipContentLengthHeader: true
});

Передача больших файлов

STOMP не является идеальным протоколом для очень крупных файлов.

Проблемы:

  • рост потребления памяти;
  • блокировка WebSocket;
  • увеличение latency;
  • ограничения брокера;
  • таймауты;
  • фрагментация пакетов.

Ограничения брокеров

Многие брокеры ограничивают размер сообщений.

Например:

  • RabbitMQ;
  • ActiveMQ;
  • Artemis;
  • Apollo.

При превышении лимита сообщение может:

  • отклоняться;
  • обрезаться;
  • приводить к закрытию соединения.

Chunking больших данных

Крупные файлы часто разбиваются на части.

Схема

  1. файл делится на блоки;
  2. каждый блок отправляется отдельно;
  3. получатель собирает файл обратно.

Пример разбиения файла

function splitBytes(bytes, chunkSize) {

    const chunks = [];

    for (let i = 0; i < bytes.length; i += chunkSize) {

        chunks.push(
            bytes.slice(i, i + chunkSize)
        );

    }

    return chunks;

}

Отправка чанков

const chunks = splitBytes(fileBytes, 64 * 1024);

chunks.forEach((chunk, index) => {

    client.publish({
        destination: '/topic/chunks',
        binaryBody: chunk,
        headers: {
            chunkIndex: index,
            totalChunks: chunks.length
        }
    });

});

Сборка чанков

const receivedChunks = [];

client.subscribe('/topic/chunks', (message) => {

    const index = Number(
        message.headers.chunkIndex
    );

    receivedChunks[index] = message.binaryBody;

});

Объединение чанков

function mergeChunks(chunks) {

    const totalLength = chunks.reduce(
        (sum, chunk) => sum + chunk.length,
        0
    );

    const result = new Uint8Array(totalLength);

    let offset = 0;

    for (const chunk of chunks) {

        result.set(chunk, offset);

        offset += chunk.length;

    }

    return result;

}

Base64 и бинарные данные

Иногда бинарные данные кодируют в Base64.

Недостатки

  • увеличение размера примерно на 33%;
  • дополнительная нагрузка на CPU;
  • лишние преобразования.

Преимущества

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

Отправка Base64

const base64 = btoa(binaryString);

client.publish({
    destination: '/topic/base64',
    body: base64
});

Почему binaryBody предпочтительнее

Передача через binaryBody:

  • быстрее;
  • эффективнее;
  • не требует перекодирования;
  • уменьшает объём трафика;
  • снижает нагрузку на память.

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

client.subscribe('/topic/data', (message) => {

    const contentType =
        message.headers['content-type'];

    console.log(contentType);

});

Автоматическая обработка по MIME

client.subscribe('/topic/data', (message) => {

    const type =
        message.headers['content-type'];

    if (type === 'application/json') {

        const text = new TextDecoder()
            .decode(message.binaryBody);

        console.log(JSON.parse(text));

    }

});

Работа с Blob

В браузерах часто используется Blob.

Преобразование:

const blob = new Blob([binaryData]);

const buffer = await blob.arrayBuffer();

const bytes = new Uint8Array(buffer);

Blob из входящего сообщения

client.subscribe('/topic/files', (message) => {

    const blob = new Blob([
        message.binaryBody
    ]);

    console.log(blob);

});

Скачивание файла

client.subscribe('/topic/download', (message) => {

    const blob = new Blob([
        message.binaryBody
    ]);

    const url = URL.createObjectURL(blob);

    const link = document.createElement('a');

    link.href = url;

    link.download = 'file.bin';

    link.click();

});

Проверка размера сообщения

console.log(binaryData.length);

Размер измеряется в байтах.


Сжатие бинарных данных

Перед отправкой возможно использование:

  • gzip;
  • deflate;
  • brotli;
  • lz-string.

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

headers: {
    'content-encoding': 'gzip'
}

Heartbeat и большие бинарные сообщения

Передача крупных данных может влиять на heartbeat-механизм STOMP.

Возможные проблемы:

  • ложные таймауты;
  • задержки ping/pong;
  • разрывы соединения;
  • повторные подключения.

Иногда требуется увеличение:

heartbeatIncoming
heartbeatOutgoing

Отладка бинарных сообщений

Для анализа удобно выводить массив байтов:

console.log(message.binaryBody);

или:

console.log(
    Array.from(message.binaryBody)
);

Hex-представление

const hex = Array.from(binaryData)
    .map(byte => byte.toString(16))
    .join(' ');

console.log(hex);

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

Распространённый способ — вычисление хеша.

Например:

  • MD5;
  • SHA-1;
  • SHA-256.

Отправитель:

headers: {
    checksum: hash
}

Получатель:

if (receivedHash !== calculatedHash) {
    console.error('Файл повреждён');
}

Типичные ошибки

Использование body вместо binaryBody

Неправильно:

client.publish({
    destination: '/topic/data',
    body: binaryData
});

Правильно:

client.publish({
    destination: '/topic/data',
    binaryBody: binaryData
});

Передача ArrayBuffer напрямую

Неправильно:

binaryBody: arrayBuffer

Правильно:

binaryBody: new Uint8Array(arrayBuffer)

Отсутствие content-type

Без MIME-типа обработка на сервере может быть затруднена.


Передача слишком больших сообщений

Огромные бинарные пакеты способны:

  • перегружать память;
  • вызывать reconnect;
  • нарушать heartbeat;
  • блокировать event loop.

Практические сценарии использования

Бинарная передача через STOMP.js активно применяется:

  • в чатах с вложениями;
  • системах видеонаблюдения;
  • потоковой передаче аудио;
  • обмене изображениями;
  • игровых серверах;
  • телеметрии;
  • IoT;
  • системах мониторинга;
  • realtime-анализе данных;
  • финансовых терминалах;
  • системах логирования;
  • распределённых вычислениях;
  • обработке protobuf-сообщений;
  • обмене сериализованными объектами.