浅析node中的stream

532 阅读3分钟

我们常说的stream由三个部分构成,即source(源头) + stream(stream中的数据为chunk 块状的) + sink(水池)


认识writeStream

首先,我们来看下面这一段代码

const fs = require('fs')
const stream = fs.createWriteStream('./big_file.txt')

for (let i = 0; i < 10000000; i++) {
  stream.write(`这里有很多数据,这是第${i + 1}条\n`)
}

stream.end()

上面的这一段代码可以创建一个很大的文件,现在,我们创建一个服务器尝试请求它。

const http = require('http')
const fs = require('fs')
const server = http.createServer()
server.on('request', (request, response) => {
  fs.readFile('./big_file.txt', (error, data) => {
    response.write(data)
    response.end()
  })
})

server.listen('8888')

我们发现,仅仅响应一个文件,node就占用了很多的内存

所以我们需要修改一下我们的代码,不能通过fs来读取,而是通过stream,这样的话,就会读取多次,每次只会读取一段chunk,而不是一次性读取整个文件,从而降低内存占用。

const http = require('http')
const fs = require('fs')
const server = http.createServer()
server.on('request', (request, response) => {
  const stream = fs.createReadStream('./big_file.txt')
  stream.pipe(response)
})

server.listen('8888')

我们看到,使用了readStream之后,内存占用减小了一半不止

上面的代码中,我们使用到了pipe,它的中文意思为 管道,相当于使用了一个管道将读写连接了起来,对数据的传输量进行了限制。

管道其实也可以通过事件来实现,但是我们一般不这么写

// stream1 一有数据就传给 stream2
stream1.on('data', (chunk)=>{
stream2.write(chunk)
})
// stream1 结束的话,同时结束 stream2
stream1.on('end', ()=>{
stream2.end()
})

Readable 和 Writable 支持的事件与方法

readable Stream 的 data事件 与 end事件,我们在上面的代码中已经使用过了,我们现在在来看两个比较重要的Writeable Stream的事件—— drain 与 finish

  • drain

drain的意思是流干了,即pipe中传来的数据已经被写完了。

这种情况常发生在读写大量数据时,由于数据传的太快了,硬盘/内存来不及写数据,会造成数据堆积,而当堆积的数据被写完时,就会触发drain事件。

我们稍稍修改一下我们的服务器,就能监听到drain事件了

const http = require('http')
const fs = require('fs')
const server = http.createServer()

server.on('request', (request, response) => {
  const stream = fs.createReadStream('./big_file.txt')
  stream.pipe(response)
  response.on('drain', () => {
    console.log('drain')
  })
})

server.listen('8888')

上面的代码会多次触发drain

我们可以通过flag来判断源头端是否读的太快了

stream1.on('data', (chunk)=>{
	let flag = stream2.write(chunk)
    //flag = true 表示正常,false表示读的太快堵车了
})
  • finish事件

finish事件会在 调用stream.end()之后 + 缓冲区的数据传给操作系统之后 就会触发

此外,我们还要注意以下,readableStream 默认是处于禁止态的,只有当添加data事件监听后才会变为流动态。我们也可以通过pause()和resume()进行控制。


双向Stream

stream除了 read 和 write 之外,还有两个双向的流,即Duplex和Transform

虽然Duplex和Transform都是双向流,但他们之间还是有区别的

Duplex类似于readableStream和writableStream的结合。

而Transform则更类似于babel,是自己读自己写的,类似于通过babel将ES6文件转换为ES5文件


自定义stream

我们上面的代码都是中使用stream,现在,我们可以尝试一下自定义stream

  1. 自定义writableStream
const stream = require('stream')
const {Writable} = stream

const outStream = new Writable({
  write(chunk, encoding, callback) {
    console.log(chunk.toString())
    callback()
  }
})

process.stdin.pipe(outStream)
  1. 自定义readableStream
const stream = require('stream')
const {Readable} = stream
const inStream = new Readable({
  read(size) {
    const char = String.fromCharCode(this.currentCharCode++)
    if (this.currentCharCode < 92) {
      this.push(char)
    }
  }
})

inStream.currentCharCode = 65
inStream.pipe(process.stdout)
  1. 自定义DuplexStream
const {Duplex} = require('stream')
const inOutStream = new Duplex({
  read(size) {
    const char = String.fromCharCode(this.currentCharCode++)
    this.push(char)
    if (this.currentCharCode > 90) {
      this.push(null)
    }
  },
  write(chunk, encoding, callback) {
    const data = chunk.toString()
    console.log(data)
    callback()
  }
})

inOutStream.currentCharCode = 65

process.stdin.pipe(inOutStream).pipe(process.stdout)

自定义DuplexStream其实就是相当于将自定义readableStream和自定义writableStream结合起来。

  1. 自定义tramsform
const {Transform} = require('stream')

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

process.stdin.pipe(tr).pipe(process.stdout)

上面的代码可以实现将小写的字母自动转换为大写。


node中内置的TransformStream——zlib

通过zlib,我们可以实现对文件的加密压缩

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

fs.createReadStream('./big_file.txt')
  .pipe(crypto.createCipher('aes192', '123456'))
  //加密
  .pipe(zlib.createGzip())
  //压缩
  .on("data", () => {
    process.stdout.write('.')
  })
  .pipe(fs.createWriteStream('./gz.gz'))
  .on('finish', () => {
    console.log('DONE')
  })

我们也可以将监听data事件单独抽离出来

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

const reportTransform = new Transform({
  transform(chunk, encoding, callback) {
    process.stdout.write('.')
    callback(null, chunk)
  }
})

fs.createReadStream('./big_file.txt')
  .pipe(crypto.createCipher('aes192', '123456'))
  .pipe(zlib.createGzip())
  .pipe(reportTransform)
  .pipe(fs.createWriteStream('./gz.gz'))
  .on('finish', () => {
    console.log('DONE')
  })

如下图所示,stream 在nodejs中有非常广泛的运用