Потоки Node.js: FileStream, канали та події

⚡ Розумний підсумок

Потоки Node.js зчитують і записують дані невеликими порціями, замість того, щоб завантажувати цілі файли в пам'ять, і на цій сторінці пояснюється створення потоків файлів, ланцюжок каналів, обробка зворотного тиску та модель подій, яка забезпечує ефективність передачі великомасштабних даних.

  • ???? Типи потоків: Node.js організовує кожен потік як доступний для читання, запису, дуплексний або трансформований, і кожен Node.js API повертає один із цих чотирьох типів.
  • 📂 Потік файлів: Функції fs.createReadStream() та fs.createWriteStream() зчитують та записують файли фрагментами, не завантажуючи весь файл у пам'ять.
  • 🔗 Piping: Метод pipe() безпосередньо з'єднує потік, що читається, з потоком, що записується, та автоматично переміщує дані між ними.
  • 🇧🇷 Протитиск: Функція write() повертає значення false, коли її буфер заповнений, а подія drain сигналізує про те, коли можна безпечно відновити запис.
  • 📡 Трансляція подій: data, end, error та finish позначають ключові моменти в життєвому циклі потоку та керують більшістю коду обробки потоків.
  • Користувацькі події: Модуль подій та EventEmitter дозволяють будь-якій програмі Node.js визначати, генерувати та прослуховувати власні іменовані події.
  • 🛠️ Контроль слухача: once(), listenerCount() та подія newListener керують кількістю разів виконання обробника та способом роботи слухачів. tracкед.

Типи потоків у Node.js

Кожен потік, який надає Node.js, належить до одного з чотирьох базових типів, і знання того, який тип повертає певний API, пояснює, як поводиться цей потік ще до написання будь-якого коду. Модуль потоку, що постачається з Node.js, визначає ці категорії, а API файлів, мережі та стиснення, що використовуються в цій статті, побудовані на їх основі.

Тип потоку Напрямок даних Приклад API Загальний випадок використання
Readable Тільки джерело (витік даних) fs.createReadStream() Читання файлу, отримання тіла HTTP-запиту
Запис Тільки пункт призначення (потік даних) fs.createWriteStream() Запис файлу, надсилання HTTP-відповіді
Дуплекс Обидва напрямки, незалежно net.Socket (TCP-сокет) Двосторонній мережевий зв'язок
Перетворення В обох напрямках дані змінюються під час передачі zlib.createGzip() Стиснення або кодування даних у середині конвеєра

Потік, що читається, та потік, що записується, – це два будівельні блоки, що використовуються в цій статті; обидва також забезпечують HTTP-веб-сервер у Node.js. Дуплексний потік поєднує обидві ролі в одному об'єкті, тоді як потік перетворення — це дуплексний потік, вихід якого змінює його вхід, наприклад, стиснення gzip, що застосовується посеред потоку.

Файловий потік у Node.js

Після визначення чотирьох категорій потоків, решта цієї статті створює передачу файлів за допомогою API потоків для читання та запису.

Node широко використовує потоки як механізм передачі даних.

Наприклад, коли ви виводите щось на консоль за допомогою функції console.log, ви фактично використовуєте потік для надсилання даних на консоль.

Node.js також має можливість передавати дані з файлів, щоб їх можна було читати та записувати належним чином. Зараз ми розглянемо приклад того, як ми можемо використовувати потоки для читання та запису з файлів. Нам потрібно виконати наведені нижче дії для цього прикладу

Крок 1) Створіть файл під назвою data.txt із наведеними нижче даними. Припустимо, що цей файл зберігається на диску D нашої локальної машини.

Підручник з Node.js

Вступ

Події

Generators

Підключення даних

Використання жасмину

Крок 2) Напишіть відповідний код, який використовуватиме потоки для читання даних із файлу.

Файловий потік у Node.js

var fs = require("fs");
var stream;
stream = fs.createReadStream("D://data.txt");

stream.on("data", function(data) {
    var chunk = data.toString();
    console.log(chunk);
}); 

