21. Node.js Lessons. Writable Response Stream (res), Pipe Method. Pt.1

21-lesson_pt1

Our next step will be using the streams to work with network connections. And we will start from delivering files to a visitor. As you may remember, we’ve already had a task like this: if a visitor requires the following url, you will give him the file. Let us create a file pipe.js with the following code (for your convenience, you can download code’ lesson from the repository because we’ll need an HTML file from there):

A solution example to this task without streams may be as follows, read the file:

once the file is read, call callback:

Next, if we’ve got an error, we send an alert, but if everything’s ok, we put a title specifying what file it is. And in response we write the file content as a call res.end(content)  that gives back the content and closes the connection. This solution generally works, but its problem is extended memory consumption because if a file is heavy,  readFile will first read it and then call a callback. As a result, if the client is slow, the whole read content will hang up in the memory, until the client gets it.

But what if we’ve got a lot of slow clients of this kind? And what if a file is big? It turns out, a server can occupy the whole available memory in just a few seconds, which is not good for us. In order to avoid it, we will change the file return code to another one that uses streams. We can already read from the file using ReadStream:

It will be an input-data stream. While an output-data stream will be a response object res, which is an object of the http.ServerResponse  class that inherits from stream.Writable.

The overall algorithm of using the streams for recording cardinally differs from those things that we’ve analyzed earlier. It looks like that.

scheme

First, we create a stream object. If we’ve got an http server, the object is already created. That is res. Next we want to send something to a client. We can do it by calling res.write and deliver our data there. It is generally a buffer or a string. Our data get added to a special stream parameter that is called a buffer. If this buffer is not that big yet, our data get added to it, and write returns true, which means we can write more. In this case, the stream itself becomes responsible for sending data. In general, the sending occurs asynchronously.

Another variant is possible, too. For example, we’ve delivered too much data at a time or if a buffer is already occupied with something, the write method can return false, which means the internal stream buffer is overloaded and you can add your record at the moment, but it will be senseless because it all will be contained in the buffer. That’s why getting false оne does not continue recording waiting for a special event called drain that will be generated with a stream, when it sends everything and the internal buffer gets empty.

Thus, we can call write multiple times, and whenever we realize all data are written down, we should call the end method. Here you can also transfer your data with the first argument. In this case, it will just call write . The most important task of end is to end the record. The stream does it and calls internal operations of closing resources (files), connections, etc., if needed. Then it generates the finish events, which means the record is finally ended.

Note that a similar event at stream.writable is called end. This difference is not just a coincidence because there are duplex streams that can read and write. Respectively, they can generate both of these events.

A stream can be destroyed at any time by calling the method destroy(). When calling this method the stream work gets ended, and all the resources associated to it get free. Of course, the finish event will never happen because this is already a successful ending for a stream work and successful delivery of all data.

We provide the right file output by using the inquiry scheme as our crib sheet. Let us do it within a separate function named sendFile.
It will get one stream for a file and the second one for its response.

At first, pipe.js will look just like that:

The first thing we will do with such function is to wait for its data:

Later, when all data is received, inside a handler readable , we will read them and send the following as the response:

Of course, it doesn’t stand any criticism because if a client cannot receive these data (for instance, due to a slow Internet connection), they will be stuck within the object res buffer:

So, if a file is quickly read, but isn’t sent yet, it will occupy a lot of memory, and we would like to avoid a scenario of this kind. Within this short code we see an example of a versatile solution for this task. Copy this code part instead of the previous one:

We also read the file content on the readable event, but we do not only send it with a call res.write, but also analyze what this call will return. If res accepts data very fast, then res.write will return true. It means, the branch if will never be performed

Respectively, we will get read-write read-write and so on.

A more interesting occasion is when  res.write returns falseIt means, when a buffer is overloaded, we temporarily reject handling readable events from the file.

This stop of a handler does not mean the file stream will quit reading data. On the contrary, it will read the data, but will do it till a certain level, fill in its inside buffer of a file object, and then, as no one requests read, this internal buffer will stay loaded till a certain level. It means, a file stream will read up something and stop there. Next we will wait for a drain event.

