Node.js Streams(流)

Wang豔芬發表於2018-04-05

流的概念

  • 流是一組有序的、有起點和終點的位元組資料傳輸手段
  • 流不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理
  • 流是一個抽象介面,被 Node 中的很多物件所實現。比如 HTTP 伺服器 request 和 response 物件都是流
  • 流 是 Node.js 的核心模組,基本上都是 stream的例項,比如 process.stdout、http.clientRequest

流的好處

  • 流是基於事件的 API,用於管理和處理資料,而且有不錯的效率
  • 藉助事件和非阻塞 I/O 庫,流模組允許在其可用的時候動態處理,在其不需要的時候釋放掉

流中的資料有兩種模式,二進位制模式和物件模式

  • 二進位制模式, 每個分塊都是 buffer 或者 string 物件
  • 物件模式, 流內部處理的是一系列普通物件

所有使用 Node.js API 建立的流物件都只能操作 strings 和 Buffer物件。但是,通過一些第三方流的實現,你依然能夠處理其它型別的 JavaScript 值 (除了 null,它在流處理中有特殊意義)。 這些流被認為是工作在 “物件模式”(object mode)。 在建立流的例項時,可以通過 objectMode 選項使流的例項切換到物件模式。試圖將已經存在的流切換到物件模式是不安全的。

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

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

第一種型別:可讀流 createReadStream

建立一個可讀流
// 引入 fs(讀取檔案) 模組
let fs = require('fs');
// 建立一個可讀流
let rs = fs.createReadStream('./1.txt',{
    flags:'r',
    encoding:'utf8',
    start:0,
    autoClose:true,
    end: 3,
    highWaterMark:3 
});
複製程式碼

API:createReadStream(path, [options]);

  1. path 是讀取檔案的路徑
  2. options 裡面有
    • flags:開啟檔案要做的操作,預設為 'r'
    • encoding:預設是null,null 代表的是 buffer
    • start:開始讀取的索引位置
    • autoClose:讀取完畢後自動關閉
    • end:結束讀取的索引位置(包括結束位置)
    • highWaterMark:讀取快取區預設的預設的大小 64kb (64*1024b)

    如果指定 encoding 為 utf8 編碼, highWaterMark 要大於 3 個位元組

可讀流的一些監聽事件
  1. data 事件
  2. end 事件
  3. error 事件
  4. open 事件
  5. close 事件

各個寫法如下:

// 流切換到流動模式,資料會被儘可能快的讀出
rs.on('data',function(data){ // 暫停模式 -> 流動模式
    console.log(data);
});

// 該事件會在讀完資料後被觸發
rs.on('end', function () {
    console.log('讀取完成');
});

// 讀檔案失敗後被觸發
rs.on('error', function (err) {
    console.log(err);
});

// 檔案開啟後被觸發
rs.on('open', function () {
    console.log('檔案開啟了');
});

// 檔案關閉後被觸發
rs.on('close', function () {
    console.log('關閉');
});
複製程式碼
設定編碼

與指定 {encoding:'utf8'} 效果相同,設定編碼

rs.setEncoding('utf8');
複製程式碼
暫停和恢復觸發 data

通過 pause() 方法和 resume() 方法

rs.on('data', function (data) {
    console.log(data);
    rs.pause(); // 暫停方法 表示暫停讀取,暫停data事件觸發
});
setTimeout(function () {
    rs.resume(); // 恢復方法
},2000);
複製程式碼

第二種型別:可寫流 createWriteStream

建立一個可寫流
// 引入 fs(讀取檔案) 模組
let fs = require('fs');
// 建立一個可寫流
let ws = fs.createWriteStream('./1.txt',{
    flags:'w',
    encoding:'utf8',
    highWaterMark:3 
});
複製程式碼

API:createWriteStream(path, [options]);

  1. path 是讀取檔案的路徑
  2. options 裡面有
    • flags:開啟檔案要做的操作,預設為 'w'
    • encoding:預設是 utf8
    • highWaterMark:寫入快取區的,預設大小 16kb
可寫流的一些方法
1. write 方法
ws.write(chunk, [encoding], [callback]);
複製程式碼
  • chunk 寫入的資料 buffer/string
  • encoding 編碼格式,chunk 為字串時有用,是個可選引數
  • callback 寫入成功後的回撥

返回值為布林值,系統快取區滿時為 false,未滿時為 true

2. end 方法
ws.end(chunk, [encoding], [callback]);
複製程式碼

表明接下來沒有資料要被寫入 Writable 通過傳入可選的 chunk 和 encoding 引數,可以在關閉流之前再寫入一段資料 如果傳入了可選的 callback 函式,它將作為 'finish' 事件的回撥函式

3. drain 方法
ws.on('drain',function(){
    console.log('drain')
});
複製程式碼
  • 當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false
  • 當前所有快取的資料塊滿了,滿了之後情況才會出發 drain
  • 一旦 write() 返回 false, 在 'drain' 事件觸發前, 不能寫入任何資料塊
4. finish 方法
ws.end('結束');
ws.on('finish',function(){
    console.log('drain')
});
複製程式碼
  • 在呼叫 end 方法,且緩衝區資料都已經傳給底層系統之後, 'finish' 事件將被觸發

第三種型別:可讀寫的流,也叫雙工流(Duplex)

雙工流,可以在同一個物件上同時實現可讀、可寫,就好像同時繼承這兩個介面。而且讀取可以沒關係(互不干擾)

// 引入雙工流模組
let {Duplex} =  require('stream');
let d = Duplex({
    read(){
        this.push('hello');
        this.push(null)
    },
    write(chunk,encoding,callback){
        console.log(chunk);
        callback();
    }
});
d.on('data',function(data){
    console.log(data);
});
d.write('hello');
複製程式碼

