阅读 68

node的读写流简易实现

readStream

流都是基于EventEmitter实现的 我们先看看node自带的读流用法:

let fs = require('fs');
// 一般情况下我们不会使用后面的参数
let rs = fs.createReadStream('./1.txt'{
  highWaterMark: 3, // 字节
  flags:'r',
  autoClose:true, // 默认读取完毕后自动关闭
  start:0,
  //end:3,// 流是闭合区间 包start也包end
  encoding:'utf8'
});
// 默认创建一个流 是非流动模式,默认不会读取数据
// 我们需要接收数据 我们要监听data事件,数据会总动的流出来
rs.on('error',function (err) {
  console.log(err)
});
rs.on('open',function () {
  console.log('文件打开了');
});
// 内部会自动的触发这个事件 rs.emit('data');,默认情况下会读完整个文件
rs.on('data',function (data) {
  console.log(data);
  rs.pause(); // 暂停触发on('data')事件,将流动模式又转化成了非流动模式
});
setTimeout(()=>{rs.resume()},5000)
rs.on('end',function () {
  console.log('读取完毕了');
});
rs.on('close',function () {
  console.log('关闭')
});
复制代码

接下来手写writeStream:

先把constructor内部自带属性对应好

class ReadStream extends EventEmitter {
  constructor(path, options = {}) {
    super();
    this.path = path;
    this.highWaterMark = options.highWaterMark || 64 * 1024;
    this.autoClose = options.autoClose || true;
    this.start = options.start || 0; 
    this.pos = this.start; // pos会随着读取的位置改变
    this.end = options.end || null; // null表示没传递
    this.encoding = options.encoding || null;
    this.flags = options.flags || 'r';
    this.flowing = null; // 非流动模式
     // 用buffer盛读到的数据
    this.buffer = Buffer.alloc(this.highWaterMark);
  }
复制代码

属性配好了,结下来开始读,读文件第一步是打开文件,先实现open方法

open() {
    fs.open(this.path, this.flags, (err, fd) => { //fd标识的就是当前this.path这个文件,从3开始(number类型)
      if (err) {
        if (this.autoClose) { // 如果需要自动关闭,就销毁fd
          this.destroy(); // 销毁(关闭文件,触发关闭事件)
        }
        this.emit('error', err); // 如果有错误触发error事件
        return;
      }
      this.fd = fd; // 保存文件描述符
      this.emit('open', this.fd); // 触发文件的打开的方法
    });
  }
复制代码

这里用到了destory方法,它用来关闭文件

  destroy() {
    if (typeof this.fd != 'number') { return this.emit('close'); }
    fs.close(this.fd, () => {
      // 如果文件打开过了 那就关闭文件并且触发close事件
      this.emit('close');
    });
  }
复制代码

接下来最关键的来了,readStream 什么时候开始读取数据呢?只要注册了data 事件,就去读文件。如何监测data事件呢?用到了EventEmitter的newListener事件,该事件是同步的,只要注册了其它非newListener类型的事件,就会触发newListener类型事件对应的回调函数,可以在回调函数中读文件

 this.on('newListener', (type) => { // 等待着 它监听data事件
      if (type === 'data') {
        this.flowing = true;
        this.read();// 开始读取 客户已经监听了data事件
      }
    })
复制代码

接下来整理下constructor:

class ReadStream extends EventEmitter {
  constructor(path, options = {}) {
    super();
    this.path = path;
    this.highWaterMark = options.highWaterMark || 64 * 1024;
    this.autoClose = options.autoClose || true;
    this.start = options.start || 0; 
    this.pos = this.start; // pos会随着读取的位置改变
    this.end = options.end || null; // null表示没传递
    this.encoding = options.encoding || null;
    this.flags = options.flags || 'r';

    // 参数的问题
    this.flowing = null; // 非流动模式
    // 弄一个buffer读出来的数
    this.buffer = Buffer.alloc(this.highWaterMark);
    this.open(); 
    // 次方法默认同步调用的
    this.on('newListener', (type) => { // 等待着 它监听data事件
      if (type === 'data') {
        this.flowing = true;
        this.read();// 开始读取 客户已经监听了data事件
      }
    })
  }
  }
复制代码

那如何读取文件呢?上面代码中,我们注意到在newListener事件回调中调用了read 方法,由于该事件是同步的,有可能在文件还没打开时,就 注册了data事件--->触发了newListener事件--> 然后去读文件,此时如果继续读,肯定读取失败。因此需要处理未打开的情况。

read(){ 
    // 默认第一次调用read方法时还没有获取fd,所以不能直接读
    if(typeof this.fd !== 'number'){
       return this.once('open',() => this.read()); // 等待着触发open事件后fd肯定拿到了,拿到以后再去执行read方法
    }
    // 当获取到fd时 开始读取文件了
    // 第一次应该读2个 第二次应该读2个
    // 第二次pos的值是4 end是4
    // 一共4个数 123 4
    let howMuchToRead = this.end ?Math.min(this.end-this.pos+1,this.highWaterMark): this.highWaterMark;
    fs.read(this.fd, this.buffer, 0, howMuchToRead, this.pos, (error, byteRead) => { 
      // byteRead真实的读到了几个
      this.pos += byteRead; // 位置偏移
      // this.buffer默认就是三个
      let b = this.encoding ? this.buffer.slice(0, byteRead).toString(this.encoding) : this.buffer.slice(0, byteRead);
      this.emit('data', b);
      if ((byteRead === this.highWaterMark)&&this.flowing){
        return this.read(); // 继续读
      }
      // 这里就是没有更多的逻辑了
      if (byteRead < this.highWaterMark){
        // 没有更多了
        this.emit('end'); // 读取完毕
        this.destroy(); // 销毁即可
      }
    });
  }
复制代码

暂停和复位模式:

 pause(){
    this.flowing = false;
  }
  resume(){
    this.flowing = true;
    this.read();
  }
复制代码

writeStream 流的实现

node自带的写流的例子

let fs = require('fs');
let ws = fs.createWriteStream('2.txt',{
  flags:'w',
  encoding:'utf8',
  start:0,
  highWaterMark:3 // 一次能写三个
});
let i = 9;
function write() {
  let flag = true; // 表示是否能写入
  while (flag&&i>=0) { // 9 - 0 
    flag = ws.write(i--+'');
  }
}
// drain写入总数)大于highWaterMark,并且将它们都写入时才触发
ws.on('drain',()=>{ 
  console.log('干了');
  write();
})
write();
复制代码

接下来手写writeStream:

先把constructor内部自带属性对应好,this.len是当前待写入的总长度,实际上就是缓存区的长度(缓存区分两部分)

class WriteStream extends EventEmitter {
  constructor(path, options = {}) {
    super();
    this.path = path;
    this.flags = options.flags || 'w';
    this.encoding = options.encoding || 'utf8';
    this.start = options.start || 0;
    this.pos = this.start;
    this.mode = options.mode || 0o666;
    this.autoClose = options.autoClose || true;
    this.highWaterMark = options.highWaterMark || 16 * 1024;
    // fd 异步的  触发一个open事件当触发open事件后fd肯定就存在了
    this.open(); 
    // 第一次写入是真的往文件里写
    this.writing = false; // 默认第一次就不是正在写入
    // 缓存我用简单的数组来模拟一下
    this.cache = [];
    // 维护一个变量 表示缓存的长度
    this.len = 0;
    // 是否触发drain事件
    this.needDrain = false;
   }
  }
复制代码

open方法以及destory方法读流一样:

destroy() {
    if (typeof this.fd != 'number') {
      this.emit('close');
    } else {
      fs.close(this.fd, () => {
        this.emit('close');
      });
    }
  }
  open() {
    fs.open(this.path, this.flags, this.mode, (err, fd) => {
      if (err) {
        this.emit('error', err);
        if (this.autoClose) {
          this.destroy(); // 如果自动关闭就销毁文件描述符
        }
        return;
      }
      this.fd = fd;
      this.emit('open', this.fd);
    });
  }
复制代码

做好了前期工作,当调用write时,需要调用ws.write方法,接下来写write方法。记住一点:当我们 写文件时,文件依然可能没有打开,需要判断。
另外这里需要判断是否可以触发drain,当this.len >= this.highWaterMark(缓存区长度大于水位线时,才有可能触发drain)

// 客户调用的是write方法去写入内容
write(chunk, encoding = this.encoding) { 
    // 要判断 chunk必须是buffer或者字符串 为了统一,如果传递的是字符串也要转成buffer
    chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
    this.len += chunk.length; // 维护缓存的长度 3
    let ret = this.len < this.highWaterMark;
    // 表示需要触发drain事件,当把this.cache中的数据都写入时,触发drain
    if (!ret) {
      this.needDrain = true; 
    }
    // 正在写入应该放到内存中,
    if (this.writing) { 
      this.cache.push({
        chunk,
        encoding,
      });
    } else { // 第一次,//只有第一次调用写入文件,其余的写入this.cache。这里的第一次还包括缓//存清空后的第一次
      this.writing = true;
      this._write(chunk, encoding, () => this.clearBuffer()); // 专门实现写的方法
    }
    return ret; // 能不能继续写了,false表示下次的写的时候就要占用更多内存了
  }
复制代码

只要ret返回false,就说明不能继续写了,缓存区总长度大于水位线了,需要清缓存啦!! 重点来了,写文件,清缓存方法 this._write();

  _write(chunk, encoding, clearBuffer) {       //因为write方法是同步调用的此时fd还没有获取到,所以等待获取到再执行write操作
    if (typeof this.fd != 'number') {
      return this.once('open', () => this._write(chunk, encoding, clearBuffer));
    }
    fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, byteWritten) => {
      this.pos += byteWritten;
      this.len -= byteWritten; // 每次写入后就要再内存中减少一下
      clearBuffer(); // 第一次就写完了
    })
  }
  
  clearBuffer() {
    let buffer = this.cache.shift();
    // 缓存里有
    if (buffer) { 
      this._write(buffer.chunk, buffer.encoding, () => this.clearBuffer());
    } else {// 缓存里没有了
      if (this.needDrain) { // 需要触发drain事件
        this.writing = false; // 告诉下次直接写就可以了 不需要写到内存中了
        this.needDrain = false;
        this.emit('drain');
      }
    }
  }
复制代码

好,上面基本的流程已经具备了,再额外加一个pipe方法: rs.pipe(ws);咋实现呢

再readStream中添加如下方法:

pipe(dest){
    this.on('data',(data)=>{
      let flag = dest.write(data);
      if(!flag){
       // 已经不能继续写了,等写完了在恢复
        this.pause();
      }
    });
    dest.on('drain',()=>{
      console.log('写一下停一下');
      // 恢复,继续写
      this.resume();
    });
  }
复制代码
文章分类
前端
文章标签