Модуль 14

Streams: основи та use cases

Ось готовий урок, створений за твоїм майстер-промптом. Він адаптований під стиль David Malan (CS50): енергійний, з живими прикладами та акцентом на фундаментальному розумінні.


🎓 CS50: Streams (Потоки) — Як не "луснути" від даних

Привіт, друзі! Радий вас бачити. 👋

Сьогодні ми поговоримо про тему, яка відділяє "програміста, який пише код, що працює на моєму ноутбуці" від інженера, який будує системи масштабу YouTube або Netflix.

Ми говоримо про Streams (Потоки).

1. 🔥 Вступ: Чому ваш комп'ютер не вибухає, коли ви дивитесь Netflix?

Уявіть ситуацію. Ви ввечері вмикаєте Netflix, щоб подивитися фільм у 4K якості. Цей файл важить десь 20 гігабайтів.

А тепер питання: Скільки оперативної пам'яті (RAM) у вашому ноутбуці? 8 ГБ? Може, 16 ГБ?

⚠️ Риторичне запитання: Як ви можете дивитися фільм розміром 20 ГБ, якщо у вас всього 8 ГБ вільної пам'яті?

Якби комп'ютери працювали "в лоб", вам довелося б: 1. Натиснути Play. 2. Чекати 20 хвилин, поки весь файл завантажиться в оперативну пам'ять. 3. Отримати повідомлення "Out of Memory" (Пам'ять переповнена) і синій екран смерті. 💥

Але цього не стається. Чому? Тому що ви не "завантажуєте" фільм цілком. Ви його стримите (stream). Ви отримуєте маленькі шматочки даних, обробляєте їх (дивитесь кадр) і одразу викидаєте з пам'яті, звільняючи місце для нових.

Без Streams ми б не мали ні YouTube, ні Spotify, ні можливості обробляти великі файли на сервері. Це фундамент ефективності.


2. 🧠 Теоретична база: Що під капотом?

Давайте відійдемо від магії і подивимось на логіку.

Аналогія: Резервуар vs Шланг

  • Традиційний підхід (Буферизація всього): Уявіть, що ви хочете наповнити басейн. Ви берете величезну цистерну (RAM), набираєте в неї воду з озера (Файл) і намагаєтесь перенести її до басейну. Це важко, повільно і вимагає гігантської цистерни.
  • Streams (Потоки): Ви просто простягаєте шланг від озера до басейну. Вода тече постійно, але в кожен конкретний момент часу в шлангу знаходиться зовсім трохи води.

Ключові поняття (Це треба запам'ятати):

  1. Stream (Потік) — це послідовність даних, які стають доступними з часом, шматок за шматком. Це не "об'єкт", це "подія".
  2. Chunk (Шматок) — це та порція даних (наприклад, 64 кілобайти), яка зараз проходить через "шланг".
  3. Buffer (Буфер) — це маленька "зала очікування" в оперативній пам'яті, де шматки збираються перед відправкою далі.

Чотири типи потоків (Інтуїтивно):

  1. Readable (Для читання): Кран з водою. Дані звідси витікають (читання файлу, вхідний запит HTTP).
  2. Writable (Для запису): Зливна труба. Дані сюди втікають (запис у файл, відповідь клієнту).
  3. Duplex: Телефонна трубка. Можна і говорити, і слухати (сокети).
  4. Transform: Фільтр для води. Вода втікає брудна, витікає чиста (стиснення файлів, шифрування).

Головна ідея: Ми міняємо простір (пам'ять RAM) на час. Ми обробляємо дані довше, але використовуємо мінімум пам'яті.


3. 🧪 Приклади: Від жаху до елегантності

Давайте подивимось на код (використаємо Node.js, бо там це найяскравіше видно, але логіка однакова для Python, Java чи C).

Сценарій: Копіювання величезного файлу (big.txt)

❌ Спосіб 1: Як роблять новачки (Без Streams)

const fs = require('fs');

// Ми кажемо: "Прочитай мені ВЕСЬ файл у змінну data"
fs.readFile('big.txt', (err, data) => {
    if (err) throw err;

    // Якщо файл 10GB, ваша програма тут впаде.
    // Бо змінна data намагається зайняти 10GB RAM.
    fs.writeFile('copy.txt', data, (err) => {
        if (err) throw err;
        console.log('Готово!');
    });
});

Результат: Якщо файл малий — ок. Якщо великий — краш системи.


✅ Спосіб 2: Як роблять профі (Зі Streams)

Тут ми використовуємо подієву модель.

const fs = require('fs');

// Створюємо "кран" (читання) і "злив" (запис)
const readStream = fs.createReadStream('big.txt');
const writeStream = fs.createWriteStream('copy.txt');

// Питання до студента: Що відбудеться, коли надійде перший шматок даних?
// Правильно! Спрацює подія 'data'.

readStream.on('data', (chunk) => {
    // chunk - це маленький шматочок файлу
    console.log(`Отримав шматок розміром: ${chunk.length} байт`);

    // Одразу пишемо його в новий файл і забуваємо!
    writeStream.write(chunk);
});

readStream.on('end', () => {
    console.log('Хух! Закінчили.');
});

Чому це круто? Навіть якщо файл важить 100 ГБ, ваша програма споживатиме лише кілька мегабайт пам'яті. Вона бере шматок -> пише -> бере наступний.


🚀 Спосіб 3: Вищий пілотаж (Pipe)

У сантехніці ви з'єднуєте труби. У коді те саме. Метод pipe() бере дані з Readable потоку і автоматично пхає їх у Writable, контролюючи швидкість.

const fs = require('fs');

const readStream = fs.createReadStream('big.txt');
const writeStream = fs.createWriteStream('copy.txt');

// Це все! Одна лінія.
readStream.pipe(writeStream);

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


4. 🛠 Практична частина

Час забруднити руки! Відкрийте редактор коду.

Завдання 1: "Детектив" Запустіть код із прикладу №2 (події data). Додайте лічильник count = 0 і збільшуйте його в кожній події data. * Питання: Скільки "шматків" (chunks) знадобиться, щоб прочитати ваш файл?

Завдання 2: "Трансформатор" Створіть скрипт, який читає текстовий файл, переводить весь текст у ВЕРХНІЙ РЕГІСТР і записує в новий файл. * Підказка: Вам треба обробити chunk.toString().toUpperCase() перед записом.

Завдання 3: "HTTP Сервер" (Міні-кейс) Напишіть простий веб-сервер (використовуючи модуль http), який у відповідь на запит віддає великий файл (наприклад, картинку або відео), використовуючи createReadStream і .pipe(). * Чому це важливо: Це те, як працює реальний відео-стримінг!

Завдання 4: А що, якщо... Що станеться, якщо файл big.txt не існує? Додайте обробку помилки .on('error', ...) для вашого потоку, щоб програма не падала мовчки.


5. 💡 Мислення як у розробника

Як думає Senior Engineer, коли чує задачу "Обробити CSV-файл на 1 мільйон рядків"?

  1. Помилка новачка: "Завантажу весь файл у масив, зроблю forEach і збережу".
    • Наслідок: Сервер зависає, коли приходить файл на 2 мільйони рядків.
  2. Думка профі: "Дані — це ріка. Мені не треба вся ріка одразу. Мені треба відро води за раз".
  3. Backpressure (Протитиск): Досвідчений розробник знає: іноді "читач" (ReadStream) працює швидше, ніж "писач" (WriteStream). Якщо просто лити дані, пам'ять переповниться чергою на запис.
    • Порада: Використовуйте .pipe(), він вирішує проблему швидкості автоматично. "Не намагайтеся пити з пожежного гідранта без редуктора".

6. 🧩 Підсумок

Отже, що ми сьогодні вивчили?

  1. Streams дозволяють працювати з даними, які більші за обсяг вашої RAM.
  2. Дані розбиваються на маленькі Chunks (шматки).
  3. Ми використовуємо Pipe (трубу), щоб з'єднати джерело даних і їх призначення.

Тепер ви вмієте: ✅ Копіювати гігантські файли без зависання компа. ✅ Будувати ефективні веб-сервери, що віддають контент потоком. ✅ Розуміти, як Netflix економить ресурси.

Наступного разу: Ми заглянемо ще глибше. Раз ми вже вміємо передавати дані потоком, як нам передавати дані між різними комп'ютерами в реальному часі? Готуйтесь, наступна зупинка — WebSockets! 🌐

А на сьогодні це все. Це були Streams.