第四種型別:轉換流(Transform)

  • 轉換流輸出是從輸入中計算出來的
  • 轉換流中,不需要實現 read 和 write 方法,只需要實現一個 transform 方法,就可以結合兩者。
// 引入轉換流
let {Transform} =  require('stream');
// 轉換流的引數和可寫流一樣
let tranform1 = Transform({
    transform(chunk,encoding,callback){
        this.push(chunk.toString().toUpperCase()); 
        callback();
    }
});
let tranform2 = Transform({
    transform(chunk,encoding,callback){
        console.log(chunk.toString());
        callback();
    }
});
process.stdin.pipe(tranform1).pipe(tranform2);
複製程式碼

pipe 方法

大家都知道,想把 Readable 的資料 寫到 Writable,需要手動將資料讀入記憶體中,然後在寫入 Writable。也就是每次傳遞資料的時候,都需要寫一下的程式碼:

readable.on('readable', (err) => {
 if(err) throw err
 writable.write(readable.read())
})
複製程式碼

為了方便使用,Node.js 提供了 pipe() 方法

readable.pipe(writable)
複製程式碼
pipe 方法的原理
var fs = require('fs');
var ws = fs.createWriteStream('./2.txt');
var rs = fs.createReadStream('./1.txt');
rs.on('data', function (data) {
    var flag = ws.write(data);
    if(!flag)
    rs.pause();
});
ws.on('drain', function () {
    rs.resume();
});
rs.on('end', function () {
    ws.end();
});
複製程式碼
unpipe 用法
  • readable.unpipe() 方法將之前通過 stream.pipe() 方法繫結的流分離
  • 如果 destination 沒有傳入, 則所有繫結的流都會被分離
let fs = require('fs');
var from = fs.createReadStream('./1.txt');
var to = fs.createWriteStream('./2.txt');
from.pipe(to);
setTimeout(() => {
console.log('關閉向2.txt的寫入');
from.unpipe(writable);
console.log('手工關閉檔案流');
to.end();
}, 1000);
複製程式碼
cork & uncork
  • 呼叫 writable.cork() 方法將強制所有寫入資料都存到記憶體中的緩衝區裡。 直到呼叫 stream.uncork() 或 stream.end() 方法時,緩衝區裡的資料才會被輸出
  • writable.uncork() 將輸出在 stream.cork() 方法被呼叫之後緩衝在記憶體中的所有資料
stream.cork();
stream.write('1');
stream.write('2');
process.nextTick(() => stream.uncork());
複製程式碼

readable

'readable' 事件將在流中有資料可供讀取時才觸發。在某些情況下,為 'readable' 事件新增回撥將會導致一些資料被讀取到內部快取中

const readable = getReadableStreamSomehow();
readable.on('readable', () => {
  // 某些資料可讀
});
複製程式碼
let fs = require('fs');
let rs = fs.createReadStream('./1.txt',{
  start:3,
  end:8,
  encoding:'utf8',
  highWaterMark:3
});
rs.on('readable',function () {
  console.log('readable');
  console.log('rs._readableState.buffer.length',rs._readableState.length);
  let d = rs.read(1);
  console.log('rs._readableState.buffer.length',rs._readableState.length);
  console.log(d);
  setTimeout(()=>{
      console.log('rs._readableState.buffer.length',rs._readableState.length);
  },500)
});
複製程式碼
  • 當流資料到達尾部時, 'readable' 事件會觸發。觸發順序在 'end' 事件之前
  • 事實上, 'readable' 事件表明流有了新的動態:要麼是有了新的資料,要麼是到了流的尾部。 對於前者, stream.read() 將返回可用的資料。而對於後者, stream.read() 將返回 null。

可讀流的兩種模式

  1. 可讀流的兩種工作模式:flowing 和 paused
  2. flowing 模式下,可讀流自動從系統底層讀取資料,通過 EventEmitter 介面的事件儘快將資料提供給應用
  3. paused 模式下,呼叫 stream.read() 方法來從流中讀取資料片段
  4. 所有初始工作模式為 paused 的 Readable 流,可以通過下面三種途徑切換到 flowing 模式
    • 監聽 'data' 事件
    • 呼叫 stream.resume() 方法
    • 呼叫 stream.pipe() 方法將資料傳送到 Writable
  5. 可讀流可以通過下面途徑切換到 paused 模式:
    • 如果不存在管道目標(pipe destination),可以通過呼叫 stream.pause() 方法實現。
    • 如果存在管道目標,可以通過取消 'data' 事件監聽,並呼叫 stream.unpipe() 方法移除所有管道目標來實現。

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

可讀流的三種狀態

在任意時刻,任意可讀流應確切處於下面三種狀態之一:

  1. readable._readableState.flowing = null
  2. readable._readableState.flowing = false
  3. readable._readableState.flowing = true
  • 若 readable._readableState.flowing 為 null,由於不存在資料消費者,可讀流將不會產生資料。 在這個狀態下,監聽 'data' 事件,呼叫 readable.pipe() 方法,或者呼叫 readable.resume() 方法, readable._readableState.flowing 的值將會變為 true 。這時,隨著資料生成,可讀流開始頻繁觸發事件。

  • 呼叫 readable.pause() 方法, readable.unpipe() 方法, 或者接收 “背壓”(back pressure), 將導致 readable._readableState.flowing 值變為 false。 這將暫停事件流,但 不會 暫停資料生成。 在這種情況下,為 'data' 事件設定監聽函式不會導致 readable._readableState.flowing 變為 true。

  • 當 readable._readableState.flowing 值為 false 時, 資料可能堆積到流的內部快取中。

相關文章