NODE Stream流总结(1)

Stream简介

流(stream)在 Node.js 中是处理流数据的抽象接口(abstract interface). Stream模块提供了基础的API, 使用这些API可以很容易地来构建实现流接口的对象. 在Node.js中有请求流、响应流、文件流等等, 这些流的底层都是使用stream模块封装的, 流是可读可写的, 并且所有流都是EventEmitter的实例, 流的特点就是有序并且有方向的.

流(stream)提供了四种类型的流:

  • Stream.Readable(创建可读流)
  • Stream.Writable(创建可写流)
  • Stream.Duplex(创建可读可写流也就是我们经常说的双工流, 我们所熟知的TCP sockets就是Duplex流的实例)
  • Stream.Transform(变换流,也是一种Duplex流,可写端写入的数据经变换后会自动添加到可读端)

可读流(Readable Stream)

可读流(Readable Stream)是对提供数据的源头(source)的抽象. 所有的 可读流都实现了 stream.Readable 类定义的接口. 例如:

  • HTTP responses, on the client
  • HTTP requests, on the server
  • fs read streams
  • TCP sockets
  • process.stdin

可读流分为两种模式, 流动模式(flowing)和暂停模式(paused), 默认情况下我们在创建读流对象之后它会处于暂停模式, 在暂停模式下, 我们必须显式调用stream.read()方法来从流中读取数据片段, 而流动模式下数据就会不断的被读出, 当然这里要注意的是流动模式下数据并不是直接流向至应用, 背后其实还存在一个缓存池, 而池的大小是在我们创建读流对象时定义的, 每次读取时最多读取的字节数不会超过池的大小, 打个比方, 如果有9个字节要读取, 池的大小为3字节, 那么就会分三次读入, 在流动模式下, 可以调用pause方法回到暂停模式, resume方法再次回到流动模式。

使用流动模式下的读流可以参考一下这段代码:

let fs = require('fs');
let path = require('path');

// 返回的是一个可读流对象
let rs = fs.createReadStream(path.join(__dirname, '1.txt'), {
    flags: 'r', // 文件的操作是读取操作
    encoding: 'utf8', // 默认是null null代表的是buffer
    autoClose: true, // 读取完毕后自动关闭
    highWaterMark: 3, // 默认是64k  64*1024b
    start: 0,
    end: 3 // 包前又包后
});

rs.setEncoding('utf8');

rs.on('open', function() {
    console.log('文件打开了');
});

rs.on('close', function() {
    console.log('关闭');
});

rs.on('error',function (err) {
    console.log(err);
});

rs.on('data',function(data) { // 暂停模式 -> 流动模式
    console.log(data);
    rs.pause(); // 暂停方法 表示暂停读取,暂停data事件触发
});

setInterval(function() {
   rs.resume(); //恢复data时间的触发
}, 3000);

rs.on('end',function() {
    console.log('end')
});
复制代码

下面让我们来看一下读流的暂停模式,在暂停模式中,我们可以监听一个readable事件,它将会在流有数据可供读取时触发,但是这不是一个自动读取的过程,它需要我们自己去调用read方法来读取数据,在监听这个事件时,默认会将缓存区先填满一次,每当缓存区读完时,这个readable事件就会再被触发,并且每次调用read方法是都会去检测一次缓存区的长度是否小于水口(HighWaterMark)大小,如果小于的话再读取一段水口大小的数据放入缓存区中。另外要注意一点的是有时候我们调用read时读取的大小可能会超过缓存区的大小,这个时候默认就会更改缓存区的大小到一个适合现在读取量的大小,然后再重新触发readable事件。

使用暂停模式的读流可以参考一下这段代码:

let fs = require('fs');
let path = require('path');

let rs = fs.createReadStream(path.join(__dirname,'./1.txt'), {
    flags: 'r',
    autoClose: true,
    encoding: 'utf8',
    start: 0,
    highWaterMark: 3 
});

// 默认会先读满
// 当缓存区为空时 会默认再去触发readable事件
// 不满缓存区就再去读取
rs.on('readable',function() {
    // 我想读五个 缓存区只有3个 它会更改缓存区的大小再去读取
    let result =  rs.read(5);
    console.log(result);
});
复制代码

可读流实现

