nodeJS Быстрый веб-сервер на javascript движке V8

17Янв/1013

Асинхронное программирование в Node.js: события, коллбеки, promises

Неблокирующие функции

Основная «фишка» node.js, как известно, в том что большинство функций в ней неблокирующие. Что это значит для программиста?

Операции в программе отнимают разное время в зависимости от того к чему мы обращаемся. Операции с регистрами — самые быстрые, потом идут операции с кэшами первого и второго уровней (1 и 5 наносекунд соответственно), операции с RAM (~80-90 нс), операции с жёстким диском (~14 миллисекунд). Чтобы понять масштаб, можно взглянуть вот на этот gif. Огромная колонна - обращение к жёсткому диску. Если сделать zoom in, сверху будут видны операции с памятью и кэшами. Обращение к сетевому серверу ещё дольше — когда придёт ответ, вообще непонятно.

Философия неблокирующего ввода-вывода состоит в том что функции вообще не должны ждать окончания длительных операций ввода-вывода (диск и сеть). Для этого функция либо принимает функцию, которую надо выполнить по завершении операции (callback), либо возвращает объект-эмиттер, на который опять же вешаются функции. Пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Оставление callback'а
server.query('Query text', function(data) {
    // Сделать что нибудь с данными
});
 
// Оставление event-эмиттера
var query_status = server.query('Query text');
query_status.addListener('success', function(data){
    // Сделать что нибудь с данными
});
 
query_status.addListener('error', function(error){
    // Если пришла ошибка, тоже что нибудь сделать
});

Эмиттеры дают больше возможностей - они позволяют вешать разные функции на разные события. Callback-стиль, в свою очередь, выглядит более компактно, его удобнее использовать для простых вызовов. В Node есть ещё promises - особый случай эмиттера, у которого есть только три события: success, error, cancel. Promise можно использовать так:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Оставление promise
var promise = server.query('Query text');
promise.addCallback(function(data){
    // Сделать что нибудь с данными
});
 
query_status.addErrback(function(error){
    // Если пришла ошибка, тоже что нибудь сделать
});
 
query_status.addCancelback(function(error){
    // Процесс был отменён
});

Кроме этого, объект Promise предоставляет метод wait(), блокирующий исполнение кода до тех пор пока promise не вернёт какой-либо результат. Официальное руководство не советует особенно увлекаться этой функцией.

Пишем асинхронный код

Если мы хотим написать асинхронную функцию, нам надо как можно быстрее отдать управление в основной код, вернув какое-либо значение. Например, мы пишем библиотеку для обращения к какому то сервису:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var tcp = require('tcp');
 
