Stream -- Node.js中最好的卻最容易被誤解的部分

oOjia發表於2018-06-03

Stream是什麼?

Streams 是一個資料集——和陣列、字串一樣。不同的是streams的資料可能不能馬上全部獲取到,他們不需要在記憶體中。這樣使得streams在處理大資料集或者來自外部的資料來源的資料塊上能發揮很大的作用。

然而,streams不僅是能用在大資料上,也給我們在程式碼中的可組合的能力。就像通過傳送其他較小的Linux命令組成強大的Linux命令一樣,我們可以在Node中用streams做同樣的事情。

流(stream)在 Node.js 中是處理流資料的抽象介面(abstract interface)。 stream 模組提供了基礎的 API 。使用這些 API 可以很容易地來構建實現流介面的物件。

Node.js 提供了多種流物件。 例如, HTTP 請求 和 process.stdout 就都是流的例項。

流可以是可讀的、可寫的,或是可讀寫的。所有流的物件都是EventEmitter的例項,都實現了EventEmitter的介面。

也就是流具有事件的能力,可以通過發射事件來反饋流的狀態。這樣我們就可以註冊監聽流的事件,來達到我們的目的。也就是我們訂閱了流的事件,這個事件觸發時,流會通知我,然後我就可以做相應的操作了。

流的型別

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

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

可讀流(stream.Readable)

可讀流有兩種模式:flowing和paused

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

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

注意:如果可讀流切換到流動模式,並且沒有消費者處理流中的資料,這些資料將會丟失。

可讀流(Readable streams)是對提供資料的 源頭(source)的抽象

可讀流的例子包括

  • HTTP responses, on the client :客戶端請求
  • HTTP requests, on the server :服務端請求
  • fs read streams :讀檔案
  • zlib streams :壓縮
  • crypto streams :加密
  • TCP sockets :TCP協議
  • child process stdout and stderr :子程式標準輸出和錯誤輸出
  • process.stdin :標準輸入

所有的 Readable 都實現了 stream.Readable 類定義的介面

通過流讀取資料

  • 用Readable建立物件readable後,便得到了一個可讀流
  • 如果實現_read方法,就將流連線到一個底層資料來源
  • 流通過呼叫_read向底層請求資料,底層再呼叫流的push方法將需要的資料傳遞過來
  • 當readable連線了資料來源後,下游便可以呼叫readable.read(n)向流請求資料,同時監聽readable的data事件來接收取到的資料

下面簡單舉個可讀流的例子:

  • 監聽可讀流的data事件,當你一旦開始監聽data事件的時候,流就可以讀檔案的內容並且發射data,讀一點發射一點讀一點發射一點
  • 預設情況下,當你監聽data事件之後,會不停的讀資料,然後觸發data事件,觸發完data事件後再次讀資料
  • 讀的時候不是把檔案整體內容讀出來再發射出來的,而且設定一個緩衝區,大小預設是64K,比如檔案是128K,先讀64K發射出來,再讀64K在發射出來,會發射兩次
  • 緩衝區的大小可以通過highWaterMark來設定
let fs = require('fs');
//通過建立一個可讀流
let rs = fs.createReadStream('./1.txt',{
    flags:'r',//我們要對檔案進行何種操作
    mode:0o666,//許可權位
    encoding:'utf8',//不傳預設為buffer,顯示為字串
    start:3,//從索引為3的位置開始讀
    //這是我的見過唯一一個包括結束索引的
    end:8,//讀到索引為8結束
    highWaterMark:3//緩衝區大小
});
rs.on('open',function () {
    console.log('檔案開啟');
});
rs.setEncoding('utf8');//顯示為字串
//希望流有一個暫停和恢復觸發的機制
rs.on('data',function (data) {
    console.log(data);
    rs.pause();//暫停讀取和發射data事件
    setTimeout(function(){
        rs.resume();//恢復讀取並觸發data事件
    },2000);
});
//如果讀取檔案出錯了,會觸發error事件
rs.on('error',function () {
    console.log("error");
});
//如果檔案的內容讀完了,會觸發end事件
rs.on('end',function () {
    console.log('讀完了');
});
rs.on('close',function () {
    console.log('檔案關閉');
});

/**
檔案開啟
334
455
讀完了
檔案關閉
**/
複製程式碼

可寫流(stream.Writable)

1.Writable流的write()方法可以把資料寫入流中。

其中,chunk是待寫入的資料,是Buffer或String物件。這個引數是必須的,其它引數都是可選的。如果chunk是String物件,encoding可以用來指定字串的編碼格式,write會根據編碼格式將chunk解碼成位元組流再來寫入。callback是資料完全重新整理到流中時會執行的回撥函式。write方法返回布林值,當資料被完全處理後返回true(不一定是完全寫入裝置哦)。

