Что такое потоки (Streams) в Node.js и зачем они нужны?

«Что такое потоки (Streams) в Node.js и зачем они нужны?» — вопрос из категории Node.js Core, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Потоки (Streams) в Node.js — это абстракции для работы с последовательными данными, которые позволяют обрабатывать информацию по частям (чанкам), не загружая весь объём в память. Это фундаментальная концепция для эффективной работы с I/O операциями.

Типы потоков:

  • Readable: Поток для чтения (например, fs.createReadStream, HTTP-запрос).
  • Writable: Поток для записи (например, fs.createWriteStream, HTTP-ответ).
  • Duplex: Читаемый и записываемый одновременно (например, TCP-сокет).
  • Transform: Разновидность Duplex, которая может модифицировать данные (например, zlib.createGzip).

Практические примеры:

  1. Копирование больших файлов с контролем памяти:

    const fs = require('fs');
    const path = require('path');
    
    const source = path.join(__dirname, 'large-video.mp4');
    const destination = path.join(__dirname, 'copy-video.mp4');
    
    const readStream = fs.createReadStream(source);
    const writeStream = fs.createWriteStream(destination);
    
    // Pipe автоматически управляет backpressure
    readStream.pipe(writeStream);
    
    writeStream.on('finish', () => {
      console.log('Файл успешно скопирован потоком.');
    });
    
    writeStream.on('error', (err) => {
      console.error('Ошибка записи:', err);
    });

    Без потоков пришлось бы использовать fs.readFile, который загрузил бы весь файл в память.

  2. Обработка данных на лету (Transform Stream):

    const { Transform } = require('stream');
    const fs = require('fs');
    
    // Создаём кастомный трансформационный поток для шифрования
    class EncryptStream extends Transform {
      constructor(shift) {
        super();
        this.shift = shift;
      }
    
      _transform(chunk, encoding, callback) {
        // Простой шифр Цезаря для примера
        const encrypted = chunk.toString().split('').map(char => {
          const code = char.charCodeAt(0);
          return String.fromCharCode(code + this.shift);
        }).join('');
        this.push(encrypted);
        callback();
      }
    }
    
    const encryptor = new EncryptStream(3);
    const reader = fs.createReadStream('secret.txt');
    const writer = fs.createWriteStream('secret.encrypted.txt');
    
    reader.pipe(encryptor).pipe(writer);
  3. Обработка HTTP-запросов/ответов:

    const http = require('http');
    const fs = require('fs');
    
    const server = http.createServer((req, res) => {
      // Отправка большого файла клиенту потоком
      const fileStream = fs.createReadStream('./large-data.json');
      res.writeHead(200, { 'Content-Type': 'application/json' });
      fileStream.pipe(res); // Данные отправляются чанками по мере чтения
    
      fileStream.on('error', (err) => {
        res.statusCode = 500;
        res.end('Internal Server Error');
      });
    });

Ключевые преимущества потоков в Node.js:

  • Эффективное использование памяти: Обработка данных чанками (по умолчанию 64KB) вместо загрузки гигабайтных файлов целиком.
  • Улучшенная производительность и отзывчивость: Данные можно начинать обрабатывать или отправлять сразу после получения первого чанка, не дожидаясь окончания передачи.
  • Контроль backpressure: Механизм pipe() автоматически приостанавливает чтение, если запись не успевает, предотвращая переполнение памяти.
  • Композиция: Потоки легко соединяются в конвейеры (pipeline) для сложной обработки (чтение -> распаковка -> парсинг -> запись).