现在就让我们来实现一个简单的可读流,从上面例子调用的on方法就可以猜出流其实是继承自EventEmitter模块,所以让我们先创建一个继承自EventEmitter模块的读流类并且定义一个构造函数及一些属性参数

let EventEmitter = require('events');
let fs = require('fs');

class ReadStream extends EventEmitter {
    constructor(path,options) {
        super();
        this.path = path;
        this.flags = options.flags || 'r';
        this.autoClose = options.autoClose || true;
        this.highWaterMark = options.highWaterMark|| 64*1024;
        this.start = options.start||0;
        this.end = options.end;
        this.encoding = options.encoding || null
        
        // 要建立一个buffer池 这个buffer就是要一次读多少
        this.buffer = Buffer.alloc(this.highWaterMark);
        
        this.pos = this.start; // pos 读取的位置 可变 start不变的
    }
}

module.exports = ReadStream;
复制代码

接着在我们需要定义一个open方法来打开文件获取到文件描述符(fd),并在构造函数中调用open方法,同时,我们需要加一个事件监听的判断来检测是否监听了data事件,如果监听就要变成流动模式。然后我们还需要定义一个destroy事件,在文件操作出错或者读完之后调用。

constructor(path,options) {
    ...
    this.open(); //打开文件 fd
    this.flowing = null; // null就是暂停模式
    this.on('newListener', (eventName, callback) => {
        if (eventName === 'data') {
            // 相当于用户监听了data事件
            this.flowing  = true;
            // 监听了 就去读
            this.read(); // 去读内容了
        }
    })
}

destroy() {
    // 先判断有没有fd 有关闭文件 触发close事件
    if (typeof this.fd === 'number') {
        fs.close(this.fd, () => {
            this.emit('close');
        });
        return;
    }
    this.emit('close'); // 销毁
};
    
open() {
    // copy 先打开文件
    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'); // 文件打开了
    });
}
复制代码

接下来就要实现read方法来读取内容了,当然这里因为open是异步操作,所以我们read一定要在open的回调触发并且获取到fd之后才可以真正开始读的流程,读取时每次做多只能读取缓存池的最大值,然后当读取位置大于末尾或者读到的字节数为0时就代表文件内容已经读取完毕,否则,就会判断是否为流动模式,如果是就递归调用read方法。

read() {
    // 此时文件还没打开
    if(typeof this.fd !== 'number') {
        // 当文件真正打开的时候 会触发open事件,触发事件后再执行read,此时fd肯定有了
        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) => {
        // 读到了多少个 累加
        if (bytesRead > 0) {
            this.pos += bytesRead;
            let data = this.encoding ? this.buffer.slice(0, bytesRead).toString(this.encoding) : this.buffer.slice(0, bytesRead);
            this.emit('data', data);
            // 当读取的位置 大于了末尾 就是读取完毕了
            if (this.pos > this.end) {
                this.emit('end');
                this.destroy();
            }
            if (this.flowing) { // 流动模式继续触发
                this.read(); 
            }
        }else{
            this.emit('end');
            this.destroy();
        }
    });
}
复制代码

接着,我们再来实现下pause和resume方法

resume() {
    this.flowing = true;
    this.read();
}
pause() {
    this.flowing = false;
}
复制代码

最后附上完整的代码

let EventEmitter = require('events');
let fs = require('fs');

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

        this.open();//打开文件 fd

        this.flowing = null; // null就是暂停模式
        // 看是否监听了data事件,如果监听了 就要变成流动模式

        // 要建立一个buffer 这个buffer就是要一次读多少
        this.buffer = Buffer.alloc(this.highWaterMark);

        this.pos = this.start; // pos 读取的位置 可变 start不变的
        this.on('newListener', (eventName,callback) => {
            if (eventName === 'data') {
                // 相当于用户监听了data事件
                this.flowing  = true;
                // 监听了 就去读
                this.read(); // 去读内容了
            }
        })
    }
    
    read(){
        // 此时文件还没打开呢
        if (typeof this.fd !== 'number') {
            // 当文件真正打开的时候 会触发open事件,触发事件后再执行read,此时fd肯定有了
            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) => {
            // 读到了多少个 累加
            if (bytesRead > 0) {
                this.pos += bytesRead;
                let data = this.encoding ? this.buffer.slice(0, bytesRead).toString(this.encoding) : this.buffer.slice(0, bytesRead);
                this.emit('data', data);
                // 当读取的位置 大于了末尾 就是读取完毕了
                if(this.pos > this.end){
                    this.emit('end');
                    this.destroy();
                }
                if(this.flowing) { // 流动模式继续触发
                    this.read(); 
                }
            }else{
                this.emit('end');
                this.destroy();
            }
        });
    }
    
    resume() {
        this.flowing = true;
        this.read();
    }
    
    pause() {
        this.flowing = false;
    }
    
    destroy() {
        // 先判断有没有fd 有关闭文件 触发close事件
        if(typeof this.fd === 'number') {
            fs.close(this.fd, () => {
                this.emit('close');
            });
            return;
        }
        this.emit('close'); // 销毁
    };
    
    open() {
        // copy 先打开文件
        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'); // 文件打开了
        });
    }
}
module.exports = ReadStream;
复制代码

