浅谈NodeJS中的流

588 阅读8分钟

正如nodeJS文档所说,流(stream)在Node.js中是处理流数据的抽象接口,它提供了基础的API,我们可以通过这些来实现流接口的对象,在nodeJS中有请求流、响应流、文件流等等,这些流的底层都是使用stream模块封装的,流是可读可写的,并且所有流都是EventEmitter的实例,这篇文章简单介绍下流模块并实现一个可读可写的文件流。

Stream模块简介

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

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

Readable可读流

所有的 Readable 都实现了 stream.Readable 类定义的接口

可读流有两种模式:流动模式(flowing)和暂停模式(paused)

说一下这两种模式,流动模式就是说将数据源源不断的提供给应用,我们用水龙头来做比喻,水龙头坏掉了,水源源不断的流出来,这就是流动模式,只要在Stream上绑定了ondata方法,流动模式就会自动触发,也就是说当我们监听了data方法,水龙头就坏掉了,触发了流动模式,数据源源不断的流出来。在这里数据并不是直接流向应用,而是先 push 到缓存池,在这个缓存池中有一个阈值highWatermark,当超过这个阈值的时候push就会返回false,两种场景下会出现这样的情况:主动执行了.pause()方法;应用读取数据的速度要比数据流向缓存池的速度慢,也就是说水龙头坏了,水流的太快,而你接的速度太慢。这种情况就叫做背压(最好的情况是应用接收一个数据,数据源就生产一个新数据,这样就能整体保持在一个水平,Readable提供pipe方法,用来实现这个功能)

另外一种就是暂停模式,在暂停模式下我们必须显示的调用stream.read()方法来从流中读取数据片段,也就是说我们的水龙头修好了,那么就变成了暂停模式,而stream.read()就是我们的水龙头开关,当调用这个方法的时候就打开了开关,数据流出来了,所有处于暂停模式的可读流,可以通过三种途径切换到流动模式:

  • 监听 'data' 事件
  • 调用 stream.resume() 方法
  • 调用 stream.pipe() 方法将数据发送到 Writable

stream.Readable类提供的事件及方法请见nodeJS官方文档

Writable可写流

可写流是对数据写入'目的地'的一种抽象

可写流的原理其实与可读流类似,当数据过来的时候会写入到资源池,当写入的速度很慢或者写入暂停时候,数据流便会进入到队列池缓存起来,我们把写入数据的源头称之为生产者,而当生产者写入的速度太快了以至于把队列池装满,就会出现背压 ,告诉生产者暂停写入数据,当队列池中的空间释放之后,可写流会发送一个drain消息告诉生产者恢复写入。

pipe(管道) 了解了可读流以及可写流,这里穿插一下pipe()方法,它绑定一个 Writable类到可读流上,将可写流自动切换到 flowing 模式并将所有数据传给绑定的Writable。数据流将被自动管理。这样,即使是可读流较快,目标可写流也不会超负荷。

Duplex双工流 和 Transform交换流

双工流是同时实现了Readable和Writable接口的流,所以说双工流是可读可写的。而交换流集成了双工流,所以说它同样适合可读可写的。

Duplex双工流的实现很简单,继承了可读流,并拥有可写流的方法有兴趣的可以看下nodeJS源码,这里不做过多介绍,我们还可以通过options参数来配置它为只可读、只可写或者半工模式。Transform交换流的输入输出是相互关联的,并在在中间做了一次转换处理,常见的有Gzip压缩、解压等,在交换流中还有两个缓存:可读端的缓存和可写端的缓存 ,还有就是在Transform交换流的内部不存在背压 ,因为它是将双工流的 Readable 连接到 Writable,由于 Readable 的生产效率与 Writable 的消费效率是一样的,所以说不存在背压问题。

文件可读流及可写流的实现

可写流WriteStream的实现

let EventEmitter = require('events');
let util = require('util');
let fs = require('fs');
util.inherits(WriteStream, EventEmitter);//继承EventEmitter类

