node可读流——readable事件原理及实现

1,989 阅读9分钟

前言
最近在学习nodejs,在学习到可读流的readable的时候产生了一些困惑。readable会在什么时间被触发,什么时间会开始继续进行数据读取,读取什么时候又会停止?

可读流中有两种读取数据流的事件

  • data
    data读取可读流的数据相当于是将水龙头完全打开,让水以highWaterMark一次的速度哗啦啦的流,直到整个文件全部读完。中途可以关闭水龙头(readable.pause()),关闭之后也可重新开启(readable.resume())。
    一个小栗子:

    • 首先在当前目录下创建一个1.txt文件,文件内容为:1234567890
    • 然后创建一个js文件,以可读流中data事件的方式来读取1.txt。代码如下:
    let fs = require('fs');
    let path = require('path');
    
    let rs = fs.createReadStream(path.join(__dirname, '1.txt'),{
        highWaterMark: 3
    });
    rs.on('data', function(data){
        console.log(data);
    })
    

    上面的方式文件会一次读取三个(highWaterMark),一口气把文件全部的内容都读去完成,输出的结果如下:

    <Buffer 31 32 33>
    <Buffer 34 35 36>
    <Buffer 37 38 39>
    <Buffer 30>
    
  • readable
    readable读取数据是可以随机的控制速率的,不限于highWaterMark。readable可以比喻为是从水池中取水,它有添水和取水时机的问题
    readable在两种情况下读取数据的情形是和data相同的,一种是rs.read()不指定size的时候,另一种是指定的size和highWaterMark相同的时候。之所以结果会相同,也与读取的规则有关。

    与data执行相同的栗子:

    let fs = require('fs');
    let path = require('path');
    
    let rs = fs.createReadStream(path.join(__dirname, '1.txt'),{
        highWaterMark: 3
    });
    rs.on('readable', function(data){
        console.log(rs.read());
    })
    

    执行结果:

    <Buffer 31 32 33>
    <Buffer 34 35 36>
    <Buffer 37 38 39>
    <Buffer 30>
    null
    

    最后为什么会有一个null呢?如果rs.read()指定了size,null又会在哪里输出呢? 我们带着这个疑问先往下看。

