6. Код состояния «206 Partial Content»:

Что ж, это последняя из трех статей, в которой представлена ​​концепция потоковой передачи с использованием кода состояния «206 Partial Content» и соответствующих заголовков.

Последние две части были в основном посвящены тому, как NodeJS отправляет информацию в браузер, преобразуя ее в двоичные данные.

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

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

ПОТОКИ:

У NodeJS есть довольно хорошее решение для этого, они внедрили потрясающие Readable Streams, эти сексуальные парни являются причиной того, что серверы могут предоставлять много данных без кэширования на диск (что медленнее).

Итак, что такое читаемый поток?

Как говорится в документации NodeJS, поток — это абстрактный интерфейс для работы с потоковыми данными… немного тавтологично, но тоже верно.

После следующих параграфов мы обнаружили, что существует довольно много типов потоков. Они представляют собой разные реализации одного и того же базового класса и по-разному используют его методы для предоставления объектам определенных функций. Все они имеют прослушиватели событий, все они работают с строками и буферами(или объекты Unit8Array), и все они реализуют высокоуровневый API, который упрощает все возможные операции.

Чтобы объяснить, почему они полезны, Документы очень конкретны:

Ключевая цель stream API, особенно метода stream.pipe(), состоит в том, чтобы ограничить буферизацию данных до приемлемого уровня, чтобы источники и получатели с разной скоростью не перегружали доступную память.

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

Зная это, давайте продолжим наш пример кодирования.

Создание и передача читаемых потоков:

Для следующего примера нам понадобится видео. В нашем случае это файл banjo.mp4, который мы позаимствовали из этого видео на YouTube. Мы поставили его на /public/media/banjo.mp4 и ждем стриминга.

Итак, вспомнив, мы также поместили наши контроллеры и функции маршрутизации в свои собственные файлы, создав такую ​​​​структуру:

Что ж, теперь нам нужно видео HTML5 с атрибутом /banjo.mp4as в качестве атрибута src. окончательный тег в выходном .html файле будет выглядеть примерно так:

<video src="/media/banjo.mp4" controls></video>

Теперь пришло время перейти к app.js и добавить условие, которое сопоставляет каждый запрос, в URL-адресе которого есть «.mp4», с дополнительной функцией, которая будет передавать наш файл.

Итак, мы добавляем следующее к нашему app.js:

if (URL.split('.')[1] === 'mp4') {
    ctrl.forVideo(req, res);
}

И весь файл будет выглядеть так:

Обратите внимание, что мы добавили эту функцию ctrl.forVideo(req, res), но еще не создали ее в файле controllers.js. Давай сделаем это.

Перейдите к контроллерам и добавьте следующую функцию к нашему объекту «ctrl»:

Там много чего происходит. Давайте посмотрим дальше комментариев.

Ядром этой функции является fs.createReadStream(), который предоставляет нам этот интерфейс для чтения файла, не затрачивая при этом всю нашу память. Мы также устанавливаем объект {start, end}, который совпадает с {start:start, end:end}. Этот метод файловой системы имеет следующую структуру:

fs.createReadStream(path[, options]).

Путь может принимать строку, URL-адрес или буфер. В нашем случае мы предоставили строку, которая также является нашим путем к файлу, который мы хотим прочитать. Часть options может быть добавлена ​​после запятой ( , ), а затем указывать объект. Если вы хотите узнать больше об опциях, проверьте этот документ.

Вернувшись к нашему коду, мы сохранили этот поток в переменной с именем «readable», а затем мы использовали readable.pipe(res). Что это было?

Если вы знакомы с Linux, вы знаете, что вывод одной операции может быть вводом другой, поэтому вы можете делать такие вещи, как history | grep systemd. Что ж, метод pipe() похож на использование | в Linux. Это позволяет нам передавать буфер из одного потока в другой. Зная, что объект http.response является доступным для записи потоком, мы можем бросить туда этот буфер, чтобы получить доступ к пользовательскому браузеру (для получения дополнительной информации о передаче читаемых объектов в доступные для записи см. эту часть документации).

Но что происходит с частью заголовков?

Если вы видели, мы использовали функцию fs.stat(), а затем использовали вывод для установки заголовков, имея в виду следующую логику:

  1. Сначала браузер делает HTTP-запрос.
  2. Сервер информирует (через «Accepts-Range») браузер, что контент может быть запрошен частично и что мы принимаем диапазоны, измеряемые в «байтах».
  3. Затем браузер повторно отправляет HTTP-запрос с нужными нам диапазонами.

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

Теперь, если вы запустите сервер и нажмете localhost:3000, вы увидите, что наше видео загружается. Если вы откроете консоль и перейдете на вкладку «Сеть», вы увидите примерно следующее:

Если посмотреть, звонков было сделано много, но каждый содержал всего 1024000 байт, то есть всего один Мб на звонок.

Также, если разобраться, теперь браузер знает, что спрашивать, и его запрос только от последнего байта до конца файла.

Тем не менее, теперь мы транслируем контролируемый поток байтов. Миссия выполнена.

7. Выводы:

Как мы видели, возможность тратить только определенное количество памяти в процессе чтения и отправки файла является чем-то важным для сервера, а также важным, если кто-то стремится иметь четкую связь между сервером и клиентом.

Потоки NodeJS — отличное решение для этого. По сути, такие фреймворки, как Express или Fastify, дают нам еще один уровень абстракции таким образом, что мы можем забыть обо всем этом и просто отправить ответ с файлом.

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

Использование этих заголовков и реализация на стороне клиента также могут быть более продуманными. Можно запросить видео через AJAX, а затем сделать новый объектный ответ с прибывшим буфером (как показано в этом примере).

Кроме того, заголовки могут быть более конкретными и иметь конец, установленный на начало плюс фрагмент, и если он больше размера файла, измените фрагмент так, чтобы он был равен data.size минус начало, и установите конец на общую длину -1.

Таким образом, диапазоны становятся более точными, динамичными и точными.

Но сейчас, я думаю, мы готовы.

Спасибо за чтение!

Если вам понравилось, пожалуйста, дайте мне знать или распространите в социальных сетях!