node中的流(stream)

淺淺藍qsl發表於2019-03-04

什麼是流

流在node中是非常重要的,gulp的task任務,檔案壓縮,和http中的請求和響應等功能的實現都是基於流來實現的。為什麼會有流的出現呢,因為我們一開始有檔案操作之後,包括寫檔案和讀檔案都會有一個問題,就是會把內容不停的讀到記憶體中,都讀取完之後再往外寫,這樣就會導致記憶體被大量佔用。為了解決這個問題流就誕生了,通過流,我們可以讀一點內容就往檔案中寫一點內容,並且可以控制讀取的速度。

流的種類有很多,最常用的有:

  • ReadStream 可讀流
  • WriteStream 可寫流
  • 雙工流
  • 轉換流
  • 物件流(gulp)

流的特點:

  • 有序的有方向的
  • 流可以自己控制速率

什麼是讀和寫呢?

  • 讀是將內容讀取到記憶體中
  • 寫是將記憶體或者檔案的內容寫入到檔案內

流都是基於原生的fs操作檔案的方法來實現的,通過fs建立流。流是非同步方法,都有回撥函式,所有的 Stream 物件都是 EventEmitter 的例項。常用的事件有:

  • open – 開啟檔案
  • data – 當有資料可讀時觸發。
  • error – 在接收和寫入過程中發生錯誤時觸發。
  • close – 關閉檔案
  • end – 沒有更多的資料可讀時觸發。
  • drain – 當快取區也執行完了觸發

可讀流

let fs = require(`fs`);
let rs = fs.createReadStream(`./2.txt`, {
  highWaterMark: 3, 
  flags:`r`,
  autoClose:true,
  start:0,
  end:3,
  encoding:`utf8`
});
複製程式碼

主要引數說明:

  • highWaterMark 檔案一次讀多少位元組,預設是64×1024
  • flags 型別,預設是r
  • autoClose 預設是true ,讀取完畢後自動關閉
  • start 讀取開始位置
  • end 讀取結束位置,star和end都是包前包後的。
  • endencoding 預設讀取的是buffer
    一般讀取可以使用預設引數。預設建立一個流是非流動模式,預設不會讀取資料,
    我們需要接收資料是基於事件的,我們要監聽一個data事件,資料會自動的流出來,資料從非流動模式變為流動模式。
    讀取之前先把檔案開啟:
rs.on(`open`,function () {
  console.log(`檔案開啟了`);
});
複製程式碼

內部會自動的觸發這個事件rs.emit(`data`),
不停的觸發data方法,直到資料讀完為止。

rs.on(`data`,function (data) {
  console.log(data);
  rs.pause(); // 暫停觸發on(`data`)事件,將流動模式又轉化成了非流動模式,可以用setTimeout(()=>{rs.resume()},5000)恢復
});
複製程式碼

檔案讀完後觸發end方法:

rs.on(`end`,function () {
  console.log(`讀取完畢了`);
});
複製程式碼

最後關閉檔案:

rs.on(`close`,function () {
  console.log(`關閉`)
});
複製程式碼

監控錯誤:

rs.on(`error`,function (err) {
  console.log(err)
});
複製程式碼

可讀流實現原理解析

let fs = require(`fs`);
let ReadStream = require(`./ReadStream`);
let rs = new ReadStream(`./2.txt`, {
  highWaterMark: 3, 
  flags:`r`,
  autoClose:true, 
  start:0,
  end:3,
  encoding:`utf8`
});
複製程式碼

ReadStream.js

let fs = require(`fs`);
let EventEmitter = require(`events`);
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事件
      }
    })
  }
  pause(){
    this.flowing = false;
  }
  resume(){
    this.flowing =true;
    this.read();
  }
  read(){ // 預設第一次呼叫read方法時還沒有獲取fd,所以不能直接讀
    if(typeof this.fd !== `number`){
       return this.once(`open`,() => this.read()); // 等待著觸發open事件後fd肯定拿到了,拿到以後再去執行read方法
    }
    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(); // 銷燬即可
      }
    });
  }
  // 開啟檔案用的
  destroy() {
    if (typeof this.fd != `number`) { return this.emit(`close`); }
    fs.close(this.fd, () => {
      // 如果檔案開啟過了 那就關閉檔案並且觸發close事件
      this.emit(`close`);
    });
  }
  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); // 觸發檔案的開啟的方法
    });
  }
}
module.exports = ReadStream;

複製程式碼

可寫流

可寫流有快取區的概念,

  • 第一次寫入是真的向檔案裡寫,第二次再寫入的時候是放到了快取區裡
  • 寫入時會返回一個boolean型別,返回為false時表示不要再寫入了,
  • 當記憶體和正在寫入的內容消耗完後,會觸發一個事件 drain,
let fs = require(`fs`);
let ws = fs.createWriteStream(`2.txt`,{
  flags: `w`, 
  highWaterMark: 3, 
  encoding: `utf8`,
  start: 0,
  autoClose: true, 
  mode: 0o666, 
});
複製程式碼

引數說明:

  • flags: 預設是w (寫)預設檔案不存在會建立,a 追加
  • highWaterMark:設定當前快取區的大小
  • encoding:檔案裡存放的都是二進位制
  • start: 從哪開始寫
  • autoClose: 預設為true,自動關閉(寫完之後銷燬)
  • mode: 寫的模式,預設0o666,可讀可寫