readable读取时机

  • 什么时机会从文件读取数据到可读流内部缓存中?

    1. 开始执行时,可读流内部缓存中没有数据,会先读取highWaterMark(默认64kb,可指定)个字节的数据到内部缓存中。
    2. readable读取之后,如果内部缓存区中的字节少于highWaterMark个字节的时候,会主动再读取highWaterMark字节的数据到内部缓存中。
    3. 如果readable读取后,内部缓存中没有数据了,会主动再读取highWaterMark字节的数据到内部缓存中。这是2中的一种特殊情形。
    4. 当readable触发时,如果缓存中的数据不够读取,则会读取最邻近的2的高次幂个字节放入缓存中。例如本次readable要读取5个字节,但是缓存中字节小于5个,则会读取2^3个字节到缓存中。
  • 什么时候会调用readable将内部缓存中的数据取出?

    1. 当可读流内部缓存区中字节数为0的时候,包括开始执行时的0和后期读取时被清空两种情形,会触发一次readable事件。
    2. 如果readable执行时,当前缓存区中的数据不够读取,则会在从文件读取数据到缓存(对应读取文件时机的第4条)后,再次触发readable事件。
    3. 当到达可读流底部的时候,也就是文件已经被全部读到缓存中,且调用过一次readable事件后,会再次触发readable事件。(这是一次校验清理缓存的过程,也是上面例子最后输出了null的原因
  • 几个简单的小栗子
    例1:与data输出类似的第二种情况,指定rs.read的size为highWaterMark:

    let fs = require('fs');
    let path = require('path');
    
    let rs = fs.createReadStream(path.join(__dirname, '1.txt'),{
        highWaterMark: 3
    });
    rs.on('readable', function(data){
        console.log(rs.read(3));
        // 每次读取后内部缓存中剩余的字节数
        console.log(rs._readableState.length)
    })
    

    输出结果:

    <Buffer 31 32 33>
    0
    <Buffer 34 35 36>
    0
    <Buffer 37 38 39>
    0
    null
    1
    <Buffer 30>
    0
    

    简单分析一下:
    第一次输出<Buffer 31 32 33>之后缓存中字节被清空,会主动再去文件中读取数据(对应读取文件的第3种情形),读取完成后会触发readable事件(对应readable触发条件第1条);
    因此会有第二次、及第三次的主动输出<Buffer 34 35 36>、<Buffer 37 38 39>;
    在第三次输出后,会主动去读取文件内容,读出最后一个字节,这时再次去调用readable,发现缓存中的字节不够要读取的3个(对应读取文件的第4种情形),rs.read会返回null,同时会再去读取文件数据,然后再次触发readable(对应readable触发条件第2条),一次有了最后一次输出<Buffer 30>;

    例2:当读到流底部的时候会自动触发一次readable事件:

    let fs = require('fs');
    let path = require('path');
    // let ReadStream = require('./chat');
    let rs = fs.createReadStream(path.join(__dirname, '1.txt'),{
        highWaterMark: 7
    });
    rs.on('readable', function(data){
        // 每次读取前内部缓存中剩余的字节数
        console.log(rs._readableState.length)
        console.log(rs.read(8));
        // 每次读取后内部缓存中剩余的字节数
        console.log(rs._readableState.length)
    })
    

    输出结果:

    7
    null
    7
    10
    <Buffer 31 32 33 34 35 36 37 38>
    2
    2
    <Buffer 39 30>
    0
    

    结果分析:
    程序开始执行的时候会先将highWaterMark个数据到内部缓存中(对应读取文件的第1种情形),读取完成之后触发readable(对应readable触发条件第1条);
    readable读取时发现内部缓存中的数据,不够要读取的8个,这时候read方法返回null,同时去读取文件2^3个字节(对应读取文件的第4种情形);
    读取之后缓存中的字节是10,文件中所有字节全部都读取出来了,再次调用readable(对应readable触发条件第2条),输出<Buffer 31 32 33 34 35 36 37 38>;
    readable读取之后,已经到达了流的底部(对应readable触发条件第3条),会触发readable时间,将缓存中内容全部读出,输出<Buffer 39 30>,缓存长度清0

模拟实现readable

看了上面的例子我可能还是有点蒙圈,毕竟有些抽象。因此给大家用fs.read的方式简单的实现了一个readable。

let fs = require('fs');
let EventEmmitter = require('events');
function computeNewHighWaterMark(n) {
    n--;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    n++;
    return n;
}
class ReadStream extends EventEmmitter {
    constructor(path, options) {
        super();
        this.path = path;
        this.flags = options.flags || 'r';
        this.autoClose = options.autoClose || true;
        this.encoding = options.encoding || 'utf8';
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.start = options.start || 0;
        // 当前缓存里内容的字节数
        this.readableLength = 0;
        // 缓存区
        this.buffers = [];
        // 当需要读取时,先判断是不是正在读取,如果正在读取时,就不要再去读取了
        this.reading = false;
        // 是否需要触发readable事件
        this.emittedReadable = false;
        // 文件当前要读取的位置
        this.pos = this.start;
        // 用于记录文件是否已经读到末尾
        this.fileSize = fs.statSync(path).size;
        // 读之前先打开文件,注意是异步的
        this.open();
        // 判断是否监听了readable事件,如果是则开始读取文件,第一次读取highWaterMark个字节
        this.on('newListener', (type) => {
            if (type === 'readable') {
                this.read();
            }
        })
    }
    // 这里的read等同于rs.read([size]),n代表要读取的字节数
    read(n) {        
        if(this.fileSize === this.pos && this.isLast){
            // 如果上一次已经将文件读完了,则直接返回内部缓存区剩余的内容
            return this.buffers.shift();
        }
        // 如果要读取的内容长度比缓存区中的长度大,则将highWaterMark置为最近的2的多少次方,重新触发read事件
        if (n > this.readableLength) {
            this.highWaterMark = computeNewHighWaterMark(n);
            this.emittedReadable = true;
            // 保证没有正在读取再进行读取
            if (!this.reading) { 
            this.reading = true;
            this._read();
            }
        }
        // 如果定义了n,则本次要读取的内容存放在curReadBuf中进行返回,即curReadBuf是读取的结果
        let curReadBuf;
        // 如果内部缓存中有则进行取出,放入要返回的curReadBuf中
        if (n > 0 && n <= this.readableLength) {
            curReadBuf = Buffer.alloc(n);
            let buf; // 用于存储每次循环从内部缓存中取出的数据
            let index = 0; // curReadBuf中当前最大的索引
            let flag = true;  // 用于在内部for循环中实现跳出while循环
            // 依次取内部缓存buffers中的数据,给curReadBuf进行赋值
            // buffers中的状态是:[buffer<5,6,7,8>,buffer<9,10,11,12>]
            while (flag && (buf = this.buffers.shift())) {
                for (let i = 0; i < buf.length; i++) {
                    curReadBuf[index++] = buf[i];
                    if (index === n) {
                        // 如果数据已经取了n个,则跳出while循环和本次的for循环,同时更新内部缓存区的长度,并将取出的多余数据塞回内部缓存区
                        flag = false;
                        this.readableLength -= n;
                        // 将剩余没有消耗的在塞回到内部缓存区
                        let r = buf.slice(i + 1);
                        if (r.length) {
                            this.buffers.unshift(r);
                        }
                        break;
                    }
                }
            }
        }
        // 如果缓存区没有内容,需要先去读取内容,然后触发readable事件,这里只是标识可以触发readable事件,没有去读取
        if (this.readableLength === 0) {
            this.emittedReadable = true;
        }
        // 如果缓存区长度小于highWaterMark,且没有正在读取,则需要去读取数据
        if ((this.readableLength < this.highWaterMark) && !this.reading) {
            this.reading = true;
            this._read();
        }
        // 如果本次读取的结果已经是文件底部了允许触发readable
        if(this.fileSize === this.pos){
            this.emittedReadable = true
        }
        return curReadBuf || null;
    }
    _read() {
        // 如果文件还没有打开则等文件打开之后再进行读取
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this._read());
        }
        // 每次读highWaterMark这么多
        let buffer = Buffer.alloc(this.highWaterMark);
        fs.read(this.fd, buffer, 0, buffer.length, this.pos, (err, byteRead) => {
            // 读完了重置reading标识
            this.reading = false;
            // 如果有读出内容,则进行标识重置及将内容放入内置的缓存区(buffers)中
            if (byteRead > 0) {
                // 维护缓存的长度
                this.readableLength += byteRead;
                // 重置下次开始读文件的位置
                this.pos += byteRead;
                // 将读取到的buffer返给内置的缓存buffers中
                this.buffers.push(buffer.slice(0, byteRead));
                if (this.emittedReadable) {
                    //默认下一次不触发readable事件
                    this.emittedReadable = false;
                    // 可以读取了,默认缓存满了
                    this.emit('readable');
                }
            } else {
                // 最后一次到达流底部的时候触发readable
                this.isLast = true;
                if (this.emittedReadable) {
                this.emittedReadable = false; 
                this.emit('readable'); 
                }
                // 当读取不到内容时触发end事件
                this.emit('end');
            }
        });
    }
    destroy() {
        fs.close(this.fd, () => {
            this.emit('close');
        })
    }
    // 打开要读取的文件
    open() {
        fs.open(this.path, this.flags, (err, fd) => {
            if (err) {
                this.emit('error', err);
                if (this.autoClose) {
                    this.destroy();
                }
                return;
            }
            this.fd = fd;
            this.emit('open', this.fd);
        })
    }
}
module.exports = ReadStream;

如何使用自己写的ReadStream呢?直接new个实例就好啦!还是来个栗子吧^_^

let path = require('path');
let ReadStream = require('./chat');
let rs = new ReadStream(path.join(__dirname, '1.txt'),{
  highWaterMark: 7
});
rs.on('readable', function(data){
  // 每次读取后内部缓存中剩余的字节数
  console.log(rs.readableLength)
  console.log(rs.read(8));
  // 每次读取后内部缓存中剩余的字节数
  console.log(rs.readableLength)
})