Тема этой главы, потоки в Node.JS. Мы постараемся разобраться в этой теме хорошо и подробно, по сколько, с одной стороны, так получается, что потоки в обычной браузерной JavaScript разработке отсутствуют, а с другой стороны, уверенное владение потоками необходимо для грамотной серверной разработке, по скольку поток, является универсальным способом работы с источниками данных, которые используются повсеместно.

Можно выделить два основных типа потоков.

Первый поток — stream.Readable — чтение.
stream.Readable это встроенный класс, который реализует потоки для чтения, как правило сам он не используется, а используются его наследники. В частности для чтения из файла есть fs.ReadSream. Для чтения запроса посетителя, server.on(‘request’, …req …), при его обработки, есть специальный объект, который мы раньше видели под именем req, первый аргумент обработчика запроса.

Второй поток — stream.Writable — запись.
stream.Writable это универсальный способ записи и здесь тоже, сам stream.Writable обычно не используется, но используются его наследники.
…в файл: fs.WriteStream
…в ответ посетителю: server.on(‘request’, …res …)

Есть и некоторые другие типы потоков, но наиболее востребованные это предыдущие два и производные от них.

Самый лучший способ разобраться с потоками это посмотреть как они работают на практике. Поэтому сейчас мы начнем с того, что используем fs.ReadStream для чтения файла.

fs.js

JavaScript

var fs = require("fs"); var stream = new fs.ReadStream(__filename); stream.on("readable", function(){ var data = stream.read(); console.log(data); }); stream.on("end", function(){ console.log("THE END"); });

var fs = require ("fs" ) ;

//fs.ReadStream наследует от stream.Readable

var stream = new fs . ReadStream (__filename ) ;

console . log (data ) ;

} ) ;

console . log ("THE END" ) ;

} ) ;

Итак, здесь я подключаю модуль fs и создаю поток. Поток это JavaScript объект, который получает информацию о ресурсе, в данном случае путь к файлу — «__filename» и который умеет с этим ресурсом работать. fs.ReadStream реализует стандартный интерфейс чтения который описан в классе stream.Readable. Посмотрим его на схеме

Когда создается объект потока — «new stream.Readable», он подключается к источнику данных, в нашем случае это файл, и пытается начать из него читать. Когда он что то прочитал, то он эмитирует событие — «readable», это событие означает, что данные просчитаны и находятся во внутреннем буфере потока, который мы можем получить используя вызов «read()». Затем мы можем что то сделать с данными — «data» и подождать следующего «readable» и снова если придется, и так дальше. Когда источник данных иссяк, бывают конечно источники которые не иссякают, например датчики случайных чисел, но размер файла то ограничен, поэтому в конце будет событие «end», которое означает, что данных больше не будет. Так же, на любом этапе работы с потоком, я могу вызвать метод «destroy()» потока. Этот метод означает, что мы больше не нуждаемся в потоке и можно его закрыть, и закрыть соответствующие источники данных, полностью все очистить.

А теперь вернемся к исходному коду. Итак здесь мы создаем ReadStream

fs.js

JavaScript

и он тут же хочет открыть файл. Но тут же, в данном случае вовсе не означает на этой же строке, потому что как мы помним, все операции с вводом выводом, реализуются через «LibUV», а «LibUV» устроено так, что все синхронные обработчики ввода вывода сработают на следующей итерации событийного цикла, то есть заведомо после того, как весь текущий JavaScript закончит работу. Это означает, что я могу без проблем навесить все обработчики и я твердо знаю что они будут установлены до того как будет считан первый фрагмент данных. Запускаю этот код и смотрим, что вывелось в консоле

Первое сработало событие ‘readable’ и оно вывело данные, сейчас это обычный буфер, но я могу преобразовать его к строке используя кодировку utf-8 обычным вызовом toString

Еще один вариант, указать кодировку непосредственно при открытии потока

тогда преобразование будет автоматическим и toString() нам не нужен.

Наконец когда файл закончился,

fs.js

JavaScript

var fs = require("fs"); //fs.ReadStream наследует от stream.Readable var stream = new fs.ReadStream(__filename, {encoding: "utf-8"}); stream.on("readable", function(){ var data = stream.read(); console.log(data); }); stream.on("end", function(){ console.log("THE END"); });

stream . on ("end" , function () {

console . log ("THE END" ) ;

} ) ;

то событие ‘end’ вывело мне в консоль «THE END». Здесь фай закончился почти сразу, поскольку он был очень маленький. Сейчас я не много модифицирую пример, сделаю вместо «__filename», то есть вместо текущего файла, файл «big.html», который в текущей директории находится.