It means, whenever the data will be successfully delivered in response (it means, we can accept something else from the file), we demonstrate our interest in readable events again and immediately call the write method. Why? Just because when we’ve waited for this drain, new data can easily come. This means, you can immediately read them:

The read call will return null, if you’ve got no data. Otherwise, they will be simply handled in the same way we’ve been talking about earlier in if.

So, we get this kind of a recursive function: to read, send whatever has been read, waite for drain whether necessary, read again, send and wait – the same actions on loop until the file ends.

tumblr_ng6oke7J1k1sxr61eo1_1280

The article materials were borrowed from the following screencast.

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

About the author

Stay Informed

It's important to keep up
with industry - subscribe!

Stay Informed

Looks good!
Please enter the correct name.
Please enter the correct email.
Looks good!

21. Уроки Node.js. Writable Поток Ответа res, Метод pipe. Pt.1

21-lesson_pt1

Нашим следующим шагом будет использование потоков для работы с сетевыми соединениями. И начнем мы с отдачи посетителю файлов. Если помните, у нас была такая задача: если посетитель запросит следующий url, то отдать ему файл. Создадим файл pipe.js со следующим кодом (для удобства вы можете скачать код нашего урока в репозитории, так как нам понадобиться HTML файл оттуда) :

Пример решения этой задачи без потоков может быть таким: читаем файл:

когда тот будет прочитан вызываем callback:

дальше при ошибке сообщаем о ней, а если все хорошо, то ставим заголовок, чтобы указать, какой это файл. И записываем содержимое файла в ответ вызовом res.end(content) , который отдает контент и завершает соединение. Это решение в принципе работает, но его проблема – это пожирание памяти, потому что если файл большой, то readFile его сначала считает, а потом вызовет callback. В результате получится, что если клиент медленный, то весь этот считанный контент зависнет в памяти до того, как клиент его получит.

А что если у нас таких медленных клиентов много? А если файл очень большой? Получается, что сервер почти мгновенно может занять всю доступную память, что совершенно неприемлемо. Чтобы такого не происходило, мы заменим код отдачи файла на принципиально другой, использующий потоки. Мы уже умеем читать из файла при помощи ReadStream:

Это будет входным потоком данных. А выходным потоком будет объект ответа res, который является объектом класса http.ServerResponse  наследующим от stream.Writable.

Общий алгоритм использования потоков для записи сильно отличатся от того,  что мы рассматривали ранее. Он выглядит так:

scheme
Вначале мы создаем объект потока. Если у нас http сервер, то этот объект уже создан. Это res. Дальше мы хотим отправить что-то клиенту. Это можно сделать вызовом res.write и передать там наши данные. Обычно это либо буфер, либо строка. Наши данные при этом добавляются к специальному свойству потока, которое называют его буфером.  Если пока этот буфер не очень большой, то данные прибавляются к нему, и write возвращает true, что означает, что мы можем писать еще. При этом обязательство по отсылке данных берет на себя уже сам поток. Как правило, эта отсылка происходит асинхронно.

Возможен и другой вариант. Например, если мы передали сразу очень много данных, или если буфер уже был чем-то занят, то метод write может вернуть false, который означает, что внутренний буфер потока переполнен и прямо сейчас запись, конечно, можно сделать, но это будет нецелесообразно, потому что в буфере все будет копиться. Поэтому при получении false обычно запись не продолжают, а ждут специального события drain, которое будет сгенерировано потоком, когда он все отошлет, то есть, когда его внутренний буфер опустеет.

Таким образом можем вызывать write много раз, и когда мы понимаем, что все данные записаны, то мы должны вызвать метод end. Тут тоже можно передать с первым аргументом данные. В этом случае он просто вызовет write . Самая главная задача end это закончить запись. Поток это делает, при необходимости вызывает внутренние операции закрытия ресурсов (файлов), соединений и т.д. И потом генерирует события finish, который означает, что запись полностью завершена.