Code Пояснення:

  1. Спочатку нам потрібно включити модулі 'fs', які містять усі функції, необхідні для створення потоків.
  2. Далі ми створюємо читабельний потік за допомогою методу createReadStream. Як вхідні дані ми вказуємо розташування нашого файлу data.txt.
  3. Функція steam.on є обробником подій, і в ній ми вказуємо перший параметр як «дані». Це означає, що щоразу, коли дані надходять у потік із файлу, виконується функція зворотного виклику. У нашому випадку ми визначаємо функцію зворотного виклику, яка виконуватиме 2 основні кроки. По-перше, це перетворити дані, зчитані з файлу, як рядок. Другим буде надсилання перетвореного рядка як виведення на консоль.
  4. Ми беремо кожен фрагмент даних, який зчитується з потоку даних, і перетворюємо його на рядок.
  5. Нарешті, ми надсилаємо вихідні дані кожного фрагмента, перетвореного в рядок, на консоль.

вихід:

Файловий потік у Node.js

  • Якщо код виконано належним чином, ви побачите наведений вище вихід у консолі. Цей результат буде таким самим, як і у файлі data.txt.

Запис у файл

Так само, як ми створюємо потік читання, ми також можемо створити потік запису для запису даних у файл. Давайте спочатку створимо порожній файл без вмісту під назвою data.txt. Припустімо, що цей файл розміщено на диску D нашого комп’ютера.

Наведений нижче код показує, як ми можемо записати дані у файл.

Файловий потік у Node.js

var fs = require("fs");
var stream;
stream = fs.createWriteStream("D://data.txt");

stream.write("Tutorial on Node.js")
stream.write("Introduction")
stream.write("Events")
stream.write("Generators")
stream.write("Data Connectivity")
stream.write("Using Jasmine") 

Code Пояснення:

  1. Ми створюємо записуваний потік за допомогою методу createWriteStream. Як вхідні дані ми вказуємо розташування нашого файлу data.txt.
  2. Далі ми використали метод stream.write для запису різних рядків тексту в наш текстовий файл. Потік подбає про запис цих даних у файл data.txt.

Якщо ви відкриєте файл data.txt, ви побачите у ньому такі дані

Підручник з Node.js

Вступ

Події

Generators

Підключення даних

використання жасмин

Канали в Node.js

Незалежне читання та запис потоків корисне, але Node.js також дозволяє потоку, що читається, підключатися безпосередньо до потоку, що записується, переміщуючи дані без ручної буферизації коду.

У програмах Node потоки можуть об’єднуватися разом за допомогою методу pipe(), який приймає два аргументи:

  • Необхідний потік із можливістю запису, який діє як адресат для даних і
  • Необов'язковий об'єкт, який використовується для передачі параметрів.

Типовий приклад використання каналів, якщо ви хочете перенести дані з одного файлу в інший.

Отже, давайте розглянемо приклад того, як ми можемо передавати дані з одного файлу в інший за допомогою каналів.

Крок 1) Створіть файл під назвою datainput.txt із наведеними нижче даними. Припустимо, що цей файл зберігається на диску D нашої локальної машини.

Підручник з Node.js

Вступ

Події

Generators

Підключення даних

Використання жасмину

Крок 2) Створіть порожній порожній файл під назвою dataOutput.txt і розмістіть його на диску D вашої локальної машини.

Крок 3) Напишіть наведений нижче код, щоб перенести дані з файлу dataOutput.txt у файл dataOutput.txt.

Канали в Node.js

var fs = require("fs");
var readStream = fs.createReadStream("D://datainput.txt");
var writeStream = fs.createWriteStream("D://dataOutput.txt");
readStream.pipe(writeStream);

Code Пояснення:

  1. Спочатку ми створюємо «потік читання» для нашого файлу datainput.txt, який містить усі наші дані, які потрібно перенести в новий файл.
  2. Потім нам потрібно створити «потік запису» до нашого файлу dataOutput.txt, який є нашим порожнім файлом і є місцем призначення для передачі даних із файлу datainput.txt.
  3. Потім ми використовуємо команду pipe для передачі даних із потоку читання в потік запису. Команда pipe візьме всі дані, які надходять у потік читання, і перемістить їх у потік запису.

Якщо ви зараз відкриєте файл dataOutput.txt, ви побачите всі дані, які містилися у файлі dataOutput.txt.

Потокові події та зворотний тиск у Node.js