function WriteStream(path, options) {
    EventEmitter.call(this);
    if (!(this instanceof WriteStream)) {
        return new WriteStream(path, options);
    }
    this.path = path;
    this.fd = options.fd;
    this.encoding = options.encoding||'utf8';
    this.flags = options.flags || 'w';
    this.mode = options.mode || 0o666;
    this.autoClose = options.autoClose || true;
    this.start = options.start || 0;
    this.pos = this.start;//开始写入的索引位置
    this.open();//打开文件进行操作
    this.writing = false;//没有在写入过程 中
    this.buffers = [];
    this.highWaterMark = options.highWaterMark||16*1024;
    //如果监听到end事件,而且要求自动关闭的话则关闭文件
    this.on('end', function () {
        if (this.autoClose) {
            this.destroy()
        }
    });
}
WriteStream.prototype.close = function(){
    //close方法
    fs.close(this.fd,(err)=>{
        if(err)
            this.emit('error',err);
    });
}
WriteStream.prototype.open = function () {
    //open方法
    fs.open(this.path, this.flags, this.mode, (err, fd) => {
        if (err)
            return this.emit('error', err);
        this.fd = fd;//把文件描述符赋给当前实例的fd属性
        //发射open事件
        this.emit('open', fd);
    });
}
/**
 * 会判断当前是后台是否在写入过程中,如果在写入过程中,
 则把这个数据放在待处理的缓存中,
 如果不在写入过程中,可以直接写。
 */
WriteStream.prototype.write = function (chunk, encoding, cb) {
    chunk= Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,this.encoding);

    //先把数据放在缓存里
    this.buffers.push({
        chunk,
        encoding,
        cb
    });

    let isFull = this.buffers.reduce((len, item) => len + item.chunk.length, 0)>=this.highWaterMark;
    //只有当缓存区写满了,那么清空缓存区的时候才会发射drain事件,否则 不发放
    this.needDrain = isFull;
    //如果说文件还没有打开,则把写入的方法压入open事件的监听函数。等文件一旦打开,立刻执行写入操作
    if (typeof this.fd !== 'number') {
         this.once('open', () => {
            this._write();
        });
        return !isFull;
    }else{
        if(!this.writing){
            setImmediate(()=>{
                this._write();
                this.writing = true;
            });
        }

        return !isFull;
    }
}
WriteStream.prototype._write = function () {
    let part = this.buffers.shift();
    if (part) {
        fs.write(this.fd,part.chunk,0,part.chunk.length,null,(err,bytesWritten)=>{
            if(err)return this.emit('error',err);
            part.cb && part.cb();
            this._write();
        });
    }else{
        //发射一个缓存区清空的事件
        this.emit('drain');
        this.writing = false;
    }
}
module.exports = WriteStream;

测试可写流代码:


let fs = require('fs');
let WriteStream = require('./WriteStream');
let ws = WriteStream('./1.txt',{
    flags:'w',
    mode:0o666,
    autoClose:true,//是否自动关闭文件
    encoding:'utf8',
    start:0,//从第个索引开始写
    highWaterMark:3
});
let i = 9;
function write(){
    let flag = true;
    while(flag && i>0){
        flag = ws.write((i--)+'');
        console.log('flag',flag);
    }
}
write();
ws.on('error',function () {
    console.error('error');
});
ws.on('drain',function () {
    console.log('drain');
    write();
});

可读流ReadStream的实现

1、暂停模式

//暂停模式
let fs = require('fs');
let EventEmitter = require('events');

class ReadStream extends EventEmitter {
    constructor(path, options) {
        super(path, options);
        this.path = path;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.buffer = Buffer.alloc(this.highWaterMark);
        this.flags = options.flags || 'r';
        this.encoding = options.encoding;
        this.mode = options.mode || 0o666;
        this.start = options.start || 0;
        this.end = options.end;
        this.pos = this.start;
        this.autoClose = options.autoClose || true;
        this.bytesRead = 0;
        this.closed = false;
        this.flowing;//控制流的模式(三种)
        this.needReadable = false;
        this.length = 0;
        this.buffers = [];
        this.on('end', function () {
            if (this.autoClose) {
                this.destroy();
            }
        });
        this.on('newListener', (type) => {
            if (type == 'data') {
                this.flowing = true;
                this.read();
            }
            if (type == 'readable') {
                this.read(0);
            }
        });
        this.open();
    }

