简单说说node里的流

723 阅读5分钟

Stream 是一个抽象的接口,Node中有很多对象实现了这个接口,例如,对http服务器发起请求的request对象就是一个Stream,还有process.studo(标准输出)。


为什么要用流

举一个读取文件的例子
使用fs.readFileSync同步读取一个文件,程序会被阻塞,所有的数据都会被读取到内存中,换用fs.readFile读取文件,程序不会被阻塞,但是所有的数据依旧会被一次性全部读取到内存中

当处理大文件压缩,归档,媒体文件和巨大的日志文件的时候,内存使用就成了问题,在这种情况下,流的优势就体现出来了
流被设计成异步的方式,相比将剩余的文件数据一次性读进内存中,还是值得读取到一个缓冲区,期望的操作将会被执行,而且结果会被写到输出流

          


node中,Stream有四种流类型:
Readable-可读操作
Writeable-可写操作
Duplex-可读写操作
Transform-操作被写入数据,然后读出结果
所有的Stream对象都是EventEmitter的实例,常用的事件有:
data-当有数据可读时触发。
end-没有更多的数据可读时触发
error-在接收和写入过程中发生错误时触发
finish-所有数据已被写入到底层系统时触发



下面为大家介绍常用的流操作
从流中读取数据
创建input.txt文件,内容如下
my name is happy
创建main.js文件,代码如下
let fs=require("fs");
//创建可读流
let rs=fs.createReadStream('input.txt');
let result=""
rs.on('data',(data)=>{//data事件,流切换为流动模式,数据会被尽可能快点流出
result+=data
})rs.on("end",()=>{//end事件会在读完数据后被触发console.log(result)})
rs.on('error',(err)=>{    console.log(err)})
以上代码执行结果如下:

my name is happy


fs.createReadStream(path,[options])
可选参数options为一个对象,可以有以下属性
  • flags 打开文件要做的操作,默认为‘r’
  • encoding 默认为 null
  • start 开始读取的索引位置
  • end 结束读取的索引位置
  • highWaterMark 读取缓存区 默认 64 kb
注:如果指定utf8编码,highWateMark要大于3个字节



写入流
创建main.js文件,代码如下:
let fs=require("fs");
let data="my name is happy,哈哈"

//创建一个可写流,写到文件 output.txt中
let ws=fs.createWriteStream("output.txt");
ws.write(data);//data需为buffer/string,返回值为布尔值,系统缓存区满时为false,没满时为true
ws.end();
ws.on("finish",()=>{
//在调用了stream.end方法后,且缓存区数据都已经传给底层系统后,finish事件将被触发
 console.log("写入完成。");
})
ws.on("error",(err)=>{
  console.log(err)
})
查看output.txt文件显示
my name is happy,哈哈

管道流
管道提供了一个输出流到输入流的机制。通常我们用于从一个流中获取数据并将数据传递到另一个流中。

                                  

如上面的图片所示,我们把文件比作装水的桶,而水就是文件里的内容,我们用管子(pipe)连接两个桶使得水从一个桶流入另一个桶,这样就慢慢的实现了大文件的复制过程。
以下实例我们通过读取一个文件的内容并将内容写入到另一个文件中
将上面的input.txt 写入到output.txt,代码如下
let fs=require("fs");
let rs=fs.createReadStream("input.txt");
let ws=fs.createWriteStream("output,txt");
rs.pipe(ws);
成功将input.txt的内容复制到output.txt中

pipe的实现原理
let fs = require('fs');
let path = require('path');
let ReadStream = require('./ReadStream');
let WriteStream = require('./WriteStream');
let rs = new ReadStream(path.join(__dirname,'./1.txt'),{
    highWaterMark:4
});

let ws = new WriteStream(path.join(__dirname,'./2.txt'),{
     highWaterMark:1
});
 rs.on('data',function(chunk){ // chunk 读到的内容
     let flag = ws.write(chunk);
     if(!flag){
         rs.pause();//暂停读取
     }
});
ws.on('drain',function(){//当缓存区充满,并且被排开后触发drain事件
    rs.resume();//恢复读取
});




链式流
链式是通过连接输出流到另一个流并创建多个流操作链的机制,链式流一般用于管道操作,
接下来我们就是用链式流来压缩和解压缩文件。

创建compress.js文件,代码如下:
let fs=require("fs");let zlib=require("zlib");//压缩input.txt文件为input.txt.gzfs.createReadStream('input.txt').pipe(zlib.createGzip())
.pipe(fs.createWriteStream('input.txt.gz'));//等价于fs.createReadStream('input.txt').pipe(zlib.createGzip())zlib.createGzip().pipe(fs.createWriteStream('input.txt.gz'));
运行代码后,我们可以看到当前目录下生成了一个input.txt的压缩文件input.txt.gz.
接下来,让我们来解压该文件,创建decompress.js,代码如下:
let fs=require("fs");
let zlib=require("zlib");
fs.createReadStream('input.txt.gz').pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('input.txt'))


下面说一下可读流简单实现
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);//截取bytesRead个buffer                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();            }        });    }    pipe(ws){        this.on('data',(chunk)=>{            let flag = ws.write(chunk);            if(!flag){                this.pause();            }        });        ws.on('drain',()=>{            this.resume();        })    }    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(){        //先打开文件        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'); // 文件打开了        });    }}