解读 node Stream(流)

670 阅读5分钟

今天来说一下 node.js 中,流的概念,Stream 是 node.js 的核心模块之一,主要用来处理文件,Stream 是有序的有方向的,有起点和终点的字节数据传输手段,可以自己控制速率。

Stream 的类型

 1、Readable:读取数据,将内容读取到内存中,也可以说数据从设备流向程序;
 2、Writable:写入数据,将内存或者文件的内容写入到文件内,数据流从程序流向设备;
 3、Duplex:双向的,既可读,又可写,比如 net.Socket()
 4、Transform:双向的,可读+可写,在读写的过程中,可以对数据进行修改,比如 zlib.createDeflate()(数据压缩/解压)。

nodeJS 经常处理的文件类型

1、普通文件

2、设备文件,如 stdin(标准输入文件)、stdout(标准输出文件)、stderr(标准出错文件)

3、网络文件,如 http、net

可读流 Readable Stream

流方向:设备(物理磁盘文件)-> nodeJS

下面我们就使用 readable(可读流) 来读取一个文件,由于 fs 内部已经引用的 Stream 模块,并且做了封装,所以,我们引入 fs 即可。

let fs=require('fs')
let readStream=fs.createReadStream('./stream/解读 node Stream(流)');
    readStream.setEncoding('utf8'); //默认buffer
    
    readStream.on("open", () => {console.log("open")})
    readStream.on('data',(data)=>{
     console.log(data)
    })
    readStream.on("close", () => {console.log("close")})
    readStream.on("end", () => { console.log("end")})
    readStream.on("error", err => { console.log(err) })


也可以传一个 Object 来描述你的设置:

let fs=require('fs')
let readStream=fs.createReadStream('./stream/解读 node Stream(流)',{
     //highWaterMark: 3, // 每次读取的字节长度  默认64kb
     flags:'r',   // flags主要用于设置文件的执行模式,读取文件为'r',写入文件为'w'
     autoClose:true, // 默认读取完毕后自动关闭
     start:0,   //开始读取的索引位置
     //end:3,   //结束读取的索引位置 流是闭合区间 包含start也包含end
     encoding:'utf8'   
 });
 readStream.on('data',(data)=>{
     console.log(data)
 })

Readable Stream 的两种模式

1、流动模式(flowing mode)
   流动模式(flowing mode)时,尽快的从底层系统读取数据并提供给程序。
2、暂停模式(paused mode) 
   暂停模式(paused mode)时, 必须调用 stream.read() 来读取数据。 
 
 我们创建的流默认是非流动模式,即暂停模式(paused mode) 。 

将流切换到流动模式(flowing mode)

需要注意的是,如果没有绑定处理数据的函数,也没有 pipe()目标,也就是没有写读到的数据流向何处,写到哪里,这样数据会丢失哦!
正式介绍将流切换到流动模式的方法:
1、添加一个['data'] 事件处理器来监听数据
2、调用 resume() 方法来明确开启数据流
3、调用 pipe() 方法来发送数据给Writable。

将流切换到暂停模式(pause mode)

1、如果没有 pipe() 目标,调用 pause() 方法
2、如果有 pipe() 目标,移除所有的 ['data']处理函数,调用 unpipe() 方法移除所有的 pipe() 目标。 实例:

let fs=require('fs')
    let readStream=fs.createReadStream('../node/stream/解读 node Stream(流).md',{
        highWaterMark: 5, 
        flags:'r', 
        start:0,
        encoding:'utf8'
    });
    readStream.on('data',(data)=>{
        console.log(data)
        readStream.pause()  //触发暂停模式,在读取一次之后(一个highWaterMark大小) 暂停
    })  
    let ti= setInterval(()=>{readStream.resume()},2000)  //每隔2秒读一次
    readStream.on('end',(data)=>{
        clearInterval(ti)  //读完后清除定时器
        console.log("读取完毕")
    })
    readStream.on('close',function () {
        console.log('关闭')  //读取完毕后 关闭文件
      });

pipe()

pipe() 方法就是帮我们实现读写数据的链接,把 Readable的数据写到 Writable中,传递数据的功能。 pipe() 实现原理:

 let fs=require('fs')
     let rs=fs.createReadStream('./stream/streamTxt')
     let ws=fs.createWriteStream('./2.txt')
     rs.on('data',()=>{
        let flag=ws.write(data)
        if(!flag){
            rs.pause()
        }
     })
     ws.on('drain',()=>{
        rs.resume()
     })
     rs.on('end',()=>{
        ws.end()
     })