到这里我们就大致上完成了流动模式的实现,现在就让我们来实现一下暂停模式,同流动模式类似我们也需要先创建一个构造函数,初始化定义一些基本选项属性,然后调用一个open放法打开文件,并且有一个destroy方法来处里关闭逻辑。

let fs = require('fs');
let EventEmitter = require('events');

class ReadStream extends EventEmitter {
    constructor(path, options) {
        super();
        this.path = path;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.autoClose = options.autoClose || true;
        this.start = 0;
        this.end = options.end;
        this.flags = options.flags || 'r';
    
        this.buffers = []; // 缓存区 
        this.pos = this.start;
        this.length = 0; // 缓存区大小
        this.emittedReadable = false;
        this.reading = false; // 不是正在读取的
        this.open();
        this.on('newListener', (eventName) => {
            if (eventName === 'readable') {
                this.read();
            }
        })
    }
    destroy() {
        if (typeof this.fd !== 'number') {
            return this.emit('close')
        }
        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');
        });
    }
}
复制代码

接着我们就来实现一下read方法,首先如上面所说的如果缓存区的大小小于水口大小那我们就要读取一次水口大小的数据进去缓存区,并且在缓存区为空时我们需要触发readable事件,这个循环一直到文件中的数据全部被读取完之后才结束并触发end事件

read(n) {
    // 当前缓存区 小于highWaterMark时在去读取
    if (this.length == 0) {
        this.emittedReadable = true;
    }
    if (this.length < this.highWaterMark) {
        if(!this.reading) {
            this.reading = true;
            this._read(); // 异步的
        }
    }
}

_read() {
    // 当文件打开后在去读取
    if (typeof this.fd !== 'number') {
        return this.once('open', () => this._read());
    }
    // 上来我要喝水 先倒三升水 []
    let buffer = Buffer.alloc(this.highWaterMark);
    fs.read(this.fd, buffer, 0, buffer.length, this.pos, (err, bytesRead) => {
        if (bytesRead > 0) {
            // 默认读取的内容放到缓存区中
            this.buffers.push(buffer.slice(0, bytesRead));
            this.pos += bytesRead; // 维护读取的索引
            this.length += bytesRead;// 维护缓存区的大小
            this.reading = false;
            // 是否需要触发readable事件
            if (this.emittedReadable) {
                this.emittedReadable = false; // 下次默认不触发
                this.emit('readable');
            }
        } else {
            this.emit('end');
            this.destroy();
        }
    })
}
复制代码

接下来就是要实现在read方法中读取缓存区的buffer并且返回的逻辑了,这里比较复杂的是因为我们的缓存区数组buffers中的每个元素其实存放着的是每次读取一个水口大小的buffer串,所以我们需要根据调用者传入的读取长度来取出对应的buffer数量,然后如果有剩余还要放回原本数组中等待下次读取

