Node.js 可讀流和可寫流

sou發表於2019-03-01

Node.js操作按需資料使用sream API介面,stream 是一個資料集,資料可能不能馬上全部獲取到,他們在緩衝區,不需要在記憶體中。適合處理大資料集或者來自外部的資料來源的資料塊 Node中很多內建模組實現了流式介面:

Node.js 可讀流和可寫流

上面的列表中的原生Node.js物件就是可讀流和可寫流的物件。有些物件是可讀流也是可寫流,如TCP sockets,zlib 和 crypto streams

這些物件是密切相關。當一個HTTP響應在客戶端上是一個可讀流,相應的在服務端是一個可寫流。這是因為在HTTP的情況下,我們基於從一物件(http.IncomingMessage)讀而從另外一個物件(http.ServerResponse)寫

stdio流(stdin,stdout,stderr)在子程式中會有反向流型別。這樣的話就能用非常簡單的方式管道傳送給其他流或者主程式的stdio流。

Node.js中有4個基本的流型別:

  1. 可讀流(Readable)
  2. 可寫流(Writable)
  3. 雙工流(Duplex)
  4. 轉換流(Transform streams)
  • 可讀流是可以被消耗的資料來源的抽象,典型例子就是fs.createReadStream方法。
  • 可寫流是可以寫入資料的目的地的抽象,典型例子就是 fs.createWriteStream 方法。
  • 雙工流既是可讀的也是可寫的,典型例子是TCP套接字。
  • 轉換流是基於雙工流的,它可以用來修改或轉換資料,因為它是寫入和讀取的。 zlib.createGzip 就是一個用gzip來壓縮資料的轉換流例子。你可以認為轉換流就是一個函式,這個函式的輸入是一個可寫流,輸出是一個可讀流,你可能也聽說過把轉換流叫做" 通過流 "。

所有的流都是 EventEmitter 的例項。他們在資料可讀或者可寫的時候發出事件。然而,我們也可以簡單的通過 pipe 方法來使用流資料。

pipe方法:

**readable**.pipe(**writableDest**)
複製程式碼

這簡單的一行,連線了可讀流的輸出——源資料和可寫流的輸入——目標。源必須是可讀流,目標必須是可寫流。當然也可以是雙工流或者轉換流,事實上,如果連線的是一個雙工流,可以鏈式呼叫pipe:

readable
  .pipe(transformStream1)
  .pipe(transformStream2)
  .pipe(finalWrtitableDest)

複製程式碼

pipe方法返回目標流,這使我們能夠執行上面的鏈式呼叫。對於流a(可讀)、b和c(雙工)和d(可寫)

a.pipe(b).pipe(c).pipe(d)

複製程式碼
上面等價於
a.pipe(b)
b.pipe(c)
c.pipe(d)
複製程式碼

pipe 方法是最簡單的方式去使用流,一般建議使用 pipe 方法或使用事件來處理流,但是要避免兩個混合使用。通常,當你使用 pipe 方法時,你不需要使用事件,但是如果你需要用更多定製的方式來處理流,那你可以只用事件。

流事件

除了從可讀流裡讀取資料和向可寫流目標寫資料外,pipe方法將自動管理沿途的一些事情。例如,它處理錯誤、檔案結束以及當一個流比另一個流慢或更快時的情況。

然而,我們也可以直接使用事件來操作流。下面是pipe方法主要用於讀取和寫入資料的事件的簡化等效程式碼:

# readable.pipe(writable) 等於下面
複製程式碼
readable.on('data', (chunk) => {
  writable.write(chunk);
});

複製程式碼
readable.on('end', () => {
  writable.end();
});
複製程式碼

以下是可讀流可寫流的重要事件以及可用方法:

Node.js 可讀流和可寫流

這些事件和函式在某種程度上是相關的,因為它們通常一起使用。

可讀流中最重要的事件是:

  • data事件,每當流將資料塊傳遞給消費者時,它就會觸發。
  • end事件,當沒有更多的資料從流中被消耗時觸發。

可寫流中最重要的事件是:

  • drain事件,這是可寫流可以接收更多資料的訊號。
  • finish事件,當所有資料都給到底層系統時觸發。

可以結合事件和函式來定製和優化流的使用。使用一個可讀的流,我們可以用pipe / unpipe方法,或read / Unshift / resume方法。使用一個可寫流,我們可以把它pipe / unpipe目的地,或是寫它的write方法呼叫end方法當我們完成。

可讀流的暫停和流(flowing)模式

可讀流有兩種主要模式,這影響我們可以使用它們的方式:

  • 它們可以是暫停(paused)模式
  • 或是流(flowing)模式

這些模式有時被稱為拉和推模式。

所有可讀的流預設情況下都是在暫停模式下啟動的,但在需要時可以輕鬆切換到流模式或者返回到暫停狀態。有時,轉換是自動發生。

當一個可讀流處於暫停模式,我們可以使用 read() 方法按需的從流中讀取資料,然而,在流模式下的可讀流,資料是不斷流動的,我們要監聽事件來使用這些資料。

在流模式下,如果沒有使用者處理資料,那麼實際上資料會丟失。這就是為什麼當我們在流模式中有可讀的流時,我們需要一個 data 事件。事實上,只要新增一個 data 事件,就可以將暫停模式轉換為流模式,刪除 data 事件,流將切換回暫停模式。其中一些這樣做事為了與舊的節點流介面向後相容。

這兩個流模式之間手動開關,可以使用 resume()pause() 方法。

Node.js 可讀流和可寫流

當使用 pipe 方法讀取可讀流時,我們不必擔心這些模式,因為pipe自動管理它們。

實現流

當我們談論Node.js中的流,主要有兩種不同的任務:

  • 實現流。
  • 使用流。

流的實現通常會 引入 (require)stream模組。

實現可寫流

為了實現可寫流,我們需要使用流模組中的 Writable 建構函式。

const { Writable } = require('stream');
複製程式碼

我們有很多方式來實現一個可寫流。例如,如果我們想要的話,我們可以繼承Writable建構函式。

class myWritableStream extends Writable {
}
複製程式碼

這裡用簡單的建構函式的方法。我們只需給 Writable 建構函式傳遞一些選項並建立一個物件。唯一需要的選項是 Writable 函式,該函式揭露資料塊要往哪裡寫。

const { Writable } = require('stream');
複製程式碼
const outStream = new Writable({
  **write**(chunk, encoding, callback) {
    console.log(chunk.toString());
    callback();
  }
});

process.stdin.pipe(outStream);
複製程式碼

這個write函式有3個引數:

  • chunk 通常是一個buffer,除非我們配置不同的流。
  • encoding 是在特定情況下需要的引數,通常我們可以忽略它。
  • callback 是在完成處理資料塊後需要呼叫的函式。這是寫資料成功與否的標誌。若要發出故障訊號,請用錯誤物件呼叫回撥函式。

outstream,我們只是用 console.log 把資料塊作為一個字串列印到控制檯,然後不用錯誤物件呼叫 callback 表示成功。這是一個非常簡單的可能也不那麼有用的 echo 流,它把收到的所有資料列印到控制檯。

為了使用這個流,我們可以直接用 process.stdin 這個可讀流,就可以把 process.stdin pipe給 outStream.

執行上面的程式碼,任何我們輸入給 process.stdin 的內容都會被 outStreamconsole.log 輸出到控制檯。

實現這個流不怎麼有用,因為它實際上被實現了而且node內建了,它等同於 process.stdout。以下一行程式碼,就是把 stdin pipe給 stdout ,就能實現之前的效果:

process.stdin.pipe(process.stdout);
複製程式碼

實現可讀流

為了實現可讀流,引用Readable介面並用它構造新物件:

const { Readable } = require('stream');
複製程式碼
const inStream = new Readable({});
複製程式碼

有一個簡單的方法來實現可讀流。我們可以直接把供使用的資料 push 出去。

const { Readable } = require('stream');

const inStream = new Readable();

inStream.push('ABCDEFGHIJKLM');

inStream.push('NOPQRSTUVWXYZ');

inStream.push(null); // No more data

inStream.pipe(process.stdout);
複製程式碼

push 一個 null 物件就意味著我們想發出訊號——這個流沒有更多資料了。

使用這個可寫流,可以直接把它pipe給 process.stdout 這個可寫流。

執行以上程式碼,會讀取 inStream 中所有的資料,並輸出在標準輸出流。很簡單,也不是很有用。

我們基本上在pipe給 process.stdout 之前把所有的資料都推到流裡了。更好的方法是按需推送。我們可以通過在一個可讀流的配置實現 read() 方法來做這件事情:

const inStream = new Readable({
  **read**(size) {
    // there is a demand on the data... Someone wants to read it.
  }
});
複製程式碼

當在可讀的流上呼叫讀方法時,實現可以將部分資料推到佇列中。例如,我們可以一次推送一個字母,從字元程式碼65(代表A),並且每推一次增加1:

const inStream = new Readable({
  read(size) {
    **this.push**(String.fromCharCode(this.currentCharCode++));
    if (this.currentCharCode > 90) {
      **this.push**(null);
    }
  }
});
inStream.currentCharCode = 65;
inStream.pipe(process.stdout);
複製程式碼

當從可讀流裡讀資料, read 方法將被持續呼叫,我們就會推送更多的字母。我們需要停止這個迴圈的條件,這就是為什麼會一個if語句當currentcharcode大於90(代表Z)是推送null。

這段程式碼相當於我們開始使用的更簡單的程式碼,但是當使用者要求時,我們正在按需推送資料。你應該經常這樣做。

相關文章