let i = 9;
function write() {
  let flag = true; // 表示是否能寫入
  while (flag&&i>=0) { // 9 - 0 
    flag = ws.write(i--+``);
  }
}
複製程式碼

drain只有嘴塞滿了吃完了才會觸發,不是消耗完就觸發

ws.on(`drain`,()=>{
  console.log(`幹了`);
  write();
})
write();
複製程式碼

可寫流實現原理

let fs = require(`fs`);
let WS = require(`./WriteStream`)
let ws = new WS(`./2.txt`, {
  flags: `w`, 
  highWaterMark: 1, 
  encoding: `utf8`,
  start: 0,
  autoClose: true, 
  mode: 0o666, 
});
let i = 9;
function write() {
  let flag = true;
  while (flag && i >= 0) {
    i--;
    flag = ws.write(`111`); // 987 // 654 // 321 // 0
    console.log(flag)
  }
}
write();
ws.on(`drain`, function () {
  console.log(`幹了`);
  write();
});
複製程式碼

WriteStream.js

let fs = require(`fs`);
let EventEmitter = require(`events`);
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;
    this.open(); // fd 非同步的  觸發一個open事件當觸發open事件後fd肯定就存在了

    // 寫檔案的時候 需要的引數有哪些
    // 第一次寫入是真的往檔案裡寫
    this.writing = false; // 預設第一次就不是正在寫入
    // 快取我用簡單的陣列來模擬一下
    this.cache = [];
    // 維護一個變數 表示快取的長度
    this.len = 0;
    // 是否觸發drain事件
    this.needDrain = false;
  }
  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`);
      }
    }
  }
  _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(); // 第一次就寫完了
    })
  }
  write(chunk, encoding = this.encoding) { // 客戶呼叫的是write方法去寫入內容
    // 要判斷 chunk必須是buffer或者字串 為了統一,如果傳遞的是字串也要轉成buffer
    chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
    this.len += chunk.length; // 維護快取的長度 3
    let ret = this.len < this.highWaterMark;
    if (!ret) {
      this.needDrain = true; // 表示需要觸發drain事件
    }
    if (this.writing) { // 正在寫入應該放到記憶體中
      this.cache.push({
        chunk,
        encoding,
      });
    } else { // 第一次
      this.writing = true;
      this._write(chunk, encoding, () => this.clearBuffer()); // 專門實現寫的方法
    }
    return ret; // 能不能繼續寫了,false表示下次的寫的時候就要佔用更多記憶體了
  }
  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);
    });
  }
}
module.exports = WriteStream;
複製程式碼

管道流

let fs = require(`fs`);
let rs = fs.createReadStream(`./2.txt`,{
  highWaterMark:1
});
let ws = fs.createWriteStream(`./1.txt`,{
  highWaterMark:3
});
rs.pipe(ws); // 會控制速率(防止淹沒可用記憶體)
複製程式碼
  • pipe方法 叫管道,可以控制速率,pipe會監聽rs的on(`data`),將讀取到的內容呼叫ws.write方法
  • 呼叫寫的方法會返回一個boolean型別
  • 如果返回了false就呼叫rs.pause()暫停讀取
  • 等待可寫流寫入完畢後 on(`drain`)在恢復讀取

pip實現原理

let RS = require(`./ReadStream`);

let WS = require(`./WriteStream`);

let rs = new RS(`./1.txt`,{
  highWaterMark:4
})
let ws = new WS(`./2.txt`, {
  highWaterMark: 1
});

rs.pipe(ws);
複製程式碼

ReadStream.js

let fs = require(`fs`);
let EventEmitter = require(`events`);
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;
    this.end = options.end || null;
    this.encoding = options.encoding || null;
    this.flags = options.flags || `r`;
    
    this.flowing = null;
    this.buffer = Buffer.alloc(this.highWaterMark);
    this.open(); 
    // {newListener:[fn]}
    this.on(`newListener`, (type) => { // 等待著 它監聽data事件
      if (type === `data`) {
        this.flowing = true;
        this.read();// 開始讀取 客戶已經監聽了data事件
      }
    })
  }
  pause(){
    this.flowing = false;
  }
  resume(){
    this.flowing =true;
    this.read();
  }
  read(){
    if(typeof this.fd !== `number`){
       return this.once(`open`,() => this.read());
    }
    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) => { 
      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(); 
      }
    });
  }
  destroy() {
    if (typeof this.fd != `number`) { return this.emit(`close`); }
    fs.close(this.fd, () => {
      this.emit(`close`);
    });
  }
  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        if (this.autoClose) {
          this.destroy();
        }
        this.emit(`error`, err); 
        return;
      }
      this.fd = fd; 
      this.emit(`open`, this.fd);
    });
  }
  pipe(dest){
    this.on(`data`,(data)=>{
      let flag = dest.write(data);
      if(!flag){
        this.pause();
      }
    });
    dest.on(`drain`,()=>{
      console.log(`寫一下聽一下`)
      this.resume();
    });
  }
}
module.exports = ReadStream;

複製程式碼

相關文章