По окончании файла наступит событие end
, в обработчике которого мы завершим ответ вызовом res.end
. Таким образом, будет закрыто исходящее соединение, потому что файл полностью отослан. Получившийся код является весьма универсальным:
var http = require('http'); var fs = require('fs'); new http.Server(function(req, res) { // res instanceof http.ServerResponse < stream.Writable if (req.url == '/big.html') { var file = new fs.ReadStream('big.html'); sendFile(file, res); } }).listen(3000); function sendFile(file, res) { file.on('readable', write); function write() { var fileContent = file.read(); // read if (fileContent && !res.write(fileContent)) { // send file.removeListener('readable', write); res.once('drain', function() { //wait file.on('readable', write); write(); }); } } file.on('end', function() { res.end(); }); }
Он реализует достаточно общий алгоритм отправки данных их одного потока в другой, используя самые стандартные методы потоков readable и writable. Об этом, конечно же, подумали и разработчики Node.js и добавили его оптимизированную реализацию в стандартную библиотеку потоков.
Соответствующий метод называется pipe
. Посмотрим на пример:
var http = require('http'); var fs = require('fs'); new http.Server(function(req, res) { // res instanceof http.ServerResponse < stream.Writable if (req.url == '/big.html') { var file = new fs.ReadStream('big.html'); sendFile(file, res); } }).listen(3000); function sendFile(file, res) { file.pipe(res); }
Он есть у всех readable потоков и работает так: readable.pipe
(куда писать, destination). Кроме того, что это всего лишь одна строка, то есть еще один бонус, например, можно один и тот же входной поток pipe
(“пайпить”) в несколько выходных:
function sendFile(file, res) { file.pipe(res); file.pipe(res); }
Например, кроме ответа клиенту будем выводить его еще стандартный вывод процесса:
file.pipe(res); file.pipe(process.stdout);
Итак, запускаем. Вывелось одновременно и в браузере, и в console
. Готов ли этот замечательный код к промышленной эксплуатации? Есть ли еще какие-то нюансы, которые нужно учесть?
Первым делом в глаза должно броситься отсутствие работы с ошибками. Если вдруг файл не найден или что-то с ним еще не так, тогда упадет весь сервер. Это не то, что нам нужно. Поэтому добавим, например, такой обработчик
function sendFile(file, res) { file.pipe(res); file.on('error', function(err) { res.statusCode = 500; res.end("Server Error"); console.error(err); }); }
Теперь мы немножко ближе к реальной жизни, и в ряде руководств такой код выдается вполне нормальный, но на самом деле это не так. Ставить такой код на живой сервер ни в коем случае нельзя. В чем же дело? Для того, чтобы продемонстрировать проблему, добавим дополнительные обработчики на события open
и close
для файла.
function sendFile(file, res) { file.pipe(res); file.on('error', function(err) { res.statusCode = 500; res.end("Server Error"); console.error(err); }); file .on('open',function() { console.log("open"); }) .on('close', function() { console.log("close"); }); }
Запускаем. Обновляю страницу. Обновляем несколько раз и смотрим в console
. Заметьте, файл перезагружается и совершенно нормально то, что файл открывается, потом он целиком отдается и закрывается.
А теперь откроем console
и запустим утилиту curl
, которая будет скачивать вот этот url
:
http://localhost:3000/big.html
с ограничением скорости 1кб/сек:
curl --limit-rate 1k http://localhost:3000/big.html
Если вы работаете под Windows, то эту утилиту можно легко найти и установить. Запускаем. Открывается файл и начинается получение. С виду все хорошо.
Жмем Ctrl+С
, прекращаю загрузку. Обратите внимание, никакого close
нету. Давайте еще раз. Получается, что если клиент открыл соединение, но закрыл его до того, как загрузка файла была завершена, то файл останется подвисшим.
А если файл остался открытым, то, во-первых, все ассоциированные с ним структуры тоже остались в памяти, во-вторых, операционные системы имеют лимит на количество одновременно открытых файлов, а в-третьих, вместе с файлом навечно зависает в памяти и соответствующий объект потока. А вместе с ним и все замыкание, в котором он находится.
Чтобы избежать этой проблемы и ее последствий, достаточно всего лишь отловить момент, когда соединение закрыто, и при этом удостовериться, что файл тоже будет закрыт.
Событие, которое нас интересует, называется res.on('close')
. Это событие отсутствует в обычном stream.Writeable
, то есть, это именно расширение стандартного интерфейса потоков. Также, как у файлов, есть close
, так и у объекта ответа ServerResponse
тоже есть close
. Но смысл второго close
сильно отличается от смысла первого, описанного выше. Это очень важно, потому что на файловом потоке close
– это нормальное завершение (файл закрывается всегда в конце), а для объекта ответа close
– это сигнал, что соединение было оборвано. При нормальном завершении происходит не close
, а finish
. Итак, если соединение было оборвано, то нам нужно закрыть файл и освободить его ресурсы, поскольку файл нам больше передавать некому. Для этого мы вызываем метод потоков file.destroy
:
res.on('close', function() { file.destroy(); });
Теперь все будет хорошо. Давайте еще раз проверим. Запускаем. Теперь наш код можно пускать на живой сервер.
Код урока вы сможете найти в нашем репозитории.
Материалы для статьи взяты из следующего скринкаста.
We are looking forward to meeting you on our website soshace.com