Виклик pipe(), як показано вище, приховує два механізми, що мають значення в масштабі: події, які генерує кожен потік, та систему зворотного тиску, яка запобігає перевантаженню повільного джерела швидким джерелом.

  • дані: випромінюється читабельним потоком щоразу, коли доступний новий фрагмент.
  • кінець: випромінюється після того, як читабельний потік доставив свій останній фрагмент.
  • закінчити: випромінюється потоком, придатним для запису, після того, як усі дані з черги очищені.
  • помилка: випускається будь-яким типом потоку у разі невдачі; завжди обробляти його, інакше процес аварійно завершує роботу.

У наведеному нижче прикладі прослуховуються всі чотири події, одночасно керуючи зворотним тиском вручну, що відповідає тій самій поведінці, яку pipe() забезпечує автоматично.

var fs = require("fs");
var readStream = fs.createReadStream("D://data.txt");
var writeStream = fs.createWriteStream("D://dataOutput.txt");

readStream.on("data", function(chunk) {
    var ok = writeStream.write(chunk);
    if (!ok) {
        readStream.pause();
    }
});

writeStream.on("drain", function() {
    readStream.resume();
});

readStream.on("end", function() {
    writeStream.end();
});

writeStream.on("finish", function() {
    console.log("Write completed.");
});

readStream.on("error", function(err) {
    console.log(err);
});

Зворотний тиск виникає, коли потік, придатний для читання, генерує дані швидше, ніж потік, придатний для запису, може їх спожити. Метод write() повертає значення false після заповнення буфера, тому наведений вище код призупиняє роботу джерела, доки потік призначення не викличе подію drain, сигналізуючи про безпеку відновлення. pipe() виконує цей цикл автоматично.

⚠ Порада: У продакшені віддавайте перевагу функції pipeline() з модуля stream над ручним ланцюжком pipe(), оскільки вона пересилає помилки з кожного потоку та автоматично закриває файлові дескриптори.

Події в Node.js

Потоки – це один із прикладів ширшого подієво-орієнтованого дизайну Node.js. У решті цієї статті розглядається, як працюють події загалом, з використанням того ж класу EventEmitter, на який внутрішньо покладаються потоки.

Події є одним із ключових понять у Node.js, і іноді Node.js називають структурою, керованою подіями.

По суті, подія - це те, що відбувається. Наприклад, якщо встановлено з’єднання з базою даних, запускається подія підключення до бази даних. Програмування, кероване подіями, полягає у створенні функцій, які запускатимуться, коли запускаються певні події.

Давайте розглянемо базовий приклад визначення події в Node.js.

Ми збираємося створити подію під назвою "data_received". Коли ця подія запускається, на консоль надсилається текст «дані отримані».

Події в Node.js

var events = require('events');
var eventEmitter = new events.EventEmitter();
eventEmitter.on('data_received', function() {
    console.log('data received succesfully.');
});

eventEmitter.emit('data_received'); 

Code Пояснення:

  1. Використовуйте функцію require, щоб включити модуль «події». За допомогою цього модуля ви зможете створювати події в Node.js.
  2. Створіть новий джерело подій. Це використовується для прив’язки події, яка в нашому випадку є «data_received», до функції зворотного виклику, яка визначена в кроці 3.
  3. Ми визначаємо керовану подіями функцію, яка каже, що якщо у разі запуску події “data_received”, ми повинні вивести на консоль текст “data_received”.
  4. Нарешті, у нас є ручний тригер нашої події за допомогою функції eventEmiter.emit. Це ініціює подію data_received.

Під час запуску програми текст «дані отримані» буде надіслано на консоль, як показано нижче.

Події в Node.js

Випромінюючі події

Окрім генерації та прослуховування подій, Node.js надає додаткові методи для керування реєстрацією та перевіркою слухачів.

Під час визначення подій існують різні методи для подій, які можна викликати. Ця тема присвячена детальному розгляду кожного з них.

  1. Одноразові обробники подій

Іноді вам може бути цікаво відреагувати на подію лише тоді, коли вона відбувається вперше. У таких ситуаціях можна використовувати метод once().

Давайте подивимося, як ми можемо використовувати метод once для обробників подій.

Випромінюючі події

