Stream di Node.js: FileStream, Pipes ed Eventi

⚡ Riepilogo intelligente

Node.js legge e scrive i dati in piccoli blocchi anziché caricare interi file in memoria. Questa pagina illustra la creazione di stream di file, il concatenamento di pipe, la gestione della contropressione e il modello a eventi che garantisce l'efficienza del trasferimento di dati su larga scala.

  • ???? Tipi di flusso: Node.js organizza ogni flusso come leggibile, scrivibile, duplex o trasformabile, e ogni API di Node.js restituisce uno di questi quattro tipi.
  • 📂 Flusso di file: Le funzioni fs.createReadStream() e fs.createWriteStream() leggono e scrivono file a blocchi senza caricare l'intero file in memoria.
  • 🔗 Piping: Il metodo pipe() collega direttamente un flusso leggibile a un flusso scrivibile e trasferisce automaticamente i dati tra di essi.
  • ️ Contropressione: La funzione write() restituisce false quando il buffer è pieno, e l'evento drain segnala quando è possibile riprendere la scrittura in sicurezza.
  • 📡 Trasmetti gli eventi in streaming: dati, fine, errore e completamento segnano i momenti chiave del ciclo di vita di uno stream e guidano la maggior parte del codice di gestione degli stream.
  • ⚡ Eventi personalizzati: Il modulo events e EventEmitter consentono a qualsiasi applicazione Node.js di definire, emettere e rimanere in ascolto dei propri eventi denominati.
  • ️ Controllo dell'ascoltatore: once(), listenerCount() e l'evento newListener gestiscono quante volte viene eseguito un gestore e come vengono gestiti i listener tracre.

Tipi di stream in Node.js

Ogni flusso esposto da Node.js rientra in uno dei quattro tipi base, e conoscere il tipo restituito da una determinata API permette di capire come si comporterà il flusso prima ancora di scrivere codice. Il modulo stream incluso in Node.js definisce queste categorie, e le API per file, rete e compressione utilizzate in questo articolo si basano tutte su di esse.

Tipo di flusso Direzione dei dati Esempio di API Caso d'uso comune
Readable Solo sorgente (i dati fluiscono in uscita) fs.createReadStream() Lettura di un file, ricezione del corpo di una richiesta HTTP
Scrivibile Solo destinazione (i dati fluiscono in entrata) fs.createWriteStream() Scrittura di un file, invio di una risposta HTTP
Duplex In entrambe le direzioni, indipendentemente net.Socket (socket TCP) Comunicazione di rete bidirezionale
Trasformare In entrambe le direzioni, i dati vengono modificati durante il transito. zlib.createGzip() Compressione o codifica dei dati a metà della pipeline

Un flusso leggibile e un flusso scrivibile sono i due elementi costitutivi utilizzati in tutto questo articolo; entrambi alimentano anche un server web HTTP In Node.js, uno stream duplex combina entrambi i ruoli in un unico oggetto, mentre uno stream di trasformazione è uno stream duplex il cui output modifica il suo input, come ad esempio la compressione gzip applicata a metà flusso.

Filestream in Node.js

Una volta definite le quattro categorie di stream, il resto di questo articolo illustra come realizzare un trasferimento di file utilizzando le API di stream leggibili e scrivibili.

Node fa ampio uso dei flussi come meccanismo di trasferimento dei dati.

Ad esempio, quando esegui l'output di qualcosa sulla console utilizzando la funzione console.log, stai effettivamente utilizzando un flusso per inviare i dati alla console.

Node.js ha anche la capacità di eseguire lo streaming di dati dai file in modo che possano essere letti e scritti in modo appropriato. Vedremo ora un esempio di come possiamo utilizzare i flussi per leggere e scrivere da file. Dobbiamo seguire i passaggi indicati di seguito per questo esempio

Passo 1) Crea un file chiamato data.txt che contiene i dati seguenti. Supponiamo che questo file sia archiviato nell'unità D del nostro computer locale.

Tutorial su Node.js

Introduzione

Eventi

Generators

Connettività dati

Usando il gelsomino

Passo 2) Scrivi il codice pertinente che utilizzerà i flussi per leggere i dati dal file.

