12.2. Асинхронный ввод/вывод в интерпретаторе Node

We use cookies. Read the Privacy and Cookie Policy

Node - это быстрый интерпретатор JavaScript, написанный на языке C++, включающий средства доступа к низкоуровневым интерфейсам Unix для работы с процессами, файлами, сетевыми сокетами и так далее, а также к клиентским и серверным интерфейсам реализации протокола HTTP. За исключением нескольких синхронных методов, имеющих специальные имена, все остальные инструменты интерпретатора Node доступа к интерфейсам Unix являются асинхронными, и по умолчанию программы, выполняемые под управлением Node, никогда не блокируются, что обеспечивает им хорошую масштабируемость и позволяет эффективно справляться с высокой нагрузкой. Поскольку прикладные программные интерфейсы являются асинхронными, интерпретатор Node опирается на использование обработчиков событий, которые часто реализуются с использованием вложенных функций и замыканий.[26]

Этот раздел освещает некоторые наиболее важные инструменты и события, имеющиеся в составе Node, но это описание ни в коем случае нельзя считать полным. Полное описание Node можно найти в электронной документации по адресу http://nodejs.org/api/.

Как получить Node

Node - это свободное программное обеспечение, которое можно загрузить по адресу http://nodejs.org. На момент написания этих строк интерпретатор все еще активно разрабатывался и скомпилированные дистрибутивы не были доступны, однако вы можете собрать собственную копию интерпретатора из исходных текстов. Примеры в этом разделе опробовались под управлением версии Node 0.4. Прикладной интерфейс интерпретатора еще не зафиксирован, однако основные функции, демонстрируемые в этом разделе, едва ли сильно изменятся в будущем.

Интерпретатор Node построен на основе механизма V8 JavaScript, разработанного компанией Google. Версия Node 0.4 использует версию V8 3.1, которая реализует все особенности ECMAScript 5, за исключением строгого режима.

После загрузки, компиляции и установки Node вы сможете запускать программы, написанные для этого интерпретатора, как показано ниже:

node program.js

******************************************

Знакомство с интерпретатором Rhino мы начали с функций print() и load(). Интерпретатор Node имеет похожие инструменты, но с другими именами:

// Подобно броузерам, для вывода отладочной информации Node определяет

// функцию console.log().

console.log("Hello Node"); // Выведет отладочную информацию в консоль

// Вместо load() в нем используется функция require().

// Она загружает и выполняет (только один) указанный модуль и возвращает объект,

// содержащий имена, экспортируемые модулем.

var fs = require("fs"); // Загрузит модуль ”fs" и вернет объект с его API

Интерпретатор Node реализует в глобальном объекте все стандартные конструкторы, свойства и функции, предусмотренные стандартом ECMAScript 5. Однако в дополнение к этому он также поддерживает клиентские функции для работы с таймером: setTimeout(), setlnterval(), clearTimeout() и clearlnterval():

//Вывести приветствие через одну секунду.

setTimeout(function() { console.log( "Привет, Мир!"); }, 1000);

Эти глобальные клиентские функции рассматриваются в разделе 14.1. Реализация Node совместима с реализациями интерпретаторов в веб-броузерах.

Интерпретатор Node также определяет и другие глобальные компоненты в пространстве имен process. Ниже перечислены некоторые из свойств этого объекта:

process.version // Строка с версией Node

process.argv // Аргументы командной строки в виде массива, argv[0] = "node"

process.env // Переменные окружения в виде объекта.

// например: process.env.PATH

process.pid // Числовой идентификатор процесса

process.getuid() // Возвращает числовой идентификатор пользователя

process.cwd() // Возвращает текущий рабочий каталог

process.chdir() // Выполняет переход в другой каталог

process.exit() // Завершает программу (после запуска всех обработчиков)

Поскольку функции и методы, реализуемые интерпретатором Node, являются асинхронными, они не блокируют выполнение программы в ожидании завершения операций. Неблокирующий метод не может вернуть результат выполнения асинхронной операции. Если в программе потребуется получить результат или просто определить, когда завершится операция, необходимо определить функцию, которую интерпретатор Node сможет вызвать, когда результат будет доступен или когда операция завершится (или возникнет ошибка). В некоторых случаях (например, в вызове setTimeout() выше) достаточно просто передать методу функцию в виде аргумента, и Node вызовет ее в соответствующий момент времени. В других случаях можно воспользоваться инфраструктурой событий интерпретатора Node. Объекты, реализованные в интерпретаторе Node, которые генерируют события (их часто называют источниками (emitter) событий), определяют метод on() для регистрации обработчиков. Они принимают в первом аргументе тип события (строку) и функцию-обработчик во втором аргументе. Для различных типов событий функциям-обработчикам передаются различные аргументы, поэтому вам может потребоваться обратиться к документации по API, чтобы точно узнать, как писать свои обработчики:

emitter.on(name, f) // Регистрирует f для обработки события name,

                    // генерируемого объектом emitter

