Как обрабатывать странно комбинированные сообщения в виде websocket?

Я подключаюсь к внешнему websocket api, используя библиотеку узлов ws (узел 10.8.0 на Ubuntu 16.04). У меня есть слушатель, который просто разбирает json и передает его обратному вызову:

this.ws.on('message', (rawdata) => {
    let data = null;
    try {
        data = JSON.parse(rawdata);
    } catch (e) {
        console.log('Failed parsing the following string as json: ' + rawdata);
        return;
    }
    mycallback(data);
});

Теперь я получаю ошибки, в которых rawData выглядит следующим образом (я отформатировал и удалил ненужное содержимое):

�~A
{
    "id": 1,
    etc..
}�~�
{
    "id": 2,
    etc..

Я тогда подумал; каковы эти персонажи? Увидев структуру, я изначально думал, что первый странный знак должен быть открывающей скобкой массива ([), а второй - запятой (,), так что он создает массив объектов.

Затем я исследовал проблему, записывая rawdata в файл всякий раз, когда он сталкивается с ошибкой синтаксического анализа JSON. Через час или около того он спас около 1500 из этих файлов ошибок, что означает, что это происходит очень часто. Я cat пару этих файлов в терминале, из которых я загрузил пример ниже:

enter image description here

Здесь интересно несколько вещей:

  1. Файлы всегда начинаются с одного из этих странных знаков.
  2. Файлы, как представляется, существуют из нескольких сообщений, которые должны были быть получены отдельно. Странные знаки разделяют эти индивидуальные сообщения.
  3. Файлы всегда заканчиваются незавершенным json-объектом.
  4. Файлы имеют разную длину. Они не всегда одинакового размера и, следовательно, не отрезаны на определенной длине.

Я не очень разбираюсь в websockets, но может быть, что мой websocket каким-то образом получает поток сообщений, который он объединяет вместе с этими странными знаками в качестве разделителей, а затем случайным образом отключает последнее сообщение? Может быть, потому, что я получаю постоянный очень быстрый поток сообщений?

Или это может быть из-за ошибки (или функциональности) на стороне сервера в том, что он объединяет эти отдельные сообщения?

Кто-нибудь знает, что здесь происходит? Все советы приветствуются!

[РЕДАКТИРОВАТЬ]

@bendataclear предложил интерпретировать его как utf8. Так я и сделал, и я вставил снимок экрана из результатов ниже. Первая печать такая же, как и есть, а вторая интерпретируется как utf8. Для меня это не похоже. Я мог бы, конечно, преобразовать в utf8, а затем разделить эти символы. Несмотря на то, что последнее сообщение всегда отключено, это, по крайней мере, сделает некоторые из сообщений доступными. Другие идеи по-прежнему приветствуются.

enter image description here

Ответ 1

Когда я много раз прочитывал ваш вопрос и тестировал и эмулировал вашу проблему, я предлагаю вам сначала сделать docker. Потому что с docker изображение как мы, вы и я могу иметь последовательную область, что, когда Websocket отправить сообщение, как вы и я получаю последовательную rawdata.

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

Фактически, когда вы rawdata доступ к rawdata в this.ws.on, так поздно что-то исправить. вы должны настроить ваш сервер для передачи правильных кодированных символов. каждый из которых действует для rawdata приводит к потере данных для вашего приложения. На самом деле, ваш сервер должен передавать правильные данные, и это настолько странно для меня, что node ws library по умолчанию использует символы utf8. Возможно, ваши персонажи, которые были созданы на вашем сервере, находятся в другом текстовом кодировании. Но серьезно я предлагаю вам прочитать этот вопрос и эту среднюю статью. Эти ссылки могут помочь вам написать ваш сервер с конфигурацией, которая передает string данные текстовой кодировки utf8.

Вы должны установить условие if чтобы просто передать строковые данные utf8.

Надеюсь, мой ответ вам поможет.

Ответ 2

Мое предположение заключается в том, что вы работаете только с английскими /ASCII-символами и что-то, вероятно, испортило поток. (ПРИМЕЧАНИЕ: я предполагаю), нет специальных символов, если это так, то я предлагаю вам передать всю строку json в эту функцию:

function cleanString(input) {
    var output = "";
    for (var i=0; i<input.length; i++) {
        if (input.charCodeAt(i) <= 127) {
            output += input.charAt(i);
        }
    }
    console.log(output);
}

//example
cleanString("�~�")

Ответ 3

Основываясь на том, что видно на ваших снимках экрана, вы получаете данные из BitMex Websocket API. Исходя из этого предположения, я попытался воспроизвести проблему, подписавшись на некоторые темы, для которых не требуется аутентификация (а не тема "позиция", которую вы, вероятно, используете), без успеха, поэтому у меня нет объяснений для описанное поведение, даже если я подозреваю, что это может быть ошибкой библиотеки websocket, которая эффективно создает сообщение данных путем объединения массива "фрагментов". Было бы интересно увидеть коды ASCII "странных" символов, а не "замены символов", отображаемых терминальной консолью, поэтому, возможно, запустить команду " xxd " в выходном файле и показать нам, что соответствующая часть может быть полезна, Тем не менее, мое первое предложение - попытаться отключить опцию " perMessageDeflate " в библиотеке websocket (которая включена по умолчанию) следующим образом:

const ws = new WebSocket('wss://www.bitmex.com/realtime?subscribe=orderBookL2', {
  perMessageDeflate: false
});

Моим вторым советом является рассмотрение использования другой библиотеки websocket; например, рассмотрите использование официальных адаптеров Bitmex Websocket.

Ответ 4

Эти символы известны как "ЗАМЕНА ХАРАКТЕРА" - используются для замены неизвестного, непризнанного или непредставимого персонажа.

От: https://en.wikipedia.org/wiki/Specials_(Unicode_block)

Символ замены (часто черный бриллиант с белым знаком вопроса или пустое квадратное поле) - это символ, найденный в стандарте Unicode в кодовой точке U + FFFD в таблице Specials. Он используется для указания проблем, когда система не может отобразить поток данных в правильный символ. Обычно это видно, когда данные недействительны и не соответствуют ни одному символу

Проверка раздела 8 протокола WebSocket Обработка ошибок:

8.1. Обработка ошибок в UTF-8 с сервера

Когда клиент должен интерпретировать поток байтов как UTF-8, но обнаруживает, что поток байтов фактически не является допустимым потоком UTF-8, тогда любые байты или последовательности байтов, которые являются недопустимыми последовательностями UTF-8, ДОЛЖНЫ быть интерпретированы как U + FFFD ХАРАКТЕР ЗАМЕНЫ.

8.2. Обработка ошибок в UTF-8 от клиента

Когда сервер должен интерпретировать поток байтов как UTF-8, но обнаруживает, что поток байтов на самом деле не является допустимым потоком UTF-8, поведение не определено. Сервер может закрыть соединение, преобразовать недопустимые последовательности байтов в U + FFFD REPLACEMENT CHARACTERs, хранить данные дословно или выполнять обработку, зависящую от приложения. Подпрограммы, наложенные на протокол WebSocket, могут определять конкретное поведение для серверов.


Зависит от используемой реализации или библиотеки, как справиться с этим, например из этого поста. Внедрение серверов Socket Socket с помощью Node.js:

socket.ondata = function(d, start, end) {
    //var data = d.toString('utf8', start, end);
    var original_data = d.toString('utf8', start, end);
    var data = original_data.split('\ufffd')[0].slice(1);
    if (data == "kill") {
        socket.end();
    } else {
        sys.puts(data);
        socket.write("\u0000", "binary");
        socket.write(data, "utf8");
        socket.write("\uffff", "binary");
    }
};

В этом случае, если будет найден, он будет делать:

var data = original_data.split('\ufffd')[0].slice(1);
if (data == "kill") {
    socket.end();
} 

Еще одна вещь, которую вы можете сделать, - это обновить узел до последней стабильной версии, начиная с этого сообщения OpenSSL и Breaking UTF-8 Change (исправлено в Node v0.8.27 и v0.10.29):

Начиная с этих выпусков, если вы попытаетесь передать строку с непревзойденной суррогатной парой, Node заменит этот символ неизвестным символом юникода (U + FFFD). Чтобы сохранить старое поведение, установите переменную среды NODE_INVALID_UTF8 на что угодно (даже ничего). Если переменная окружения присутствует вообще, она вернется к старому поведению.

Ответ 5

Кажется, что у вашего вывода есть пробелы. Если у вас есть пробелы или вы найдете какие-либо специальные символы, используйте Unicode для их полного заполнения.

Вот список символов Unicode

Это может помочь, я думаю.

Ответ 6

Проблема, которая у вас есть, заключается в том, что одна сторона отправляет JSON в разных кодировках, как и другая сторона, которую он intepretes.

Попробуйте решить эту проблему с помощью следующего кода:

const { StringDecoder } = require('string_decoder');

this.ws.on('message', (rawdata) => {
    const decoder = new StringDecoder('utf8');
    const buffer = new Buffer(rawdata);
    console.log(decoder.write(buffer));
});

Или с utf16:

const { StringDecoder } = require('string_decoder');

this.ws.on('message', (rawdata) => {
    const decoder = new StringDecoder('utf16');
    const buffer = new Buffer(rawdata);
    console.log(decoder.write(buffer));
});

Пожалуйста, прочтите: Документация по строковому декодеру

Ответ 7

Я знаю, что я немного опаздываю на это, но ваш вопрос меня очень заинтриговал, поэтому я все еще пытаюсь найти разумный ответ.
Начнем с теории.
Как указано в RFC 6455, сообщения websocket состоят из "кадров":

После успешного рукопожатия клиенты и серверы передают данные взад и вперед в концептуальных единицах, упомянутых в этой спецификации, как "сообщения". На проводе сообщение состоит из одного или нескольких кадров. Сообщение WebSocket не обязательно соответствует кадрированию определенного сетевого уровня, поскольку фрагментированное сообщение может быть объединено или разделено посредником. Кадр имеет связанный тип. Каждый кадр, принадлежащий тому же сообщению, содержит один и тот же тип данных.

Поэтому давайте сначала посмотрим, как определяются фреймы. Это то, что RFC предоставляет нам:

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

Как вы можете видеть, каждый "заголовок кадра" имеет переменную длину, которая зависит от длины данных полезной нагрузки:

Длина полезной нагрузки: 7 бит, 7 + 16 бит или 7 + 64 бит

Длина "данных полезной нагрузки" в байтах: если 0-125, то есть длина полезной нагрузки. Если 126, следующие 2 байта, интерпретируемые как 16-разрядное целое число без знака, являются длиной полезной нагрузки. Если 127, следующие 8 байт интерпретируются как 64-разрядное целое без знака (самый старший бит ДОЛЖЕН быть 0) - длина полезной нагрузки. Многобайтовые величины длины выражаются в байтах сети. Обратите внимание, что во всех случаях минимальное количество байтов ДОЛЖНО использоваться для кодирования длины, например, длина строки длиной в 124 байт не может быть закодирована как последовательность 126, 0, 124. Длина полезной нагрузки равна длина "данных расширения" + длина "данных приложения". Длина "данных расширения" может быть равна нулю, и в этом случае длина полезной нагрузки является длиной "данных приложения".

Поэтому "странные символы" в начале каждой из ваших строк "rawData" - это просто "байты заголовков фреймов", и нет необходимости их кодировать (или декодировать), так как в нормальных условиях они должны не находиться в строке данных полезной нагрузки.
Символы "тильда" (~ = 126 ASCII = 0x7E HEX), которые хорошо видны на ваших снимках экрана, являются значением поля "полезной нагрузки" нескольких кадров.
По какой-то неясной причине ваш обратный вызов "onMessage" передается некорректной строкой, состоящей из нескольких кадров, вместо данных с одной полезной нагрузкой кадра.
Проблема, с которой вы столкнулись , уже была обнаружена другим пользователем в апреле 2017 года, которая открыла проблему № 1063, но, не имея возможности создать воспроизводимый тестовый пример, проблема была закрыта через несколько дней без ответа; однако это, вероятно, означает, что проблема не связана с конкретной версией узла (или версией библиотеки), поскольку он использовал другую среду от вашей (за исключением ОС Linux).
Я прочитал в своем комментарии еще один ответ, что проблема связана с 100% -ным всплеском в использовании ЦП, поэтому я попытался воспроизвести проблему со следующим кодом:

const WebSocket = require('./ws');

/*
** Reference: https://www.bitmex.com/app/wsAPI
** Topics that does not require authentication:
*/
var topics = [
  "announcement",// Site announcements
  "chat",        // Trollbox chat
  "connected",   // Statistics of connected users/bots
  "funding",     // Updates of swap funding rates. Sent every funding interval (usually 8hrs)
  "instrument",  // Instrument updates including turnover and bid/ask
  "insurance",   // Daily Insurance Fund updates
  "liquidation", // Liquidation orders as they're entered into the book
  "orderBookL2", // Full level 2 orderBook
  "orderBook10", // Top 10 levels using traditional full book push
  "publicNotifications", // System-wide notifications (used for short-lived messages)
  "quote",       // Top level of the book
  "quoteBin1m",  // 1-minute quote bins
  "quoteBin5m",  // 5-minute quote bins
  "quoteBin1h",  // 1-hour quote bins
  "quoteBin1d",  // 1-day quote bins
  "settlement",  // Settlements
  "trade",       // Live trades
  "tradeBin1m",  // 1-minute trade bins
  "tradeBin5m",  // 5-minute trade bins
  "tradeBin1h",  // 1-hour trade bins
  "tradeBin1d"   // 1-day trade bins
];

var address = 'wss://testnet.bitmex.com/realtime?subscribe=' + topics.join(',');
console.log(address);
var options = { perMessageDeflate: false };
const ws = new WebSocket(address, options);

function mycallback() {
  // ???
}

/*    
** The following code dumps the the received data on the socket,
** in either hex and ASCII format. It does that by attaching 
** a listener for the 'data' event on the internal socket.
*/
ws.on('open', function () {
  ws._socket.on('data', (rawdata) => { 
    console.log(rawdata);       // Hexadecimal
    console.log('' + rawdata);      // ASCII
    //process.exit(1);
  });
});

/*
** Websocket message handler 
*/
ws.on('message', (rawdata) => {
    let data = null;
    try {
        data = JSON.parse(rawdata);
        console.log(rawdata);
    } catch (e) {
        console.log('Failed parsing the following string as json: ' + rawdata);
        process.exit(1);
    }
    mycallback();
});

При отключенном сжатии мы можем сбросить "оригинальные" фреймы websocket.
Например, вы можете увидеть что-то вроде этого:

<Buffer 81 7e 00 c1 7b 22 69 6e 66 6f 22 3a 22 57 65 6c 63 6f 6d 65 20 74 6f 20 74 68 65 20 42 69 74 4d 45 58 20 52 65 61 6c 74 69 6d 65 20 41 50 49 2e 22 2c ... >
�~�{"info":"Welcome to the BitMEX Realtime API.","version":"2018-09-28T00:14:48.000Z","timestamp":"2018-09-30T11:32:47.737Z","docs":"https://testnet.bitmex.com/app/wsAPI","limit":{"remaining":39}}

См. Эти "странные символы" в начале дампа ASCII?
Они представляют собой 4 байта "заголовка кадра": 0x81, 0x7e, 0x00, 0xc1
Несмотря на все мои усилия, я не смог воспроизвести проблему, поэтому у меня нет объяснения этого странного поведения.
В исходном коде библиотеки websocket я заметил следующий комментарий:

Возможно, закрытый кадр не был получен или событие 'end', например, если сокет был уничтожен из-за ошибки. Убедитесь, что поток receiver закрыт после записи на него оставшихся буферизованных данных. Если читаемая сторона сокета находится в текущем режиме, тогда нет буферизованных данных, поскольку все уже было написано и readable.read() вернет значение null. Если вместо этого сокет приостанавливается, любые возможные буферизованные данные будут считаться одним блоком и синхронно испускаться в одном событии 'data'.

Возможно, если бы вы могли дать некоторые подсказки о том, как воспроизводить всплеск процессора, кто-то может найти решение этой загадки...