node.js中流(Stream)的深度剖析

嘿_那個誰發表於2018-02-02

流(stream)在 Node.js 中是處理流資料的抽象介面(abstract interface)。stream 模組提供了基礎的 API 。使用這些 API 可以很容易地來構建實現流介面的物件。Node.js 提供了多種流物件。流可以是可讀的、可寫的,或是可讀寫的。所有的流都是 EventEmitter 的例項。

Node.js 中有四種基本的流型別:

  • Readable - 可讀的流 (例如 fs.createReadStream()).
  • Writable - 可寫的流 (例如 fs.createWriteStream()).
  • Duplex - 可讀寫的流 (例如 net.Socket).
  • Transform - 在讀寫過程中可以修改和變換資料的 Duplex 流 (例如 zlib.createDeflate()).

接下來讓我們一起去看看stream中的流是怎麼樣來工作的。

可寫流 Writable

fs.createWriteStream(path[, options])建立一個可寫流,對這個不太瞭解的可以檢視fs.createWriteStream(path[, options])
  let fs=require('fs');
  let ws=fs.createWriteStream('2.txt',{
      highWaterMark:3
  })
  ws.write('我們都是好孩子,哈哈、、、','utf8',(err)=>{
      if(err){
        console.log(err);
      }
  })
複製程式碼

那麼這樣一個可寫流究竟是如何實現的呢?我們將通過手寫程式碼來模擬fs.createWriteStream的功能來解析node中可寫流的工作原理,下面們將通過一張圖解來大概看看我們手寫程式碼有哪些功能點,圖片如下:

image

通過上面的圖解程式碼的功能也就很明顯了,下面我們就一一來實現,首先是建立一個類,構建好一個類的大體骨架:

     let fs=require('fs');
     let EventEmiter=require('events');
     class MyWriteStream extends EventEmiter{
       constructor(path,options){
         super();
         this.path=path;//路徑
         this.flags=options.flags||'w';//模式
         this.encoding=options.encoding||null;//編碼格式
         this.fd=options.fd||null;//開啟檔案的標識位
         this.mode=options.mode||0o666;//寫入的mode
         this.autoClose=options.autoClose||true;//是否自動關閉
         this.start=options.start||0;//寫入的開始位置
         this.pos=this.start;//寫入的標示位置
         this.writing=false;//是否正在寫入的標識
         this.highWaterMark=options.highWaterMark||1024*16;//每次寫入的最大值
         this.buffers = [];//快取區
         this.length = 0;//表示快取區位元組的長度
     
         this.open();
       }
       open(){
         
       }
       write(){
         
       }
       _write(chunk,encoding,callback){
         
       }
       clearBuffer(){
         
       }
       destroy(){
         
       }
     }
     
     module.exports=MyWriteStream;
複製程式碼
  • open方法

如思維導圖所示,open方法的功能主要是開啟對應路徑的檔案與觸發open事件,所以對應的程式碼片段如下:

    open(){
        fs.open(this.path,this.flags,this.mode,(err,fd)=>{
          if(err){
            if(this.autoClose){
              this.destroy();
            }
            this.emit('error',err);
            return;
          }
          this.fd=fd;
          this.emit('open');
        })
      }
複製程式碼
  • write方法程式碼段如下:
    write(data,encoding,callback){
        let chunk = Buffer.isBuffer(data)?data:Buffer.from(data,this.encoding);
        let len=chunk.length;
        this.length+=len;
        //判斷當前最新的快取區是否小於每次寫入的最大值
        let ret = this.length < this.highWaterMark;
        if (this.writing) {//表示正在向檔案寫資料,則當前資料必須放在快取區裡
          this.buffers.push({
            chunk,
            encoding,
            callback
          });
        } else {//直接呼叫底層的寫入方法進行寫入
          //在底層寫完當前資料後要清空快取區
          this.writing = true;
          this._write(chunk, encoding, () => {this.clearBuffer();callback&&callback()});
        }
        return ret;
      }