emitter.addListener(name, f) // То же самое: addListener() - синоним для оп()

emitter.once(name, f)        // Для обработчиков однократного срабатывания,

                             // затем f автоматически удаляется

emitter.listeners(name)      // Возвращает массив функций-обработчиков

emitter.removeListener(name, f) // Удаляет обработчик f

emitter.removeAHListeners(name) // Удаляет все обработчики события name

Объект process, представленный выше, является источником событий. Ниже приводится пример обработчиков некоторых его событий:

// Событие "exit" отправляется перед завершением работы Node,

process.on("exit", function() { console.log("Goodbye"); });

// Необработанные исключения генерируют события, если имеется хотя бы один

// зарегистрированный обработчик. В противном случае исключение

// заставляет интерпретатор Node вывести сообщение и завершить работу,

process.on("uncaughtException", function(e) { console.log(Exception, e); });

// Сигналы POSIX, такие как SIGINT, SIGHUP и SIGTERM, также генерируют события

process.on("SIGINT", function() { console.logCIgnored Ctrl-C"); });

Поскольку интерпретатор Node позволяет выполнять высокопроизводительные операции ввода/вывода, его прикладной интерфейс к потокам ввода/вывода является одним из наиболее часто используемых в программах. Потоки, открытые для чтения, генерируют события, когда появляются данные, готовые для чтения. В следующем примере предполагается, что s - это поток, открытый для чтения, созданный где-то в другом месте программы. Ниже будет показано, как создавать объекты потоков для файлов и сетевых сокетов:

// Входной поток

s.on("data", f); // При появлении данных передать их функции f() в аргументе

s.on("end", f); // событие "end" возникает по достижении конца файла,

              // когда данные больше не могут поступить

s.оп("еrror", f); // Если что-то не так, передаст исключение функции f()

s.readable // => true, если поток по-прежнему открыт для чтения

s.pause();// Приостановит отправку событий "data".

        // Например, чтобы уменьшить скорость выгрузки

s.resume();  // Возобновит отправку событий "data"

// Определяет кодировку, если обработчику события "data" данные должны

// передаваться в виде строк

s.setEncoding(enc); // Как декодировать байты: "utf8”, "ascii" или "base64"

Потоки, открытые для записи, не так тесно связаны с событиями, как потоки, открытые для чтения. Метод write() таких потоков используется для отправки данных, а метод end() - для закрытия потока, когда все данные будут записаны. Метод write() никогда не блокируется. Если интерпретатор Node окажется не в состоянии записать данные немедленно и во внутреннем буфере потока не окажется сводного места, метод write() вернет false. Чтобы узнать, когда интерпретатор вытолкнет буфер и данные фактически будут записаны, можно зарегистрировать обработчик события «drain»:

// Выходной поток s:

s.write(buffer);          // Запись двоичных данных

s.write(string, encoding) // Запись строковых данных.

                          // по умолчанию encoding = "utf-8"

s.end()                   // Закроет поток.

s.end(buffer);            // Запишет последнюю порцию данных и закроет поток.

s.end(str, encoding)      // Запишет последнюю строку и закроет поток

s.writeable;              // true, если поток по-прежнему открыт для записи

s.on("drain", f)          // f() будет вызываться при опустошении внутр. буфера

Как видно из примеров выше, потоки ввода/вывода, реализованные в интерпретаторе Node, могут работать и с двоичными, и с текстовыми данными. Текст передается с использованием простых строк JavaScript. Байты обрабатываются с помощью специфического для Node типа данных Buffer. Буферы в интерпретаторе Node являются объектами, подобными массивам, с фиксированной длиной, элементами которых могут быть только числа в диапазоне от 0 до 255. Программы, выполняющиеся под управлением Node, часто интерпретируют буферы как непрозрачные блоки данных, читая их из одного потока и записывая в другой. Тем не менее байты в буфере доступны как обычные элементы массива, а сами буферы имеют методы, позволяющие копировать байты из одного буфера в другой, получать срезы буфера, записывать строки в буфер с использованием заданной кодировки и декодировать буфер или его часть обратно в строку:

var bytes = new Buffer(256);           //Создать новый буфер на 256 байт

for (var i = 0; i < bytes.length; i++) //Цикл по индексам

bytes[i]=i                             //Установить каждый элемент в буфере

var end = bytes.slice(240,256)         //Получить срез буфера

end[0]                                 //=> 240: end[0] = bytes[240]

end[0]=0                               //Изменить элемент среза

bytes[240]                             //=> 0; буфер тоже изменится

var more=new Buffer(8);                //Создать новый отдельный буфер

end.copy(more, 0, 8, 16)               //Скопировать элементы 8-15 из end[] в more[]

more[0]                                //=> 248

// Буферы также позволяют выполнять преобразования двоичных данных в строки

// и обратно. Допустимые кодировки: "utf8", "ascii" и "base64".

// По умолчанию используется "utf8".

var buf = new Buffer("2пr", "utf8”); // Закодировать текст в байты, UTF-8

buf.length                           // => 3 символа занимают 4 байта

buf.toSt ring()                      // => "2яг": обратно в текст

buf = new Buffer(10);                // Новый буфер фиксированной длины

var len = buf.write("пr2", 4);       // Записать текст, начиная с 4-го байта

buf.toString(”utf8”,4, 4+len)        // => "пr2": декодировать диапазон байтов

Инструменты интерпретатора Node для работы с файлами и файловой системой находятся в модуле «fs»:

var fs = require("fs"); // Загрузит инструменты для работы с файловой системой

Для большинства своих методов этот модуль предоставляет синхронные версии. Любой метод, имя которого оканчивается на «Sync», является блокирующим методом, возвращающим значение и возбуждающим исключение. Методы для работы с файловой системой, имена которых не оканчиваются на «Sync», являются неблокирующими - они возвращают результаты или ошибки посредством передаваемых им функций обратного вызова. Следующий фрагмент демонстрирует, как реализуется чтение текстового файла с использованием блокирующего метода и как реализуется чтение двоичного файла с использованием неблокирующего метода:

// Синхронное чтение файла. Следует передать имя кодировки,

// чтобы в результате получить текст, а не двоичные байты,

var text = fs.readFileSync("config.json", "utf8");

// Асинхронное чтение двоичного файла. Следует передать функцию, чтобы получить данные

fs.readFile("image.png", function(err, buffer) {

  if (err) throw err; // Если что-то пошло не так

    process(buffer); // Содержимое файла в параметре buffer

});

Для записи в файл существуют аналогичные функции writeFile() и writeFileSync():

fs.writeFile("config.json", JSON.stringify(userprefs));

Функции, представленные выше, интерпретируют содержимое файла как единственную строку или объект Buffer. Кроме того, для чтения и записи файлов интерпретатор Node определяет также API потоков ввода/вывода. Функция ниже копирует содержимое одного файла в другой:

// Копирование файлов с применением API потоков ввода/вывода.

// Чтобы определить момент окончания копирования,

// ей нужно передать функцию обратного вызова

function fileCopy(filename1, filename2, done) {

  var input = fs.createReadStream(filenamel); // Входной поток

  var output = fs.createWriteStream(filename2); // Выходной поток

  input.on("data", function(d) { output.write(d); }); // Копировать

  input.on("error", function(err) { throw err; });    // Сообщить об ошибке

  input.on("end", function() { // По исчерпании входных данных

    output.end(); // закрыть выходной поток

    if (done) done(); // И известить вызвавшую программу

  });

}

Модуль «fs» включает также несколько методов, возвращающих список содержимого каталогов, атрибуты файлов и т. д. Следующая ниже программа для интерпретатора Node использует синхронные методы для получения списка содержимого каталога, а также для определения размеров файлов и времени последнего их изменения:

#! /usr/local/bin/node

var fs = require("fs"), path = require("path"); // Загрузить модули

var dir = process.cwd();                        // Текущий каталог

var files = fs.readdirSync(dir);                // Или из команд. строки

var files = fs.readdirSync(dir);                // Прочитать содер. кат-га

process.stdout.write("Name Size Date ");     // Вывести заголовок

files.forEach(function(filename) {              // Для каждого файла

  var fullname = path.join(dir,filename);       // Объед. имя и каталог

  var stats = fs.statSync(fullname);            // Получить атрибуты файла

  if (stats.isDirectory()) filename += "/";     // Пометить подкаталоги

  process.stdout.write(filename +               // Вывести имя файла

        " " + stats.size + " " +              // размер файла

        stats.mtime + ” ");                    // и время поcл. изменения

});

Обратите внимание на комментарий #! в первой строке, в примере выше. Это специальный комментарий, используемый в Unix, чтобы объявить сценарий, следующий далее, исполняемым, определив файл интерпретатора, который должен его выполнить. Интерпретатор Node игнорирует подобные строки комментариев, когда они находятся в первых строках файлов.

Модуль «net» определяет API для организации взаимодействий по протоколу TCP. (Для выполнения сетевых взаимодействий на основе дейтаграмм можно использовать модуль «dgram».) Ниже приводится пример очень простого сетевого TCP-сервера, реализованного на основе особенностей Node:

// Простой эхо-сервер, реализованный на основе особенностей Node:

// он ожидает соединений на порту с номером 2000 и отправляет обратно клиенту

// все данные, которые получит от него.

var net = require('net');

var server = net.createServer();

server.listen(2000, function() {console.log("Прослушивается порт 2000");});

server.on("connection", function(stream) {

  console.log("Принято соединение от", stream.remoteAddress);

  stream.on("data", function(data) { stream.write(data); });

  stream.on("end", function(data) { console.log("Соединение закрыто"); });

});

В дополнение к базовому модулю «net» в интерпретаторе Node имеется встроенная поддержка протокола HTTP в виде модуля «http». Особенности его использования демонстрируют примеры, следующие ниже.