Code Пояснення:

  1. Тут ми використовуємо метод «один раз», щоб сказати, що для події «data_received» функція зворотного виклику має бути виконана лише один раз.
  2. Тут ми вручну запускаємо подію data_received.
  3. Коли подія data_received запускається знову, цього разу нічого не станеться. Це пов’язано з першим кроком, де ми сказали, що подія може бути ініційована лише один раз.

Якщо код виконано належним чином, у журналі виведеться «data_received successfully». Це повідомлення з’явиться на консолі лише один раз.

  1. Перевірка слухачів подій

У будь-який момент свого життя джерело подій може мати нуль або більше слухачів, приєднаних до нього. Слухачів для кожного типу події можна перевірити кількома способами.

Якщо ви зацікавлені лише у визначенні кількості приєднаних слухачів, тоді шукайте не далі, ніж метод EventEmitter.listenerCount().

(Примітка: Слухачі важливі, тому що основна програма повинна знати, чи додаються слухачі на льоту до події, інакше програма працюватиме неправильно, оскільки будуть викликані додаткові слухачі.)

Випромінюючі події

Code Пояснення:

  1. Ми визначаємо тип eventEmitter, який необхідний для використання методів, пов’язаних із подіями.
  2. Потім ми визначаємо об’єкт під назвою emitter, який використовуватиметься для визначення наших обробників подій.
  3. Ми створюємо 2 обробники подій, які практично нічого не роблять. У нашому прикладі це просто, щоб показати, як працює метод listenerCount.
  4. Тепер, коли ви викликаєте метод listenerCount для нашої події data_received, він надішле кількість слухачів подій, приєднаних до цієї події, у журнал консолі.

Якщо код виконано належним чином, у журналі консолі буде показано значення 2.

  1. Подія newListener

Щоразу, коли реєструється новий обробник подій, джерело подій створює подію newListener. Ця подія використовується для виявлення нових обробників подій. Ви зазвичай використовуєте подію newListener, коли вам потрібно виділити ресурси або виконати певну дію для кожного нового обробника події.

Випромінюючі події

var events = require('events');
var eventEmitter = events.EventEmitter;
var emitter = new eventEmitter();
emitter.on("newListener", function(eventName, listener) {
    console.log("Added listener for " + eventName + " events");
});
emitter.on('data_received', function() {});
emitter.on('data_received', function() {}); 

Code Пояснення:

  1. Ми створюємо новий обробник події для події newListener. Тож щоразу, коли реєструється новий обробник подій, на консолі відображатиметься текст «Додано слухач для» + назва події.
  2. Тут ми пишемо на консоль текст «Added listener for» + назву події для кожної зареєстрованої події.
  3. Ми визначаємо 2 обробники подій для нашої події data_received.

Якщо наведений вище код виконано належним чином, у консолі буде показано наведений нижче текст. Це лише показує, що обробник події newListener було запущено двічі.

Додано слухач подій data_received

Додано слухач подій data_received

Поширені запитання

fs.readFile() завантажує весь файл у пам'ять перед тим, як повернути його, тоді як fs.createReadStream() доставляє файл невеликими порціями в міру їх надходження. Потоки використовують набагато менше пам'яті для великих файлів і дозволяють обробці розпочатися до завершення читання.

Так. Викличте .pipe() один раз для кожного пункту призначення, наприклад, readStream.pipe(destinationA), а потім readStream.pipe(destinationB). Node.js доставляє однакові фрагменти до кожного пункту призначення, і кожен потік, доступний для запису, все ще керує своїм власним зворотним тиском незалежно.

Так. Передати читабельний потік через потік перетворення, такий як zlib.createGzip(), а потім у записуваний потік: readStream.pipe(zlib.createGzip()).pipe(writeStream). Файл стискається під час потокової передачі, без завантаження всього файлу в пам'ять.

Вони можуть швидко створювати робочий readStream та конвеєрний шаблон, але часто пропускають обробник подій помилок та обробку зворотного тиску. Завжди перевіряйте код потоку, згенерований штучним інтелектом, на наявність обох прогалин, перш ніж використовувати його у продакшені.

Вони можуть запропонувати ймовірні причини, такі як відсутній слухач стоку або необроблена помилка, але підтвердження справжньої причини все одно вимагає ручної перевірки стану потоку та журналів програми.

Підсумуйте цей пост за допомогою: