Flujos de Node.js: FileStream, Pipes y Eventos

โšก Resumen inteligente

Node.js utiliza flujos para leer y escribir datos en pequeรฑos fragmentos en lugar de cargar archivos completos en la memoria, y esta pรกgina explica la creaciรณn de flujos de archivos, el encadenamiento de tuberรญas, el manejo de la contrapresiรณn y el modelo de eventos que mantiene la eficiencia en la transferencia de datos a gran escala.

  • Lรญnea Tipos de flujo: Node.js organiza cada flujo como legible, escribible, dรบplex o transformable, y cada API de Node.js devuelve uno de estos cuatro tipos.
  • ๐Ÿ“‚ Flujo de archivos: Las funciones fs.createReadStream() y fs.createWriteStream() leen y escriben archivos en fragmentos sin cargar el archivo completo en la memoria.
  • ๐Ÿ”— Piping: El mรฉtodo pipe() conecta un flujo de lectura directamente con un flujo de escritura y transfiere los datos entre ellos automรกticamente.
  • ๐Ÿ‡ง๐Ÿ‡ท Contrapresiรณn: La funciรณn write() devuelve false cuando su bรบfer estรก lleno, y el evento drain indica cuรกndo es seguro reanudar la escritura.
  • ๐Ÿ“ก Transmisiones en directo: Los estados data, end, error y finish marcan los momentos clave en el ciclo de vida de un flujo de datos y son los que rigen la mayor parte del cรณdigo de gestiรณn de flujos.
  • โšก Eventos personalizados: El mรณdulo events y EventEmitter permiten que cualquier aplicaciรณn Node.js defina, emita y escuche sus propios eventos con nombre.
  • ๐Ÿ› ๏ธ Control del oyente: once(), listenerCount() y el evento newListener gestionan cuรกntas veces se ejecuta un controlador y cรณmo se ejecutan los oyentes. tracrey

Tipos de flujos en Node.js

Cada flujo que expone Node.js se clasifica en uno de cuatro tipos bรกsicos, y conocer el tipo que devuelve una API determinada explica cรณmo se comporta dicho flujo antes de escribir cualquier cรณdigo. El mรณdulo de flujo que viene con Node.js define estas categorรญas, y las API de archivos, redes y compresiรณn que se utilizan en este artรญculo se basan en ellas.

Tipo de corriente Direcciรณn de datos API de ejemplo Caso de uso comรบn
Readable Solo fuente (los datos fluyen hacia afuera) fs.createReadStream() Leer un archivo, recibir un cuerpo de solicitud HTTP
Writable Solo destino (flujo de datos entrante) fs.createWriteStream() Escribir un archivo, enviar una respuesta HTTP
Duplex Ambas direcciones, independientemente net.Socket (socket TCP) Comunicaciรณn de red bidireccional
Transformaciรณn En ambas direcciones, los datos se modifican durante la transmisiรณn. zlib.createGzip() Comprimir o codificar datos en la fase intermedia del proceso.

Un flujo legible y un flujo escribible son los dos bloques de construcciรณn utilizados a lo largo de este artรญculo; ambos tambiรฉn impulsan un servidor web HTTP En Node.js, un flujo dรบplex combina ambos roles en un solo objeto, mientras que un flujo de transformaciรณn es un flujo dรบplex cuya salida modifica su entrada, como la compresiรณn gzip aplicada a mitad del flujo.

Flujo de archivos en Node.js

Una vez definidas las cuatro categorรญas de flujo, el resto de este artรญculo describe cรณmo se realiza una transferencia de archivos utilizando las API de flujo legible y escribible.

Node hace un uso extensivo de transmisiones como mecanismo de transferencia de datos.

Por ejemplo, cuando envรญa algo a la consola usando la funciรณn console.log, en realidad estรก usando una secuencia para enviar los datos a la consola.

Node.js tambiรฉn tiene la capacidad de transmitir datos desde archivos para que puedan leerse y escribirse adecuadamente. Ahora veremos un ejemplo de cรณmo podemos usar transmisiones para leer y escribir desde archivos. Necesitamos seguir los pasos que se mencionan a continuaciรณn para este ejemplo.

Paso 1) Cree un archivo llamado data.txt que tenga los siguientes datos. Supongamos que este archivo estรก almacenado en la unidad D de nuestra mรกquina local.

Tutorial sobre Node.js

Introducciรณn

Eventos

Generators

Conectividad de datos

