9.流之可读流与文件可读流

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()读取文件
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值