node中的流
1. 流的概念
- 流是一组有序的,有起点和终点的字节数据传输手段
- 它不关心文件的整体内容,只关注是否从文件中读到了数据,以及读到数据之后的处理
- 流是一个抽象接口,被 Node 中的很多对象所实现。比如fs模块就是基于流来写的,HTTP 服务器request和response对象都是流。
- 一般读取大文件的时候才用流,小文件直接fd.readFile就可以了
2. 可读流createReadStream
2.1 创建可读流
let rs = fs.createReadStream(path.resolve(__dirname, 'test.txt'), { // path可以是绝对可以是相对路径
flags: 'r', // 创建可读流的标识是r 读取文件
encoding: null, // 编码默认null,null就表示读出来的结果是buffer类型
autoClose: true, // 读取完毕后自动关闭,默认就是true
start: 0, // 包前 自洁素
// end: 4, // 包后 字节数
highWaterMark: 2 // 每次读多少个字节,如果不写,默认是64k,即64*1024
})
2.2 可读流监听的一系列事件
- err事件
- open事件
- data事件
- end事件
- close事件
以下所有的on都是内部emit出来的
2.2.1 err事件
rs.on('err', (err) => {
console.log(err)
})
2.2.2 open事件
rs.on('open', (fd) => {
console.log(fd); // fd 打开的文件的描述符
})
2.2.3 data事件
监听文件读取,会不停监听读取,直至读完。除非内部有
rs.pause()
使得读取停下来
rs.on('data', (chunk) => {
console.log(chunk); // 每次读取的内容
})
2.2.4 end事件
rs.on('end', () => {
console.log('读完了');
})
2.2.5 close事件
rs.on('close', () => {
console.log('文件关闭了');
});
2.3 可读流常用的方法
- setEncoding(‘utf8’)
- pause()
- resume()
2.3.1 setEncoding
与指定{encoding:‘utf8’}效果相同,设置编码
rs.setEncoding('utf8');
2.3.2 pause 与 resume
- pause代表停止流的读取,并不是停止代码执行。
rs.on('data', (chunk) => {
rs.pause();
console.log(chunk);
})
setInterval(() => { // 每个1s恢复读取
rs.resume()
}, 1000)
上面这段代码的意思是:监听读取,但是第一次读取就会停下来,打印chunk内容,1s后恢复读取,又停下来,打印chunk内容,1s后又恢复读取,如此循环,直至读完,但是读完没有销毁定时器,需要销毁。
2.4 可读流特点
- 必须要有一个on(‘data’), 以及一个on(‘end’),如果是文件流还会提供两个方法,一个是open,一个是errpr
- 控制读取的速率,可以用rs.pause() 和 rs.resume()
- 如何将读到的内容拼起来:
- 如果encoding是null,也就是读出来的是buffer,那么可以这么拼接内容:
定义一个数组arr,每次读到的内容push到这个数组中,然后利用buffer.concat(arr)得到所有内容拼成的buffer,然后对这个所有内容toString()就可以得到字符串
2.5 可读流 与 文件可读流
2.5.1 可读流
- 正常的可读流 是继承于readable接口,并不需要用到fs模块
- 可读流是继承自Readable的,Readable是stream下的一个类
- 当可读流调用on(‘data’)的时候,默认谁去调用Readable这个类的read方法,而这个read方法又会去调用子类(可读流)的_read()方法。
- 这个_read()方法中,会有一个push方法,每次读到数据会调用一次push方法,这个push方法是可读流的父类Readable提供的,调用push相当于将读到的结果放入,然后就会触发on(‘data’)事件
- 当push方法放入的数据为null的时候,代表读取结束了,就会触发on(‘end’)事件
const { Readable } = require("stream");
class MyRead extends Readable {
_read(){
let i = 0;
this.push('ok'); // 这个push是readable提供的 只要我们调用push将结果放入,就会触发on('data')事件
this.push(null); // 放入null的时候就代表结束了,会触发on('end')事件
}
}
let mr = new MyRead();
mr.on("data", function (chunk) { // 调用on('data')的时候,默认会去调用Readable这个类中的read方法,而这个read方法中又会去调用我们自己实现的MyRead类中的_read方法
console.log(chunk)
});
mr.on('end', () => {
console.log('结束了')
})
// 文件可读流 跟 可读流 不是一样的
// 正常的可读流 是继承于readable接口,并不需要用到fs模块
// 文件可读流 内部是使用fs.open fs.close on('data') on('end')
// 这里on('data')就会去调用父类Readable的read方法,父类的read会调用子类的_read方法,这样的好处是,可以实现子类_read的自定义。不管你子类_read是怎么写的,父类帮你调用即可。
class Parent{
read(){
this._read()
}
}
class ChildA extends Parent{
_read(){
console.log('ChildA的_read写法')
}
}
class ChildB extends Parent{
_read(){
console.log('ChildB的_read写法')
}
}
let ca = new ChildA()
ca.read();// 这个最终会调用自己的_read方法,这个方法可以自己来定义
let cb = new ChildB()
cb.read();// 这个最终会调用自己的_read方法,这个方法可以自己来定义
2.5.2 文件可读流
-
文件可读流 内部是使用fs.open fs.close on(‘data’) on(‘end’)等方法
-
文件可读流 源码中其实就是return ReadStream实例,ReadStream主要继承events类,因为需要用到发布订阅
-
文件可读流 在constructor中就会默认调用open方法,也就是打开文件,拿到文件描述符fd,并且emit(‘open’)
-
然后 用on(‘newListener’)监听用户绑定的事件,当绑定事件为data的时候,就会去读文件,也就是走read方法,read方法内部其实就是fs.read方法,一部分一部分的读取,读取一部分就会emit(‘data’)事件,读完就会emit(‘end’)事件,读完后还会关闭文件,那就会emit(‘close’)事件
-
当然还提供了pause和resume方法,其实就是用一个变量去控制是否正在读,该变量如果false,则表示暂停了,true就表示可以读
-
自己手写一般文件可读流
// 自己手写一般文件可读流
// 由于文件可读流主要是return了ReadStream实例,所以我们主要实现ReadStream
const fs = require('fs');
const EventEmitter = require('events')
class ReadStream extends EventEmitter {
constructor(path, options={}){
super();
this.path = path;
this.flags = options.flags || 'r';
this.encoding = options.encoding || null;
this.start = options.start || 0;
this.end = options.end || undefined;
this.highWaterMark = options.highWaterMark || 64 * 1024;
if (typeof this.autoClose === 'undefined') {
this.autoClose = true;
} else {
this.autoClose = options.autoClose;
}
this.offset = this.start; // 读的时候的偏移量
this.flowing = false; // 默认是非流动模式,也就是暂停读取模式
// 会立马open
this.open(); // 默认就调用打开文件夹事件
// 怎么知道用户绑定了open方法,可以监听newListener事件,当用户绑定任何一个方法的时候,会触发该事件
this.on('newListener', (type) => {
if (type === 'data') { // 说明绑定了该事件,那么就需要去进行读写操作
this.flowing = true;
this.read(); // 读取文件
}
})
}
pause(){
this.flowing = false;
}
resume(){
if (!this.flowing) {
this.flowing = true;
this.read(); // 继续读
}
}
// destroy(){}
open(){
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
// this.destroy(); // 把这个流销毁
return this.emit('error', err)
}
this.fd = fd; // 将文件描述符保存起来
this.emit('open', fd)
})
}
read(){
/*
这里为什么要判断下this.fd是否为number?
因为在constructor中监听了newListener事件,这个时间是events模块的,代表一旦绑定事件就触发newListener事件。
当用户绑定data事件的时候,会去调用read方法,但是read方法中需要用到fd,这个fd是open中的,而open是异步的,
所以有可能先执行read再执行完open,那么执行read时还没拿到fd,这个判断是否为number就是判断是否拿到了fd,
如果没拿到,那么手动去监听一次open事件,等执行open事件的时候去调用read方法
*/
if (typeof this.fd !== 'number') { // 保证fd一定存在
return this.once('open', () => this.read())
}
// fd一定存在了, buffer是内存,内存是引用类型,所以不能用this.buffer = Buffer.alloc(this.highWaterMark),这样相当于每次都改了同一个buffer的值,由于是内存,是引用关系,俺么之前读出来的也会被改掉。所以需要每次都去创建一个buffer
const buffer = Buffer.alloc(this.highWaterMark)
let howMuchToRead = Math.min((this.end - this.offset + 1), this.highWaterMark) // 真正要读取的个数
fs.read(this.fd, buffer, 0, howMuchToRead, this.offset, (err, bytesRead) => {
if (bytesRead) {
this.offset += bytesRead;
this.emit('data', buffer.slice(0, bytesRead))
if (this.flowing) {
this.read(); // 继续读
}
} else { // 读完了
this.emit('end')
if (this.autoClose) {
fs.close(this.fd, () => {
this.emit('close')
})
}
}
})
}
}
module.exports = ReadStream;
- 用自己写的文件可读流读取文件
// 文件基于流进行了封装 封装了基于文件的可读流和可写流
// const Stream = require('stream'); // node内置的流模块,fs模块是基于这个模块来写的,http模块也继承stream来写的
const fs = require('fs')
const path = require('path')
const ReadStream = require('./ReadStream')
// 读取大文件的时候才用流,小文件直接fd.readFile就可以了
// fs.createReadStream 内部继承了stream模块,并且基于 fs.open fs.read fs.close方法来实现的
let rs = new ReadStream(path.resolve(__dirname, 'test.txt'), {
flags: 'r', // 创建可读流的标识是r 读取文件
encoding: null, // 编码默认null,null就表示读出来的是buffer类型
autoClose: true, // 读取完毕后自动关闭,默认就是true
start: 0, // 包前 自洁素
end: 100, // 包后 字节数
highWaterMark: 2 // 每次读多少个字节,如果不写,默认是64k,即64*1024
})
let arr = []
// 监听文件出错
rs.on('error', function(err){
console.log(err)
})
// 监听文件打开
rs.on('open', (fd) => {
console.log(fd);
})
// 监听文件读取
rs.on('data', (chunk) => {
rs.pause()
arr.push(chunk)
console.log(chunk)
})
// 监听文件读取完毕
rs.on('end', () => {
console.log(Buffer.concat(arr).toString())
console.log('结束了')
})
setInterval(() => {
rs.resume()
}, 1000)
2.5.3 可读流与文件可读流的异同
- 相同点:
- 都基于订阅发布模式,继承了events模块
- 都有监听on(‘data’) on(‘end’)方法
- 不同点:
- 可读流是继承自readable接口的,并不需要fs模块
- 文件可读流内部是使用fs.open fs.read fs.close 等文件操作方法
- 两者的实现原理是不一样的
2.5.4 整理出的面试题
2.5.4.1 可读流与文件可读流的异同
见上面
2.5.4.2 createReadStream的原理/ReadStream的原理/可读流的原理
createReadStream 返回了ReadStream的实例,这个实例继承了Readable类。
而ReadStream类在其原型上定义了open、_read、_destroy等方法。
这个Readable类主要是提供了read方法,这个read方法主要是调用子类的read方法,假设叫做_read。而_read才是真正去读文件的逻辑。当on(‘data’)的时候,就会去调用类Readable的read方法,父类的read会调用子类ReadStream的_read方法读取文件内容,
那么,再说回来,createReadStream 返回了ReadStream的实例,归根结底还是ReadStream这个类,那这个类怎么实现的?主要就是设计了open、_read、pause、resume、destroy等方法
- ReadStream一执行就会调用open打开文件,open方法会拿到文件描述符fd,并且调用this.emit(‘open’, fd)把文件描述符暴露出去,当监听on(‘open’)的时候能够拿到这个文件描述符
- 然后,打开文件后就要去读文件了,什么时候去读文件呢?会监听newListener事件,这个事件属于events模块的,表示有新的事件绑定监听的时候会触发,那么如果新增的监听事件是data,也就是监听了on(‘data’),那么就会去执行this.read(),这个read其实是父类Readable的,Readabel的read方法会去调用子类的_read()
- 那么,_read()方法, 就会调用fd.read去分段读取文件,读取一次emit(‘data’)一次,读完emit(‘end’)。然后就关闭文件,并emit(‘close’)。
- 当然还有一个pause()和resume()方法,这两个方法其实就是用一个变量的true与false来控制this._read(),也就是控制读写。
- 还有_destroy方法,就是fs.close关闭文件
以上就是整个ReadSteam的逻辑,其实createReadStream的逻辑。
最后说一下可读流与文件可读流是有区别的: - 文件可读流是createReadStream也就是ReadStream,是继承了Readable,并且有fs文件操作,open、close等
- 而可读流并没有open、close等的文件操作,主要是提供了read方法去调用子类的_read
2.5.4.3 如果问:ReadStream中打开了文件,为什么不直接读文件,而是要监听newListener事件,判断为data的时候再去读?
- 因为用户可能并没有写on(‘data’),那这样读取文件就白读了,所以要等用户绑定上去的时候再去读。
2.5.4.4 如果问:打开文件,然后监听到data事件,就开始读取文件了,会不会读不到文件?因为打开文件是异步的,也就是还没等打开就开始读了?
- 打开是异步的,还没打开就开始读了,这个情况ReadStream是这么解决的:
- 读之前,先判断文件描述符fd是否为number类型,是的话表示已经打开了,不是的话就还没打开,如果是number,那么就用this.once(‘open’, () => this._read())绑定一次性的open事件,因为文件打开是会emit(‘open’)的,所以等文件open了,再去执行this._read()读取文件