《node-Stream-2》

698 阅读9分钟

Stream一共有四种流的类型。分别是:

  • 可读 Readable
  • 可写 Writable
  • 可读可写的 Duplex
  • 可读可写 中间可做转换 Transform

Duplex 指的是我们既可以从流里读,也可以往流里写。这两个过程互不干扰,独立的。也就是说不能自己读到自己写的,读和写是完全分开的。

Transform 也是可以读可以写,区别是,我们往流里写入一段数据,会自动做一种转换,转换成另一种形式,再由我们读出来。就好像我们在流的入口写,在流的出口等着数据出来。即操作被写入数据,然后读出结果。

一. 可读流

可读流是对提供数据的来源的一种抽象。 可读流除了常用的data,end事件,还有一些其他事件和方法。

const stream=fs.createReadStream('./big_file.txt') 当创建了一个流的时候,就有了这个可读的流对象。然后一般监听它的data事件接收数据。那如果没监听data事件,数据会往外流吗?怎么知道数据何时才能流动呢?这就与流的状态有关了。

可读流有两种状态:静止态paused 流动态flowing

顾名思义,静止态时数据就无法流动了,流动态时数据才会流动。默认是处于pause态的。也就是说,只创建了这个流,它是没有流动的,处于pause态。

只有当监听了data事件,状态才会变为flowing态。数据就开始流动了。删掉了data事件监听,就变为了pause态。

调用pipe()方法将数据发送到可写流,也会将状态切换到flowing

pause()方法会让状态变为pause态。resume()方法,恢复成flowing态。

const fs=require('fs')

const stream=fs.createReadStream('./big_file.txt')
stream.on('data',chunk=>{
    console.log(`接收到 ${chunk.length} 字节的数据`);
    stream.pause()
    console.log('暂停')
    setTimeout(()=>{
        console.log('数据重新开始流动')
        stream.resume()
    },3000)
})

这个文件很大,执行的结果就是先打印出来接收到 ${chunk.length} 字节的数据,然后打印'暂停',三秒后打印了'数据重新开始流动'。一直这样循环,知道所有数据都被读完了。

创建自定义的可读流

还是先引入stream模块的Readable类

1. 先把所有数据都push进去,再一次性读

// readable1.js
const {Readable}=require('stream')

const readStream=new Readable()
readStream.push('abcdefg')
readStream.push('hijklmn')
readStream.push(null)

readStream.on('data',(chunk)=>{
    process.stdout.write(chunk)
    console.log('数据读了一次')
})

第一种实现方法,就只new一个可读流对象readStream 。使用readStream.push分两次添加数据。被添加的数据会在之后触发的data事件或者read()方法时被读取。 然后监听这个可读流的data事件,把数据写到可写流process.stdout里,输出。

可以看到这个流被读了两次。因为是先把所有数据都push进去,再等着别人从流里读出来。

2. 别人调一次,才给一次数据

// readable2.js
const {Readable}=require('stream')

const readStream=new Readable({
    read(size){
        console.log('被读取了一次')
        const char=String.fromCharCode(this.currentCharCode++)
        this.push(char)
        if(this.currentCharCode>90){  // 已经到Z了
            this.push(null)
        }
    }
})

readStream.currentCharCode=65 // A

readStream.pipe(process.stdout)

和上一种不同的就是在可读流readStream里写一个read方法,这个read方法会在调用readStream.pipe(process.stdout)时,每次从readStream里读一次数据,就被调用一次,类似data事件,是在不断被触发的。

在read里,先把code转换成对应的字符,A-Z。然后push进去。加到Z时,push null,表示结束。上边说过,push的数据会在之后的read方法被调用时被读取出来。

最后把流里读到的数据通过pipe流到输出控制台。

后边一直到Z的输出没有截取。可以看到,和上一种不同的是,read方法被调用了26次。也就是说,这次的数据是按需供给的,别人读一次,我这个可读流才给一次数据。而不是先把数据给完,再等着别人读。

二. 可写流

可写流是对数据要被写入的目的地的一种抽象。 比如fs文件系统的写入流,http模块作为服务器的响应也是一种可写流。

可读流可以关注数据何时从源头流出来。可写流就不一样了,可写流是我们主动去写给别人的,所以不需要什么是否流动的状态。我们调用stream.write(data)就是开始写了,数据就自然流动了。不调用,自然就不写。

但是如果我们写的太快,或者一次写的太多,就有可能导致这个流被阻塞了,像堵车一样。这时就不可再写了。

1. drain事件

当我们调用stream.write(data) 得到的返回值是false时,就表明数据积压了,不能再写了。这时就不能再调用write了。而要监听drain事件 ,它会在可以继续写入数据时被触发。 drain就是干涸的意思,就是水流干了,可以继续加水了,可以继续写数据了。

// 向可写流中写入数据一百万次。
// 留意背压(back-pressure)。
function writeOneMillionTimes(writer, data, encoding, callback) {
  let i = 1000000;
  write();
  function write() {
    let ok = true;
    do {
      i--;
      if (i === 0) {
        // 最后一次写入。
        writer.write(data, encoding, callback);
      } else {
        // 检查是否可以继续写入。 
        // 不要传入回调,因为写入还没有结束。
        ok = writer.write(data, encoding);
      }
    } while (i > 0 && ok);
    if (i > 0) {
      // 被提前中止。
      // 当触发 'drain' 事件时继续写入。
      writer.once('drain', write);
    }
  }
}

数据流中的积压问题

2. finish事件

调用了 stream.end() 而且缓冲数据都已传给底层系统之后触发 。

