渴望力量嗎?少年!流的原理

ldzsl發表於2021-09-09

流(stream),看一個人流不流逼,就看你對流的理解了

學習本無底,前進莫徬徨

今天跟大家分享的是node.js中的流(stream)。它的作用大家應該都在平常使用node的時候看到過,比如:

  • gulp中的pipe就是流的一種方法,透過可寫流和可讀流的配合,達到不佔用多餘快取的一種讀寫方式。

  • express和koa中的res和req也是流,res是可寫流,req是可讀流,他們都是透過封裝node中的net模組的socket(雙工流,即可寫、可讀流)而來的。

  • 。。。

可能很多時候大家都知道怎麼用,但不瞭解它的原理,很尷尬,就像這樣

圖片描述

image

何謂流?

  • 流是一組有序的,有起點和終點的位元組資料傳輸手段。

  • 它不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理。

  • 流是一個抽象介面,被 Node 中的很多物件所實現。比如HTTP 伺服器request和response物件都是流。

  • 流被分為Readable(可讀流)、Writable(可寫流)、Duplex(雙工流)、Transform(轉換流)

流中的是什麼?

  • 二進位制模式:每個分塊都是buffer、string物件。

  • 物件模式:流內部處理的是一系列普通物件。

可讀流

可讀流分為flowingpaused兩種模式

引數

  • path:讀取的檔案的路徑

  • option:

    • highWaterMark:水位線,一次可讀的位元組,一般預設是64k

    • flags:標識,開啟檔案要做的操作,預設是r

    • encoding:編碼,預設為buffer

    • start:開始讀取的索引位置

    • end:結束讀取的索引位置(包括結束位置)

    • autoClose:讀取完畢是否關閉,預設為true

let ReadStream = require('./ReadStream')//讀取的時候預設讀64k let rs = new ReadStream('./a.txt',{  highWaterMark: 2,//一次讀的位元組 預設64k
  flags: 'r',      //標示 r為讀 w為寫
  autoClose: true, //預設讀取完畢後自動關閉
  start: 0,  end: 5,          //流是閉合區間包start,也包end 預設是讀完
  encoding: 'utf8' //預設編碼是buffer})

方法

data:切換到流動模式,可以流出資料

rs.on('data', function (data) {    console.log(data);
});

open:流開啟檔案的時候會觸發此監聽

rs.on('open', function () {    console.log('檔案被開啟');
});

error:流出錯的時候,監聽錯誤資訊

rs.on('error', function (err) {    console.log(err);
});

end:流讀取完成,觸發end

rs.on('end', function (err) {    console.log('讀取完成');
});

close:關閉流,觸發

rs.on('close', function (err) {    console.log('關閉');
});

pause:暫停流(改變流的flowing,不讀取資料了);resume:恢復流(改變流的flowing,繼續讀取資料)

//流透過一次後,停止流動,過了2s後再動rs.on('data', function (data) {
    rs.pause();    console.log(data);
});
setTimeout(function () {
    rs.resume();
},2000);

fs.read():可讀流底層呼叫的就是這個方法,最原生的讀方法

//fd檔案描述符,一般透過fs.open中獲取//buffer是讀取後的資料放入的快取目標//0,從buffer的0位置開始放入//BUFFER_SIZE,每次放BUFFER_SIZE這麼長的長度//index,每次從檔案的index的位置開始讀//bytesRead,真實讀到的個數fs.read(fd,buffer,0,BUFFER_SIZE,index,function(err,bytesRead){

})

那讓我們自己來實現一個可愛的讀流吧!

圖片描述

