В предыдущих главах подробно рассматривался базовый язык JavaScript, и вскоре мы перейдем ко второй части книги, в которой рассказывается о том, как JavaScript встраивается в веб-браузеры, и описывается обширнейший API клиентского JavaScript. JavaScript – это язык программирования для Веб, и большая часть программного кода на языке JavaScript написана для выполнения в веб-браузерах. Однако JavaScript – это быстрый и универсальный язык с широкими возможностями, и нет никаких причин, по которым JavaScript не мог бы использоваться для решения других задач программирования. Поэтому, прежде чем перейти к знакомству с клиентским JavaScript, мы коротко рассмотрим две другие реализации JavaScript.
Название главы говорит, что она посвящена «серверному» JavaScript, а обычно для создания серверов и для управления ими используются интерпретаторы Node и Rhino. Но под словом «серверный» можно также понимать «все, что за пределами веб-браузера». Программы, выполняемые под управлением Rhino, способны создавать графические интерфейсы пользователя, используя фреймворк Swing для языка Java. А интерпретатор Node может выполнять программы на языке JavaScript, способные манипулировать файлами подобно тому, как это делают сценарии командной оболочки.
Цель этой короткой главы состоит в том, чтобы осветить некоторые направления за пределами веб-браузеров, где может использоваться язык JavaScript. Здесь не предпринимается попытка в полном объеме охватить интерпретатор Rhino или Node, а обсуждаемые здесь прикладные интерфейсы не описываются в справочном разделе. Очевидно, что в одной главе невозможно сколько-нибудь полно описать платформу Java или POSIX API, поэтому раздел об интерпретаторе Rhino предполагает, что читатели имеют некоторое знакомство с Java, а раздел об интерпретаторе Node предполагает знакомство с низкоуровневыми прикладными интерфейсами Unix.
Rhino – это интерпретатор JavaScript, написанный на языке Java, цель которого – упростить возможность создания программ на языке JavaScript, которые могли бы использовать мощь платформы Java. Интерпретатор Rhino автоматически выполняет преобразование простых типов JavaScript в простые типы Java и наоборот, благодаря чему сценарии на языке JavaScript могут читать и изменять свойства и вызывать методы объектов на языке Java.
Интерпретатор Rhino – свободное программное обеспечение, разработанное проектом Mozilla. Вы можете загрузить копию по адресу http://www.mozilla.org/rhino/. Версия Rhino 1.7r3 реализует ECMAScript 3 и ряд расширений языка, описанных в главе 11. Rhino – достаточно зрелый программный продукт, и новые его версии появляются не так часто. На момент написания этих строк в репозитории с исходными текстами была доступна предварительная версия 1.7rЗ, включающая частичную реализацию стандарта ECMAScript 5.
Rhino распространяется в виде JAR-архива. Запускается он командой, как показано ниже:
java -jar rhino1_7R2/js.jar program.js
Если опустить параметр program.js, интерпретатор Rhino запустится в интерактивном режиме, который позволит вам опробовать простенькие программы и однострочные примеры.
Rhino определяет несколько важных глобальных функций, не являющихся частью базового языка JavaScript:
// Специфические глобальные функции, определяемые интерпретатором: // Введите help() в строке приглашения rhino, // чтобы получить дополнительную информацию print(x); // Глобальная функция вывода в консоль version(170); // Требует от Rhino задействовать особенности версии 1.7 load(filename....); // Загружает и выполняет один или более файлов // с программами на языке JavaScript readFile(file); // Читает текстовый файл и возвращает его содержимое в виде строки readUrl(url); // Читает текстовое содержимое по адресу URL и возвращает // в виде строки spawn(f); // Вызывает f() или загружает и выполняет файл f // в новом потоке выполнения runCommand(cmd, // Запускает системную команду с нулем или более [args...]); // аргументов командной строки quit() // Завершает работу Rhino
Обратите внимание на функцию print()
: мы будем использовать ее в этом разделе
вместо console.log()
. Интерпретатор Rhino представляет пакеты и классы Java
как объекты JavaScript:
// Глобальная переменная Packages определяет корень иерархии пакетов Java Packages.any.package.name // Любой пакет из CLASSPATH java.lang // Глобальная переменная Java - краткая ссылка на Packages.Java, javax.swing // а javax - краткая ссылка на Packages.javax // Классы: доступны как свойства пакетов var System = java.lang.System; var JFrame = javax.swing.JFrame;
Поскольку пакеты и классы представлены как объекты JavaScript, их можно присваивать переменным, чтобы давать им более короткие имена. Но при желании их можно импортировать более формальным способом:
var ArrayList = java.util.ArrayList; // Создать краткое имя для класса importClass(java.util.HashMap); // Аналог: var HashMap=java.util.HashMap // Импорт пакета (отложенный) с помощью importPackage(). // Не следует импортировать java.lang: слишком много конфликтов имен // с глобальными переменными JavaScript. importPackage(Java.util); importPackage(Java.net);
// Другой прием: передать произвольное количество классов и пакетов функции // JavaImporter() и использовать возвращаемый объект с инструкцией with var guipkgs = JavaImporter(java.awt, Java.awt.event, Packages.javax.swing); with (guipkgs) { /* Здесь определены такие классы, как Font, ActionListener и JFrame */ }
С помощью ключевого слова new
можно создавать экземпляры классов языка
Java, как если бы это были классы JavaScript:
// Объекты: создание из классов Java с помощью ключевого слова new var f = new java.io.File("/tmp/test"); // Этот объект используется ниже var out = new Java.io.FileWriter(f);
Интерпретатор Rhino позволяет использовать JavaScript-оператор instanceof
для
работы с объектами и классами на языке Java:
f instanceof java.io.File // => true out instanceof java.io.Reader // => false: объект Writer, а не Reader out instanceof java.io.Closeable // => true: Writer реализует Closeable
Как видно из приведенных выше примеров создания объектов экземпляров, интерпретатор Rhino позволяет передавать значения конструкторам Java и присваивать возвращаемые ими значения переменным JavaScript. (Обратите внимание на неявное преобразование типов, выполняемое интерпретатором Rhino в этом примере: JavaScript-строка «/tmp/test» автоматически преобразуется в Java значение типа java.lang.String) Методы Java очень похожи на конструкторы Java, и Rhino позволяет программам на языке JavaScript вызывать методы на языке Java:
// Статические методы на языке Java действуют подобно функциям JavaScript java.lang.System.getProperty("java.version") // Вернет версию Java var isDigit = java.lang.Character.isDigit; // Присвоит статич. метод переменной isDigit("٢") // => true: Арабская цифра 2 // Вызвать методы экземпляра созданных выше объектов f и out на языке Java out.write("Hello World\n"); out.close(); //out.close(); var len = f.length();
Кроме того, Rhino позволяет получать и изменять значения статических полей Java-классов и полей экземпляров Java-объектов из программы на языке JavaScript. В классах на языке Java часто не определяют общедоступные поля, отдавая предпочтение методам доступа. Если в Java-классе определены методы доступа, Rhino обеспечивает доступ к ним, как к свойствам объекта на языке JavaScript:
// Прочитать значение статического поля Java-класса var stdout = java.lang.System.out; // Rhino отображает методы доступа в отдельные свойства JavaScript f.name // => "/tmp/test": вызовет f.getName() f.directory // => false: вызовет f.isDirectory()
В языке Java имеется возможность создавать перегруженные версии методов, имеющие одинаковые имена, но разные сигнатуры. Обычно интерпретатор Rhino способен определить, какую версию метода следует вызвать, опираясь на типы аргументов, которые передаются программой на языке JavaScript. Однако иногда бывает необходимо явно идентифицировать метод по имени и сигнатуре:
//Предположим, что Java-объект o имеет метод f, который принимает целое // или вещественное число. В JavaScript необходимо будет явно указать сигнатуру: o['f(int)'](3); // Вызвать метод, принимающий целое число o['f(float)'](Math.PI); // Вызвать метод, принимающий вещественное число
Для итераций по методам, полям и свойствам Java-классов можно использовать
цикл for/in
importClass(java.lang.System); for(var m in System) print(m); // Выведет статические члены java.lang.System for(m in f) print(m); // Выведет члены экземпляра java.io.File // Обратите внимание, что таким способом нельзя перечислить классы в пакете for (c in java.lang) print(c); // Этот прием не сработает
Rhino позволяет программам на языке JavaScript получать и изменять значения
элементов Java-массивов, как если бы они были JavaScript-массивами. Конечно,
Java-массивы отличаются от JavaScript-массивов: они имеют фиксированную
длину, их элементы имеют определенный тип, и они не имеют
JavaScript-методов, таких как slice()
. В JavaScript не существует
синтаксических конструкций, которые могли бы использоваться интерпретатором Rhino
для создания Java-массивов в программах java.lang.reflect.Array:
// Создать массив из 10 строк и массив из 128 байтов var words = java.lang.reflect.Array.newInstance(java.lang.String, 10); var bytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 128); // После создания с массивами можно работать как с JavaScript-массивами: for(var i = 0; i < bytes.length; i++) bytes[i] = i;
Программирование на языке Java часто связано с реализацией интерфейсов. Чаще всего с этой необходимостью приходится сталкиваться при разработке графических интерфейсов, когда каждый обработчик события должен реализовать интерфейс приемника событий. Следующие примеры демонстрируют, как это сделать:
// Интерфейсы: Реализация интерфейсов выглядит следующим образом: var handler = new java.awt.event.FocusListener({ focusGained: function(e) { print("got focus"); }, focusLost: function(e) { print("lost focus"); } }); // Аналогично выполняется расширение абстрактных классов var handler = new java.awt.event.WindowAdapter({ windowClosing: function(e) { java.lang.System.exit(0); } }); // Когда интерфейс определяет единственный метод, можно использовать простую функцию button.addActionListener(function(e) { print("button clicked"); }); // Если все методы интерфейса или абстрактного класса имеют одну и ту же сигнатуру, // в качестве реализации можно использовать единственную функцию, // a Rhino будет передавать ей имя метода в последнем аргументе frame.addWindowListener(function(e, name) { if (name === "windowClosing") java.lang.System.exit(0); }); // Если необходимо определить объект, реализующий несколько интерфейсов, // можно использовать класс JavaAdapter: var o = new JavaAdapter(java.awt.event.ActionListener, java.lang.Runnable, { run: function() {}, // Реализует интерфейс Runnable actionPerformed: function(e) {} // Реализует интерфейс ActionListener });
Когда Java-метод возбуждает исключение, интерпретатор Rhino продолжает его
распространение как JavaScript-исключения. Получить оригинальный Java-объект
java.lang.Exception можно через свойство javaException
JavaScript-объекта Error
:
try { java.lang.System.getProperty(null); // null - недопустимый } catch(e) { // e - JavaScript-исключение print(e.javaException); // это обертка для java.lang.NullPointerException }
Здесь необходимо сделать последнее замечание по поводу преобразования типов
в Rhino. Интерпретатор Rhino автоматически преобразует простые числа,
логические значения и null
. Java-тип char
интерпретируется в языке JavaScript как
число, так как в языке JavaScript отсутствует символьный тип. JavaScript-строки
автоматически преобразуются в Java-строки, но (и это может быть камнем
преткновения) Java-строки остаются объектами java.lang.String и не преобразуются
обратно в JavaScript-строки. Взгляните на следующую строку из примера,
приводившегося ранее:
var version = java.lang.System.getProperty("java.version");
После выполнения этой инструкции переменная version
будет хранить объект
java.lang.String. Он обычно ведет себя как JavaScript-строка, но существуют
важные отличия. Во-первых, Java-строка вместо свойства length имеет метод length()
.
Во-вторых, оператор typeof
возвращает тип «object» для Java-строк. Java-строку
нельзя преобразовать в JavaScript-строку вызовом метода toString()
, потому что
все Java-объекты имеют собственные методы toString()
, возвращающие
экземпляры java.lang.String. Чтобы преобразовать Java-значение в строку, его нужно
передать JavaScript-функции String()
:
var version = String(java.lang.System.getProperty("java.version"));
В примере 12.1 приводится простое приложение для интерпретатора Rhino, демонстрирующее большую часть возможностей и приемов, описанных выше. Пример использует пакет javax.swing со средствами построения графических интерфейсов, пакет jjava.net с инструментами организации сетевых взаимодействий, пакет java.io потокового ввода/вывода и инструменты языка Java многопоточного выполнения для реализации простого приложения менеджера загрузки, которое загружает файлы по адресам URL и отображает ход выполнения загрузки. На рис. 12.1 показано, как выглядит окно приложения в процессе загрузки двух файлов.
Рис. 12.1. Графический интерфейс, созданный с помощью Rhino
Пример 12.1. Приложение менеджера загрузки для Rhino
/* * Приложение менеджера загрузки с простым графическим интерфейсом, * построенным средствами языка Java */ // Импортировать графические компоненты из библиотеки Swing // и несколько других классов importPackage(javax.swing); importClass(javax.swing.border.EmptyBorder); importClass(java.awt.event.ActionListener); importClass(java.net.URL); importClass(java.io.FileOutputStream); importClass(java.lang.Thread); // Создать графические элементы управления var frame = new JFrame("Rhino URL Fetcher"); // Окно приложения var urlfield = new JTextField(30); // Поле ввода URL var button = new JButton("Download"); // Кнопка запуска загрузки var filechooser = new JFileChooser(); // Диалог выбора файла var row = Box.createHorizontalBox(); // Контейнер для поля и кнопки var col = Box.createVerticalBox(); // Для строки и индикатора хода // выполнения операции var padding = new EmptyBorder(3,3,3,3); // Отступы для строк // Объединить все компоненты и отобразить графический интерфейс row.add(urlfield); // Поместить поле ввода в строку row.add(button); // Поместить кнопку в строку col.add(row); // Поместить строку в колонку frame.add(col); //Поместить колонку во фрейм row.setBorder(padding); // Добавить отступы вокруг строки frame.pack(); // Определить минимальный размер frame.visible = true; // Вывести окно // Эта функция вызывается, когда в окне что-то происходит. frame.addWindowListener(function(e, name) { // Если пользователь закрыл окно, завершить приложение. if (name === "windowClosing") // Rhino добавляет аргумент name java.lang.System.exit(0); }); // Эта функция вызывается, когда пользователь щелкает на кнопке button.addActionListener(function() { try { // Создать объект java.net.URL для представления URL источника. // (Автоматически будет проверена корректность ввода пользователя) var url = new URL(urlfield.text); // Предложить пользователю выбрать файл для сохранения содержимого URL var response = filechooser.showSaveDialog(frame); // Завершить, если пользователь щелкнул на кнопке Cancel if (response != JFileChooser.APPROVE_OPTION) return; // Иначе получить объект java.io.File, представляющий файл назначения var file = filechooser.getSelectedFile(); // Запустить новый поток выполнения для загрузки url new java.lang.Thread(function() { download(url,file); }).start(); } catch(e) { // Вывести диалог, если что-то пошло не так JOptionPane.showMessageDialog(frame, e.message, "Exception", JOptionPane.ERROR_MESSAGE); } }); // Использует java.net.URL и др. для загрузки содержимого URL и использует java.io.File // и др. для сохранения этого содержимого в файле. Отображает ход выполнения загрузки // в компоненте JProgressBar. Эта функция вызывается в новом потоке выполнения. function download(url, file) { try { // Каждый раз, когда запускается загрузка очередного файла, // необходимо добавить в окно новую строку для отображения URL, // имени файла и индикатора хода выполнения операции var row = Box.createHorizontalBox(); // Создать строку row.setBorder(padding); // Добавить отступы var label = url.toString() + ": "; //: "; // Отобразить URL row.add(new JLabel(label)); // в компоненте JLabel var bar = new JProgressBar(0, 100); // Создать полосу индикатора bar.stringPainted = true; // Отобразить имя файла bar.string = file.toString(); // в полосе индикатора row.add(bar); // Добавить индикатор в строку col.add(row); // Добавить строку в колонку frame.pack(); // Изменить размер окна // Здесь еще не известен размер загружаемого файла, // индикатор просто воспроизводит анимационный эффект bar.indeterminate = true; // Установить соединение с сервером и получить размер загружаемого // файла, если это возможно var conn = url.openConnection(); // Получить java.net.URLConnection conn.connect(); // Подключиться и ждать заголовки var len = conn.contentLength; // Проверить, получена ли длина файла if (len) { // Если длина известна, тогда bar.maximum = len; // настроить индикатор на вывод bar.indeterminate = false; // процента выполнения задания } // Получить потоки ввода и вывода var input = conn.inputStream; // Прочитать байты с сервера var output = new FileOutputStream(file); // Записать в файл // Создать входной буфер в виде массива размером 4 Кбайта var buffer = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 4096 var num; while((num=input.read(buffer)) != -1) { // Читать до признака EOF output.write(buffer, 0, num); // Записать байты в файл bar.value += num; // Обновить индикатор } output.close(); // Закрыть потоки по завершении input.close(); } catch(e) { // Если что-то пошло не так, вывести ошибку в индикаторе if (bar) { bar.indeterminate = false; // Остановить анимацию bar.string = e.toString(); // Заменить имя файла сообщением } } }
Node – это быстрый интерпретатор JavaScript, написанный на языке C++, включающий средства доступа к низкоуровневым интерфейсам Unix для работы с процессами, файлами, сетевыми сокетами и так далее, а также к клиентским и серверным интерфейсам реализации протокола HTTP. За исключением нескольких синхронных методов, имеющих специальные имена, все остальные инструменты интерпретатора Node доступа к интерфейсам Unix являются асинхронными, и по умолчанию программы, выполняемые под управлением Node, никогда не блокируются, что обеспечивает им хорошую масштабируемость и позволяет эффективно справляться с высокой нагрузкой. Поскольку прикладные программные интерфейсы являются асинхронными, интерпретатор Node опирается на использование обработчиков событий, которые часто реализуются с использованием вложенных функций и замыканий¹.
Этот раздел освещает некоторые наиболее важные инструменты и события, имеющиеся в составе Node, но это описание ни в коем случае нельзя считать полным. Полное описание Node можно найти в электронной документации по адресу http://nodejs.org/api/.
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. Однако
в дополнение к этому он также поддерживает клиентские функции для работы
с таймером set setTimeout(), setInterval(), clearTimeout(), and clearInterval()
:
// Вывести приветствие через одну секунду. setTimeout(function() { console.log("Hello World"); }, 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) // Ditto: addListener() - синоним для on() emitter.once(name, f) // Для обработчиков однократного срабатывания, // затем f автоматически удаляется emitter.listeners(name) // Возвращает массив функций-обработчиков emitter. removeListener(name, f) // Удаляет обработчик f emitter. removeAllListeners(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.logflgnored Ctrl-C"); });
Поскольку интерпретатор Node позволяет выполнять высокопроизводительные
операции ввода/вывода, его прикладной интерфейс к потокам ввода/вывода
является одним из наиболее часто используемых в программах. Потоки, открытые
для чтения, генерируют события, когда появляются данные, готовые для чтения.
В следующем примере предполагается, что s
– это поток, открытый для чтения,
созданный где-то в другом месте программы. Ниже будет показано, как
создавать объекты потоков для файлов и сетевых сокетов:
// Входной поток s: s.on("data", f); // При появлении данных передать их функции f() в аргументе s.on("end", f); // событие "end" возникает по достижении конца файла, // когда данные больше не могут поступить. s.on("error", 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] is 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.toString() // => "2πr": обратно в текст buf = new Buffer(10); // Новый буфер фиксированной длины var len = buf.write("πr²", 4); // Записать текст, начиная с 4-го байта buf.toString("utf8",4, 4+len) // => "πr²": декодировать диапазон байтов
Инструменты интерпретатора 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(filename1); // Входной поток 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(); // Текущий каталог if (process.argv.length > 2) dir = process.argv[2]; // Или из командной строки var files = fs.readdirSync(dir); // Прочитать содержимое каталога process.stdout.write("Name\tSize\tDate\n"); // Вывести заголовок files.forEach(function(filename) { // Для каждого файла var fullname = path.join(dir,filename); // Объединить имя и каталог var stats = fs.statSync(fullname); // Получить атрибуты файла if (stats.isDirectory()) filename += "/"; // Пометить подкаталоги process.stdout.write(filename + "\t" + // Вывести имя файла stats.size + "\t" + // размер файла stats.mtime + "\n"); // и время последнего изменения });
Обратите внимание на комментарий, начинающийся с символов
#!
в первой строке последнего примера. Это
специальный комментарий, используемый в 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("Accepting connection from", stream.remoteAddress); stream.on("data", function(data) { stream.write(data); }); stream.on("end", function(data) { console.log("Соединение закрыто"); }); });
В дополнение к базовому модулю «net» в интерпретаторе Node имеется встроенная поддержка протокола HTTP в виде модуля «http». Особенности его использования демонстрируют примеры, приведенные ниже.
В примере 12.2 приводится реализация простого HTTP-сервера, основанная на особенностях интерпретатора Node. Она обслуживает файлы в текущем каталоге и дополнительно реализует два адреса URL специального назначения, которые обслуживаются особым образом. В этой реализации используется модуль «http», входящий в состав интерпретатора Node, и применяются API доступа к файлам и потокам ввода/вывода, демонстрировавшиеся выше. В примере 18.17 в главе 18 демонстрируется аналогичный специализированный HTTP-сервер.
Пример 12.2. HTTP-сервер, основанный на особенностях 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("Accepting connection from", stream.remoteAddress); stream.on("data", function(data) { stream.write(data); }); stream.on("end", function(data) { console.log("Соединение закрыто"); }); });// Простой NodeJS HTTP-сервер, обслуживающий файлы в текущем каталоге // и реализующий два специальных адреса URL для нужд тестирования. // Подключение к серверу выполняется по адресу http://localhost:8000 // или http://127.0-0.1:8000 // Сначала необходимо загрузить используемые модули var http = require('http'); // API HTTP-сервера var fs = require('fs'); // Для работы с локальными файлами var server = new http.Server(); // Создать новый HTTP-сервер server.listen(8000); // Прослушивать порт 8000. // Для регистрации обработчиков событий в Node используется метод "оп()". // Когда сервер получает новый запрос, для его обработки вызывается функция, server.on("request", function (request, response) { // Выполнить разбор адреса URL ar url = require('игГ).parse(request.url); // Специальный адрес URL, который вынуждает сервер выполнить задержку перед ответом. // Это может быть полезно для имитации работы с медленным сетевым подключением, if (url.pathname === "/test/delay") { // Величина задержки определяется из строки запроса // или устанавливается равной 2000 миллисекунд var delay = parseInt(url.query) || 2000; // Установить код состояния и заголовки ответа response.writeHead(200, {"Content-Type": "text/plain; charset=UTF-8"}); // Начать отправку ответа немедленно esponse.write("Задержка на " + delay + " миллисекунд..."); // А затем завершить другой функцией, которая будет вызвана позже. setTimeout(function() { esponse.write("готово."); response.end(); }, delay); } // Если запрошен адрес "/test/mirror", отправить запрос обратно целиком. // Удобно, когда необходимо увидеть тело и заголовки запроса, lse if (url.pathname === "/test/miггог") { // Код состояния и заголовки ответа response.writeHead(200, {"Content-Type": "text/plain; charset=UTF-8"}); // Вставить в ответ тело запроса response.write(request.method + " " + request.url + " HTTP/" + request.httpVersion + "\r\n"); // И заголовки запроса for(var h in request.headers) { response.write(h + ": " + request.headers[h] + "\r\n"); } response.write("\r\n"); // За заголовками следует дополнительная пустая строка // Завершение отправки ответа выполняется следующими функциями-обработчиками: // Если в chunk передается тело запроса, вставить его в ответ. request.on("data", function(chunk) { response.write(chunk); }); // Когда достигнут конец запроса, ответ также завершается. request.on("end", function(chunk) { response.end(); }); } // Иначе обслужить файл из локального каталога, else { // Получить имя локального файла и определить тип его содержимого по расширению. var filename = url.pathname.substring(1); // удалить начальный / var type; switch(filename.substring(filename.lastIndexOf(".")+1)) { // расшир. case "html": case "htm": type = "text/html; charset=UTF-8"; break; case "js": type = "application/javascript; charset=UTF-8"; break; case "css": type = "text/css; charset=UTF-8"; break; case "txt" : type = "text/plain; charset=UTF-8"; break; case "manifest": type = "text/cache-manifest; charset=UTF-8"; break; default: type = "application/octet-stream"; break; } // Прочитать файл в асинхронном режиме и передать его содержимое единым блоком // в функцию обратного вызова. Для очень больших файлов лучше было бы // использовать API потоков ввода/вывода с функцией fs.createReadStream(). fs.readFile(filename, function(err, content) { if (err) { //f (err) { И если по каким-то причинам невозможно прочитать файл response.writeHead(404, { // Отправить 404 Not Found "Content-Type": "text/plain; charset=UTF-8"}); response.write(err.message); // Тело сообщения об ошибке response.end(); // Завершить отправку } else { // Иначе, если файл успешно прочитан. response.writeHead(200, // Установить код состояния и тип MIME {"Content-Type": type}); response.write(content); // Отправить содержимое файла response.end(); // И завершить отправку } }); } });
В примере 12.3 определяется несколько вспомогательных клиентских функций, использующих модуль «http», позволяющих выполнять GET- и POST-запросы протокола HTTP. Пример оформлен как модуль «httputils», который можно использовать в собственных программах, например:
var httputils = require("./httputils"); // Отметьте отсутствие расширения ".js"
httputils.get(url, function(status, headers, body) { console.log(body); });
При выполнении программного кода модуля функция require()
не использует
обычную функцию eval()
. Модули выполняются в специальном окружении,
чтобы они не могли определять глобальные переменные или как-то иначе изменять
глобальное пространство имен. Это специализированное окружение для
выполнения модулей всегда включает глобальный объект с именем exports
. Модули
экспортируют свои API, определяя свойства этого объекта¹.
Пример 12.3. Модуль «httputils» для интерпретатора Node
// Модуль "httputils" для интерпретатора Node. // // Выполняет асинхронный HTTP GET-запрос для указанного URL и передает // код состояния HTTP, заголовки и тело ответа указанной функции обратного // вызова. Обратите внимание, как этот метод экспортируется через объект exports. exports.get = function(url, callback) { // Разобрать URL и получить необходимые фрагменты rl = require('игГ).parse(url); var hostname = url.hostname, port = url.port || 80; var path = url.pathname, query = url.query; if (query) path += "?" + query; // Выполняем простой GET-запрос var client = require("http").createClient(port, hostname); var request = client.request("GET", path, { "Host": hostname // Заголовки запроса }); request.end(); // Функция обработки ответа после начала его получения request.on("response", function(response) { // Указать кодировку, чтобы тело возвращалось как строка, а не байты response.setEncoding("utf8"); // Для сохранения тела ответа по мере получения var body = "" response.on("data", function(chunk) { body += chunk; }); // По окончании тела ответа вызвать функцию обратного вызова response.on("end", function() { if (callback) callback(response.statusCode, response.headers, body); }); }); }; // Простой HTTP POST-запрос с данными в теле запроса exports.post = function(url, data, callback) { // Разобрать URL и получить необходимые фрагменты url = require('url').parse(url); var hostname = url.hostname, port = url.port || 80; var path = url.pathname, query = url.query; if (query) path += "?" + query; // Определить тип данных, отправляемых в теле запроса var type; if (data == null) data = ""; if (data instanceof Buffer) // Двоичные данные type = "application/octet-stream"; else if (typeof data === "string") // Строковые данные type = "text/plain; charset=UTF-8"; else if (typeof data === "object") { // Пары имя/значение data = require("querystring").stringify(data); type = "application/x-www-form-urlencoded"; } // Выполнить POST-запрос и отправить тело запроса var client = require("http").createClient(port, hostname); var request = client.request("POST", path, { "Host": hostname, "Content-Type": type }); request.write(data); // Отправить тело запроса request.end(); request.on("response", function(response) { // Обработать ответ response.setEncoding("utf8"); // Предполагается текст var body = "" // Для хранения тела ответа response.on("data", function(chunk) { body += chunk; }); response.on("end", function() { // По завершении вызвать обработчик if (callback) callback(response.statusCode, response.headers, body); }); }); };