Streams em Node.js podem ser complicados, mas vale a pena dedicar um tempo para entendê-los.

Principais conclusões

  • Streams em Node.js são uma ferramenta fundamental para processamento e transferência de dados, tornando-os ideais para aplicativos em tempo real e orientados a eventos.
  • Para criar um fluxo gravável em Node.js, você pode usar a função createWriteStream() do módulo fs, que grava dados em um local específico.
  • Legível, gravável, duplex e transformação são os quatro tipos de fluxos no Node.js, cada um com seu próprio caso de uso e funcionalidade.

Um stream é uma ferramenta de programação fundamental que lida com o fluxo de dados. Basicamente, um fluxo normalmente representa a transferência sequencial de bytes de um ponto para outro. A documentação oficial do Node.js define um stream como uma interface abstrata que você pode usar para trabalhar com dados.

A transferência de dados em um computador ou em uma rede é um uso ideal de um fluxo.

Fluxos em Node.js

Os streams desempenharam um papel essencial no sucesso do Node.js. Eles são ideais para processamento de dados em tempo real e aplicativos orientados a eventos, dois recursos importantes do ambiente de execução Node.js.

Para criar um novo stream em Node.js, você precisará usar a API stream, que funciona exclusivamente com Strings e Dados de buffer do Node.js. Node.js possui quatro tipos de fluxos: gravável, legível, duplex e transformação.

Como criar e usar um fluxo gravável

Um fluxo gravável permite gravar ou enviar dados para um local específico. O módulo fs (sistema de arquivos) possui uma classe WriteStream, que você pode usar para criar um novo fluxo com o fs.createWriteStream() função. Esta função aceita o caminho para o arquivo no qual você deseja gravar os dados, bem como um conjunto opcional de opções.

const {createWriteStream} = require("fs");

(() => {
const file = "myFile.txt";
const myWriteStream = createWriteStream(file);
let x = 0;
const writeNumber = 10000;

const writeData = () => {
while (x < writeNumber) {
const chunk = Buffer.from(`${x}, `, "utf-8");
if (x writeNumber - 1) return myWriteStream.end(chunk);
if (!myWriteStream.write(chunk)) break;
x++
}
};

writeData();
})();

Este código importa o criarWriteStream() função, que a função de seta anônima em seguida, usa para criar um fluxo que grava dados em myFile.txt. A função anônima contém uma função interna chamada escreverDados() que grava dados.

O criarWriteStream() A função funciona com um buffer para gravar uma coleção de números (0–9.999) no arquivo de destino. Porém, ao executar o script acima, ele cria um arquivo no mesmo diretório que contém os seguintes dados:

A coleção atual de números termina em 2.915, mas deveria incluir números até 9.999. Essa discrepância ocorre porque cada WriteStream usa um buffer que armazena uma quantidade fixa de dados por vez. Para saber qual é esse valor padrão, você precisará consultar o alto WaterMark opção.

console.log("The highWaterMark value is: " +
myWriteStream.writableHighWaterMark + " bytes.");

Adicionar a linha de código acima à função anônima produzirá a seguinte saída no terminal:

A saída do terminal mostra que o padrão alto WaterMark valor (que é personalizável) é 16.384 bytes. Isso significa que você só pode armazenar menos de 16.384 bytes de dados neste buffer por vez. Portanto, até o número 2.915 (mais todas as vírgulas e espaços) representa a quantidade máxima de dados que o buffer pode armazenar de uma vez.

A solução para o erro de buffer é usar um evento de fluxo. Um fluxo encontra vários eventos em estágios distintos do processo de transferência de dados. O ralo event é a opção adequada para esta situação.

No escreverDados() função acima, a chamada para o Gravação do WriteStream() função retorna verdadeiro se o pedaço de dados (ou buffer interno) estiver abaixo do alto WaterMark valor. Isso indica que o aplicativo pode enviar mais dados para o stream. Contudo, assim que escrever() função retorna falso, o loop é interrompido porque você precisa drenar o buffer.

myWriteStream.on('drain', () => {
console.log("a drain has occurred...");
writeData();
});

Inserindo o ralo código de evento acima na função anônima esvaziará o Buffer do WriteStream quando estiver na capacidade. Em seguida, ele relembra o escreverDados() método, para que ele possa continuar gravando dados. A execução do aplicativo atualizado produzirá a seguinte saída:

Você deve observar que o aplicativo teve que drenar o Buffer WriteStream três vezes durante sua execução. O arquivo de texto também sofreu algumas alterações:

Como criar e usar um fluxo legível

Para ler dados, comece criando um fluxo legível usando o fs.createReadStream() função.

const {createReadStream} = require("fs");

(() => {
const file = "myFile.txt";
const myReadStream = createReadStream(file);

myReadStream.on("open", () => {
console.log(`The read stream has successfully opened ${file}.`);
});

myReadStream.on("data", chunk => {
console.log("The file contains the following data: " + chunk.toString());
});

myReadStream.on("close", () => {
console.log("The file has been successfully closed.");
});
})();

