Stream初识

235 阅读5分钟

说明:文章中的代码运行环境是Nodejs v12

Stream是什么?

了解Stream前需要知道流。
流是用于Node.js中处理数据的抽象接口,流可以是可读的、可写的、或者两个都可以的。所有的流都是EventEmitter的实例。
Node.js提供了许多流对象,HTTP Request、HTTP Response、process.stdout都是流的实例。
Stream模块提供了实现流接口的APIXnip2022-07-14_21-12-31.jpg github.com/nodejs/node…

流使用案例—文件下载

nodejs内存了解

使用文件读取方式响应

const http = require('http');
const fs = require('fs');
const path = require('path');

function showMemory() {
    const format = (bytes) => {
        return (bytes / 1024 / 1024).toFixed(2) + ' MB';
    };
    let mem = process.memoryUsage();
    let str = 'Process memory \n ';
    Object.keys(mem).forEach(key => {
        str += key + ':' + format(mem[key])+ '  ';
    });
    console.log(str);
}


http.createServer((req, res) => {
    const filename = 'read.txt';// 6.4MB
    const filepath = path.resolve(__dirname, filename);
    console.log('filepath: ' + filepath);
    showMemory();
    console.time('file');

    res.setHeader("Content-Type", "application/octet-stream");
    res.setHeader("Content-Disposition", "attachment; filename="+encodeURI(filename));

    fs.readFile(filepath, (_, data) => {
        res.write(data);
        res.end();
        
        console.timeEnd('file');
        console.log('**** response end **** \n');
        showMemory();
    });

  }).listen(3000);

Xnip2022-07-14_21-00-24.jpg 进程使用的内存,明显增加,增量和文件大小接近

使用stream下载文件

http.createServer((req, res) => {
    const filename = 'read.txt';// 6.4MB
    const filepath = path.resolve(__dirname, filename);
    console.log('filepath: ' + filepath);
    showMemory();
    console.time('file');
    
    // 读取
    const readerStream = fs.createReadStream(filepath);
    readerStream.pipe(res);
    
    res.setHeader("Content-Type", "application/octet-stream");
    res.setHeader("Content-Disposition", "attachment; filename="+encodeURI(filename));
    console.log('**** response end **** \n');
    console.timeEnd('file');
    showMemory();

  }).listen(3000);

Xnip2022-07-14_20-56-25.jpg 进程内存变化比较小,响应时间也更短

流类型

Node.js有四种基本的流类型:

  • 可读流(stream.Readable)
    可以从中读取数据的流,如fs.createWriteStream()
  • 可写流(stream.Writeable)
    可以写入数据的流,如fs.createReadStream()
  • 双向流(stream.Duplex)
    可以写、读,如net.Socket
  • 转换流(stream.Transform)
    基于双向流,在读或写的时候用来更改或转换数据,如zlib.createDeflate()

流消费者的API

所有的流都是EventEmitter的实例,可以通过on来监听事件。
可读流、可写流的事件和函数较多,这里只列出了一些重要的,流的属性这里未说明,可去官方文档查看详细完整内容。

可读流

Events

  • data 流将数据的所有权移交给消费者时触发。
  • end 流中没有更多数据供消费者使用时触发
  • pause/resume 暂停/恢复
  • close 当流及其底层资源(如文件描述符)都已关闭,则触发close事件。该事件表明不再触发更多事件,并且不会发生进一步计算。
  • error
  • readable

Methods

  • setEncoding(string)
  • pause/resume()
  • pipe/unpipe(WriteableStream)
  • read([size])

demo

const fs = require('fs');

// 打开一个流:
const rs = fs.createReadStream('small.txt');

const chunks = [];
let size = 0;
// 流传给消费者一个数据块的时候触发
rs.on('data', function (chunk) {
    console.log('DATA:')
    chunks.push(chunk);
    size = size + chunk.byteLength;
});
// 流中没有可以消费的数据时触发
rs.on('end', function () {
    console.log('END, file content is: ');
    const buffer = Buffer.concat(chunks, size);
    console.log(buffer.toString('utf8'));
});

rs.on('error', function (err) {
    console.log('ERROR: ' + err);
});

fs.createReadStream默认的hightwaterMark是64kb(stream 的Readable是16kb),所以这里的data只触发了一次

Xnip2022-07-18_09-17-33.jpg

改变hightWaterMark的值,使data可以多次触发。(图中注释有误, 应该是10byte) Xnip2022-07-18_09-23-33.jpg

两种模式

可读流有两种模式,会在读取数据过程中进行转换 Xnip2022-07-19_08-51-14.jpg

const fs = require('fs');

// 打开一个流,highWaterMark每次读取100 byte, small.txt有701 byte
const rs = fs.createReadStream('small.txt', {highWaterMark: 100});