var my_connector = function(port, data, callback) {
    var server_conn = tcp.createConnection(port);
    server_conn.addListener('connect', function () {
        // Подключились
        server_conn.send(data, 'ascii');
        //Отправили данные, ждём ответа
        server_conn.addListener('receive', function(answer) {
            callback(answer);
        }
    }
    return true;
}

Здесь возврат произойдёт сразу после установки первого внутреннего callback’а - того, который ждёт подключения. Скрипт будет выполняться дальше, а переданная нами функция будет терпеливо дожидаться конца операции. Этот подход работает, пока нам нужно обработать только одно событие — завершение операции. Чтобы хотя бы сигнализировать о том была ли операция успешной, нам придётся либо передавать статус в callback, вроде этого:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var tcp = require('tcp');
 
var my_connector = function(port, data, callback) {
    var server_conn = tcp.createConnection(port);
    server_conn.addListener('receive', function(answer) {
        callback(answer, 0); // Второй параметр - код успешной операции
    }
 
    // Соединение закрыто
    server_conn.addListener('close', function(haserror) {
        if (haserror) {
            callback(null, 3); // Второй параметр - код ошибки при соединении
        } else {
            callback(null, 1); // Второй параметр - код закрытия соединения
        }
    }
    return true;
}

Это не очень красиво выглядит, но будет работать, пока не понадобится обрабатывать пять-шесть различных ситуаций. После этого код обработки разрастётся до неприличия.

Сделаем то же самое с использованием promise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var tcp = require('tcp');
 
var my_connector = function(port, data) {
    var server_conn = tcp.createConnection(port);
    var promise = new process.Promise(); // создаём объект Promise
    server_conn.addListener('connect', function () {
        // Подключились
        server_conn.send(data, 'ascii');
        //Отправили данные, ждём ответа
        server_conn.addListener('receive', function(answer) {
            promise.emitSuccess(answer);
        }
        server_conn.addListener('close', function(haserror) {
            promise.emitError(haserror);
        }
    }
    return promise;
}

Управление снова возвращается сразу после иницализации соединения, но теперь функцию нужно использовать по другому:

1
2
3
4
5
6
7
8
9
10
11
var sys = require('sys');
 
var connection = my_connector(1445, 'hello'); // Получили объект promise
 
connection.addCallback(function(data) {
    sys.puts('Data received: ' + data); // Обработчик успешного завершения
});
 
connection.addErrback(function(error) {
    sys.puts('Error happened: ' + error); // Обработчик ошибок
});

В качестве «синтаксического сахара» можно комбинировать эти два подхода: принимать необязательный callback вторым параметром, и сразу вешать его на promise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var my_connector = function(port, data, callback) {
    var tcp = require('tcp');
    var server_conn = tcp.createConnection(port);
    var promise = new process.Promise(); // создаём объект Promise
 
    server_conn.addListener('connect', function () {
        // Подключились
        server_conn.send(data, 'ascii');
        //Отправили данные, ждём ответа
        server_conn.addListener('receive', function(answer) {
            promise.emitSuccess(answer);
        }
        server_conn.addListener('close', function(haserror) {
            promise.emitError(haserror);
        }
    }
 
    // Если передан callback, вешаем его сразу
    if (callback) {
        promise.addCallback(callback);
    }
 
    return promise;
}

Эту функцию теперь можно использовать двумя путями: передавая callback сразу или вешая его потом. Точно так же можно использовать EventEmitter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var my_connector = function(port, data, callback) {
    var tcp = require('tcp');
    var server_conn = tcp.createConnection(port);
    var conn_event = new process.EventEmitter(); // создаём эмиттер
 
    server_conn.addListener('connect', function () {
        // Подключились
        server_conn.send(data, 'ascii');
        //Отправили данные, ждём ответа
        server_conn.addListener('receive', function(answer) {
            conn_event.emit('success', answer);
        }
        server_conn.addListener('close', function(haserror) {
            conn_event.emit('error', haserror);
        }
    }
 
    if (callback) {
        conn_event.addListener('success', callback);
    }
 
    return conn_event;
}

Обращение к эмиттеру происходит немного не так как к promise. Событие запускается функцией event.emit('eventname', param1, param2...);, обработчики вешаются методом addListener() с указанием названия события:

1
2
3
4
5
6
7
8
9
10
11
var sys = require('sys');
 
var connection = my_connector(1445, 'hello'); // Получили эмиттер
 
connection.addListener('success', function(data) {
    sys.puts('Data received: ' + data); // Обработчик успешного завершения
});
 
connection.addListener('error', function(error) {
    sys.puts('Error happened: ' + error); // Обработчик ошибок
});

Ещё одна особенность promise: выполнится только одно событие. Например, если в нашем примере будут получены данные (сработает emitSuccess()), а потом соединение оборвётся (сработает emitFailure()) - будет выполнена только функция - обработчик ответа, повешенная addCallback, но не addErrback.

Источник: Механический мир

Комментарии (13) Пинги (0)
  1. Поправьте, пожалуйста, кавычки в 5 конструкциях:

    var tcp = require("tcp");

  2. Поправьте, пожалуйста, кавычки в 5 конструкциях:

    var tcp = require("tcp");

  3. Спасибо, поправил!

  4. Можете удалить мои первые два сбившихся комментария и этот тоже.

  5. Да ладно.. Не помешают =))

  6. > операции с жёстким диском (~14 микросекунд).

    Миллисекунд. На картинке кстати правильно подписано.

  7. добрый день,

    не нашел подходящего поста, поэтому пишу тут

    я только начал изучать node.js, он мне нужен для реализации comet-сервера с использованием streaming, до этого пробовал «наш» realplexor от dklab, хорошая вещь, но мне не подходит лонг-поллинг, информация обновляется по несколько раз в секунду… короче…

    я поставил node, и поставил вот этот модуль http://github.com/LearnBoost/Socket.IO, он как бы работет, ошибок не выдает, но почему-то не срабатывает событие «onClientMessage», хотя другие события (подключение, отключение клиента) срабатывают

    подскажите пожалуйста в чем может быть проблема, спасибо!

  8. Я им не пользовался пока, но скачал исходники и не нашёл там в исходниках упоминания такого события. Это точно делается именно так? )

  9. Да, это 100%, так показано в примере на их сайте, и в документации такая запись:

    Methods:

    addListener(event, ЮЛ)

    Adds a listener for the specified event. Optionally, you can pass it as an option to io.listen, prefixed by on. For example: onClientConnect: function(){}

    а ниже:

    Events:

    clientConnect(client)

    Fired when a client is connected. Receives the Client instance as parameter

    clientMessage(message, client)

    Fired when a message from a client is received. Receives the message and Client instance as parameter

    clientDisconnect(client)

    Fired when a client is disconnected. Receives the Client instance as parameter

    Important note: this in the event listener refers to the Listener instance.

    так вот все события срабатывают, видно когда, кто отключился/подключился, а сообщений нет

    конечно было бы здорово почитать статью об этом в вашем исполнении))

    спасибо

  10. Ок, постараюсь посмотреть в чём там может быть дело.

  11. Статья готова, читайте :) Вообще лучше посмотреть ещё и в консоли клиента: возможно, message даже не отправляется.

  12. в текущей версии node.js (а вернее начиная с 0.1.30 кажется) убрали promise совсем, можете вы показать как нужно изменить код чтобы он работал?


Оставить комментарий


Нет обратных ссылок на эту запись.