Файл big.html большой, по этому событие readable срабатывало многократно и каждый раз мы получали очередной фрагмент данных в виде буфера. Так же обратите внимание на вывод null который нас постоянно преследует, о причине этого вывода вы можете прочесть в документации , там сказано, что после того как данные заканчиваются readable возвращает null. Возвращаясь к нашему буферу, давайте я выведу в консоль его размер и заодно сделаю проверку на но то чтоб вывод был не null

fs.js

JavaScript

var fs = require ("fs" ) ;

//fs.ReadStream наследует от stream.Readable

stream . on ("readable" , function () {

var data = stream . read () ;

} ) ;

stream . on ("end" , function () {

console . log ("THE END" ) ;

} ) ;

Эти числа, не что иное как длина прочитанного фрагмента файла, потому что поток когда открывает файл, он читает из него не весь файл конечно же, а только кусок и помещает его в свою внутреннюю переменную и максимальный размер, это как раз шестьдесят четыре килобайта. Пока мы не вызовем stream.read(), он дальше читать не будет. После того как я получил очередные данные, то внутренний буфер очищается и он может еще фрагмент прочитать, и так далее и так далее, последний фрагмент имеет длину остатка данных. На этом примере мы отлично видим важное преимущество использования потоков, они экономят память, какой бы большой файл не был, все равно, единовременно мы обрабатываем вот такой небольшой фрагмент. Второе, менее очевидное преимущество, это универсальность интерфейса. Здесь

fs.js

JavaScript

var fs = require("fs"); //fs.ReadStream наследует от stream.Readable var stream = new fs.ReadStream("big.html"); stream.on("readable", function(){ var data = stream.read(); if(data != null)console.log(data.length); }); stream.on("end", function(){ console.log("THE END"); });

var stream = new fs . ReadStream ("big.html" ) ;

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

fs.js

JavaScript

var fs = require("fs"); var stream = new OurStream("our resourse"); stream.on("readable", function(){ var data = stream.read(); if(data != null)console.log(data.length); }); stream.on("end", function(){ console.log("THE END"); });

var fs = require ("fs" ) ;

var stream = new OurStream ("our resourse" ) ;

stream . on ("readable" , function () {

var data = stream . read () ;

if (data != null ) console . log (data . length ) ;

} ) ;

stream . on ("end" , function () {

console . log ("THE END" ) ;

} ) ;

Потому что потоки это в первую очередь интерфейс, то есть в теории, если наш поток реализует необходимые события и методы, в частности наследует от stream.Readable, то все должно работать хорошо, но это конечно же только в том случае если мы не использовали специальных возможностей, которые есть у файловых потоков. В частности у потока ReadStream есть дополнительные события

fs.js

JavaScript

var fs = require("fs"); //fs.ReadStream наследует от stream.Readable var stream = new fs.ReadStream("big.html"); stream.on("readable", function(){ var data = stream.read(); if(data != null)console.log(data.length); }); stream.on("end", function(){ console.log("THE END"); });

var fs = require ("fs" ) ;

//fs.ReadStream наследует от stream.Readable

var stream = new fs . ReadStream ("big.html" ) ;

stream . on ("readable" , function () {

var data = stream . read () ;

if (data != null ) console . log (data . length ) ;

} ) ;

stream . on ("end" , function () {

console . log ("THE END" ) ;

} ) ;

Здесь изображена схема именно для fs.ReadStram и новые события изображены красным



nodejs stream (3)

Я думаю, вы слишком задумываетесь, как все это работает, и мне это нравится.

Какие потоки хороши для

Потоки хороши для двух вещей:

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

    они также хорошо связывают программы вместе (функции чтения). Так же, как и в командной строке, вы можете комбинировать разные программы для получения желаемого результата. Пример: cat file | grep word cat file | grep word .

Как они работают под капотом...

Большинство из этих операций, которые требуют времени для обработки и могут дать вам частичные результаты по мере их получения, не выполняются Node.js, они выполняются V8 JS Engine, и они передают эти результаты только JS, чтобы вы могли работать с ними.

Чтобы понять ваш пример http, вам нужно понять, как работает http

Существуют различные кодировки, которые может отправлять веб-страница. В начале был только один способ. Когда вся страница была отправлена, когда она была запрошена. Теперь для этого есть более эффективные кодировки. Один из них фрагментируется, когда части веб-страницы отправляются до отправки всей страницы. Это хорошо, потому что веб-страницу может обрабатываться по мере ее получения. Представьте себе веб-браузер. Он может начать рендеринг веб-сайтов до завершения загрузки.

Ваши вопросы и ответы.

Во-первых, потоки Node.js работают только в одной программе Node.js. Потоки Node.js не могут взаимодействовать с потоком на другом сервере или даже в программе.

Это означает, что в приведенном ниже примере Node.js не может разговаривать с веб-сервером. Он не может сказать, чтобы он остановился или возобновился.

Node.js <-> Network <-> Webserver