2.Writable流的end()方法可以用來結束一個可寫流。它的三個引數都是可選的。chunk和encoding的含義與write方法類似。callback是一個可選的回撥,當你提供它時,它會被關聯到Writable的finish事件上,這樣當finish事件發射時它就會被呼叫。

常用的事件:

drain事件:當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false。 一旦所有當前所有快取的資料塊都排空了(被作業系統接受來進行輸出), 那麼 'drain' 事件就會被觸發

finish事件:在呼叫了 stream.end() 方法,且緩衝區資料都已經傳給底層系統之後, 'finish' 事件將被觸發。

可寫流是對資料寫入'目的地'的一種抽象 可寫流的例子包括了:

  • HTTP requests, on the client 客戶端請求
  • HTTP responses, on the server 伺服器響應
  • fs write streams 檔案
  • zlib streams 壓縮
  • crypto streams 加密
  • TCP sockets TCP伺服器
  • child process stdin 子程式標準輸入
  • process.stdout, process.stderr 標準輸出,錯誤輸出

下面舉個可寫流的簡單例子(當你往可寫流裡寫資料的時候,不是會立刻寫入檔案的,而是會很寫入快取區,快取區的大小就是highWaterMark,預設值是16K。然後等快取區滿了之後再次真正的寫入檔案裡)

let fs = require('fs');
let ws = fs.createWriteStream('./2.txt',{
   flags:'w',
   mode:0o666,
   start:3,
   highWaterMark:3//預設是16K
});
複製程式碼
  • 如果快取區已滿 ,返回false,如果快取區未滿,返回true
  • 如果能接著寫,返回true,如果不能接著寫,返回false
  • 按理說如果返回了false,就不能再往裡面寫了,但是如果你真寫了,如果也不會丟失,會快取在記憶體裡。等快取區清空之後再從記憶體裡讀出來
let flag = ws.write('1');
console.log(flag);//true
flag =ws.write('2');
console.log(flag);//true
flag =ws.write('3');
console.log(flag);//false
flag =ws.write('4');
console.log(flag);//false
複製程式碼

'drain' 事件

如果呼叫 stream.write(chunk) 方法返回 false,流將在適當的時機觸發 ‘drain’ 事件,這時才可以繼續向流中寫入資料

當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false。 一旦所有當前所有快取的資料塊都排空了(被作業系統接受來進行輸出), 那麼 ‘drain’ 事件就會被觸發

建議, 一旦 write() 返回 false, 在 ‘drain’ 事件觸發前, 不能寫入任何資料塊

舉個簡單的例子說明一下:

let fs = require('fs');
let ws = fs.createWriteStream('2.txt',{
    flags:'w',
    mode:0o666,
    start:0,
    highWaterMark:3
});
let count = 9;
function write(){
 let flag = true;//快取區未滿
//寫入方法是同步的,但是寫入檔案的過程是非同步的。
//在真正寫入檔案後還會執行我們的回撥函式
 while(flag && count>0){
     console.log('before',count);
     flag = ws.write((count)+'','utf8',(function (i) {
         return ()=>console.log('after',i);
     })(count));
     count--;
 }
}
write();//987
//監聽快取區清空事件
ws.on('drain',function () {
    console.log('drain');
    write();//654 321
});
ws.on('error',function (err) {
    console.log(err);
});
/**
before 9
before 8
before 7
after 9
after 8
after 7
**/
複製程式碼

如果已經不再需要寫入了,可以呼叫end方法關閉寫入流,一旦呼叫end方法之後則不能再寫入

比如在ws.end();後寫ws.write('x');,會報錯write after end

'pipe'事件

  • linux精典的管道的概念,前者的輸出是後者的輸入
  • pipe是一種最簡單直接的方法連線兩個stream,內部實現了資料傳遞的整個過程,在開發的時候不需要關注內部資料的流動
  • 這個方法從可讀流拉取所有資料, 並將資料寫入到提供的目標中 自動管理流量,將資料的滯留量限制到一個可接受的水平,以使得不同速度的來源和目標不會淹沒可用記憶體 預設情況下,當源資料流觸發 end的時候呼叫end(),所以寫入資料的目標不可再寫。傳 { end:false }作為options,可以保持目標流開啟狀態
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();
});
複製程式碼
pipe的用法:
let fs = require('fs');
let rs = fs.createReadStream('./1.txt',{
  highWaterMark:3
});
let ws = fs.createWriteStream('./2.txt',{
    highWaterMark:3
});
rs.pipe(ws);
//移除目標可寫流
rs.unpipe(ws);
複製程式碼
  • 當監聽可讀流data事件的時候會觸發回撥函式的執行
  • 可以實現資料的生產者和消費者速度的均衡
rs.on('data',function (data) {
    console.log(data);
    let flag = ws.write(data);
   if(!flag){
       rs.pause();
   }
});
複製程式碼

相關文章