Skip to content

Оперируем двоичными данными в Node.js. Часть 1: отправка данных

12/01/2010

Итак, со времени написания предыдущей статьи я лучше разобрался с механизмом использования двоичных данных в 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

Реклама
7 комментариев
  1. Интересно пишете, Сергей, спасибо 🙂

    Подсмотрел исходники bits.js и подумалось мне, что такое надо бы реализовать на сях внутри ноды. Опционально, конечно. А то все эти charCodeAt() и toString() мне кажутся сверхизбыточными даже для v8.

    Что скажете? 😉

    • Да, в идеале это должно быть реализовано на C/C++ 🙂

      Если делать это C-модулем, есть два варианта. Первый — просто портировать bits.js на Си. Там всё равно будет тот же подход на строках а-ля «первые байты символов», но с дополнительными методами и String в качестве прототипа 🙂 Всё равно нужны будут аналоги CharCodeAt и substr для лёгкой резки данных, и т.д.

      Второй способ — попытаться дополнить JavaScript именно новым типом данных. Имхо это будет не очень хорошей идеей — на то он и стандарт 🙂 И лезть для этого придётся внутрь v8, а не ноды.

  2. как в javascript двоичные числа выводить алерт(0х55)шеснадцатеричные а двоичные

  3. 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;
    }

Trackbacks & Pingbacks

  1. Оперируем двоичными данными в Node.js. Часть 1: отправка данных « nodeJS

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: