Оперируем двоичными данными в Node.js. Часть 1: отправка данных
Итак, со времени написания предыдущей статьи я лучше разобрался с механизмом использования двоичных данных в Node.js. Механизм этот не то чтобы очевидный, но он есть, и он работает. Как я уже упоминал, оперирование двоичными данными критически важно для использования двоичных протоколов.
Этой статьи могло не быть без кода коннектора Postgres <=> Node.js, написанного товарищем creationix. Спасибо ему за отличные исходники, в которых вполне реально разобраться без обращения непосредственно к автору 🙂
Итак, предыдущий способ (с использованием PHP.js
) потерпел неудачу. В Google-группе nodejs мне посоветовали взглянуть на коннектор Postgres: это как раз реализация бинарного протокола для Node.js. После нескольких часов хакинга стало понятно, как работает обмен двоичными данными в Node. Итак, всё по порядку:
Хранение двоичных данных
Как известно, стандарт ECMAScript не определяет бинарного типа данных. Всё что он нам предлагает: строки, числа, boolean, объекты и массивы (которые тоже объекты). В Node.js хранение и использование двоичных данных реализовано через строки: каждый байт представлен символом. Но т.к. строки в JavaScript по стандарту всегда в Unicode, реально используется только первый байт. В остальных может быть что угодно. Плюсы такого подхода: можно перебирать байты итерацией и length
работает нормально.
Библиотека bits.js
, написанная creationix’ом для его коннектора, даёт нам простые функции для упаковки 16- и 32-битных чисел (только в формате unsigned int) ,строк в двух видах: с нулевым конечным байтом и без него, а также объектов. Последнее мы пока трогать не будем: для реализации основ протокола сойдут и числа со строками. Код с использованием bits.js
выглядит так:
var request = new bits.Encoder(0, 278); // В качестве заголовка передаются два int16 (необязательных). Это сделано для облегчения отправки заголовка request.push_int32(0); // добавляем числа request.push_int32(20); request.push_raw_string('Jingle Bells'); // Добавление строки request.push_cstring('Jingle Bells'); // Добавление строки с null-байтом в конце request_string = request.toString(); // получаем строку двоичных данных с заголовком и длиной контента (5-8 байты, int32) request_raw_string = request.toRawString(); // получаем строку двоичных данных без заголовка
Для простоты использования со Sphinx я переработал присоединение заголовка — конструктор теперь принимает два int16: команды Sphinx-сервера, и добавляет после них длину запроса в виде int32. Структуру протокола Sphinx я передрал прямо из PHP API, недолго думая. Как вариант, можно было бы воспользоваться MySQL API, которое предоставляет Sphinx, но мне было интересно именно общаться с демоном searchd
напрямую.
Соединение с сервером
Соединение со Sphinx начинается с отправки версии протокола. Мы отправляем серверу 4 байта: 0x00000001
. Сервер ответит тем же — версии протокола совпадают. Во всех примерах используется версия Sphinx 0.9.9-release.
Создаём TCP-соединение:
var sys = require("sys"); var Sphinx = {port: 9312}; var server_conn = tcp.createConnection(Sphinx.port); server_conn.setNoDelay(true); // убираем Nagle алгоритм, чтобы не мешал
Итак, нам надо отправить версию протокола, получить ответ, и начать обмен. Если Вы попробуете это сделать сами (это несложно), при отправке запроса обнаружится что предыдущий callback, ждущий версию протокола, всё ещё висит. Надо его убрать после получения номера версии:
server_conn.addListener('connect', function () { // Посылаем версию протокола sys.puts('Sending version number...'); // Здесь нам надо послать 4 байта, '0x00000001' server_conn.send((new bits.Encoder()).push_int32(1).toRawString(), 'binary'); // заголовок тут нам не нужен // Ждём ответа server_conn.addListener('receive', function(data) { // Сразу убираем повешенный на receive коллбек var receive_listeners = server_conn.listeners('receive'); var i; for (i = 0; i < receive_listeners.length; i++) { // Здесь мы получили все listeners, повешенные на событие receive от этого сервера // Т.к. он сейчас единственный, убираем все server_conn.removeListener('receive', receive_listeners[i]); } var protocol_version = (new bits.Decoder(data)).shift_int32();
Connection.removeListener()
принимает два параметра — событие и ссылку на функцию. Если эта ф-ция повешена обработчиком на событие, callback удаляется. Т.к. мы использовали в качестве обратного вызова анонимную функцию, мы не можем просто указать её имя. Поэтому мы берём ссылку на неё из массива listeners, полученного с помощью connection.listeners('receive')
.
Создание и отправка запроса
Построение запроса выглядит просто:
var query = "test"; var request = (new bits.Encoder(0, 278)).push_int32(0).push_int32(20); request.push_int32(Sphinx.searchMode.ALL).push_int32(Sphinx.rankingMode.BM25); request.push_int32(Sphinx.sortMode.RELEVANCE); request.push_int32(0); // "sort by" is not supported yet request.push_int32(query.length); // Query text length request.push_raw_string(query); // Query text request.push_int32(0); // weights is not supported yet request.push_int32(1).push_raw_string('*'); // Indices used request.push_int32(1); // id64 range marker request.push_int32(0).push_int32(0).push_int32(0).push_int32(0); // No limits for range request.push_int32(0); // filters is not supported yet request.push_int32(Sphinx.groupMode.DAY); // Basic grouping is supported request.push_int32(0); // Groupby length request.push_int32(1000); // Maxmatches, default to 1000 request.push_int32("@group desc".length); // Groupsort request.push_raw_string("@group desc"); request.push_int32(0); // Cutoff request.push_int32(0); // Retrycount request.push_int32(0); // Retrydelay request.push_int32(0); // Group distinct request.push_int32(0); // anchor is not supported yet request.push_int32(0); // Per-index weights is not supported yet request.push_int32(0); // Max query time is set to 0 request.push_int32(0); // Per-field weights is not supported yet request.push_int32(0); // Comments is not supported yet request.push_int32(0); // Atribute overrides is not supported yet request.push_int32(1).push_raw_string('*'); // Select-list server_conn.send(request.toString(), 'binary');
Обратите внимание: в 15 строке нам нужно не 4 int32, а 2 int64. Потом можно будет написать функцию кодирующую int64 по образцу int32, но т.к. пока нам нужны там только нулевые байты (это минимальный и максимальный id документа), сойдёт и так.
В строке server_conn.send(request.toString(), 'binary')
‘binary’ включает бинарный режим передачи данных. Если мы обмениваемся только бинарными данными, можно перевести соединие в двоичный режим, чтобы не указывать это каждый раз:
server_conn.setEncoding('binary'); server_conn.send(data1); // пересылаются в двоичном формате server_conn.send(data2); server_conn.send(data3);
После отправки запроса вешаем callback слушать ответ, и ждём. Придут нам тоже двоичные данные.
Ответ сервера (мы ищем строку «test»):
titlecontengroup_id date_addedK8YK8YK8YNtest [162]
Это уже похоже на вменяемый ответ. Мы запросили строку test, пришло 162 байта (я вывожу длину ответа после строки, т.к. большинство символов консоль просто не напечатает). Запуском search test
в командной строке можно проверить, что с тестовыми базами это даёт три совпадения. В следующей статье будем парсить этот ответ и приводить его к удобному для JS виду.
Любую двоичную строку в bits.js
можно вывести простой итерацией:
for (x = 0; x < request.toString().length; x++) { sys.puts(x + ':' + request.toString().charCodeAt(x).toString(16)); }
Это выведет строку request примерно в таком виде:
0:0 1:0 2:2A 3:16 4:0 5:9
Исходники проекта (а в будущем и документацию) можно найти на Github. Т.к. получен хоть какой то результат 🙂 проект получает репозиторий и собственное имя: limestone.js
.
Ссылки по теме
Библиотека bits.js
из коннектора postgres.js
Исходники limestone.js
на GitHub
Сам сервер Sphinx
Интересно пишете, Сергей, спасибо 🙂
Подсмотрел исходники bits.js и подумалось мне, что такое надо бы реализовать на сях внутри ноды. Опционально, конечно. А то все эти charCodeAt() и toString() мне кажутся сверхизбыточными даже для v8.
Что скажете? 😉
Да, в идеале это должно быть реализовано на C/C++ 🙂
Если делать это C-модулем, есть два варианта. Первый — просто портировать bits.js на Си. Там всё равно будет тот же подход на строках а-ля «первые байты символов», но с дополнительными методами и String в качестве прототипа 🙂 Всё равно нужны будут аналоги CharCodeAt и substr для лёгкой резки данных, и т.д.
Второй способ — попытаться дополнить JavaScript именно новым типом данных. Имхо это будет не очень хорошей идеей — на то он и стандарт 🙂 И лезть для этого придётся внутрь v8, а не ноды.
как в javascript двоичные числа выводить алерт(0х55)шеснадцатеричные а двоичные
I have been trying to fetch sphinx data using Node.js and limestone. I got everything from sphinx instead the float value. In my Sphinx index Height is defined as float value («sql_attr_float = Height») and but the Node.js returning some integer value.
Eg: if Height value in sphinx is 172.72, then i got «1127004242» from the Node.js and limestone
Please help me on this.
Below is the limestone code which will handling float value but not returning float data,
// FLOAT size attributes (32 bits)
if (output.attributes[attribute].type == Sphinx.attribute.FLOAT) {
attr_value = response.int32();
match.attrs[output.attributes[attribute].name] = attr_value;
continue;
}
Yeah, float attribute is bugged right now. We need good float value reader in buffer_extras.js. Maybe I can tackle it today, maybe not.
Hi, Any update on this and i got another error with limestone when querying MVA field.