複製程式碼
  • _write方法如下:
    _write(chunk,encoding,callback){
        if(typeof this.fd != 'number'){
          return this.once('open',()=>this._write(chunk, encoding, callback));
        }
        fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,bytesWrite)=>{
          if(err){
            if(this.autoClose){
              this.destroy();
              this.emit('error',err);
            }
          }else{
              this.pos += bytesWrite;
              //寫入多少資料,快取區減少多少位元組
              this.length -= bytesWrite;
              callback && callback();
          }
        })
      }
複製程式碼
  • destroy方法,程式碼如下:
      destroy(){
          fs.close(this.fd,()=>{
            this.emit('end');
            this.emit('close');
          })
        }
複製程式碼
  • clearBuffer方法,程式碼如下:
    clearBuffer(){
        let data = this.buffers.shift();
        if(data){
          this._write(data.chunk,data.encoding,()=>{this.clearBuffer();data.callback()})
        }else{
          this.writing = false;
          //快取區清空了
          this.emit('drain');
        }
      }
複製程式碼
  • 最後完整的程式碼如下:
      let fs=require('fs');
      let EventEmiter=require('events');
      
      class MyWriteStream extends EventEmiter{
        constructor(path,options){
          super();
          this.path=path;//路徑
          this.flags=options.flags||'w';//模式
          this.encoding=options.encoding||null;//編碼格式
          this.fd=options.fd||null;//開啟檔案的標識位
          this.mode=options.mode||0o666;//寫入的mode
          this.autoClose=options.autoClose||true;//是否自動關閉
          this.start=options.start||0;//寫入的開始位置
          this.pos=this.start;//寫入的標示位置
          this.writing=false;//是否正在寫入的標識
          this.highWaterMark=options.highWaterMark||1024*16;//每次寫入的最大值
          this.buffers = [];//快取區
          this.length = 0;//表示快取區位元組的長度
      
          this.open();
        }
        open(){
          fs.open(this.path,this.flags,this.mode,(err,fd)=>{
            if(err){
              if(this.autoClose){
                this.destroy();
              }
              this.emit('error',err);
              return;
            }
            this.fd=fd;
            this.emit('open');
          })
        }
        write(data,encoding,callback){
          let chunk = Buffer.isBuffer(data)?data:Buffer.from(data,this.encoding);
          let len=chunk.length;
          this.length+=len;
          //判斷當前最新的快取區是否小於每次寫入的最大值
          let ret = this.length < this.highWaterMark;
          if (this.writing) {//表示正在向檔案寫資料,則當前資料必須放在快取區裡
            this.buffers.push({
              chunk,
              encoding,
              callback
            });
          } else {//直接呼叫底層的寫入方法進行寫入
            //在底層寫完當前資料後要清空快取區
            this.writing = true;
            this._write(chunk, encoding, () => {this.clearBuffer();callback&&callback()});
          }
          return ret;
        }
        _write(chunk,encoding,callback){
          if(typeof this.fd != 'number'){
            return this.once('open',()=>this._write(chunk, encoding, callback));
          }
          fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,bytesWrite)=>{
            if(err){
              if(this.autoClose){
                this.destroy();
                this.emit('error',err);
              }
            }else{
                this.pos += bytesWrite;
                //寫入多少資料,快取區減少多少位元組
                this.length -= bytesWrite;
                callback && callback();
            }
          })
        }
        clearBuffer(){
          let data = this.buffers.shift();
          if(data){
            this._write(data.chunk,data.encoding,()=>{this.clearBuffer();data.callback()})
          }else{
            this.writing = false;
            //快取區清空了
            this.emit('drain');
          }
        }
        destroy(){
          fs.close(this.fd,()=>{
            this.emit('end');
            this.emit('close');
          })
        }
      }
      
      module.exports=MyWriteStream;
複製程式碼

可讀流 Readable - 可讀的流 (例如 fs.createReadStream()).

