node的讀寫流簡易實現

蘆夢宇發表於2018-06-03

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();
    });
  }
複製程式碼

相關文章