Filestream in 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 Spiegazione:-

  1. Dobbiamo prima includere i moduli 'fs' che contengono tutte le funzionalità richieste per creare flussi.
  2. Successivamente creiamo un flusso leggibile utilizzando il metodo createReadStream. Come input forniamo la posizione del nostro file data.txt.
  3. La funzione steam.on è un gestore di eventi e in esso specifichiamo il primo parametro come "dati". Ciò significa che ogni volta che i dati arrivano nel flusso dal file, viene eseguita una funzione di callback. Nel nostro caso, stiamo definendo una funzione di callback che effettuerà 2 passaggi fondamentali. Il primo è convertire i dati letti dal file come una stringa. Il secondo sarebbe inviare la stringa convertita come output alla console.
  4. Stiamo prendendo ogni blocco di dati letto dal flusso di dati e convertendolo in una stringa.
  5. Infine, inviamo alla console l'output di ogni blocco convertito in stringa.

Produzione:

Filestream in Node.js

  • Se il codice viene eseguito correttamente, vedrai l'output sopra nella console. Questo output sarà uguale a quello nel file data.txt.

Scrittura su un file

Allo stesso modo in cui creiamo un flusso di lettura, possiamo anche creare un flusso di scrittura per scrivere dati su un file. Creiamo prima un file vuoto senza contenuto chiamato data.txt. Supponiamo che questo file sia inserito nell'unità D del nostro computer.

Il codice seguente mostra come possiamo scrivere i dati nel file.

Filestream in 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 Spiegazione:-

  1. Stiamo creando un flusso scrivibile utilizzando il metodo createWriteStream. Come input forniamo la posizione del nostro file data.txt.
  2. Successivamente abbiamo utilizzato il metodo stream.write per scrivere le diverse righe di testo nel nostro file di testo. Lo stream si occuperà di scrivere questi dati nel file data.txt.

Se apri il file data.txt, ora vedrai i seguenti dati nel file

Tutorial su Node.js

Introduzione

Eventi

Generators

Connettività dati

utilizzando Jasmine

Pipe in Node.js

La possibilità di leggere e scrivere flussi di dati in modo indipendente è utile, ma Node.js consente anche di connettere direttamente un flusso leggibile a un flusso scrivibile, trasferendo i dati senza la necessità di codice di buffering manuale.

All'interno delle applicazioni Node, i flussi possono essere convogliati insieme utilizzando il metodo pipe(), che accetta due argomenti:

  • Un flusso scrivibile obbligatorio che funge da destinazione per i dati e
  • Un oggetto facoltativo utilizzato per passare le opzioni.

Un tipico esempio di utilizzo delle pipe, se si desidera trasferire dati da un file all'altro.

Vediamo quindi un esempio di come possiamo trasferire dati da un file all'altro utilizzando le pipe.

Passo 1) Crea un file chiamato datainput.txt che contiene i dati seguenti. Supponiamo che questo file sia archiviato nell'unità D del nostro computer locale.

Tutorial su Node.js

Introduzione

Eventi

Generators

Connettività dati

Usando il gelsomino

Passo 2) Crea un file vuoto chiamato dataOutput.txt e posizionalo sull'unità D del tuo computer locale.

Passo 3) Scrivi il codice seguente per effettuare il trasferimento dei dati dal file datainput.txt al file dataOutput.txt.

Pipe in Node.js

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

Code Spiegazione:-

  1. Stiamo innanzitutto creando un "readstream" nel nostro file datainput.txt che contiene tutti i nostri dati che devono essere trasferiti nel nuovo file.
  2. Dobbiamo quindi creare un "writestream" nel nostro file dataOutput.txt, che è il nostro file vuoto ed è la destinazione per il trasferimento dei dati dal file datainput.txt.
  3. Utilizziamo quindi il comando pipe per trasferire i dati dal flusso di lettura al flusso di scrittura. Il comando pipe prenderà tutti i dati che entrano nel readstream e li invierà al writestream.

Se ora apri il file dataOutput.txt, vedrai tutti i dati che erano presenti nel file datainput.txt.

Gestione degli eventi di streaming e della contropressione in Node.js

