Node.js Stream入门

338 阅读6分钟

流的重要性

流是Node.js最重要的组成和设计模式之一。社区流行这样一句花:“stream all the things”。可见stream在node.js的重要性,了解并学会它必须提上日程,在此记录下对stream的理解,如有错误欢迎指出。那我们开始深挖它吧!

流的分类

  • stream.Readable:读取数据
  • stream.Writable:写入数据
  • stream.Duplex:可读+可写
  • stream.Transform:在读写过程中,可对数据进行修改

可读流 Readable Stream

从可读流中获取数据有两种模式:非流动模式和流动模式

非流动模式

添加一个对于readable事件的监听器,在读取新的数据时进行通知。可以通过read()方法来实现,数据可以是String、Buffer、nul

readbale.read([size]);//读取指定size的数据

流动模式

是给data事件添加一个监听器。只要流中的数据可读,便会立即被推送到data事件的监听器。

如何从流中读取数据

let fs = require('fs');
let path = require('path');
//创建一个可读流。返回的是一个可读流对象
let rs = fs.createReadStream(path.join(__dirname,'./1.txt'),{
    flag:'r',//文件的操作是读取操作
    encoding:'utf8',//默认是null,null代表的是buffer
    autoClose:true,//读取完毕后自动关闭
    highWaterMark:3, //默认是64k 1024*6b
    start:0, //开始读取位置
    end:10 // 包前又包后,即读取11位
});  
//默认情况下,不会将文件中的内容输出,内容会先创建一个buffer先读取3b的内容,放在那里不输出,非流动模式
re.setEncoding('utf8');//设置编码格式 和encoding:'utf8'功能一样
rs.on('open',function(data){
    console.log('open');
})
rs.on('close',function(data){
    console.log('close');
})
rs.on('error',function(err){
    console.log('err');
})
//流动模式会疯狂触发data事件,直到数据读取完毕
rs.on('data',function(data){ //暂停模式 -> 流动模式
    console.log(data);
})
rs.on('end',function(data){
    console.log('end');
})

执行结果

open
1.txt的内容
end
close

可读流的暂停与恢复

rs.on('data',function(data){ //暂停模式 -> 流动模式
    console.log(data);
    rs.pause();//暂停方法 表示暂停读取,暂停data事件触发,并不会导致流转换回非流动模式(就像播放器的暂停)
})
setTimeout(function(){
    re.resume();//过3秒恢复读取,恢复data事件的触发(就像播放器的播放按钮)
},3000)

可读流的实现

创建可读流实例

let fs = require('fs');
let EventEmitter = require('events');
class ReadStream extends EventEmitter{
    constructor(path,options){ //两个参数:路径和对象
        super();
        this.path = path;
        this.flags = options.flags || 'r';
        this.autoClose = options.autoClose || true;  //自动关闭
        this.encoding = options.encoding || null;
        this.highWaterMark = options.highWaterMark || 64*1024;  //最高水位线
        this.start = options.start || 0;
        this.end = options.end;
        this.len = 0; //当前存多少水  喝水是同步,倒水是异步,喝的仍然是当前杯子的水
        this.buffer = Buffer.alloc(this.highWaterMark); //建立一个buffer每次读highWaterMark这么多
        this.reading = false; //如果正在读取,就不会再读取
        this.emittedReadable = false; //当缓存区长度为0时才会触发事件
        this.pos = this.start;//读取的位置

        this.open();  //打开文件,调用时还没拿到fd(fd:文件描述符)

        //看是否监听了data事件,如果监听了,就要变成流动模式
        this.flowing = null; //null就是暂停模式
        this.on('newListener',(eventName,callback)=>{
            if(eventName === 'data'){ //看是否监听了data事件
                this.flowing = true;
                this.read(); //开始读取 读highWaterMark的长度
            }
        })
    }
}
module.exports = ReadStream;

我们来按照流的读取顺序来一一实现可读流

open方法

open(){ //打开文件
    fs.open(this.path,this.flags,(err,fd)=>{
        if(err){ //打开文件如果报错触发error事件
            this.emit('error',err);
            if(this.autoClose){ //是否自动关闭
                this.destory(); //有问题就销毁掉
            }
            return;
        }
        this.fd = fd; //保存文件描述符
        this.emit('open');  //文件打开了
    })
}

read方法

//把读取的内容放在buffer里
read(){
    if(typeof this.fd !== 'number'){ //此时文件还没打开
        //当文件真正打开的时候,会出发open事件,触发事件后再执行read,此时fd肯定有了
        return this.once('open',()=>this.read());
    }
    // 当获取到fd时 开始读取文件了
    let howMuchToRead = this.end?Math.min(this.end-this.pos+1,this.highWaterMark): this.highWaterMark;
    fs.read(this.fd,buffer,0,howMuchToRead,this.pos,(err,byteRead)=>{ //从哪开始读:0 ; 读取长度:howMuchToRead
        if(byteRead>0){
            this.pos += byteRead;
            let data = this.encoding?this.buffer.slice(0,byteRead).toString(this.encoding):this.buffer.slice(0,byteRead);
            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();
        }
    })
}