rs.setEncoding('utf8');

let count = 0;
// 流传给消费者一个数据块的时候触发
rs.on('data', function (chunk) {
    count++;
    console.log('触发了data事件:count = ' + count);
    if(count === 5) {
        rs.pause();
        console.log('\n调用pause,转为暂停模式');
        console.log('stream readableFlowing: ', rs.readableFlowing);
        console.log('stream isPaused: ', rs.isPaused());
        setTimeout(() => {
            console.log('\n1秒后,调用resume方法,转为流动模式');
            rs.resume();
        }, 1000);
    }
});
// 流中没有可以消费的数据时触发
rs.on('end', function () {
    console.log('END');
});

rs.on('resume', function () {
    console.log('触发了resume事件');
});

Xnip2022-07-19_09-04-47.jpg

可写流

Events

  • drain 如果stream.write(chunk)返回false,drain事件将在适合继续写入时触发
  • finish 调用stream.end(),并且所有数据都刷新到底层系统后,则触发finish事件
  • pipe 可读流上调用stream.pipe(wirteStream),将此可写流添加到目标集时,触发pipe事件
  • unpipe 可读流上调用stream.unpipe(wirteStream),将此可写流移出目标集时,触发unpipe事件
  • close
  • error

Methods

  • setDefaultEncoding(encoding)
  • write(chunk[,encoding][,callback])
    如果内部缓冲区小于后创建流时配置的highWaterMark,则返回值为 true。
    如果返回 false,则应停止继续将数据写入流,直到发出 'drain' 事件
  • end(chunk[,encoding][,callback])
    不再有数据写入可写流

demo

const fs = require('fs');

const ws = fs.createWriteStream('write.txt', {
    highWaterMark: 4
});

ws.setDefaultEncoding('utf8');

console.log(ws.write('立'));
console.log('缓冲区中写入的字节数', ws.writableLength);
console.log(ws.write('夏前后 '));

ws.on('drain', () => {
    console.log('\n触发了drain事件');
    console.log('缓冲区中写入的字节数', ws.writableLength);
    console.log(ws.write('种瓜点豆'));
    ws.end();
});
ws.on('close',  () => {
    console.log('\n触发了close事件');
});
ws.on('finish', () => {
    console.log('\n触发了finish事件');
});

Xnip2022-07-20_09-29-47.jpg

双工流

双工流,同时实现了可读流和可写流的API。注意双工流的可读流和可写流是相互独立的。

client.js

const net = require('net');

const client = net.connect({port: 3000, host: '127.0.0.1'}, function() {
    console.log('Client: 与服务器端连接完成');
    // 可写流的API
    client.write('hello Server');
});

const chunks = [];
let size = 0;
// 可读流的API
client.on('data', function(chunk) {
    chunks.push(chunk);
    size = size + chunk.byteLength;
});

client.on('end', function() {
    console.log('\nClient: 触发了end事件');
    const buffer = Buffer.concat(chunks, size);
    console.log('Client: 收到服务器端数据,内容为{'+ buffer.toString('utf8') +'}');

});

client.on('close', function() {
    console.log('\nClient: 触发了close事件');
});

client即是双工流,可写流用于向服务器端发送数据,可读流用户接收服务器端的响应,二者互不干扰。下面的server对象同理。

server.js

const net = require('net');

// tcp服务端
const server = net.createServer(function(socket){
    console.log('Server: 收到来自客户端的请求');

    socket.on('data', function(data) {
        // 给客户端返回数据
        console.log('\nServer: 收到客户端数据,内容为{'+ data +'}');
        socket.write('hello Client');
        socket.end();
    });

    socket.on('end', function(){
        console.log('\nServer: 触发了end事件');

    });

    socket.on('close', function(){
         console.log('\nServer: 触发了close事件');
    });
});

server.listen(3000, '127.0.0.1', function(){
    console.log('Server: listen 3000');
});

Xnip2022-07-24_16-34-01.jpg

转换流

转换流也是双工流,它的区别是转换流中可读流和可写流有关联。
对数据先执行写入流,处理后通过可读流将数据响应出来。
常见的转换流有zlib, crypto

const crypto = require('crypto');

const md5 = crypto.createHash('md5');
console.log('md5 is transform stream: ' + isTransformStream(md5));

const message = 'hello';
const digest = md5.update(message, 'utf8').digest('hex');
console.log('\ndigest: ' + digest);	

function isTransformStream (stream) {
    const isStream = stream !== null &&
    typeof stream === 'object' &&
    typeof stream.pipe === 'function';
    if(isStream) {
        return typeof stream._transform === 'function' 
            && typeof stream._transformState === 'object';
    }
    return false;
}

Xnip2022-07-24_17-07-05.jpg

参考文章