image

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.flags = options.flags || 'r'
    this.start = options.start || 0
    this.pos = this.start     //會隨著讀取的位置改變
    this.autoClose = options.autoClose || true
    this.end = options.end || null
    //預設null就是buffer
    this.encoding = options.encoding || null

    //引數的問題
    this.flowing = null //非流動模式
    //建立個buffer用來儲存每次讀出來的資料
    this.buffer = Buffer.alloc(this.highWaterMark)    //開啟這個檔案
    this.open()    //此方法預設同步呼叫 每次設定on監聽事件時都會呼叫之前所有的newListener事件
    this.on('newListener',(type)=>{// 等待著他監聽data事件
      if(type === 'data'){        this.flowing = true
        //開始讀取 客戶已經監聽的data事件
        this.read()
      }
    })
  }  //預設第一次呼叫read方法時fd還沒獲取 所以不能直接讀
  read(){    if(typeof this.fd != 'number'){      //等待著觸發open事件後fd肯定拿到了 再去執行read方法
      return this.once('open',()=>{this.read()})
    }    //每次讀的時候都要判斷一下下次讀幾個 如果沒有end就根據highWaterMark來(讀所有的) 如果有且大於highWaterMark就根據highWaterMark來 如果小於highWaterMark就根據end來
    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,byteRead)=>{      this.pos += byteRead      let b = this.encoding?this.buffer.slice(0,byteRead).toString(this.encoding):this.buffer.slice(0,byteRead)      this.emit('data',b)      //如果讀取到的數量和highWaterMark一樣 說明還得繼續讀
      if((byteRead === this.highWaterMark)&&this.flowing){        this.read()
      }      if(byteRead {      this.emit('close')
    })
  }
  pause(){    this.flowing = false
  }
  resume(){    this.flowing = true
    this.read()
  }
  open(){    //fd表示的就是當前this.path的這個檔案,從3開始(number型別)
    fs.open(this.path,this.flags,(err,fd)=>{      //有可能fd這個檔案不存在 需要做處理
      if(err){        //如果有自動關閉 則幫他銷燬
        if(this.autoClose){          //銷燬(關閉檔案,觸發關閉檔案事件)
          this.destory()
        }        //如果有錯誤 就會觸發error事件
        this.emit('error',err)        return
      }      //儲存檔案描述符
      this.fd = fd      //當檔案開啟成功時觸發open事件
      this.emit('open',this.fd)
    })
  }
}

Readable

這個方法是可讀流的一種暫停模式,他的模式可以參考為讀流是往水杯倒水的人,Readable是喝水的人,他們之間存在著一種聯絡,只要Readable喝掉一點水,讀流就會繼續往裡倒

Readable是什麼?

  • 他會在剛開始監聽Readable的時候就觸發流的,此時流就會讀取一次資料,之後流會監聽,如果有人讀過流(喝過水),並且減少,就會再去讀一次(倒點水)

  • 主要可以用來做行讀取器(LineReader)

let fs = require('fs')let read = require('./ReadableStream')let rs = fs.createReadStream('./a.txt', {  //每次讀7個
  highWaterMark: 7})//如果讀流第一次全部讀下來並且小於highWaterMark,就會再讀一次(再觸發一次readable事件)//如果rs.read()不加引數,一次性讀完,會從快取區再讀一次,為null//如果readable每次都剛好讀完(即rs.read()的引數剛好和highWaterMark相等),就會一直觸發readable事件,如果最後不足他想喝的數,他就會先觸發一次null,最後把剩下的喝完//一開始快取區為0的時候也會預設調一次readable事件rs.on('readable', () => {  let result = rs.read(2)  console.log(result)
})

實戰:行讀取器(平常我們的檔案可能有回車、換行,此時如果要每次想讀一行的資料,就得用到readable)

let EventEmitter = require('events')//如果要將內容全部讀出就用on('data'),精確讀取就用on('readable')class LineReader extends EventEmitter {  constructor(path) {    super()    this.rs = fs.createReadStream(path)    //回車符的十六進位制
    let RETURN = 0x0d
    //換行符的十六進位制
    let LINE = 0x0a
    let arr = []    this.on('newListener', (type) => {      if (type === 'newLine') {        this.rs.on('readable', () => {          let char          //每次讀一個,當讀完的時候會返回null,終止迴圈
          while (char = this.rs.read(1)) {            switch (char[0]) {              case RETURN:                break;              //Mac下只有換行符,windows下是回車符和換行符,需要根據不同的轉換。因為我這裡是Mac
              case LINE:                //如果是換行符就把陣列轉換為字串
                let r = Buffer.from(arr).toString('utf8')                //把陣列清空
                arr.length = 0
                //觸發newLine事件,把得到的一行資料輸出
                this.emit('newLine', r)                break;              default:                //如果不是換行符,就放入陣列中
                arr.push(char[0])
            }
          }
        })
      }
    })    //以上只能取出之前的換行符前的程式碼,最後一行的後面沒有換行符,所以需要特殊處理。當讀流讀完需要觸發end事件時
    this.rs.on('end', () => {      //取出最後一行資料,轉成字串
      let r = Buffer.from(arr).toString('utf8')
      arr.length = 0
      this.emit('newLine', r)
    })
  }
}let lineReader = new LineReader('./a.txt')
lineReader.on('newLine', function (data) {  console.log(data)
})