Обращаем ваше внимание, что аналогичное событие у stream.Writable называется end. Это различие неслучайно, потому что есть потоки duplex, которые умеют и читать, и писать. Соответственно, они могут генерировать как одно событие, так и другое.

Поток в любой момент можно разрушить вызовом метода destroy(). При вызове этого метода работа потока прекращается, и все ассоциированные с ним ресурсы будут освобождены. Конечно, событие finish уже никогда не состоится, потому что это уже успешное окончание работы потока, успешная отдача всех данных.

Реализуем правильную отдачу файла, используя схему справок как шпаргалку. Будем делать то в отдельной функции, которая будет называться sendFile.
Она будет принимать один поток для файла и второй поток для ответа.

Для начала pipe.js будет выглядеть так:

Первое, что мы будем делать с такой функцией – это ждать данных:

Затем, когда они получены, то внутри обработчика readable читать их и отправлять в ответ:

Конечно же, она не выдерживает никакой критики, поскольку в том случае, если клиент пока не может получить эти данные (например, потому что у него медленная скорость соединения), то они зависнут в буфере объекта res:

Таким образом, если файл быстро считан, но пока не отправлен, то он займет большое количество памяти, а этого мы как раз хотели избежать. В этом небольшом коде изложен пример универсального решения этой задачи, скопируйте эту часть кода в замен ранее написанной:

Мы тоже читаем содержимое из файла на событие readable, но мы не просто отправляем его вызовом res.write, а еще и анализируем, что этот вызов вернет. Если res принимает данные очень быстро, то res.write будет возвращать  true. Это означает, что ветка if никогда не выполнится

Соответственно, мы получим read-writeread-write и т.д.

Более интересный случай, когда  res.write вернул falseТо есть, когда буфер переполнен, в этом случае мы временно отказываемся обрабатывать события readableна файле.

Само по себе такое снятие обработчика не означает, что файловый поток перестанет читать данные. Нет, он будет читать данные, но дочитает их до определенного уровня, заполнит свой внутренний буфер объекта файла, и затем, так как никто  не вызывает read, то этот внутренний буфер останется заполнен на определенном уровне, то есть, файловый поток что-то считает и там остановиться. Далее мы дождемся события drain

То есть, когда данные будут успешно отданы в ответ (означает, что мы можем принять что-то еще из файла), мы вновь показываем свой интерес в событиях readable и вызываем метод write сразу. Зачем? Просто потому что, пока мы ждали этого drain, новые данные вполне могли придти. Это означает, что имеет смысл их тут же прочитать:

Вызов read вернет null в том случае, если данных нет. А если есть, то они просто будут обработаны тем же способом, о котором мы говорили раньше в if.

Получается такая вот своеобразная рекурсивная функция: считать, отправить то, что считано, при необходимости подождать  drain , считать, отправить и подождать, и т.д. по циклу, пока файл не закончится.

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

 

About the author

Stay Informed

It's important to keep up
with industry - subscribe!

Stay Informed

Looks good!
Please enter the correct name.
Please enter the correct email.
Looks good!

Related articles

26.03.2024

An Introduction to Clustering in Node.js

Picture your Node.js app starts to slow down as it gets bombarded with user requests. It's like a traffic jam for your app, and no developer likes ...

15.03.2024

JAMstack Architecture with Next.js

The Jamstack architecture, a term coined by Mathias Biilmann, the co-founder of Netlify, encompasses a set of structural practices that rely on ...

Rendering Patterns: Static and Dynamic Rendering in Nextjs

Next.js is popular for its seamless support of static site generation (SSG) and server-side rendering (SSR), which offers developers the flexibility ...

No comments yet

Sign in

Forgot password?

Or use a social network account

 

By Signing In \ Signing Up, you agree to our privacy policy

Password recovery

You can also try to

Or use a social network account

 

By Signing In \ Signing Up, you agree to our privacy policy