pipe()的简单应用:

    let fs=require('fs')
    let rs=fs.createReadStream('./stream/streamTxt')
    let ws=fs.createWriteStream('./2.txt')
    rs.pipe(ws)

unpipe()

.unpipe([destination]),指定解除导流的流,[destination]如果没有传入,则解绑所有的流

nodeJS常见的可读流

http responses, on the client
http requests, on the server
fs read streams
zlib streams
crypto streams
tcp sockets
child process stdout and stderr
process.stdin

可写流 Writable Stream

将内存或者文件的内容写入到文件内。

流方向:程序(自己的文件或内容) -> 设备

    let fs=require('fs')
    let writeStream=fs.createWriteStream('./stream/streamTxt')
    writeStream.write('123') //写入的内容必须是buffer或者string,写是异步的方法
    writeStram.end()  //结束
    writeStream.write('456') //写在writeStram.end()后面会报错,写不进去了

如果你写的目录下没有该文件,那么他会自动创建一个文件,并且写入内容,如果该文件中有内容,他会清空,重新写入新的内容。
也可以传入对象设置参数:

    let writeStream=fs.createWriteStream('./stream/streamTxt',{
        flags:'w',
        encoding:'utf8',
        start:0,
        highWaterMark:5   //默认 16kb
    })

接下来我们看一个问题,如果我们设定 highWaterMark 为5,即每次写入5个字节,但是我们实际写入的是6个字节,那会如何呢?

   let flag = writeStream.write(1+''); //true 表明真的写进去了,false,表示存在缓存区中,过一会写完了,会去清空缓存区,继续写入
   console.log(flag);   //true
   flag = writeStream.write(122 + '');
   console.log(flag);   //true
   flag = writeStream.write(122222 + '');
   console.log(flag);   //false  
   flag = writeStream.write(1 + '');
   console.log(flag);  //false  

虽然写入时会把写不进去的暂时存在缓存区,但是我们也不能太过分的不停地写,最好是等它写完了我们再继续写,于是,我们就需要一个方法,来知道它写完了,这个方法就是: "drain",看一下"drain"事件的用法:

    let fs=require('fs')
    let ws = fs.createWriteStream('2.txt',{
        flags:'w',
        encoding:'utf8',
        start:0,
        highWaterMark:5 
      });
    let flag = ws.write(1+''); 
    console.log(flag);  //true
    flag = ws.write(122 + '');
    console.log(flag); //true
    
    ws.on('drain',()=>{ //不会触发
        console.log("已写完")
        flag = ws.write(122222 + '');
        console.log(flag);
    })
    
    let fs=require('fs')
    let ws = fs.createWriteStream('2.txt',{
        flags:'w',
        encoding:'utf8',
        start:0,
        highWaterMark:5 
      });
    let flag = ws.write(1+''); 
    console.log(flag);  //true
    flag = ws.write(122 + '');
    console.log(flag); //true
    
    flag = ws.write(122222 + '');
    console.log(flag); //false
    ws.on('drain',()=>{ //触发
        console.log("已写完")
        flag = ws.write(12 + '');
        console.log(flag);
    })
  

我们可以看到,只有进入缓存区,再清空缓存区时,才会触发"drain"事件。

双工流(Duplex streams)

Duplex streams同时实现了 Readable 和Writable 接口。 Duplex streams的例子包括

tcp sockets
zlib streams
crypto streams
let fs = require('fs');
let { Duplex } = require('stream');
class MyDuplex extends Duplex{
  _read(){
    this.push('hello');
    this.push(null); //停止读取
  }
  _write(chunk,encoding,callback){
    console.log(chunk);
    callback();
  }
}
let r = new MyDuplex();
r.on('data',function (data) {
    console.log(data);
});
r.write('hello');

转换流(Transform streams)

Transform streams 是双工流,写入数据,然后读出结果, 它实现了readable 和 writable 。 Transform streams 的例子包括:

zlib streams
crypto streams

实现:

const {Transform} = require('stream');
const upperCase = new Transform({
    transform(chunk, encoding, callback) {
        this.push(chunk.toString().toUpperCase());
        callback();
    }
});

process.stdin.pipe(upperCase).pipe(process.stdout);