Асинхронное программирование в 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.
Источник: Механический мир
Нет обратных ссылок на эту запись.
Январь 17th, 2010 - 17:47
Поправьте, пожалуйста, кавычки в 5 конструкциях:
var tcp = require("tcp");
Январь 17th, 2010 - 17:48
var tcp = require("tcp");
Январь 17th, 2010 - 17:48
Поправьте, пожалуйста, кавычки в 5 конструкциях:
var tcp = require("tcp");
Январь 17th, 2010 - 18:02
Спасибо, поправил!
Январь 17th, 2010 - 18:09
Можете удалить мои первые два сбившихся комментария и этот тоже.
Январь 17th, 2010 - 18:18
Да ладно.. Не помешают =))
Февраль 11th, 2010 - 19:00
> операции с жёстким диском (~14 микросекунд).
Миллисекунд. На картинке кстати правильно подписано.
Июнь 7th, 2010 - 11:47
добрый день,
не нашел подходящего поста, поэтому пишу тут
я только начал изучать node.js, он мне нужен для реализации comet-сервера с использованием streaming, до этого пробовал «наш» realplexor от dklab, хорошая вещь, но мне не подходит лонг-поллинг, информация обновляется по несколько раз в секунду… короче…
я поставил node, и поставил вот этот модуль http://github.com/LearnBoost/Socket.IO, он как бы работет, ошибок не выдает, но почему-то не срабатывает событие «onClientMessage», хотя другие события (подключение, отключение клиента) срабатывают
подскажите пожалуйста в чем может быть проблема, спасибо!
Июнь 9th, 2010 - 16:30
Я им не пользовался пока, но скачал исходники и не нашёл там в исходниках упоминания такого события. Это точно делается именно так? )
Июнь 10th, 2010 - 06:40
Да, это 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.
так вот все события срабатывают, видно когда, кто отключился/подключился, а сообщений нет
конечно было бы здорово почитать статью об этом в вашем исполнении))
спасибо
Июнь 12th, 2010 - 06:10
Ок, постараюсь посмотреть в чём там может быть дело.
Июнь 15th, 2010 - 14:26
Статья готова, читайте
Вообще лучше посмотреть ещё и в консоли клиента: возможно, message даже не отправляется.
Октябрь 13th, 2010 - 13:06
в текущей версии node.js (а вернее начиная с 0.1.30 кажется) убрали promise совсем, можете вы показать как нужно изменить код чтобы он работал?