那麼Readable到底是怎樣的存在呢?我們接下來實現他的原始碼,看看內部到底怎麼回事

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.flags = options.flags || 'r'
    this.start = options.start || 0
    this.pos = this.start     //會隨著讀取的位置改變
    this.autoClose = options.autoClose || true
    this.end = options.end || null
    //預設null就是buffer
    this.encoding = options.encoding || null

    //引數的問題
    this.reading = false //非流動模式
    //建立個buffer用來儲存每次讀出來的資料
    this.buffers = []    //快取區長度
    this.len = 0
    //是否要觸發readable事件
    this.emittedReadable = false
    //觸發open獲取檔案的fd識別符號
    this.open()    //此方法預設同步呼叫 每次設定on監聽事件時都會呼叫之前所有的newListener事件
    this.on('newListener',(type)=>{// 等待著他監聽data事件
      if(type === 'readable'){        //開始讀取 客戶已經監聽的data事件
        this.read()
      }
    })
  }  //readable真正的原始碼中的方法,計算出和n最接近的2的冪次數
  computeNewHighWaterMark(n) {
    n--;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    n++;    return n;
  }
  read(n){    //當讀的數量大於水平線,會透過取2的冪次取比他大和最接近的數
    if(this.len 0 && n,buffer,buffer]
      //每次取出快取前的第一個buffer
      while(flag && (buf = this.buffers.shift())){        for(let i=0;i{this._read()})
    }    //先讀這麼多buffer
    let buffer = Buffer.alloc(this.highWaterMark)
    fs.read(this.fd,buffer,0,buffer.length,this.pos,(err,byteRead)=>{      if(byteRead > 0){        //當第一次讀到資料後,改變reading的狀態,如果觸發read事件,可能還會在觸發第二次_read
        this.reading = false
        //每次讀到資料增加快取取得長度
        this.len += byteRead        //每次讀取之後,會增加讀取的檔案的讀取開始位置
        this.pos += byteRead        //將讀到的buffer放入快取區buffers中
        this.buffers.push(buffer.slice(0,byteRead))        //觸發readable
        if(this.emittedReadable){          this.emittedReadable = false
          //可以讀取了,預設開始的時候杯子填滿了
          this.emit('readable')
        }
      }else{        //沒讀到就出發end事件
        this.emit('end')
      }
    })
  }
  destory(){    if(typeof this.fd != 'number'){      return this.emit('close')
    }    //如果檔案被開啟過 就關閉檔案並且觸發close事件
    fs.close(this.fd,()=>{      this.emit('close')
    })
  }
  open(){    //fd表示的就是當前this.path的這個檔案,從3開始(number型別)
    fs.open(this.path,this.flags,(err,fd)=>{      //有可能fd這個檔案不存在 需要做處理
      if(err){        //如果有自動關閉 則幫他銷燬
        if(this.autoClose){          //銷燬(關閉檔案,觸發關閉檔案事件)
          this.destory()
        }        //如果有錯誤 就會觸發error事件
        this.emit('error',err)        return
      }      //儲存檔案描述符
      this.fd = fd      //當檔案開啟成功時觸發open事件
      this.emit('open',this.fd)
    })
  }
}
  • Readable和讀流的data的區別就是,Readable可以控制自己從快取區讀多少和控制讀的次數,而data是每次讀取都清空快取,讀多少輸出多少

  • 我們可以看一下下面這個例子

let rs = fs.createReadStream('./a.txt')
rs.on('data',(data)=>{  console.log(data)
})//因為上面的data事件把資料讀了,清空快取區。所以導致下面的readable讀出為nullrs.on('readable',()=>{  let result = r.read(1)  console.log(result)
})

