阅读 887

Node.js流(一) 可读流及可读流的实现

流(Stream)到底是什么

流(Stream)是数据的集合,就跟数组和字符串一样。不同点就在于Streams可能不是立刻就全部可用,并且不会全部载入内存。这使得他非常适合处理大量数据,或者处理每隔一段时间有一个数据片段传入的情况。

Node.js 提供了多种流对象。 例如, HTTP 请求 和 process.stdout就都是流的实例。

流可以是可读的、可写的,或是可读写的。所有的流都是 EventEmitter 的实例。

流的类型

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

  • Readable - 可读流 (例如 fs.createReadStream())
  • Writable - 可写流 (例如 fs.createWriteStream())
  • Duplex - 可读写的流(双弓流) (例如 net.Socket)
  • Transform - 在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate())

本次介绍可读流及其源码实现

fs.createReadStream()可读流介绍

可读流的创建

let fs = require('fs')

let rs = fs.createReadStream('./1.txt', {
  highWaterMark: 3, // 字节
  flags:'r',
  autoClose:true, // 默认读取完毕后自动关闭
  start:0,
  end:3,// 流是闭合区间 包start也包end
  encoding:'utf8'
})
复制代码
  • highWaterMark每次读取的字节数,默认为64kb
  • autoClose 读取结束是否关闭文件
  • startend 读取文件结束(包含)和开始(包含)的位置,
  • encoding 读取流之后的编码

通过fs.createReadStream()就可以创建一个可读流。

可读流的事件

可读流有如下事件

rs.on('open', function () {
    console.log('文件打开了')
})
rs.on('data', function (data) {
    console.log('输出数据', data.toString())
})
rs.on('end', function () {
    console.log('读取完毕')
})
rs.on('close', function () {
    console.log('文件关闭了')
})
rs.on('error', function (err) {
    console.log('出错了', err)
})
复制代码
  • open 文件打开时,会被触发
  • data 读取流的数据时触发
  • end 流读取结束触发
  • close 文件关闭时触发
  • error 流读取失败触发

可读流的方法

可读流有两个很重要的模式(flawing)影响了我们使用的方式。

  • 暂停模式
  • 流动模式

所有的可读流开始的时候都是默认暂停模式,但是它们可以轻易的被切换成流动模式,当我们需要的时候又可以切换成暂停模式。有时候这个切换是自动的。

可以使用resume()pause()方法在这两种模式之间切换。

当一个流是流动模式的时候,数据是持续的流动,我们需要使用事件去监听数据的变化。

在流动模式中,如果可读流没有监听者,可读流的数据会丢失。这就是为什么当可读流逝流动模式的时候,我们必须使用data事件去监听数据的变化。事实上,只需添加一个data事件处理程序即可将暂停的流转换为流模式

可读流的实现

可以根据上面介绍的可读流特性,实现一个可读流的类

引入Node.js 模块

显然可读流是需要 fsevents 这两个模块的

let fs = require('fs')
let EventEmit = require('events')
复制代码

构造方法

class ReadStream extends EventEmit {
    constructor(path, options = {}) {
        super()
        this.path = path
        this.highWaterMark = options.highWaterMark || 64 * 1024
        this.autoClose = options.autoClose || true
        this.start = options.start || 0
        this.end = options.end || null
        this.encoding = options.encoding || null
        this.flags = options.flags || 'r'

        // 参数处理
        this.flawing = false // 默认暂停模式
    }
}    
module.exports = ReadStream    
复制代码

ReadStream类是继承events模块的,默认是暂停模式(this.flawing = false),其他是一些参数的处理。

读取数据之前的一些处理

读数据之前,需要打开文件,open方法实现

class ReadStream extends EventEmit {
    constructor(path, options = {}) {
        ...
        // 参数处理
        this.flawing = false
        // 打开文件 异步
        this.open()
    }
    open() {
        fs.open(this.path, this.flags, (err, fd) => {
            if (err) {
                this.emit('error', err)
                this.destroy()
                return
            }
            this.fd = fd
            this.emit('open')
        })
    }
}  
复制代码

需要用到fs.open(),这个方法是异步的。打开文件失败则发射error事件,并销毁(destroy)可读流的实例。成功则保存文件描述符(fd),发射open事件

destroy()方法实现

destroy() {
    if (typeof this.fd !== 'number') {
        // 文件没有打开
        return this.emit('close')
    }
    fs.close(this.fd, () => {
        this.emit('close')
    })
}
复制代码

分两种情况

  • 文件打开失败,直接发射close事件
  • 流读取结束需要关闭,使用fs.close()关闭文件,回调中触发close事件

开始读取数据

read()方法之后(这是异步的,文件描述符并没拿到)就需要读取数据了。流创建时,默认是暂停模式,只有添加了data事件,才会转换为流动模式。

构造方法中添加:

//同步执行
class ReadStream extends EventEmit {
    constructor(path, options = {}) {
        ...
        // 打开文件 异步
        this.open()
        //同步执行
        this.on('newListener', (type) => {
            if (type === 'data') {
                this.flawing = true
                this.read()
            }
        })
    }
}    
复制代码

当实例上添加有data事件,就调用read()方法读取数据。事件监听这里是同步的,通俗的说: read()要比open先执行。明白这点很关键,后面的read要处理fd没有拿到。

read方法实现:

read() {
    // 文件没有打开,可能就开始读取
    if(typeof this.fd !== 'number') {
        return this.once('open', this.read)
    }
    let howMuchToRead = this.end ?Math.min(this.highWaterMark, this.end - this.pos + 1) : this.highWaterMark 
    fs.read(this.fd, this.buffer, 0, howMuchToRead, this.pos, (err, bytesRead) => {
        // 读取完毕,位置向后移动
        this.pos += bytesRead
        // 发射数据
        let result = this.buffer.slice(0, bytesRead)
        let encodedResult = this.encoding ? result.toString(this.encoding) : result
        this.emit('data', encodedResult);
        // 如果还没结束,继续读
        if(bytesRead === this.highWaterMark && this.flawing) {
            this.read()
        }
        // 没有读满,说明结束了
        if(bytesRead < this.highWaterMark) {
            this.emit('end')
            this.destroy()
        }
    })
}
复制代码

1、可读流添加data事件时,会成流动模式,开始执行read()方法读取数据,此时文件并没有打开,因此需要open事件触发后,执行read方法。

2、fs.read(this.fd, this.buffer, 0, howMuchToRead, this.pos, callback)方法介绍

  • this.fd 打开文件拿到的文件描述符
  • this.buffer 读取文件buffer存放。构造函数中初始化this.buffer = Buffer.alloc(this.highWaterMark)
  • 0 存放到this.buffer中的偏移量
  • howMuchToRead 每次从文件中读取的长度
  • this.pos 每次从文件中读取的位置,需要自己累加维护
  • callback 读取之后的回调,其中bytesRead是buffer的长度

要点提醒:

1、每次读取长度howMuchToRead的计算;

2、发射数据时需要从this.buffer中截取bytesRead位数;

3、暂停模式下不能读取(this.flawing=== false)

4、剩下的就是正常流程:this.pos维护累加、没读完继续读取、读完之后发射end事件,并销毁。

pause()和resume()的事件

直接上代码了,一看就明白

pause() {
    this.flawing = false
}

resume() {
    this.flawing = true
    this.read()
}
复制代码

结语

以上就是全部了,谢谢阅读!如有纰漏,多多指正。

文章分类
前端
文章标签