Usando jazmรญn

Paso 2) Escriba el cรณdigo relevante que utilizarรก secuencias para leer datos del archivo.

Flujo de archivos en 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 Explicaciรณn:-

  1. Primero debemos incluir los mรณdulos 'fs' que contienen toda la funcionalidad necesaria para crear transmisiones.
  2. A continuaciรณn, creamos una secuencia legible utilizando el mรฉtodo โ€“ createReadStream. Como entrada, damos la ubicaciรณn de nuestro archivo data.txt.
  3. La funciรณn steam.on es un controlador de eventos y en รฉl, especificamos el primer parรกmetro como "datos". Esto significa que cada vez que ingresan datos en la secuencia del archivo, se ejecuta una funciรณn de devoluciรณn de llamada. En nuestro caso, estamos definiendo una funciรณn de devoluciรณn de llamada que realizarรก 2 pasos bรกsicos. La primera es convertir los datos leรญdos del archivo como una cadena. El segundo serรญa enviar la cadena convertida como salida a la consola.
  4. Tomamos cada fragmento de datos que se lee del flujo de datos y lo convertimos en una cadena.
  5. Finalmente, enviamos la salida de cada fragmento de cadena convertido a la consola.

Salida:

Flujo de archivos en Node.js

  • Si el cรณdigo se ejecuta correctamente, verรก el resultado anterior en la consola. Esta salida serรก la misma que la del archivo data.txt.

Escribir en un archivo

De la misma manera que creamos una secuencia de lectura, tambiรฉn podemos crear una secuencia de escritura para escribir datos en un archivo. Primero creemos un archivo vacรญo sin contenido llamado data.txt. Supongamos que este archivo estรก ubicado en la unidad D de nuestra computadora.

El siguiente cรณdigo muestra cรณmo podemos escribir datos en el archivo.

Flujo de archivos en 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 Explicaciรณn:-

  1. Estamos creando una secuencia de escritura utilizando el mรฉtodo โ€“ createWriteStream. Como entrada, damos la ubicaciรณn de nuestro archivo data.txt.
  2. A continuaciรณn usamos stream.write un mรฉtodo para escribir las diferentes lรญneas de texto en nuestro archivo de texto. La transmisiรณn se encargarรก de escribir estos datos en el archivo data.txt.

Si abre el archivo data.txt, ahora verรก los siguientes datos en el archivo

Tutorial sobre Node.js

Introducciรณn

Eventos

Generators

Conectividad de datos

El uso de Jazmรญn

Tuberรญas en Node.js

Leer y escribir flujos de forma independiente es รบtil, pero Node.js tambiรฉn permite que un flujo de lectura se conecte directamente a un flujo de escritura, transfiriendo datos sin necesidad de cรณdigo de almacenamiento en bรบfer manual.

Dentro de las aplicaciones de Node, las transmisiones se pueden canalizar entre sรญ utilizando el mรฉtodo pipe(), que toma dos argumentos:

  • Una secuencia de escritura requerida que actรบa como destino de los datos y
  • Un objeto opcional utilizado para pasar opciones.

Un ejemplo tรญpico de uso de canalizaciones, si desea transferir datos de un archivo a otro.

Entonces, veamos un ejemplo de cรณmo podemos transferir datos de un archivo a otro usando tuberรญas.

Paso 1) Cree un archivo llamado datainput.txt que tenga los siguientes datos. Supongamos que este archivo estรก almacenado en la unidad D de nuestra mรกquina local.

Tutorial sobre Node.js

Introducciรณn

Eventos

Generators

Conectividad de datos

Usando jazmรญn

Paso 2) Cree un archivo vacรญo en blanco llamado dataOutput.txt y colรณquelo en la unidad D de su mรกquina local.

Paso 3) Escriba el siguiente cรณdigo para llevar a cabo la transferencia de datos desde el archivo datainput.txt al archivo dataOutput.txt.

Tuberรญas en Node.js

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

Code Explicaciรณn:-

  1. Primero estamos creando una "secuencia de lectura" para nuestro archivo datainput.txt que contiene todos nuestros datos que deben transferirse al nuevo archivo.
  2. Luego necesitamos crear un "flujo de escritura" en nuestro archivo dataOutput.txt, que es nuestro archivo vacรญo y es el destino para la transferencia de datos desde el archivo datainput.txt.
  3. Luego usamos el comando pipe para transferir los datos del flujo de lectura al flujo de escritura. El comando pipe tomarรก todos los datos que ingresan al flujo de lectura y los enviarรก al flujo de escritura.

