第七十七期:Node中的streams流(pipe管道和pump泵)

680 阅读4分钟

这里记录工作中遇到的技术点,以及自己对生活的一些思考,周三或周五发布。

封面图

Node中的streams流

streams流是Node中的最好的特性之一。它在我们的开发过程当中可以帮助我们做很多事情。比如通过流的方式梳理大量数据,或者帮我们分离应用程序。

和streams流相关的内容有哪些呢?大致有这么几点:

  • 处理大量数据
  • 使用管道方法
  • 转换流
  • 读写流
  • 解耦I/O

保持管道流的活力

通常情况下,当原始流通过管道连接到目标流时,目标流会随着原始流的结束而结束。

有时候我们希望在原始流结束之后额外再去做一些别的操作。这时候怎么办呢,我们来看一个例子:

名字随便起一个,start.js

const net = require('net')
const fs = require('fs')

net
  .createServer((socket) => {
    const content = fs.createReadStream(__filename)
    content.pipe(socket)
    content.on('end', () => {
      socket.end('\n========Footer=====\n')
    })
  })
  .listen(4000)

这时候我们执行

node start.js

然后我们发一个请求:

curl http://localhost:4000

会发现有报错信息

这是因为:当content可读流结束后,与之连接的socket流也就结束了。这时候,我们想要在socket后面添加内容就不可能了。

我们可以修改我们的代码如下:

const net = require('net')
const fs = require('fs')

net
  .createServer((socket) => {
    const content = fs.createReadStream(__filename)
    content.pipe(socket, { end: false })
    content.on('end', () => {
      socket.end('=========Footer========')
    })
  })
  .listen(4000)

这次我们在content.pipe中加入了第二个参数end:false。这告诉管道方法避免在源流结束时结束目标流,这时候我们的代码就不会报错。

相应的我们可以收到返回的信息:

生产中的管道流

pipe方法是streams流中一个非常重要的特性。它可以让我们把多个流组合成一行代码。

作为Node核心的一部分,它在进程运行时间不太重要的情况下非常有用。比如我们常用的cli工具。

但是不好的一点是它的错误处理。假如管道流中有一个流出现错误,它往往直接取消管道连接,然后将剩余的流进行销毁。这样一来,他们就不会泄露资源,但是有可能会导致内存泄露。

再来看一个例子:

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

const server = http.createServer((req, res) => {
  fs.createReadStream('veryBigData.file').pipe(res)
})

server.listen(4000)

这个服务在用管道给用户返回数据,因此很有可能会产出内存开销以及文件相关的信息泄露。

如果http响应在文件被完全传输给用户之前关闭,文件相关的一些信息肯定会泄露,以及文件流也会产生一些内存开销,文件流也会留在内存中,因为我们没有关闭它。

所以我们需要一些错误处理机制,能够在适当的时候销毁我们管道中的流。

这需要提到另外一个模块儿---pump(泵)。pump专门用来处理这些问题。

pump(泵)

我们再来尝试一个例子:

const fs = require('fs')
const http = require('http')
const pump = require('pump')

const server = http.createServer((req, res) => {
  const stream = fs.createReadStream('veryBigData.file')
  pump(stream, res, done)
})

function done(err) {
  if (err) {
    return console.log('文件导流出现错误----', err)
  }

  console.log('文件导流成功')
}

server.listen(4000)

这时候,运行这个文件,发起请求,我们会发现它报错了。

每个传递到pump方法中的流都会被传给下一个流。如果上一个传入的是个函数,pump会在所有流都完成后执行这个方法。

pump内部有些附加的方法。比如关闭,错误处理以及在不影响其他流的情况下关闭另外一个流的方法。

如果其中一个流关闭,其他流将被销毁,并调用传递给pump的回调函数。

当然我们也可以手动去处理这些错误或者在数据关闭时销毁流,比如:


const server = http.createServer((req, res) => {
  const stream = fs.createReadStream('veryBigData.file')
  stream.pipe(res)
  res.on('close',()=>{
    stream.destory()
  })
})

server.listen(4000)

但是通常情况下,使用pump(泵)更加方便,也更加安全。

pumpify

在编写管道的时候,尤其是作为一个单独的模块的时候。我们可能会希望将这些方法导出为外部的用户。这时候怎么办呢?

正如我们之前说的:管道由一系列传输流组成。我们将数据写入管道中的第一个流,然后数据通过它传输,直到写入最后一个流。

我们再看个例子:

const {createGzip} = require('zlib')
const {crateCipher, createCipheriv} = require('crypto')
const pumpify = require('pumpify')
const base64 = require('base64-encode-stream')

function pipeline(){
  const stream1 = createGzip()
  const stream2 = createCipheriv()
  const stream3 = base64()

  return pumpify(stream1,stream2,stream3)
}

这种写法有点类似Promisify 。

最后

  • 公众号《JavaScript高级程序设计》
  • 公众号内回复”vue-router“ 或 ”router“即可收到 VueRouter源码分析的文档。
  • 回复”vuex“ 或 ”Vuex“即可收到 Vuex 源码分析的文档。

全文完,如果喜欢。

请点赞和"在看"吧,最好也加个"关注",或者分享到朋友圈。