read(n) { 
    // 如果n>0 去缓存区中取吧
    let buffer=null;
    let index = 0; // 维护buffer的索引的
    let flag = true;
    if (n > 0 && n <= this.length) { // 读的内容 缓存区中有这么多
        // 在缓存区中取 [[2,3],[4,5,6]]
        buffer = Buffer.alloc(n); // 这是要返回的buffer
        let buf;
        while (flag && (buf = this.buffers.shift())) {
            for (let i = 0; i < buf.length; i++) {
                buffer[index++] = buf[i];
                if (index === n) { // 拷贝够了 不需要拷贝了
                    flag = false;
                    this.length -= n;
                    let bufferArr = buf.slice(i + 1); // 取出留下的部分
                    // 如果有剩下的内容 在放入到缓存中
                    if (bufferArr.length > 0) {
                        this.buffers.unshift(bufferArr);
                    }
                    break;
                }
            }
        }
    }
    // 当前缓存区 小于highWaterMark时在去读取
    if (this.length == 0) {
        this.emittedReadable = true;
    }
    if (this.length < this.highWaterMark) {
        if(!this.reading) {
            this.reading = true;
            this._read(); // 异步的
        }
    }
    return buffer
}
复制代码

最后在read中我们还需要处理前面提到的调用read时传入读取长度可能大于缓存区现有的数据长度,这里我们会先去更改缓存区的大小到一个适合现在读取量的大小,然后读入符合这个大小的buffer到缓存区中,再重新触发readable事件

function computeNewHighWaterMark(n) {
  n--;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  n++;
 return n;
}

read(n) { // 想取1个
    if (n > this.length) {
        // 更改缓存区大小  读取五个就找 2的几次放最近的
        this.highWaterMark = computeNewHighWaterMark(n)
        this.emittedReadable = true;
        this._read();
    }

    // 如果n>0 去缓存区中取吧
    let buffer = null;
    let index = 0; // 维护buffer的索引的
    let flag = true;
    if (n > 0 && n <= this.length) { // 读的内容 缓存区中有这么多
        // 在缓存区中取 [[2,3],[4,5,6]]
        buffer = Buffer.alloc(n); // 这是要返回的buffer
        let buf;
        while (flag && (buf = this.buffers.shift())) {
            for (let i = 0; i < buf.length; i++) {
                buffer[index++] = buf[i];
                if (index === n) { // 拷贝够了 不需要拷贝了
                    flag = false;
                    this.length -= n;
                    let bufferArr = buf.slice(i+1); // 取出留下的部分
                    // 如果有剩下的内容 在放入到缓存中
                    if (bufferArr.length > 0) {
                        this.buffers.unshift(bufferArr);
                    }
                    break;
                }
            }
        }
    }
    // 当前缓存区 小于highWaterMark时在去读取
    if (this.length == 0) {
        this.emittedReadable = true;
    }
    if (this.length < this.highWaterMark) {
        if (!this.reading) {
            this.reading = true;
            this._read(); // 异步的
        }
    }
    return buffer;
}
复制代码

最后附上完整的代码

let fs = require('fs');
let EventEmitter = 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 EventEmitter {
    constructor(path, options) {
        super();
        this.path = path;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.autoClose = options.autoClose || true;
        this.start = 0;
        this.end = options.end;
        this.flags = options.flags || 'r';

        this.buffers = []; // 缓存区 
        this.pos = this.start;
        this.length = 0; // 缓存区大小
        this.emittedReadable = false;
        this.reading = false; // 不是正在读取的
        this.open();
        this.on('newListener', (eventName) => {
            if (eventName === 'readable') {
                this.read();
            }
        })
    }
    
    read(n) { 
        if (n > this.length){
            // 更改缓存区大小  读取五个就找 2的几次放最近的
            this.highWaterMark = computeNewHighWaterMark(n)
            this.emittedReadable = true;
            this._read();
        }

        // 如果n>0 去缓存区中取吧
        let buffer = null;
        let index = 0; // 维护buffer的索引的
        let flag = true;
        if (n > 0 && n <= this.length) { // 读的内容 缓存区中有这么多
            // 在缓存区中取 [[2,3],[4,5,6]]
            buffer = Buffer.alloc(n); // 这是要返回的buffer
            let buf;
            while (flag && (buf = this.buffers.shift())) {
                for (let i = 0; i < buf.length; i++) {
                    buffer[index++] = buf[i];
                    if(index === n){ // 拷贝够了 不需要拷贝了
                        flag = false;
                        this.length -= n;
                        let bufferArr = buf.slice(i+1); // 取出留下的部分
                        // 如果有剩下的内容 在放入到缓存中
                        if(bufferArr.length > 0) {
                            this.buffers.unshift(bufferArr);
                        }
                        break;
                    }
                }
            }
        }
        // 当前缓存区 小于highWaterMark时在去读取
        if (this.length == 0) {
            this.emittedReadable = true;
        }
        if (this.length < this.highWaterMark) {
            if(!this.reading){
                this.reading = true;
                this._read(); // 异步的
            }
        }
        return buffer;
    }
    
    // 封装的读取的方法
    _read() {
        // 当文件打开后在去读取
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this._read());
        }
        // 上来我要喝水 先倒三升水 []
        let buffer = Buffer.alloc(this.highWaterMark);
        fs.read(this.fd, buffer, 0, buffer.length, this.pos, (err, bytesRead) => {
            if (bytesRead > 0) {
                // 默认读取的内容放到缓存区中
                this.buffers.push(buffer.slice(0, bytesRead));
                this.pos += bytesRead; // 维护读取的索引
                this.length += bytesRead;// 维护缓存区的大小
                this.reading = false;
                // 是否需要触发readable事件
                if (this.emittedReadable) {
                    this.emittedReadable = false; // 下次默认不触发
                    this.emit('readable');
                }
            } else {
                this.emit('end');
                this.destroy();
            }
        })
    }
    destroy() {
        if (typeof this.fd !== 'number') {
            return this.emit('close')
        }
        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');
        });
    }
}

