По окончании файла наступит событие 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 blog.soshace.com