Si ahora abre el archivo dataOutput.txt, verรก todos los datos que estaban presentes en el archivo datainput.txt.

Eventos de transmisiรณn y contrapresiรณn en Node.js

Llamar a pipe(), como se muestra arriba, oculta dos mecanismos importantes a gran escala: los eventos que emite cada flujo y el sistema de contrapresiรณn que impide que una fuente rรกpida sature un destino lento.

  • datos: emitido por un flujo legible cada vez que hay un nuevo fragmento disponible.
  • fin: Se emite una vez que una secuencia legible ha entregado su รบltimo fragmento.
  • terminar: emitido por un flujo escribible una vez que se han vaciado todos los datos en cola.
  • error: Emitido por cualquiera de los tipos de flujo en caso de fallo; siempre manรฉjelo o el proceso fallarรก.

El ejemplo que se muestra a continuaciรณn escucha los cuatro eventos mientras gestiona manualmente la contrapresiรณn, el mismo comportamiento que pipe() proporciona automรกticamente.

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 contrapresiรณn se produce cuando un flujo de lectura genera datos mรกs rรกpido de lo que un flujo de escritura puede consumirlos. El mรฉtodo `write()` devuelve `false` una vez que su bรบfer se llena, por lo que el cรณdigo anterior pausa el origen hasta que el destino emite un evento de drenaje, lo que indica que es seguro reanudarlo. El mรฉtodo `pipe()` realiza este ciclo automรกticamente.

โš  Consejo: En producciรณn, es preferible usar la funciรณn pipeline() del mรณdulo stream en lugar de una cadena pipe() manual, ya que reenvรญa los errores de cada flujo y cierra automรกticamente los descriptores de archivo.

Eventos en Node.js

Los flujos son un ejemplo del diseรฑo orientado a eventos de Node.js. El resto de este artรญculo analiza cรณmo funcionan los eventos en general, utilizando la misma clase EventEmitter en la que se basan internamente los flujos.

Los eventos son uno de los conceptos clave en Node.js y, a veces, se hace referencia a Node.js como un marco basado en eventos.

Bรกsicamente, un evento es algo que sucede. Por ejemplo, si se establece una conexiรณn con una base de datos, se activa el evento de conexiรณn de la base de datos. La programaciรณn impulsada por eventos consiste en crear funciones que se activarรกn cuando se activen eventos especรญficos.

Veamos un ejemplo bรกsico de definiciรณn de un evento en Node.js.

Vamos a crear un evento llamado "datos_recibidos". Cuando se activa este evento, el texto "datos recibidos" se enviarรก a la consola.

Eventos en 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 Explicaciรณn:-

  1. Utilice la funciรณn require para incluir el mรณdulo 'eventos'. Con este mรณdulo, podrรกs crear eventos en Node.js.
  2. Crea un nuevo emisor de eventos. Esto se utiliza para vincular el evento, que en nuestro caso es "datos_recibidos" a una funciรณn de devoluciรณn de llamada que se define en el paso 3.
  3. Definimos una funciรณn controlada por eventos que dice que si en caso de que se active el evento "data_received", debemos enviar el texto "data_received" a la consola.
  4. Finalmente, tenemos un disparador manual de nuestro evento usando la funciรณn eventEmiter.emit. Esto activarรก el evento data_received.

Cuando se ejecuta el programa, el texto "datos recibidos" se enviarรก a la consola como se muestra a continuaciรณn.

Eventos en Node.js

Eventos de emisiรณn

Ademรกs de emitir y escuchar eventos, Node.js ofrece mรฉtodos adicionales para controlar cรณmo se registran e inspeccionan los oyentes.

Al definir eventos, existen diferentes mรฉtodos para los eventos que se pueden invocar. Este tema se centra en mirar cada uno de ellos en detalle.

  1. Controladores de eventos รบnicos

A veces puede que le interese reaccionar ante un evento sรณlo la primera vez que ocurre. En estas situaciones, puede utilizar el mรฉtodo once().

Veamos cรณmo podemos utilizar el mรฉtodo once para controladores de eventos.

Eventos de emisiรณn

Code Explicaciรณn:-

  1. Aquรญ estamos usando el mรฉtodo "once" para decir que para el evento "data_received", la funciรณn de devoluciรณn de llamada solo debe ejecutarse una vez.
  2. Aquรญ estamos activando manualmente el evento "datos_recibidos".
  3. Cuando el evento 'data_received' se active nuevamente, esta vez no sucederรก nada. Esto se debe al primer paso en el que dijimos que el evento solo podรญa activarse una vez.