    open() {
    //实现open方法
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
            if (err) {
                if (this.autoClose) {
                    this.destroy();
                    return this.emit('error', err);
                }
            }
            this.fd = fd;
            this.emit('open');
        });
    }

    read(n) {
        if (typeof this.fd != 'number') {
        //判断文件是否打开
            return this.once('open', () => this.read());
        }
        n = parseInt(n, 10);
        if (n != n) {
            n = this.length;
        }
        if (this.length == 0)
            this.needReadable = true;
        let ret;
        if (0 < n < this.length) {
            ret = Buffer.alloc(n);
            let b;
            let index = 0;
            while (null != (b = this.buffers.shift())) {
                for (let i = 0; i < b.length; i++) {
                    ret[index++] = b[i];
                    if (index == ret.length) {
                        this.length -= n;
                        b = b.slice(i + 1);
                        this.buffers.unshift(b);
                        break;
                    }
                }
            }
            if (this.encoding) ret = ret.toString(this.encoding);
        }

        let _read = () => {
            let m = this.end ? Math.min(this.end - this.pos + 1, this.highWaterMark) : this.highWaterMark;
            fs.read(this.fd, this.buffer, 0, m, this.pos, (err, bytesRead) => {
                if (err) {
                    return
                }
                let data;
                if (bytesRead > 0) {
                    data = this.buffer.slice(0, bytesRead);
                    this.pos += bytesRead;
                    this.length += bytesRead;
                    if (this.end && this.pos > this.end) {
                        if (this.needReadable) {
                            this.emit('readable');
                        }

                        this.emit('end');
                    } else {
                        this.buffers.push(data);
                        if (this.needReadable) {
                            this.emit('readable');
                            this.needReadable = false;
                        }

                    }
                } else {
                    if (this.needReadable) {
                        this.emit('readable');
                    }
                    return this.emit('end');
                }
            })
        }
        if (this.length == 0 || (this.length < this.highWaterMark)) {
            _read(0);
        }
        return ret;
    }

    destroy() {
        fs.close(this.fd, (err) => {
            this.emit('close');
        });
    }

    pause() {
        this.flowing = false;
    }

    resume() {
        this.flowing = true;
        this.read();
    }

    pipe(dest) {
        this.on('data', (data) => {
            let flag = dest.write(data);
            if (!flag) this.pause();
        });
        dest.on('drain', () => {
            this.resume();
        });
        this.on('end', () => {
            dest.end();
        });
    }
}
module.exports = ReadStream;

2、流动模式

let EventEmitter = require('events');
let fs = require('fs');
class ReadStream extends EventEmitter {
    constructor(path, options) {
        super(path, options);
        this.path = path;
        this.flags = options.flags || 'r';
        this.mode = options.mode || 0o666;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.pos = this.start = options.start || 0;
        this.end = options.end;
        this.encoding = options.encoding;
        this.flowing = null;
        this.buffer = Buffer.alloc(this.highWaterMark);
        this.open();//准备打开文件读取
        //当给这个实例添加了任意的监听函数时会触发newListener
        this.on('newListener',(type,listener)=>{
            //如果监听了data事件,流会自动切换的流动模式
            if(type == 'data'){
              this.flowing = true;
              this.read();
            }
        });
    }
    read(){
        if(typeof this.fd != 'number'){
            return this.once('open',()=>this.read());
        }
        let howMuchToRead = this.end?Math.min(this.end - this.pos + 1,this.highWaterMark):this.highWaterMark;
        //this.buffer并不是缓存区
        fs.read(this.fd,this.buffer,0,howMuchToRead,this.pos,(err,bytes)=>{//bytes是实际读到的字节数
            if(err){
                if(this.autoClose)
                    this.destroy();
                return this.emit('error',err);
            }
            if(bytes){
                let data = this.buffer.slice(0,bytes);
                this.pos += bytes;
                data = this.encoding?data.toString(this.encoding):data;
                this.emit('data',data);
                if(this.end && this.pos > this.end){
                   return this.endFn();
                }else{
                    if(this.flowing)
                      this.read();
                }
            }else{
                return this.endFn();
            }

        })
    }
    endFn(){
        this.emit('end');
        this.destroy();
    }
    open() {
        fs.open(this.path,this.flags,this.mode,(err,fd)=>{
           if(err){
               if(this.autoClose){
                   this.destroy();
                   return this.emit('error',err);
               }
           }
           this.fd = fd;
           this.emit('open');
        })
    }
    destroy(){
        fs.close(this.fd,()=>{
            this.emit('close');
        });
    }
    pipe(dest){
        this.on('data',data=>{
            let flag = dest.write(data);
            if(!flag){
                this.pause();
            }
        });
        dest.on('drain',()=>{
            this.resume();
        });
    }
    pause(){
        this.flowing = false;
    }
    resume(){
       this.flowing = true;
       this.read();
    }
}
module.exports = ReadStream;

fs模块包含stream方法,fs是一个子类,其中的——read方法会调用父类的read方法

对于代码中所提及的Buffer的进制不太了解的同学请参见文章作为前端需要了解的编码知识 。 可读流及可写流方法请见nodeJS中文文档 这里不对方法做过多介绍,欢迎各位交流,发表你对nodeJS中Stream的理解!