module.exports = ReadStream;
复制代码

LineReader

最后,结合上面所说的暂停模式readable,我们来实现一个行读取器的例子,我们先定义好一个行读取器类和它的测试代码,它实现的功能就是我们通过创建一个LineReader对象并传入要读取的文件,然后监听line事件,在每次读取到一行数据时就会触发line的回调函数。

// LineReader 行读取器
let fs = require('fs');
let EventEmitter = require('events');
let path = require('path');

class LineReader extends EventEmitter {

}

let lineReader = new LineReader(path.join(__dirname, './2.txt'));
lineReader.on('line', function (data) {
    console.log(data); // abc , 123 , 456 ,678
})
复制代码

接下来我们就可以来实现LineReader的构造函数了,我们首先要创建一个文件的可读流,然后定义在对象开始监听line事件时我们就会监听可读流的readable事件,并且我们需要定义一个叫buffer的临时数组,用来每次读取一行时记录当前行的buffer数据,然后在调用line的回调函数时传入,接着在readable事件的回调中我们就会一个字符一个字符的读取内容,如果判断已经到一行数据的结尾我们就会触发一次line事件,要注意的是,对于判断是否是新一行数据的逻辑在windows和mac下是不同的,在window下 换行回车是\r\n,在mac下只是\n,同时在判断到\r之后我们都需要读取多一个字节看下是不是\n,如果不是的话那它就是一个正常的内容,我们就需要将他放入buffer数组中等到下一次输出,最后我们需要监听end事件即当读流读取完所有数据之后将最后的buffer也就是最后一行数据传入line回调函数中

constructor(path) {
    super();
    this.RETURN = 0x0d;
    this.LINE = 10;
    this.buffer = [];
    this._rs = fs.createReadStream(path); // 默认情况下会先读highWaterMark
    this.on('newListener', (eventName) => {
        if (eventName === 'line') {
            this._rs.on('readable', () => {
                let char;
                // 读出来的内容都是buffer类型
                while (char = this._rs.read(1)) {
                    let current = char[0];
                    switch (current) {
                        // 当碰到\r时表示这一行ok了
                        case this.RETURN:
                            this.emit('line', Buffer.from(this.buffer).toString());
                            this.buffer.length = 0;
                            let c = this._rs.read(1);
                            // 读取\r后 看一下下一个是不是\n 如果不是就表示他是一个正常的内容
                            if (c[0] !== this.LINE) {
                                this.buffer.push(c[0]);
                            }
                            break;
                        case this.LINE: // mac只有\r 没有\n
                            this.emit('line', Buffer.from(this.buffer).toString());
                            this.buffer.length = 0;
                        default:
                            this.buffer.push(current);
                    }
                }
            });
            this._rs.on('end', () => {
                this.emit('line', Buffer.from(this.buffer).toString());
                this.buffer.length = 0
            });
        }
    })
}
复制代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值