La chiamata a pipe(), come mostrato sopra, nasconde due meccanismi che contano su larga scala: gli eventi emessi da ogni flusso e il sistema di contropressione che impedisce a una sorgente veloce di sovraccaricare una destinazione lenta.

  • dati: emesso da un flusso leggibile ogni volta che è disponibile un nuovo blocco.
  • end: emesso una volta che un flusso leggibile ha consegnato il suo ultimo blocco.
  • finire: emesso da un flusso scrivibile una volta che tutti i dati in coda sono stati svuotati.
  • errore: emesso da entrambi i tipi di flusso in caso di errore; gestirlo sempre, altrimenti il ​​processo si arresta in modo anomalo.

L'esempio seguente si mette in ascolto di tutti e quattro gli eventi gestendo manualmente la contropressione, lo stesso comportamento fornito automaticamente da 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);
});

La contropressione si verifica quando un flusso leggibile produce dati più velocemente di quanto un flusso scrivibile possa consumarli. Il metodo write() restituisce false una volta che il suo buffer è pieno, quindi il codice sopra mette in pausa la sorgente finché la destinazione non emette un evento di svuotamento, segnalando che è sicuro riprendere. pipe() esegue questo ciclo automaticamente.

⚠ Suggerimento: In produzione, è preferibile utilizzare la funzione pipeline() del modulo stream rispetto a una catena pipe() manuale, poiché inoltra gli errori da ogni stream e chiude automaticamente i descrittori di file.

Eventi in Node.js

Gli stream sono un esempio della progettazione basata sugli eventi di Node.js. Il resto di questo articolo esamina il funzionamento generale degli eventi, utilizzando la stessa classe EventEmitter su cui si basano internamente gli stream.

Gli eventi sono uno dei concetti chiave in Node.js e talvolta Node.js viene definito un framework basato sugli eventi.

Fondamentalmente un evento è qualcosa che accade. Ad esempio, se viene stabilita una connessione a un database, viene attivato l'evento di connessione al database. La programmazione guidata dagli eventi consiste nel creare funzioni che verranno attivate quando vengono attivati ​​eventi specifici.

Diamo un'occhiata a un esempio base di definizione di un evento in Node.js.

Creeremo un evento chiamato "data_received". Quando viene attivato questo evento, il testo "dati ricevuti" verrà inviato alla console.

Eventi in 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 Spiegazione:-

  1. Utilizza la funzione require per includere il modulo 'eventi'. Con questo modulo sarai in grado di creare eventi in Node.js.
  2. Crea un nuovo emettitore di eventi. Viene utilizzato per associare l'evento, che nel nostro caso è "data_received" a una funzione di callback definita nel passaggio 3.
  3. Definiamo una funzione guidata dagli eventi che dice che se nel caso in cui viene attivato l'evento "data_received", dovremmo inviare il testo "data_received" alla console.
  4. Infine, abbiamo un trigger manuale del nostro evento utilizzando la funzione eventEmiter.emit. Ciò attiverà l'evento data_received.

Quando il programma viene eseguito, il testo "dati ricevuti" verrà inviato alla console come mostrato di seguito.

Eventi in Node.js

Emissione di eventi

Oltre a emettere e ad ascoltare eventi, Node.js espone ulteriori metodi per controllare come i listener vengono registrati e ispezionati.

Quando si definiscono gli eventi, esistono diversi metodi per gli eventi che possono essere richiamati. Questo argomento si concentra sull'esame di ciascuno di essi in dettaglio.

  1. Gestori di eventi una tantum

A volte potresti essere interessato a reagire a un evento solo la prima volta che si verifica. In queste situazioni, puoi utilizzare il metodo once().

Vediamo come utilizzare il metodo once per i gestori di eventi.

Emissione di eventi

Code Spiegazione:-

  1. Qui stiamo usando il metodo 'once' per dire che per l'evento 'data_received', la funzione di callback dovrebbe essere eseguita solo una volta.
  2. Qui stiamo attivando manualmente l'evento 'data_received'.
  3. Quando l'evento 'data_received' viene nuovamente attivato, questa volta non accadrà nulla. Ciò è dovuto al primo passaggio in cui abbiamo detto che l'evento poteva essere attivato solo una volta.

