26Oct

lesson21pt2

По окончании файла наступит событие 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();  
});

Теперь все будет хорошо. Давайте еще раз проверим. Запускаем. Теперь наш код можно пускать на живой сервер.

Код урока вы сможете найти в нашем репозитории.

1140_pyramids_and_sphinx_of_egypt_556884029e546

Материалы для статьи взяты из следующего скринкаста.

We are looking forward to meeting you on our website soshace.com

Доклад. Agile (вводная часть). Scrum

Гибкая методология разработки (Agile) описывает набор принципов для разработки программного обеспечения в соответствии с которым требования и решения развиваются за счет совместных усилий самоорганизующихся кросс-функциональных команд.

Leave a Reply