自定義可讀流

因為createReadStream內部呼叫了ReadStream類,ReadStream又實現了Readable介面,ReadStream實現了_read()方法,所以我們透過自定義一個類繼承stream模組的Readable,並在原型上自定義一個_read()就可以自定義自己的可讀流

let { Readable } = require('stream')class MyRead extends Readable{  //流需要一個_read方法,方法中push什麼,外面就接收什麼
  _read(){    //push方法就是上面_read方法中的push一樣,把資料放入快取區中
    this.push('100')    //如果push了null就表示沒有東西可讀了,停止(如果不寫,就會一直push上面的值,死迴圈)
    this.push(null)
  }
}

可寫流

  • 如果檔案不存在會建立,如果有內容會被清空

  • 讀取到highWaterMark的時候就會輸出

  • 第一次是真的寫到檔案 後面就是寫入快取區 再從快取區裡面去取

引數(和可讀流的類似)

  • path:寫入的檔案的路徑

  • option:

    • highWaterMark:水位線,一次可寫入快取中的位元組,一般預設是64k

    • flags:標識,寫入檔案要做的操作,預設是w

    • encoding:編碼,預設為buffer

    • start:開始寫入的索引位置

    • end:結束寫入的索引位置(包括結束位置)

    • autoClose:寫入完畢是否關閉,預設為true

let ReadStream = require('./ReadStream')//讀取的時候預設讀64k let rs = new ReadStream('./a.txt',{  highWaterMark: 2,//一次讀的位元組 預設64k
  flags: 'r',      //標示 r為讀 w為寫
  autoClose: true, //預設讀取完畢後自動關閉
  start: 0,  end: 5,          //流是閉合區間包start,也包end 預設是讀完
  encoding: 'utf8' //預設編碼是buffer})

方法

write

let fs = require('fs')let ws = fs.createWriteStream('./d.txt',{  flags: 'w',  encoding: 'utf8',  start: 0,  //write的highWaterMark只是用來觸發是不是幹了
  highWaterMark: 3 //寫是預設16k})//返回boolean 每當write一次都會在ws中吃下一個饅頭 當吃下的饅頭數量達到highWaterMark時 就會返回false 吃不下了會把其餘放入快取 其餘狀態返回true//write只能放string或者bufferflag = ws.write('1','utf8',()=>{  console.log(i)
})

drain

//drain只有嘴塞滿了 吃完(包括記憶體中的,就是地下的)才會觸發 這裡是兩個條件 一個是必須是吃下highWaterMark個饅頭 並且在吃完的時候才會callbackws.on('drain',()=>{  console.log('幹了')
})

圖片描述

image

fs.write():可讀流底層呼叫的就是這個方法,最原生的讀方法

//wfd檔案描述符,一般透過fs.open中獲取//buffer,要取資料的快取源//0,從buffer的0位置開始取//BUFFER_SIZE,每次取BUFFER_SIZE這麼長的長度//index,每次寫入檔案的index的位置//bytesRead,真實寫入的個數fs.write(wfd,buffer,0,bytesRead,index,function(err,bytesWrite){

})

透過程式碼實現