destroy方法

destroy(){
    //先判断有没有fd,有,关闭文件,触发close事件
    if(typeof this.fd!== 'number'){
      return this.emit('close'); //销毁
    }
    fs.close(this.fd,()=>{
      this.emit('close');
    })
}

pause和resume方法

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

可写流 Writable Stream

可写流表示数据的目的地,使用流模块提供的抽象类Writeable实现。

let fs = require('fs');
let ws = fs.createWriteStream('./1.txt',{
    flaga:'w',//读取的类型:写
    mode:0o666,
    autoClose:true,
    highWaterMark:3, //默认写是16k
    encoding:'utf8',
    start:0
});
//flag表示的并不是是否写入,表示的是能否继续写,但是返回false内容不会丢失,还是会把内容放到内存中
let flag = ws.write('test','utf8',()=>{}) //参数:写入的内容(必须是字符串或者buffer)、编码格式、callback
console.log(flag);

ws.end(); //结束写入,调用此方法会把缓存区强制全部写入,并且关闭文件
ws.end('end'); //end传递参数,会调用write方法将内容写入完毕之后再关闭

//如果调用end方法之后就不会触发drain 
ws.on('drain',function(){
    console.log('drain');
})

//常见报错:write after end,即end之后就不用在调用write方法

可写流的实现

创建可写流实例

let fs = require('fs');
let EventEmitter = require('events');
class WriteStream extends EventEmitter{
    constructor(path,options){
        super();
        this.highWaterMark = options.highWaterMark || 16*1024;
        this.mode = options.mode;
        this.start = options.start || 0;
        this.flags = options.flags || 'w';
        this.encoding = options.encoding || 'utf8';

        //可读流 要又一个缓存区,当正在写入文件时,内容要写入到缓存中
        //在源码中是一个链表,我们这里把它改成数组来实现,做一个小改动 =>[]
        this.buffer = [];
        this.writing = false; //标识 是否正在写入
        this.needDrain = false; //是否满足触发drain事件
        this.pos = 0; //记录写入的位置
        this.length = 0; //记录缓存区的大小
        this.open();
    }
}
module.exports = WriteStream;

open方法

不管读写文件,你首先得打开吧,要不读个xx?haha,开玩笑啦,言归正传,open方法实现

open(){
    fs.open(this.path,this.flags,this.mode,()=>{
        if(err){
            this.emit('error',err);
            if(this.autoClose){ //看是否自动关闭
                this.destroy(); //销毁
            }
            return;
        }
        this.fd = fd;
            this.emit('open');
    })
}

write方法

可写流顾名思义,当然是write最重要喽,了解了write方法,万里长征就跨出了一大步,来吧骚年

write(chunk,encoding=this.encoding,callback){
    chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,encoding);//判读chunk是否是buffer,不是buffer转为buffer
    //write 返回一个boolean类型
    this.length+=chunk.length;
    let ret = this.length<this.highWaterMark; //比较是否达到了缓存区的大小
    this.needDrain = !ret; //是否需要触发needDrain
    //判断是否正在写入,如果正在写入,就写入到缓存区中
    if(this.writing){
        this.buffers.push({
            encoding,
            chunk,
            callback
        })
    }else{
        //专门用来将内容写入到文件内
        this.writing = true;
        this._write(chunk,encoding,()=>this.clearBuffer());
    }
    return ret;
}
//清空缓存区
clearBuffer(){
    let buffer = this.buffer.shift();
    if(buffer){
        this._write(buffer.chunk,buffer.encoding,()=>this.clearBuffer()); //不断回调清空buffer
    }else{
        if(this.needDrain){ //判断是否需要触发drain
            this.emit('drain');
        }
    }
}
//这里才是真正的写^_^
_write(chunk,encoding,callback){
    if(typeof this.fd!=='number'){ //先判断文件是否打开即fd的存在
        return this.once('open',()=>this._write(chunk,encoding,callback));
    }
    fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWritten)=>{
        this.length -= byteWritten;
        this.pos += byteWritten;
        this.write = false;
        callback();//清空缓存区内容
    });
}

desroty方法

destroy(){
    if(typeof this.fd!=='number'){
        return this.emit('close');
    }
    fs.close(this.fd,()=>{
        this.emit('close');
    })
}

也许今天写的比较浅显,随着时间的推移也许会理解的更透彻,记录下今天,如果某天对流的概念理解更多,会持续修改补充此文章。2018-06-19