O script acima usa o criarReadStream() método para acessar o arquivo que o código anterior criou: myFile.txt. O criarReadStream() A função aceita um caminho de arquivo (que pode estar na forma de string, buffer ou URL) e várias opções opcionais como argumentos.

Na função anônima, existem vários eventos de fluxo importantes. No entanto, não há sinal do ralo evento. Isso ocorre porque um fluxo legível apenas armazena dados em buffer quando você chama o método stream.push (pedaço) funcionar ou usar o legível evento.

O abrir O evento é acionado quando fs abre o arquivo que você deseja ler. Quando você anexa o dados evento para um fluxo implicitamente contínuo, faz com que o fluxo faça a transição para o modo de fluxo. Isso permite que os dados passem assim que estiverem disponíveis. A execução do aplicativo acima produz a seguinte saída:

Como criar e usar um fluxo duplex

Um fluxo duplex implementa interfaces de fluxo graváveis ​​e legíveis, para que você possa ler e gravar nesse fluxo. Um exemplo é um soquete TCP que depende do módulo net para sua criação.

Uma maneira simples de demonstrar as propriedades de um fluxo duplex é criar um servidor e cliente TCP que transfira dados.

O arquivo server.js

const net = require('net');
const port = 5000;
const host = '127.0.0.1';

const server = net.createServer();

server.on('connection', (socket)=> {
console.log('Connection established from client.');

socket.on('data', (data) => {
console.log(data.toString());
});

socket.write("Hi client, I am server " + server.address().address);

socket.on('close', ()=> {
console.log('the socket is closed')
});
});

server.listen(port, host, () => {
console.log('TCP server is running on port: ' + port);
});

O arquivo client.js

const net = require('net');
const client = new net.Socket();
const port = 5000;
const host = '127.0.0.1';

client.connect(port, host, ()=> {
console.log("connected to server!");
client.write("Hi, I'm client " + client.address().address);
});

client.on('data', (data) => {
console.log(data.toString());
client.write("Goodbye");
client.end();
});

client.on('end', () => {
console.log('disconnected from server.');
});

Você notará que os scripts do servidor e do cliente usam um fluxo legível e gravável para se comunicar (transferir e receber dados). Naturalmente, o aplicativo do servidor é executado primeiro e começa a escutar conexões. Assim que você inicia o cliente, ele se conecta ao servidor usando o número da porta TCP.

Depois de estabelecer uma conexão, o cliente inicia a transferência de dados gravando no servidor usando seu WriteStream. O servidor registra os dados que recebe no terminal e, em seguida, grava os dados usando seu WriteStream. Finalmente, o cliente registra os dados que recebe, grava dados adicionais e depois se desconecta do servidor. O servidor permanece aberto para que outros clientes se conectem.

Como criar e usar um fluxo de transformação

Os fluxos de transformação são fluxos duplex nos quais a saída está relacionada, mas diferente da entrada. Node.js tem dois tipos de streams de transformação: zlib e crypto streams. Um fluxo zlib pode compactar um arquivo de texto e descompactá-lo após a transferência do arquivo.

O aplicativo compressFile.js

const zlib = require('zlib');
const { createReadStream, createWriteStream } = require('fs');

(() => {
const source = createReadStream('myFile.txt');
const destination = createWriteStream('myFile.txt.gz');

source.pipe(zlib.createGzip()).pipe(destination);
})();

Este script simples pega o arquivo de texto original, compacta-o e armazena-o no diretório atual. Este é um processo simples graças ao fluxo legível cano() método. Os pipelines de fluxo removem o uso de buffers e canalizam dados diretamente de um fluxo para outro.

No entanto, antes que os dados cheguem ao fluxo gravável no script, é necessário um pequeno desvio por meio do método createGzip() do zlib. Este método compacta o arquivo e retorna um novo objeto Gzip que o fluxo de gravação recebe.

O aplicativo decompressFile.js

const zlib = require('zlib'); 
const { createReadStream, createWriteStream } = require('fs');
 
(() => {
const source = createReadStream('myFile.txt.gz');
const destination = createWriteStream('myFile2.txt');

source.pipe(zlib.createUnzip()).pipe(destination);
})();

Este script acima pega o arquivo compactado e o descompacta. Se você abrir o novo meuArquivo2.txt arquivo, você verá que ele contém os mesmos dados do arquivo original:

Por que os fluxos são importantes?

Os fluxos melhoram a eficiência da transferência de dados. Fluxos legíveis e graváveis ​​servem como base que permite a comunicação entre clientes e servidores, bem como a compactação e transferência de arquivos grandes.

Os streams também melhoram o desempenho das linguagens de programação. Sem fluxos, o processo de transferência de dados torna-se mais complexo, exigindo maior entrada manual dos desenvolvedores e resultando em mais erros e problemas de desempenho.