let fs = require('fs')let EventEmitter = require('events')//只有第一次write的時候直接用_write寫入檔案 其餘都是放到cache中 但是len超過了highWaterMark就會返回false告知需要drain 很佔快取//從第一次的_write開始 回去一直透過clearBuffer遞迴_write寫入檔案 如果cache中沒有了要寫入的東西 會根據needDrain來判斷是否觸發乾點class WriteStream extends EventEmitter{  constructor(path,options = {}){    super()    this.path = path    this.highWaterMark = options.highWaterMark || 64*1024
    this.flags = options.flags || 'r'
    this.start = options.start || 0
    this.pos = this.start    this.autoClose = options.autoClose || true
    this.mode = options.mode || 0o666
    //預設null就是buffer
    this.encoding = options.encoding || null

    //開啟這個檔案
    this.open()    //寫檔案的時候需要哪些引數
    //第一次寫入的時候 是給highWaterMark個饅頭 他會硬著頭皮寫到檔案中 之後才會把多餘吃不下的放到快取中
    this.writing = false
    //快取陣列
    this.cache = []    this.callbackList = []    //陣列長度
    this.len = 0
    //是否觸發drain事件
    this.needDrain = false
  }

  clearBuffer(){    //取快取中最上面的一個
    let buffer = this.cache.shift()    if(buffer){      //有buffer的情況下
      this._write(buffer.chunk,buffer.encoding,()=>this.clearBuffer(),buffer.callback)
    }else{      //沒有的話 先看看需不需要drain
      if(this.needDrain){        //觸發drain 並初始化所有狀態
        this.writing = false
        this.needDrain = false
        this.callbackList.shift()()        this.emit('drain')
        
      }      this.callbackList.map(v=>{
        v()
      })      this.callbackList.length = 0
    }
  }
  _write(chunk,encoding,clearBuffer,callback){    //因為write方法是同步呼叫的 所以可能還沒獲取到fd
    if(typeof this.fd != 'number'){      //直接在open的時間物件上註冊一個一次性事件 當open被emit的時候會被呼叫
      return this.once('open',()=>this._write(chunk,encoding,clearBuffer,callback))
    }
    fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWrite)=>{      this.pos += byteWrite      //每次寫完 相應減少記憶體中的數量
      this.len -= byteWrite      if(callback) this.callbackList.push(callback)      //第一次寫完
      clearBuffer()
      
    })
  }  //寫入方法
  write(chunk,encoding=this.encoding,callback){    //判斷chunk必須是字串或者buffer 為了統一都變成buffer
    chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,encoding)    //維護快取的長度 3
    this.len += chunk.length    let ret = this.len this.clearBuffer(),callback)
    }    // console.log(ret)
    //能不能繼續寫了 false代表下次寫的時候更佔記憶體
    return ret
  }

  destory(){    if(typeof this.fd != 'number'){      return this.emit('close')
    }    //如果檔案被開啟過 就關閉檔案並且觸發close事件
    fs.close(this.fd,()=>{      this.emit('close')
    })
  }
  open(){    //fd表示的就是當前this.path的這個檔案,從3開始(number型別)
    fs.open(this.path,this.flags,(err,fd)=>{      //有可能fd這個檔案不存在 需要做處理
      if(err){        //如果有自動關閉 則幫他銷燬
        if(this.autoClose){          //銷燬(關閉檔案,出發關閉檔案事件)
          this.destory()
        }        //如果有錯誤 就會觸發error事件
        this.emit('error',err)        return
      }      //儲存檔案描述符
      this.fd = fd      //當檔案開啟成功時觸發open事件
      this.emit('open',this.fd)
    })
  }
}

自定義可寫流

因為createWriteStream內部呼叫了WriteStream類,WriteStream又實現了Writable介面,WriteStream實現了_write()方法,所以我們透過自定義一個類繼承stream模組的Writable,並在原型上自定義一個_write()就可以自定義自己的可寫流

let { Writable } = require('stream')class MyWrite extends Writable{
  _write(chunk,encoding,callback){    //write()的第一個引數,寫入的資料
    console.log(chunk)    //這個callback,就相當於我們上面的clearBuffer方法,如果不執行callback就不會繼續從快取中取出寫
    callback()
  }
}let write = new MyWrite()
write.write('1','utf8',()=>{  console.log('ok')
})

pipe

管道流,是可讀流上的方法,至於為什麼放到這裡,主要是因為需要2個流的基礎知識,是可讀流配合可寫流的一種傳輸方式。如果用原來的讀寫,因為寫比較耗時,所以會多讀少寫耗記憶體,但用了pipe就不會了,始終用規定的記憶體。

用法

let fs = require('fs')//pipe方法叫管道 可以控制速率let rs = fs.createReadStream('./d.txt',{  highWaterMark: 4})let ws = fs.createWriteStream('./e,txt',{  highWaterMark: 1})//會監聽rs的on('data')將讀取到的資料,透過ws.write的方法寫入檔案//呼叫寫的一個方法 返回boolean型別//如果返回false就呼叫rs的pause方法 暫停讀取//等待可寫流 寫入完畢在監聽drain resume rsrs.pipe(ws) //會控制速率 防止淹沒可用記憶體