Si el cรณdigo se ejecuta correctamente, el resultado en el registro serรก "datos_recibidos correctamente". Este mensaje solo aparecerรก una vez en la consola.

  1. Inspeccionar oyentes de eventos

En cualquier momento de su vida, un emisor de eventos puede tener cero o mรกs oyentes asociados. Los oyentes de cada tipo de evento se pueden inspeccionar de varias maneras.

Si solo estรก interesado en determinar el nรบmero de oyentes conectados, no busque mรกs que el mรฉtodo EventEmitter.listenerCount().

(Nota: Los oyentes son importantes porque el programa principal debe saber si se estรกn agregando oyentes sobre la marcha a un evento; de lo contrario, el programa funcionarรก mal porque se llamarรก a oyentes adicionales).

Eventos de emisiรณn

Code Explicaciรณn:-

  1. Estamos definiendo un tipo eventEmitter que es necesario para utilizar los mรฉtodos relacionados con eventos.
  2. Luego estamos definiendo un objeto llamado emisor que se utilizarรก para definir nuestros controladores de eventos.
  3. Estamos creando 2 controladores de eventos que bรกsicamente no hacen nada. En nuestro ejemplo, esto se mantiene simple para mostrar cรณmo funciona el mรฉtodo listenerCount.
  4. Ahora, cuando invoque el mรฉtodo listenerCount en nuestro evento data_received, enviarรก la cantidad de detectores de eventos adjuntos a este evento en el registro de la consola.

Si el cรณdigo se ejecuta correctamente, el valor de 2 se mostrarรก en el registro de la consola.

  1. El nuevo evento Listener

Cada vez que se registra un nuevo controlador de eventos, el emisor de eventos emite un evento newListener. Este evento se utiliza para detectar nuevos controladores de eventos. Normalmente utiliza el evento newListener cuando necesita asignar recursos o realizar alguna acciรณn para cada nuevo controlador de eventos.

Eventos de emisiรณn

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 Explicaciรณn:-

  1. Estamos creando un nuevo controlador de eventos para el evento "newListener". Entonces, cada vez que se registra un nuevo controlador de eventos, el texto "Oyente agregado para" + el nombre del evento se mostrarรก en la consola.
  2. Aquรญ escribimos en la consola el texto โ€œOyente agregado paraโ€ + el nombre del evento para cada evento registrado.
  3. Estamos definiendo 2 controladores de eventos para nuestro evento 'data_received'.

Si el cรณdigo anterior se ejecuta correctamente, el siguiente texto se mostrarรก en la consola. Simplemente muestra que el controlador de eventos 'newListener' se activรณ dos veces.

Se agregรณ un oyente para eventos data_received

Se agregรณ un oyente para eventos data_received

Preguntas Frecuentes

fs.readFile() carga un archivo completo en la memoria antes de devolverlo, mientras que fs.createReadStream() entrega el archivo en pequeรฑos fragmentos a medida que llegan. Los flujos utilizan mucha menos memoria para archivos grandes y permiten que el procesamiento comience antes de que finalice la lectura.

Sรญ. Llama a .pipe() una vez por destino, por ejemplo, readStream.pipe(destinoA) seguido de readStream.pipe(destinoB). Node.js entrega los mismos fragmentos a cada destino, y cada flujo de escritura gestiona su propia contrapresiรณn de forma independiente.

Sรญ. Se puede canalizar un flujo de lectura a travรฉs de un flujo de transformaciรณn como zlib.createGzip(), y luego a un flujo de escritura: readStream.pipe(zlib.createGzip()).pipe(writeStream). El archivo se comprime a medida que se transmite, sin necesidad de cargarlo completamente en la memoria.

Pueden redactar rรกpidamente cรณdigo repetitivo para readStream y pipe, pero con frecuencia omiten el controlador de eventos de error y el manejo de la contrapresiรณn. Siempre revise el cรณdigo de flujo generado por IA para detectar estas deficiencias antes de usarlo en producciรณn.

Pueden sugerir posibles causas, como la falta de un detector de drenaje o un evento de error no controlado, pero para confirmar la causa real aรบn es necesario revisar manualmente el estado del flujo y los registros de la aplicaciรณn.

Resumir este post con: