node
流
可读流
流是Node.js最重要的组成和设计模式之一,同时也是最容易让人产生误解的地方。流,不仅因为其在技术层面表现出的良好性能和高效率,更因为他的优雅,以及能完美契合Node.js的变成思想。
流为什么很重要
我们都知道,Node.js是以事件为基础的,实时处理是处理I/O操作最高效的方法,尽快接收输入内容,并经过程序处理尽快输出结果。
那什么是流呢?
流是一组有序的,有起点和终点的字节数据传输手段 它不关心文件的整体内容,只关注是否从文件中读到了数据,以及读到数据之后的处理 流是一个抽象接口,被 Node 中的很多对象所实现。比如HTTP 服务器request和response对象都是流。
从Node.js的核心模块开始,流是随处可见的。比如,fs模块使用的createReadStream()方法读取文件,使用createWriteStream()来写文件,HTTP的request和response对象本质来说也是流。
流的分类:四种基本的流类型 每个流的实例都是stream模块提供的四个基本抽象类之一的实现:
- Readable - 可读的流 (例如 fs.createReadStream()).
- Writable - 可写的流 (例如 fs.createWriteStream()).
- Duplex - 可读写的流 (例如 net.Socket).
- Transform - 在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate()). 实际上,流可以产生好几类事件,比如在可读流完成读取之后会产生end事件,或者在发生错误时产生error事件。
流中的数据有两种模式,二进制模式和对象模式.
二进制模式, 每个分块都是buffer或者string对象. 对象模式, 流内部处理的是一系列普通对象.
所有使用 Node.js API 创建的流对象都只能操作 strings 和 Buffer对象。但是,通过一些第三方流的实现,你依然能够处理其它类型的 JavaScript 值 (除了 null,它在流处理中有特殊意义)。 这些流被认为是工作在 “对象模式”(object mode)。 在创建流的实例时,可以通过 objectMode 选项使流的实例切换到对象模式。试图将已经存在的流切换到对象模式是不安全的。
现在先从简单入手,先聊聊可读流~
可读流
可读流读取数据的两种模式:流动模式(flowing
) 和暂停模式(paused
)
在flowing模式下,从流中读取数据的方式是给data事件添加一个监听器,在该模式下数据不是通过read()来获取。而是,只要流中的数据可读,便会立即被推送到data事件的监听器。 在 paused 模式下,必须显式调用 stream.read() 方法来从流中读取数据片段。 所有初始工作模式为 paused 的 Readable 流,可以通过下面三种途径切换到 flowing 模式:
- 监听 'data' 事件
- 调用 stream.resume() 方法
- 调用 stream.pipe() 方法将数据发送到 Writable
可读流可以通过下面途径切换到 paused 模式:
- 如果不存在管道目标(pipe destination),可以通过调用 stream.pause() 方法实现。
- 如果存在管道目标,可以通过取消 'data' 事件监听,并调用 stream.unpipe() 方法移除所有管道目标来实现。
如果 Readable 切换到 flowing 模式,且没有消费者处理流中的数据,这些数据将会丢失。 比如, 调用了 readable.resume() 方法却没有监听 'data' 事件,或是取消了 'data' 事件监听,就有可能出现这种情况。
实现可读流
现在,先实现一个可读流的例子。
/*
var rs = fs.createReadStream(path,[options]);
path读取文件的路径
options
flags打开文件要做的操作,默认为'r'
encoding默认为null
start开始读取的索引位置
end结束读取的索引位置(包括结束位置)
highWaterMark读取缓存区默认的大小64kb
*/
let fs = require('fs');
let path = require('path');
let rs = fs.createReadStream(path.join(__dirname,'文件名'),{// 返回的是一个可读流对象
flags:'r',// 文件的操作是读取操作
encoding:'utf8',// 默认是null null代表的是buffer
autoClose:true,// 读取完毕后自动关闭
highWaterMark:3,//最高水位线 默认是64k 64*1024b
start:0,//文件是有序的,从哪读到哪,可以自己定,从0开始读
end:9,//读10个数,包前又包后,0-9,既有前面又有后面
});
//rs.setEncoding('utf8');//设置读取的编码格式,一般是utf8,与指定{encoding:'utf8'}效果相同,设置编码
// 默认情况下 不会将文件中的内容输出
// 也就是说把这个流往这里一放,它不会自动读文件
// 内部会先创建一个buffer先读取3b,因为先把highWaterMark的值为3,这个文件不会全读了先放在这,什么都不动。这种模式叫非流动模式 / 暂停模式
// 那什么时候开始真正工作呢?你需要干一件事。这个流是基于事件的,所以我们要先监听一下事件,事件名是内置好的
// 操作的是一个文件,需要把文件打开
rs.on('open',function(){//监听open事件
console.log('文件开启');
})
// 文件有打开就有关闭
rs.on('close',function(){
console.log('文件关闭');
});
// 因为事件都是异步的,触发的时候会触发回调函数,可能会有一些错误,错误需要捕获
rs.on('error',function(err){//监听error事件
console.log(err);
});
rs.on('data',function(data){// 监听了data事件,流就从暂停模式->流动模式,但流动模式会疯狂触发data事件,不停的触发,知读取取完毕
console.log(data) // 读取10个数
rs.pause(); //暂停方法 表示暂停读取,暂停data事件触发
});
// 希望过一段时间继续读取
/*setTimeout(function(){
rs.resume();//恢复data事件触发,继续读取,变为流动模式,但又会触发pause,所以这里可以使用setInterval,每隔一段时间执行一次
},1000);*/
setInterval(function(){
rs.resume();//恢复触发data
},1000);
// 读完以后,告知流结束,该事件会在读完数据后被触发
rs.on('end',function(){
console.log('end')
})
以上实现的结果是;
文件开启
hel
low
orl
d
end
文件关闭
//注:我文件的数据是helloworld!
根据上面的案例,简单实现一个可读流的库(功能尚未完善):
// 可读流的实现
// 可读流是基于事件的,所以先把事件的模块先引进来
let EventEmitter = require('events');
let fs = require('fs');
// 实现一个createReadStream的类,类继承EventEmitter
class RreadStream extends EventEmitter {
// 类需要路径 对象 两个参数
constructor(path,options){
// 继承了必须干一件事
super();//固定的写法
this.path = path;
this.flags = options.flags || 'r';//如果没传,就给默认参数
this.autoClose = options.autoClose || true;//默认true
this.highWaterMark = options.highWaterMark|| 64*1024;//默认是64k
this.start = options.start||0;//默认开始是0
this.end = options.end;
this.encoding = options.encoding || null;//默认null
// 默认情况下,要先打开文件开始读,先写一个打开文件的方法
this.open();//打开文件 目的是为了获取fd--文件描述符,有了这个描述符,我们才能去读取内容
// 是否监听了data事件,如果监听了,就变成流动模式
this.flowing = null; // null就是暂停模式
// 要建立一个buffer 这个buffer就是要一次读多少
this.buffer = Buffer.alloc(this.highWaterMark);
this.pos = this.start; // pos 表示读取的位置 可变 start不变的
// 如果有新事件绑定了,就会触发newListener
this.on('newListener',(eventName,callback)=>{
if(eventName === 'data'){//如果事件名==data
// 相当于用户监听了data事件
this.flowing = true;//变成流动模式
// 监听了 就去读
this.read(); // 读内容了,需要实现一个read方法
}
})
}
open(){
fs.open(this.path,this.flags,(err,fd)=>{//路径,读(打开文件的目的),回调函数(箭头函数防止this混乱),err表示读文件出错了,fs表示文件描述符
if(err){
this.emit('error',err);//文件出错,触发error事件
if(this.autoClose){ // 是否自动关闭--防止文件出错,不能访问
// 如果文件可以自动关闭,那文件关掉再触发
this.destroy();//销毁掉,需要实现一个destroy方法
}
return;//报错就不往下走
}
this.fd = fd; // 如果文件没有出错,就把文件挂在当前的实例上 即保存文件描述符
this.emit('open'); // 文件打开成功了,监听open
});
};
// destroy方法
destroy(){
// 先判断有没有fd描述符 有则关闭文件 触发close事件
if(typeof this.fd ==='number'){//有的话一定是number类型,true说明打开过要销毁
fs.close(this.fd,()=>{//把文件先关掉,再销毁,触发close
this.emit('close');
});
return;
}
this.emit('close'); // 销毁 , 需实现一个close方法
};
resume(){//恢复触发data
this.flowing = true;
this.read();
}
pause(){//暂停方法 表示暂停读取,暂停data事件触发
this.flowing = false;
}
read(){
// 先判断此时文件有没打开
if(typeof this.fd !== 'number'){//如果文件没打开 ,触发一次open事件
// 文件打开 会触发open事件,触发事件后再执行read,此时fd肯定有了
return this.once('open',()=>this.read());//open走一次,就触发一次,之后不再走了,因为已经有文件了。
// 当触发open,事件已经被触发了,当触发的时候,会走对应的回调函数
}
// 此时有fd了
// 每次读多少个。如果没有end,随便读,读多少个都可以。如果有highWaterMark,可能读到某一个就停住了
let howMuchToRead = this.end?Math.min(this.highWaterMark,this.end-this.pos+1):this.highWaterMark;//读的时候,用结尾的进去当前位置
// 比方说:想读4个,但写的是3,每次读3个
// 第一次读123 下一次读一个4
fs.read(this.fd,this.buffer,0,howMuchToRead,this.pos,(err,bytesRead)=>{
// 读到了多少个 累加
if(bytesRead>0){
this.pos+= bytesRead;
// 判断有没传encoding,如果传了,转成字符串的形式,如果没传,说明默认是null
// this.buffer.slice(0,bytesRead)由buffer第0个开始就截,读一个就取一个,读两个就取两个,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();
}
});
}
}
module.exports = RreadStream
参考: Node.js文档