fs.createReadStream()建立一個可讀流(例如 fs.createReadStream()),可讀流其實與可寫流很相似,但是可讀流事實上工作在下面兩種模式之一:flowing 和 paused 。

  • 在 flowing 模式下, 可讀流自動從系統底層讀取資料,並通過 EventEmitter 介面的事件儘快將資料提供給應用。
  • 在 paused 模式下,必須顯式呼叫 stream.read() 方法來從流中讀取資料片段。

可讀流可以通過下面途徑切換到 paused 模式:

  • 如果不存在管道目標(pipe destination),可以通過呼叫 stream.pause() 方法實現。
  • 如果存在管道目標,可以通過取消 'data' 事件監聽,並呼叫 stream.unpipe() 方法移除所有管道目標來實現。

這裡需要記住的重要概念就是,可讀流需要先為其提供消費或忽略資料的機制,才能開始提供資料。如果消費機制被禁用或取消,可讀流將 嘗試 停止生成資料。

注意: 為了向後相容,取消 'data' 事件監聽並 不會 自動將流暫停。同時,如果存在管道目標(pipe destination),且目標狀態變為可以接收資料(drain and ask for more data),呼叫了 stream.pause() 方法也並不保證流會一直 保持 暫停狀態。

注意: 如果 Readable 切換到 flowing 模式,且沒有消費者處理流中的資料,這些資料將會丟失。 比如, 呼叫了 readable.resume() 方法卻沒有監聽 'data' 事件,或是取消了 'data' 事件監聽,就有可能出現這種情況。

flowing模式
   //flowing 模式下createReadStream的工作程式碼如下:
   let fs=require('fs');
   let rs=fs.createReadStream('2.txt',{
     highWaterMark:3,
     encoding:'utf8'
   })
   rs.on('data',(data)=>{
     console.log(data);
   })
    
複製程式碼