自己實現一下

let fs = require('fs')//這兩個是上面自己寫的ReadStream和WriteStreamlet ReadStream = require('./ReadStream')let WriteStream = require('./WriteStream')//如果用原來的讀寫,因為寫比較耗時,所以會多讀少寫,耗記憶體ReadStream.prototype.pipe = function(dest){  this.on('data',(data)=>{    let flag = dest.write(data)    //如果寫入的時候嘴巴吃滿了就不繼續讀了,暫停
    if(!flag){      this.pause()
    }
  })  //如果寫的時候嘴巴里的吃完了,就會繼續讀
  dest.on('drain',()=>{    this.resume()
  })  this.on('end',()=>{    this.destory()    //清空快取中的資料
    fs.fsync(dest.fd,()=>{
      dest.destory()
    })
  })
}

雙工流

有了雙工流,我們可以在同一個物件上同時實現可讀和可寫,就好像同時繼承這兩個介面。 重要的是雙工流的可讀性和可寫性操作完全獨立於彼此。這僅僅是將兩個特性組合成一個物件。

let { Duplex } = require('stream')//雙工流,可讀可寫class MyDuplex extends Duplex{
  _read(){    this.push('hello')    this.push(null)
  }
  _write(chunk,encoding,clearBuffer){    console.log(chunk)
    clearBuffer()
  }
}let myDuplex = new MyDuplex()//process.stdin是node自帶的process程式中的可讀流,會監聽命令列的輸入//process.stdout是node自帶的process程式中的可寫流,會監聽並輸出在命令列中//所以這裡的意思就是在命令列先輸出hello,然後我們輸入什麼他就出來對應的buffer(先作為可讀流出來)process.stdin.pipe(myDuplex).pipe(process.stdout)

轉換流

轉換流的輸出是從輸入中計算出來的。對於轉換流,我們不必實現readwrite的方法,我們只需要實現一個transform方法,將兩者結合起來。它有write方法的意思,我們也可以用它來push資料。

let { Transform } = require('stream')class MyTransform extends Transform{
  _transform(chunk,encoding,callback){    console.log(chunk.toString().toUpperCase())
    callback()
  }
}let myTransform = new MyTransform()class MyTransform2 extends Transform{
  _transform(chunk,encoding,callback){    console.log(chunk.toString().toUpperCase())    this.push('1')    // this.push(null)
    callback()
  }
}let myTransform2 = new MyTransform2()//此時myTransform2被作為可寫流觸發_transform,輸出輸入的大寫字元後,會透過可讀流push字元到下一個轉換流中//當寫入的時候才會觸發transform的值,此時才會push,所以後面的pipe拿到的chunk是前面的push的值process.stdin.pipe(myTransform2).pipe(myTransform)

總結

可讀流

  • 在 flowing 模式下, 可讀流自動從系統底層讀取資料,並透過 EventEmitter 介面的事件儘快將資料提供給應用。

  • 在 paused 模式下,必須顯式呼叫 stream.read() 方法來從流中讀取資料片段。

  • 所有初始工作模式為 paused 的 Readable 流,可以透過下面三種途徑切換到 flowing 模式:

    • 監聽 'data' 事件

    • 呼叫 stream.resume() 方法

    • 呼叫 stream.pipe() 方法將資料傳送到 Writable

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

    • 如果不存在管道目標(pipe destination),可以透過呼叫 stream.pause() 方法實現。

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

可寫流

  • 需要知道只有在真正的吃滿了,並且等到把嘴裡的和地上的饅頭(快取中的)都吃下了才會觸發drain事件

  • 第一次寫入會直接寫入檔案中,後面會從快取中一個個取

雙工流

  • 只是對可寫可讀流的一種應用,既可作為可讀流,也能作為可寫流,並且作為可讀或者可寫時時隔離

轉換流

  • 一般轉換流是邊輸入邊輸出的,而且一般只有觸發了寫入操作時才會進入_transform方法中。跟雙工流的區別就是,他的可讀可寫是在一起的。

OK,講完收工,從此你就是魔王

圖片描述

image



作者:Shinemax
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2325/viewspace-2813007/,如需轉載,請註明出處,否則將追究法律責任。

相關文章