- O que são streams
- Por quê streams
- Um exemplo de streams
- pipe()
- APIs Node.js baseadas em streams
- Diferentes tipos de stream
- Como criar um readable stream
- Como criar um writable stream
- Como lemos dados de um readable stream
- Como enviar dados para um writable stream
- Sinalizando para um writable stream que você terminou de escrever
- Como criar um transform stream
Os streams são um dos conceitos fundamentais que alimentam os aplicativos Node.js.
Eles são uma maneira de lidar com a leitura/gravação de arquivos, comunicações de rede ou qualquer tipo de troca de informações de ponta a ponta de maneira eficiente.
Streams não são um conceito exclusivo do Node.js. Eles foram introduzidos no sistema operacional Unix décadas atrás, e os programas podem interagir uns com os outros passando fluxos através do operador pipe (|
).
Por exemplo, da maneira tradicional, quando você diz ao programa para ler um arquivo, o arquivo é lido na memória, do início ao fim, e então você o processa.
Usando streams você lê peça por peça, processando seu conteúdo sem manter tudo na memória.
O módulo stream
do Node.js fornece a base sobre a qual todas as APIs de streaming são criadas. Todos os streams são instâncias de EventEmitter.
Streams basicamente fornecem duas vantagens principais sobre o uso de outros métodos de manipulação de dados:
- Eficiência de memória: você não precisa carregar grandes quantidades de dados na memória antes de poder processá-los.
- Eficiência de tempo: leva muito menos tempo para iniciar o processamento de dados, pois você pode iniciar o processamento assim que os tiver, em vez de esperar até que toda a carga de dados esteja disponível.
Um exemplo típico é a leitura de arquivos de um disco.
Usando o módulo fs do Node.js, você pode ler um arquivo e servi-lo por HTTP quando uma nova conexão for estabelecida com seu servidor HTTP:
const http = require('http');
const fs = require('fs');
const server = http.createServer(function (req, res) {
fs.readFile(`${__dirname}/data.txt`, (err, data) => {
res.end(data);
});
});
server.listen(3000);
readFile()
lê o conteúdo completo do arquivo e invoca a função de callback quando termina.
res.end(data)
na callback retornará o conteúdo do arquivo para o cliente HTTP.
Se o arquivo for grande, a operação levará um pouco de tempo. Aqui está a mesma coisa escrita usando streams:
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
const stream = fs.createReadStream(`${__dirname}/data.txt`);
stream.pipe(res);
});
server.listen(3000);
Em vez de esperar até que o arquivo seja totalmente lido, começamos a transmiti-lo para o cliente HTTP assim que temos um bloco de dados pronto para ser enviado.
O exemplo acima usa a linha stream.pipe(res)
: o método pipe()
é chamado no stream do arquivo.
O que faz este código? Ele pega a fonte e a canaliza para um destino.
Você o chama no stream de origem, portanto, nesse caso, o stream de arquivo é canalizado para a resposta HTTP.
O valor de retorno do método pipe()
é o stream de destino, que é uma coisa muito conveniente que nos permite encadear várias chamadas pipe()
, assim:
src.pipe(des1).pipe(dest2);
Esta construção é o mesmo que fazer:
src.pipe(dest1);
dest1.pipe(dest2);
Devido às suas vantagens, muitos módulos principais do Node.js fornecem recursos nativos de manipulação de stream, principalmente:
process.stdin
retorna um stream conectada ao stdin.process.stdout
retorna um stream conectado ao stdout.process.stderr
retorna um stream conectado ao stderr.fs.createReadStream()
cria um arquivo readable stream.fs.createWriteStream()
cria um arquivo writable stream.net.connect()
inicia uma conexão baseada em stream.http.request()
retorna uma instância da classehttp.ClientRequest
, que é um writable stream.zlib.createGzip()
compacta um dado usando gzip (um algoritmo de compressão) para um stream.zlib.createGunzip()
descompacta um stream gzip.zlib.createDeflate()
compacta dados usando deflate (um algoritmo de compressão) para um stream.zlib.createInflate()
descompacta um stream deflate.
Existem quatro classes de stream:
Readable
: um stream que pode ser usado para ler dados dele. Em outras palavras, é apenasreadonly
(RO).Writable
: um stream que pode ser usado para gravar dados nele. É apenaswriteonly
(WO).Duplex
: um stream que pode ler e gravar dados, basicamente é uma combinação de umReadable
stream eWritable
stream.Transform
: um streamDuplex
que lê os dados, transforma os dados e, em seguida, grava os dados transformados no formato desejado.
Obtemos o Readable stream do módulo stream
, inicializamos e implementamos com o método readable._read()
.
Primeiro crie um objeto de stream:
const Stream = require('stream');
const readableStream = new Stream.Readable();
então implemente o _read
:
readableStream._read = () => {};
Você também pode implementar o _read
usando a opção read
:
const readableStream = new Stream.Readable({
read() {},
});
Agora que a stream foi inicializada, nós podemos enviar dados para ela:
readableStream.push('hi!');
readableStream.push('ho!');
Para criar um writable stream, estendemos o objeto Writable básico e implementamos seu método _write()
.
Primeiro crie um objeto de stream:
const Stream = require('stream');
const writableStream = new Stream.Writable();
então implemente o _write
:
writableStream._write = (chunk, encoding, next) => {
console.log(chunk.toString());
next();
};
Agora você pode canalizar um readable stream em:
process.stdin.pipe(writableStream);
Como lemos dados de um readable stream? Usando um writable stream:
const Stream = require('stream');
const readableStream = new Stream.Readable({
read() {},
});
const writableStream = new Stream.Writable();
writableStream._write = (chunk, encoding, next) => {
console.log(chunk.toString());
next();
};
readableStream.pipe(writableStream);
readableStream.push('hi!');
readableStream.push('ho!');
Você também pode consumir um readable stream diretamente, usando o readable
event:
readableStream.on('readable', () => {
console.log(readableStream.read());
});
Usando o método write()
.
writableStream.write('hey!\n');
Use o método end()
:
const Stream = require('stream');
const readableStream = new Stream.Readable({
read() {},
});
const writableStream = new Stream.Writable();
writableStream._write = (chunk, encoding, next) => {
console.log(chunk.toString());
next();
};
readableStream.pipe(writableStream);
readableStream.push('hi!');
readableStream.push('ho!');
readableStream.on('close', () => writableStream.end());
writableStream.on('close', () => console.log('ended'));
readableStream.destroy();
No exemplo acima, end()
é chamado dentro de um listener para o evento close
no readable stream para garantir que ele não seja chamado antes que todos os eventos de gravação tenham passado pelo pipe, pois isso faria com que um evento error
fosse emitido. Chamar destroy()
no readable stream faz com que o evento close ja emitido. O listener do evento close
no writable stream demonstra a conclusão do processo conforme ele é emitido após a chamada para end()
.
Obtemos o Transform stream do módulo stream
, inicializamos e implementamos com o método transform._transform()
.
Primeiro, crie um objeto transform stream:
const { Transform } = require('stream');
const transformStream = new Transform();
então implemetamos o _transform
:
transformStream._transform = (chunk, encoding, callback) => {
transformStream.push(chunk.toString().toUpperCase());
callback();
};
Pipe para um readable stream:
process.stdin.pipe(transformStream).pipe(process.stdout);