其實,flowing模式下的可讀流的流程與可讀流差異不大,所以,這裡就不再畫原理分析圖了,可以參考上述可寫流的原理分析圖;手寫原理分析完整程式碼如下:

    let EventEmitter = require('events');
    let fs = require('fs');
    class ReadStream extends EventEmitter {
      constructor(path, options) {
        super(path, options);
        this.path = path;
        this.flags = options.flags || 'r';
        this.mode = options.mode || 0o666;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.pos = this.start = options.start || 0;
        this.end = options.end;
        this.encoding = options.encoding;
        this.flowing = null;
        this.buffer = Buffer.alloc(this.highWaterMark);
        this.open();
        this.on('newListener',(type,listener)=>{
          if(type == 'data'){
            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,(err,bytes)=>{
          if(err){
            if(this.autoClose)
              this.destroy();
            return this.emit('error',err);
          }
          if(bytes){
            let data = this.buffer.slice(0,bytes);
            this.pos += bytes;
            data = this.encoding?data.toString(this.encoding):data;
            this.emit('data',data);
            if(this.end && this.pos > this.end){
              return this.endFn();
            }else{
              if(this.flowing)
                this.read();
            }
          }else{
            return this.endFn();
          }
    
        })
      }
      endFn(){
        this.emit('end');
        this.destroy();
      }
      open() {
        fs.open(this.path,this.flags,this.mode,(err,fd)=>{
          if(err){
            if(this.autoClose){
              this.destroy();
              return this.emit('error',err);
            }
          }
          this.fd = fd;
          this.emit('open');
        })
      }
      destroy(){
        fs.close(this.fd,()=>{
          this.emit('close');
        });
      }
      pipe(dest){
        this.on('data',data=>{
          let flag = dest.write(data);
          if(!flag){
            this.pause();
          }
        });
        dest.on('drain',()=>{
          this.resume();
        });
      }
      pause(){
        this.flowing = false;
      }
      resume(){
        this.flowing = true;
        this.read();
      }
    }
    module.exports = ReadStream;
   
複製程式碼

#####paused 模式

   //fs.createReadStream原生api的程式碼如下:
   let fs=require('fs');
   let rs=fs.createReadStream('2.txt',{
     highWaterMark:3,
     encoding:'utf8'
   })
   rs.on('readable',()=>{
     console.log(rs.read());
   })
     
複製程式碼

這裡主要和flowing模式大同小異,只是這種模式下,讀取到的資料會放到資料片段裡面先快取起來,並觸發readable事件,再通過read方法來讀取已讀取到的資料片段。原理解析程式碼如下:

    let fs = require('fs');
    let EventEmitter = require('events');
    class ReadStream extends EventEmitter {
      constructor(path, options) {
        super(path, options);
        this.path = path;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.buffer = Buffer.alloc(this.highWaterMark);
        this.flags = options.flags || 'r';
        this.encoding = options.encoding;
        this.mode = options.mode || 0o666;
        this.start = options.start || 0;
        this.end = options.end;
        this.pos = this.start;
        this.autoClose = options.autoClose || true;
        this.bytesRead = 0;
        this.closed = false;
        this.flowing;
        this.needReadable = false;
        this.length = 0;
        this.buffers = [];
        this.on('end', function () {
          if (this.autoClose) {
            this.destroy();
          }
        });
        this.on('newListener', (type) => {
          if (type == 'data') {
            this.flowing = true;
            this.read();
          }
          if (type == 'readable') {
            this.read(0);
          }
        });
        this.open();
      }
      open() {
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
          if (err) {
            if (this.autoClose) {
              this.destroy();
              return this.emit('error', err);
            }
          }
          this.fd = fd;
          this.emit('open');
        });
      }
    
      read(n) {
        if (typeof this.fd != 'number') {
          return this.once('open', () => this.read());
        }
        n = parseInt(n, 10);
        if (n != n) {
          n = this.length;
        }
        if (this.length == 0)
          this.needReadable = true;
        let ret;
        if (0 < n < this.length) {
          ret = Buffer.alloc(n);
          let b;
          let index = 0;
          while (null != (b = this.buffers.shift())) {
            for (let i = 0; i < b.length; i++) {
              ret[index++] = b[i];
              if (index == ret.length) {
                this.length -= n;
                b = b.slice(i + 1);
                this.buffers.unshift(b);
                break;
              }
            }
          }
          if (this.encoding) ret = ret.toString(this.encoding);
        }
        let _read = () => {
          let m = this.end ? Math.min(this.end - this.pos + 1, this.highWaterMark) : this.highWaterMark;
          fs.read(this.fd, this.buffer, 0, m, this.pos, (err, bytesRead) => {
            if (err) {
              return
            }
            let data;
            if (bytesRead > 0) {
              data = this.buffer.slice(0, bytesRead);
              this.pos += bytesRead;
              this.length += bytesRead;
              if (this.end && this.pos > this.end) {
                if (this.needReadable) {
                  this.emit('readable');
                }
                this.emit('end');
              } else {
                this.buffers.push(data);
                if (this.needReadable) {
                  this.emit('readable');
                  this.needReadable = false;
                }
              }
            } else {
              if (this.needReadable) {
                this.emit('readable');
              }
              return this.emit('end');
            }
          })
        }
        if (this.length == 0 || (this.length < this.highWaterMark)) {
          _read(0);
        }
        return ret;
      }
      destroy() {
        fs.close(this.fd, (err) => {
          this.emit('close');
        });
      }
      pause() {
        this.flowing = false;
      }
      resume() {
        this.flowing = true;
        this.read();
      }
      pipe(dest) {
        this.on('data', (data) => {
          let flag = dest.write(data);
          if (!flag) this.pause();
        });
        dest.on('drain', () => {
          this.resume();
        });
        this.on('end', () => {
          dest.end();
        });
      }
    }
    module.exports = ReadStream;
複製程式碼

以上就是個人大致對node中的stream的工作原理理解,歡迎大家多多指正,謝謝!

參考資料:

Node.js v8.9.3 文件

相關文章