Что действительно происходит, так это то, что Node.js запрашивает веб-страницу, и он начинает ее загружать, и нет возможности остановить эту загрузку. Просто снимите сокет.

Итак, что на самом деле происходит, когда вы делаете в Node.js .pause или.continue?

Он начинает буферизовать запрос до тех пор, пока вы не будете готовы снова его использовать. Но загрузка никогда не прекращалась.

Event Loop

У меня есть целый ответ, чтобы объяснить, как работает Event Loop, но я думаю, что вам лучше .

Первое, что нужно отметить: stream.js-потоки не ограничиваются HTTP-запросами. HTTP-запросы / Сетевые ресурсы - всего лишь один пример потока в node.js.

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

Скажем, у вас есть файл (размером несколько гигабайт) и вы хотите преобразовать все строчные буквы в верхние регистры и записать результат в другой файл. Наивный подход прочитал бы весь файл, используя fs.readFile (обработка ошибок опущена для краткости):

fs . readFile ("my_huge_file" , function (err , data ) { var convertedData = data . toString (). toUpperCase (); fs . writeFile ("my_converted_file" , convertedData ); });

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

var readStream = fs . createReadStream ("my_huge_file" ); var writeStream = fs . createWriteStream ("my_converted_file" ); readStream . on ("data" , function (chunk ) { var convertedChunk = chunk . toString (). toUpperCase (); writeStream . write (convertedChunk ); }); readStream . on ("end" , function () { writeStream . end (); });

Этот подход намного лучше:

  1. Вы будете иметь дело только с небольшими частями данных, которые легко впишутся в вашу оперативную память.
  2. Вы начинаете обрабатывать, как только первый байт прибывает, и не тратьте время на то, чтобы ничего не делать, а ждать.

После открытия потока node.js откроет файл и начнет читать с него. Как только операционная система передает некоторые байты в поток, который читает файл, он будет передан вместе с вашим приложением.

Возвращаясь к потокам HTTP:

  1. Здесь также актуальна первая проблема. Возможно, злоумышленник отправляет вам большие объемы данных, чтобы перегрузить вашу RAM и удалить (DoS) вашу услугу.
  2. Однако вторая проблема еще важнее в этом случае: сеть может быть очень медленной (думаю, смартфоны), и это может занять много времени, пока все будет отправлено клиентом. Используя поток, вы можете начать обработку запроса и сократить время отклика.

При приостановке HTTP-потока: это не выполняется на уровне HTTP, но ниже. Если вы приостановите поток, узел node.js просто прекратит чтение из базового сокета TCP. То, что происходит тогда, зависит от ядра. Он все равно может буферизовать входящие данные, поэтому он готов для вас, как только вы закончите свою текущую работу. . Приложениям не нужно иметь дело с этим. Это не их дело. На самом деле приложение отправителя, вероятно, даже не понимает, что вы больше не активно читаете!

Таким образом, это в основном предоставление данных, как только оно доступно, но без подавляющего количества ресурсов. Основная сложная работа выполняется либо операционной системой (например, net , fs , http), либо автором используемого потока (например, zlib который является потоком Transform и обычно прикрепляется к fs или net).

Нижеследующая диаграмма кажется довольно точным обзором / диаграммой 10.000 футов для класса узловых потоков.

Кроме того, я не знаю, как высоко ваш счетчик идет, но если вы заполнить буфер, он перестанет передавать данные в поток преобразования, и в этом случае completed никогда не будет на самом деле ударить, потому что вы не r перейти к пределу счетчика. Попытайтесь изменить свой highwatermark .

EDIT 2: A Little Better Объяснение

Как вы хорошо знаете transform stream дуплекс поток , который в основном означает, что он может принимать данные из источника, и он может передавать данные в пункт назначения. Это обычно называют чтением и письмом. transform stream наследует как от read stream , так и от write stream , реализованных Node.js. Однако есть одно предостережение: transform stream не должен реализовывать функции _read или _write. В этом смысле вы можете подумать об этом как о менее известном passthrough stream .

Если вы думаете о том, что transform stream использует write stream , вы должны также подумать о том, что в потоке записи всегда есть пункт назначения, чтобы сбрасывать его содержимое. Задача у вас есть , так это то, что при создании transform stream вы не можете указать место для отправки вашего контента. Единственный способ передать данные полностью через поток преобразования - передать его потоку записи, иначе, по существу, ваши потоки будут скопированы и не смогут принимать больше данных, потому что нет места для данных.

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

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

Я бы рискнул сказать, что если вы увеличите свой highwatermark для потока преобразования, вы сможете увеличить свой порог и все еще иметь код. Однако этот метод неверен. Труба ваш поток в поток записи, который будет отправлять данные Девы/нулевой способ Creat этого потока записи:

Var writer = fs.createWriteStream("/dev/null");

раздел в документации Node.js на buffering объяснить ошибку вы работаете в.