Se il codice viene eseguito correttamente, l'output nel registro sarà "data_received con successo". Questo messaggio apparirà solo una volta nella console.

  1. Ispezione dei listener di eventi

In qualsiasi momento della sua vita, un emettitore di eventi può avere zero o più ascoltatori ad esso collegati. Gli ascoltatori per ciascun tipo di evento possono essere controllati in diversi modi.

Se sei interessato solo a determinare il numero di ascoltatori collegati, non cercare oltre il metodo EventEmitter.listenerCount().

(Nota: Gli ascoltatori sono importanti perché il programma principale dovrebbe sapere se gli ascoltatori vengono aggiunti al volo a un evento, altrimenti il ​​programma non funzionerà correttamente perché verranno chiamati ulteriori ascoltatori.)

Emissione di eventi

Code Spiegazione:-

  1. Stiamo definendo un tipo eventEmitter richiesto per utilizzare i metodi relativi agli eventi.
  2. Stiamo quindi definendo un oggetto chiamato emettitore che verrà utilizzato per definire i nostri gestori di eventi.
  3. Stiamo creando 2 gestori di eventi che praticamente non fanno nulla. Questo è mantenuto semplice per il nostro esempio solo per mostrare come funziona il metodo listenerCount.
  4. Ora, quando invochi il metodo listenerCount sul nostro evento data_received, invierà il numero di listener di eventi allegati a questo evento nel registro della console.

Se il codice viene eseguito correttamente, il valore 2 verrà mostrato nel registro della console.

  1. Il nuovo evento Listener

Ogni volta che viene registrato un nuovo gestore di eventi, l'emettitore di eventi emette un evento newListener. Questo evento viene utilizzato per rilevare nuovi gestori eventi. In genere utilizzi l'evento newListener quando devi allocare risorse o eseguire alcune azioni per ogni nuovo gestore eventi.

Emissione di eventi

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 Spiegazione:-

  1. Stiamo creando un nuovo gestore di eventi per l'evento "newListener". Pertanto, ogni volta che viene registrato un nuovo gestore eventi, nella console verrà visualizzato il testo "Listener aggiunto per" + il nome dell'evento.
  2. Qui stiamo scrivendo sulla console il testo “Listener aggiunto per” + il nome dell'evento per ogni evento registrato.
  3. Stiamo definendo 2 gestori di eventi per il nostro evento "data_received".

Se il codice precedente viene eseguito correttamente, nella console verrà visualizzato il testo seguente. Mostra semplicemente che il gestore eventi "newListener" è stato attivato due volte.

Aggiunto ascoltatore per eventi data_received

Aggiunto ascoltatore per eventi data_received

DOMANDE FREQUENTI

Il metodo `fs.readFile()` carica un intero file in memoria prima di restituirlo, mentre `fs.createReadStream()` consegna il file in piccoli blocchi man mano che arrivano. Gli stream utilizzano molta meno memoria per i file di grandi dimensioni e consentono l'avvio dell'elaborazione prima che la lettura sia completata.

Sì. Chiama .pipe() una volta per destinazione, ad esempio readStream.pipe(destinazioneA) seguito da readStream.pipe(destinazioneB). Node.js consegna gli stessi chunk a ogni destinazione e ogni stream scrivibile gestisce comunque la propria contropressione in modo indipendente.

Sì. È possibile instradare un flusso leggibile attraverso un flusso di trasformazione come zlib.createGzip(), quindi in un flusso scrivibile: readStream.pipe(zlib.createGzip()).pipe(writeStream). Il file viene compresso durante lo streaming, senza mai caricare l'intero file in memoria.

Possono redigere rapidamente codice di base per readStream e pipe, ma spesso omettono la gestione degli eventi di errore e la gestione della contropressione. È sempre consigliabile rivedere il codice di stream generato dall'IA per individuare entrambe le lacune prima di utilizzarlo in produzione.

Possono suggerire cause probabili, come un listener di svuotamento mancante o un evento di errore non gestito, ma per confermare la causa reale è comunque necessario controllare manualmente lo stato del flusso e i log dell'applicazione.

Riassumi questo post con: