關於node.js中流的理解

shawlp發表於2019-04-20

什麼是流

流是一個在node中與流資料工作的抽象介面,stream模組提供了一個基本的API,使得比較容易建立一個實現了流的介面的物件。node.js中提供了很多的流物件,例如,一個發向http伺服器的請求,process.stdout都是流的例項。 流可以是可讀的,可寫或者兩者兼備。所有的流都是EventEmitter的例項。

流的型別

在node中有4種基本的流型別:

  1. Writable(可寫流):可以把資料寫進流裡
  2. Readable (可讀流): 在該流上可以讀取資料
  3. Duplex (雙工流): 在該流上,既可以寫資料,又可以讀取資料
  4. Transform (轉換流): 當資料在被寫或被讀的時候,可以修改資料的格式(例如壓縮)

物件模式

node建立的所有流都是對字串或者是Buffer物件操作。然而,也有與js的其他型別值工作的流實現。這些流被視作為在物件模式裡操作。 當流在被建立的時候,使用了objectMode選項,流的例項就被轉換為物件模式。試著去轉換一個流為物件模式,這並不是安全的。

Buffering(資料緩衝中...)

所有的writable和readable流將會把資料儲存在內部的buffer中,這資料可以分別使用writable.writableBuffer或者readable.redableBuffer取得。 一定數量的資料緩衝能力取決於傳遞進流構造器中的highWaterMark選項。對於正常的流來說,highWaterMark選項明確了位元組的資料量。對於在物件模式中操作的流,highWaterMark明確了物件的數量。

當例項呼叫stream.push(chunk)資料緩衝在readable流中,如果流的消費者沒有呼叫stream.read(),資料將會待在內部佇列直到被消費。

一旦內部讀取的buffer的資料量達到了highWaterMark標明的閥值,流將會暫時的停止從源頭讀取資料,直到緩衝的資料能被消費的時候會再次讀取

當 writable.write(chunk)方法被重複呼叫的時候,資料被緩衝在writable流裡。當內部寫的緩衝資料的總數量低於highWaterMark標明的閥值,呼叫writable.write()將會返回true,一旦內部緩衝的資料量達到或者超過highWaterMark時,將會返回false。

stream.pipe()是為了限制資料的緩衝在一個可接受的水平,使得源頭和目的地不同的緩衝速度將不會那沖垮空閒的記憶體。

因為Duplex和Transform流都是可讀且可寫的,每個內部都維護了兩個獨立的內部緩衝用於讀和寫,使得每一邊在維護一個合適且高效的資料流時都能夠獨立的進行操作。

可讀流

可讀流是一種資料被消費的源頭的抽象,所有的可讀流例項實現了stream.Readable定義的介面。

例項如下,首先建立了一個可讀流,傳入要讀的檔案位置還有一些讀取設定,從哪裡開始讀,設定緩衝過程中讀取的最大閥值highWaterMark,開啟檔案時,資料被讀取流讀取到緩衝中,當定義一個data事件處理器時,此時緩衝中的資料被消費,當流中無資料時或者資料讀取完畢時會觸發end事件,之後關閉檔案。

let fs = require('fs');
let path = require('path');

let rs = fs.createReadStream(path.join(__dirname, '1.txt'), {
    flags: 'r', // 讀取操作
    encoding: 'utf8', // 預設為null,null代表的返回buffer
    autoClose: true, // 讀取完自動關閉
    highWaterMark: 3, // 預設是64k, 64*1024b
    start: 3, // 從第3個位元組開始讀取
    end: 8 // 包括索引8
});

// 也可以手動設定編碼或者在options宣告
// rs.setEncoding('utf8');

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

// 當流或其底層資源被關閉時觸發
rs.on('close', function() {
	console.log('檔案關閉了');
});

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

// 當流將資料塊傳送給消費者後觸發
rs.on('data', function(data) {
    console.log('data', data);
    // 停止觸發data事件,暫停讀取
    rs.pause();
    setTimeout(function() {
	    // 恢復讀取,暫停模式(需顯示呼叫stream.read())切換為流動模式(資料自動從底層系統讀取,並通過EventEmitter介面的事件儘可能快的被提供給應用程式)
	    rs.resume();
	    }, 1000);
    });
    
    // 表明流有新動態,要麼有新的資料,要麼到達流的盡頭,若同時使用data事件,當呼叫rs.read方法才會觸發data事件
    // rs.on('readable', function() {
    // console.log('readable data', rs.read());
// });

// 流中無資料時或者讀取資料完畢觸發
rs.on('end', function() {
	console.log('end');
});

// 檔案開啟了
// data 456
// data 789
// end
// 檔案關閉了 
複製程式碼

兩種讀取模式

可讀流有兩種讀取的模式:暫停模式和流動模式。這些模式與物件流模式不同,一個可讀流可以是物件流,也可以不是也不管它是處在流動模式下還是暫停模式下。

  1. 在流動模式下,資料被系統自動讀取並且使用EventEmitter介面能夠儘可能快的提供給應用消費
  2. 在暫停模式中,stream.read()必須被顯示呼叫,才能夠從流中讀取資料塊。

所有的可讀流開始處於一個暫停模式但是可以切換為流動模式通過以下一種方式:

  1. 新增一個data事件處理器
  2. 呼叫stream.resume()方法
  3. 呼叫stream.pipe(),使得資料是可寫的

所有的可讀流可以切換為暫停模式,通過以下一種方式:

  1. 如果沒有管道的目的地,通過呼叫stream.pause()
  2. 如果有管道的目的地,通過移除所有管道的目的地。大多數管道的目的地能夠通過呼叫stream.unpipe()移除

最重要的是要明白readable直到有消費或者忽略資料被提供,才會產生資料。如果消費機制被帶走了或者被停止了,那麼readable會停止產生資料。

出於背後相容性的原因,移除data事件處理器並不會自動的暫停流。統一,如果有管道的目的地,呼叫stream.pause()也不會保證流會暫停。

如果readable切換為流動模式,並且沒有消費者去消費處理資料,資料將會丟失。

新增readable的事件處理器可以自動的使得流停止流動,資料可以通過readable.read()去消費。如果readable的事件處理器被移除,流可以再次流動如果有個data事件處理器的話

可寫流

可寫流是資料被寫入的目的地的抽象,所有的可寫流例項實現了stream.Writable定義的介面

以下是我寫的一個例子,首先建立了一個可寫流物件,傳入一個引數為需要寫入的檔案位置,一個引數是配置引數,highWaterMark是在寫入的時候指定寫入資料的閾值,若內部緩衝小於閾值時,會返回true,若返回false,應該停止寫入資料,此時緩衝區已經達到閾值,滿了,若所有緩衝的資料塊被消費完了,清空了會觸發一個drain事件。

let fs = require('fs');
let path = require('path');

let ws = fs.createWriteStream(path.join(__dirname, '2.txt'), {
     flags: 'w',
     encoding: 'utf8',
     mode: 0o666,
     autoClose: true,
     start: 0,
     highWaterMark: 3 // 最高水平線,指定了位元組總數
 });

// 內部的緩衝小於建立的配置的highWaterMark,返回true
let flag = ws.write('0', 'utf8', ()=>{});
console.log(flag); // true

flag = ws.write('1', 'utf8', ()=>{});
console.log(flag); // true

// 若返回false,則應該停止向流寫入資料
flag = ws.write('2', 'utf8', ()=>{});
console.log(flag); // 返回false,寫完3時,緩衝區已達到highWaterMark:3時,表明快取區滿了

// 所有緩衝的資料塊都被排空了,當達到highWaterMark時,表明快取區滿了,滿了後被清空了才會觸發drain
ws.on('drain', function() {
console.log('drain');
flag = ws.write('3', 'utf8', ()=>{});
console.log(flag); // true
});

// 可寫流和可讀流都會在內部的緩衝器中儲存資料 
複製程式碼

相關文章