因为我们向流写数据并不是直接写进去的,而是先写进缓冲区里,当缓冲区的数据达到一定量。再传给流。所以finish事件会在所有事情全部做完就触发。

3. 创建自定义的可写流

前边我们都是在调用比如fs的可写流或http的响应可写流。如何自定义一个可写流呢?自定义一个可写流就是我们自己写一个流对象,别人调用这个流对象的write方法,就能往这个流对象里写入数据。

// writable.js
const {Writable}=require('stream')

const outStream=new Writable({
    write(chunk,encoding,callback){
        console.log(chunk.toString())
        callback()    // 必须调用,才能进入下一次
    }
})

process.stdin.pipe(outStream)
// pipe相当于监听前边可读流的data事件,把chunk写到后边的可写流outStream里
//process.stdin.on('data',chunk=>{
	//outStream.write(chunk)  相当于调用了outStream的write方法
//})

首先从stream模块引入它的Writable类,因为可写流Writable就是stream.Writable这个类的实例。

然后new一个流对象outStream ,写一个write方法 ,该方法就是别人调用时我们该怎么做的一个函数。我们就只把数据打印出来。

最后 process.stdin是一个可读流,就是用户的输入。把用户输入的数据通过pipe流到这个可写流对象里。

当执行node writable.js 可以看到命令行在等待输入,我们输入什么,就会打印出来相同的内容:

三. 创建自定义的duplex流

就是把可读流和可写流结合起来,在流对象里同时实现一个write方法和一个read方法。让这个流既能读也能写。

const {Duplex}=require('stream')

const duplexStream=new Duplex({
    write(chunk,encoding,callback){
        console.log('我现在是个可写流,这是我接受的数据:')
        console.log(chunk.toString())
        callback()
    },
    read(size){
        console.log('我现在是个可读流,这是我要给你的数据')
        this.push(String.fromCharCode(this.currentCode++))
        if(this.currentCode>67){
            this.push(null)
        }
    }
})
duplexStream.currentCode=65

作为可写流:process.stdin.pipe(duplexStream)

process.stdin.pipe(duplexStream).pipe(process.stdout) 同时实现,在终端会先作为可读流,把ABC输出到控制台;再作为可写流,等待用户输入,调用write把输入的数据打印出来:

四. 创建自定义的transform流

transform流就是我在流的入口传数据,它先作为可写流接受数据。然后把接受的数据做一个转换。再作为可读流,将转换后的数据吐出来。我在流的出口把数据读出来。

const {Transform}=require('stream')

const upperCaseTr=new Transform({
    transform(chunk,encoding,callback){
        const upperChunk=chunk.toString().toUpperCase()
        this.push(upperChunk)
        callback()
    }
})

process.stdin.pipe(upperCaseTr).pipe(process.stdout)

我们实现的转换功能是把流入的字符转成大写。

在流对象里实现一个transform方法。

  1. 先作为可写流,接受一个chunk,把chunk变成大写字符
  2. 紧接着作为可读流,调用push方法添加数据,等待被读取

从控制台等待用户的输入,通过管道流入这个transform流对象,经过转换,再把数据流入控制台的输出。

五. 其他内置的transform Stream

1. 文件压缩

文件压缩的过程可以用流的形式实现。

const fs=require('fs')
const zlib=require('zlib')

const r=fs.createReadStream('./input.txt')
const w=fs.createWriteStream('./input.txt.gz')
r.pipe(zlib.createGzip()).pipe(w)

创建可读流r,可写流w,和zlib.createGzip()得到的一个Gzip对象用于压缩。从源文件一点点读,读出的内容流入Gzip流对象进行压缩(transform),再流入目标文件。就实现了文件的压缩。

2. 升级版1

const fs=require('fs')
const zlib=require('zlib')

const r=fs.createReadStream('./input.txt')
const w=fs.createWriteStream('./input.txt.gz')
r
    .pipe(zlib.createGzip())
    .on('data',()=>process.stdout.write('.'))
    .pipe(w)
    .on('finish',()=>console.log('done'))

添加了两个监听器。

  1. 每一块数据流入gzip里压缩后,监听data事件。那么一有数据被压缩完成,data事件就会被触发,它就将作为一个可读流把压缩数据吐出去。我在这个回调里往控制台输出一个. 可以用于观察这一个文件被分成了几次压缩。(一有数据,就打个点)
  2. on监听是不影响数据流动的。在每一次压缩后的数据流入新文件后,监听这个可写流的finish事件,当所有数据都传完了,被触发,打印一个标记。

所以升级后,如果正在压缩,控制台就会不断的打点。如果压缩完毕,就输出done。可以用于观察压缩进度以及何时完成。可以看到它分成了两次压缩

3. 升级版-任意变换

const fs=require('fs')
const zlib=require('zlib')
const {Transform}=require('stream')

const r=fs.createReadStream('./input.txt')
const w=fs.createWriteStream('./input.txt.gz')

const reportProgress=new Transform({
    transform(chunk,encoding,callback){
        process.stdout.write('.')
        callback(null,chunk)  // 相当于加一个this.push(chunk)都可以把数据吐出去
    }
})

r
    .pipe(zlib.createGzip())
    .pipe(reportProgress)
    .pipe(w)
    .on('finish',()=>console.log('done'))

new Transform创建一个transform流对象。在对象里定义一个transform方法:一有数据进来,我就在控制台打个点,而且把数据原封不动地传出去。

数据被压缩后,通过pipe再流入这个transform流对象->流入压缩文件。

实现的效果和2相同。就是把观察进度的监听data事件换成了一个变换流。我们可以在transform方法里定义任何变换。这个例子里没有变换,直接把chunk传出去。

六. 总结

总结一下node js中都有哪些流: