Node.js Stream(流)

WZZ發表於2019-02-24

流的概念

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

可讀流

一、兩種模式

可讀流會工作在下面兩種模式之一

  • flowing模式:可讀流 自動(不斷的) 從底層讀取資料(直到讀取完畢),並通過EventEmitter 介面的事件儘快將資料提供給應用
  • paused模式:必須顯示呼叫stream.read() 方法來從流中讀取資料片段

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

  • 監聽 `data` 事件。
  • 呼叫 stream.resume() 方法。
  • 呼叫 stream.pipe() 方法將資料傳送到 Writable。

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

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

下面演示如何從流中讀取資料
注:檔案1.txt中的內容是1234567890

let fs = require("fs");
let rs = fs.createReadStream(`1.txt`,{   //這些引數是可選的,不需要精細控制可以不設定
    flags:`r`,      //檔案的操作是讀取操作
    encoding:`utf8`,//預設是null,null代表buffer,會按照encoding輸出內容
    highWaterMark:3,//單位是位元組,表示一次讀取多少位元組,預設是64k
    autoClose:true,//讀完是否自動關閉
    start:0,       //讀取的起始位置
    end:9          //讀取的結束位置,包括9這個位置的內容
})
//rs.setEncoding(`utf8`);  //可以設定編碼方式

rs.on(`open`,function(){
    console.log(`open`);
})

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

rs.on(`error`,function(){
    console.log(`error`);
})
rs.on(`end`,function(){
    console.log(`end`);
})
rs.on(`close`,function(){
    console.log(`close`);
})
複製程式碼

執行結果

open
123
456
789
0
end
close
複製程式碼

1、fs.createReadStream建立可讀流例項時,預設開啟檔案,觸發open事件(並不是每個流都會觸發open事件),但此時並不會將檔案中的內容輸出(因為處於‘暫停模式’,沒有事件消費),而是將資料儲存到內部的緩衝器buffer,buffer的大小取決於highWaterMark引數,讀取大小達到highWaterMark指定的閾值時,流會暫停從底層資源讀取資料,直到當前緩衝器的資料被消費

2、這裡的rs可以理解為流的消費者,當消費者監聽了`data`事件時,就開始消費資料,可讀流會從paused切換到flowing“流動模式”,不斷的向消費者提供資料,直到沒有資料

3、從列印結果可以看出,可讀流每次讀取highWaterMark個資料,交給消費者,所以先列印123,再列印456 … …

4、當讀完檔案,也就是資料被完全消費後,觸發end事件

5、最後流或者底層資原始檔關閉後,這裡就是1.txt這個檔案關閉後,觸發close事件

6、error事件通常會在底層系統內部出錯從而不能產生資料,或當流的實現試圖傳遞錯誤資料時發生。

7、fs.createReadStream第二個引數是可選的,可不填,或只設定部分,比如編碼,不需要精細控制可以不設定

模式切換

rs.on(`data`,function(data){ // 暫停模式 -> 流動模式
    console.log(data);
    rs.pause(); // 暫停方法 表示暫停讀取,暫停data事件觸發
});
setTimeout(function () {
    rs.resume(); //恢復data事件觸發,變為流動模式
},1000)
//結果 open  123  456
複製程式碼

1、上例當監聽data事件時,可讀流處於flowing模式,呼叫了pause()方法,會暫停data事件的觸發,切換到paused模式
2、resume()可以恢復data事件觸發,再切換到flowing模式
3、上例中,setTimeout中切換流到flowing模式後,data事件觸發,但又遇到pause(),所以暫停了輸出,只列印到6

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

二、readable事件

let fs = require(`fs`);
let rs = fs.createReadStream(`1.txt`,{
    highWaterMark:2
});

rs.on(`readable`,function(){
    console.log(`begin`);
    let result = rs.read(2);
    console.log(`result `+result);
});
複製程式碼

`readable` 事件將在流中有資料可供讀取時觸發
上面已經說過,當我們建立可讀流時,就會先把快取區填滿(highWaterMark為指定的單次快取區大小),等待消費
如果快取區被清空(消費)後,會觸發readable事件
到達流資料尾部時,readable事件也會觸發,觸發順序在end事件之前

rs.read(size)

  • 該方法從內部緩衝區中收取並返回一些資料,如果沒有可讀資料,返回null
  • size是可選的,指定要讀取size個位元組,如果沒有指定,內部緩衝區所包含的所有資料將返回
  • 如果size位元組不可讀,返回null,如果此時流沒有結束(除非流已經結束),會將所有保留在內部緩衝區的資料將被返回。比如:檔案中有1個可讀位元組,但是指定size為2,這時呼叫read(2)會返回null,如果流沒有結束,那麼會再次觸發readable事件,將已經讀到內部緩衝區中的那一個位元組也返回
  • rs.read()方法只應該在暫停模式下的可讀流上執行,在流動模式下,read會自動呼叫,直到內部緩衝區資料完全耗盡

所以,上例中,如果檔案1.txt中內容是 a,輸出結果

begin
result null
begin
result a
複製程式碼

說明:highWaterMark是2,但檔案只有a,所以只有1個位元組在快取區,而size指定了2,2個位元組被認為是不可讀的,返回null;再次觸發readable,將快取區內容全部返回

如果內容是ab

begin
result ab
begin
result null
複製程式碼

說明:highWaterMark是2,所以一開始快取中有2個位元組,size指定了2,所以將ab全部讀取,快取清空——>繼續快取,發現到檔案末尾,於是觸發readable返回null

如果內容是abc,輸出

begin
result ab
begin
result null
begin
result c
複製程式碼

說明:一開始快取了2個,被消費掉,繼續快取c,並觸發readable,再次read(2),此時沒有2個位元組的資料,被認為是不可讀的,返回null,並且再次觸發readable將快取中剩餘資料讀取返回

如果內容是abcd,輸出

begin
result ab
begin
result cd
begin
result null
複製程式碼

說明:先讀完2個位元組,即ab輸出,快取區被清空,所以會再次觸發readable事件,再read(2)讀出cd,繼續自動快取,發現到了檔案末尾,又會觸發readable,返回null

在某些情況下,為 `readable` 事件新增回撥將會導致一些資料被讀取到內部快取中

這句話我的理解是,當消費資料大小 < 快取區大小,可讀流會自動新增highWaterMark個資料到快取,那麼新新增的資料和之前快取區中未被消費的資料加一起,有可能超過了highWaterMark大小,即快取區大小增加了

下面將highWaterMark改為3,read(1)再來看看怎麼執行的

let rs = fs.createReadStream(`1.txt`,{
    highWaterMark:3
});
rs.on(`readable`,function(){
    console.log(`begin`);
    let result = rs.read(1);
    console.log(`result `+result);
});
複製程式碼

當1.txt內容是 a,輸出

begin
result a
begin
result null
複製程式碼

說明:快取中只有a,也只讀了一個(read(1)),消費後,快取區清空,再去讀取時,已經到了檔案末尾,返回null

當1.txt內容是 ab,輸出

begin
result a
begin
result b
複製程式碼

說明:快取中有ab,當讀完a後,繼續快取,發現到了檔案末尾,觸發readable,而此時快取中還有b,因此將b返回

當讀取個數size > 快取區個數,會去更改快取區的大小highWaterMark(規則為找滿足>=size的最小的2的幾次方)

let rs = fs.createReadStream(`1.txt`,{
    highWaterMark:3
});

rs.on(`readable`,function(){
    console.log(`begin`);
    let result = rs.read(4);
    console.log(`result `+result);
});
複製程式碼

當1.txt中內容是abcdefgh,輸出

begin
result null
begin
result abcd
複製程式碼

說明:讀取的size(4)>快取,認為是不可讀的,size返回null;這時會重新計算highWaterMark大小,離4最近的是2的2次方,為4,所以highWaterMark此時等於4,返回了abcd;繼續快取efgh

但如果1.txt內容是abcdefg,輸出

begin
result null
begin
result abcd
begin
result efg
複製程式碼

同上,但當返回abcd繼續自動快取4個時,發現讀到檔案末尾,將快取資料返回,所以efg也輸出

可寫流

可寫流是對資料寫入`目的地`的一種抽象。

可寫流基本用法

let fs = require(`fs`);
let ws = fs.createWriteStream(`./1.txt`,{
    flags:`w`,
    mode:0o666,
    autoClose:true,
    highWaterMark:3, // 預設是16k ,而createReadStream是64k
    encoding:`utf8`,//預設是utf8
    start:0
});
for(let i = 0;i<4;i++){
    let flag =  ws.write(i+``);
    console.log(flag)
}
ws.end("ok");// 標記檔案末尾

ws.on(`open`,function(){
    console.log(`open`)
});

ws.on(`error`,function(err){
    console.log(err);
});

ws.on(`finish`,function(err){
    console.log(`finish`);
});

ws.on(`close`,function(){
    console.log(`close`)
});
複製程式碼

列印結果

true
true
false
false
open
finish
close
複製程式碼

寫入檔案1.txt的結果
0123ok

1、fs.createWriteStream建立可寫流,同樣預設會開啟檔案

2、可寫流通過反覆呼叫 ws.write(chunk) 方法將資料放到內部緩衝器
寫入的資料chunk必須是字串或者buffer
write雖然是個非同步方法,但有返回值,這個返回值flag的含義,不是檔案是否寫入,而是表示能否繼續寫入
即緩衝器總大小 < highWaterMark時,可以繼續寫入,flag為true;
一旦內部緩衝器大小達到或超過highWaterMark,flag返回false;
注意,即使flag為flase,寫入的內容也不會丟失

3、上例中指定的highWaterMark是3,呼叫write時一次寫入了一個位元組,當呼叫第三次write方法時,緩衝器中的資料大小達到3這個閾值,開始返回flase,所以先列印了兩次true,後列印了兩次false

4、ws.end(“ok”); end方法用來標記檔案末尾,表示接下來沒有資料要寫入可寫流;
可以傳入可選的 chunk 和 encoding 引數,在關閉流之前再寫入一段資料;
如果傳入了可選的 callback 函式,它將作為 `finish` 事件的回撥函式。所以`ok`會被寫入檔案末尾。
注意,ws.write()方法必須在ws.end()方法之前呼叫

5、在呼叫了 ws.end() 方法,且緩衝區資料都已經傳給底層系統(這裡是檔案1.txt)之後, `finish` 事件將被觸發。

6、`close` 事件將在流或其底層資源(比如一個檔案)關閉後觸發。`close`事件觸發後,該流將不會再觸發任何事件。不是所有 可寫流/可讀流 都會觸發 `close` 事件。

drain事件

如果呼叫 stream.write(chunk) 方法返回 false,`drain` 事件會在適合恢復寫入資料到流的時候觸發。

drain觸發條件

  • 緩衝器滿了,即write返回false
  • 緩衝器的資料都寫入到流,即資料都被消費掉後,才會觸發

將上例中for迴圈改為如下

let i = 8;
function write(){
    let flag = true;
    while(i>0&&flag){
        flag = ws.write(--i+``,`utf8`,()=>{});
        console.log(flag)
    }

    if(i <= 0){
        ws.end("ok");
    }
 }
 write();
 // drain只有當快取區充滿後 ,並且被消費後觸發
 ws.on(`drain`,function(){
   console.log(`drain`);
   write();
 });
複製程式碼

列印

true
true
false
open
drain
true
true
false
drain
true
true
finish
close
複製程式碼

檔案1.txt寫入 76543210ok

上例當write返回為false,即緩衝器滿了時,停止while迴圈,等待;當緩衝器資料都寫入1.txt之後,會觸發drain事件,這時繼續write,直到寫到0,停止寫入,呼叫end,在檔案末尾寫入ok,關閉檔案

管道流 & pipe事件

管道提供了一個輸出流到輸入流的機制。通常我們用於從一個流中獲取資料並將資料傳遞到另外一個流中

如下,將1.txt的內容,按照讀一點,寫一點的方式 寫入2.txt

let fs = require(`fs`);
let rs = fs.createReadStream(`1.txt`,{
    highWaterMark:4
});
let ws = fs.createWriteStream(`2.txt`,{
    highWaterMark:3
});
rs.pipe(ws);   //可讀流上呼叫pipe()方法,pipe方法就是讀一點寫一點
複製程式碼

這段程式碼工作原理類似於下面這段程式碼

rs.on(`data`,function(chunk){ // chunk 讀到的內容
    let flag = ws.write(chunk);
    if(!flag){  //如果緩衝器滿了,寫不下了,就停止讀
        rs.pause();
    }
});
ws.on(`drain`,function(){ //當快取都寫到檔案了,恢復讀
    console.log(`寫一點`);
    rs.resume();
});
複製程式碼

參考資料
1、nodejs.cn/api/stream.…
2、www.runoob.com/nodejs/node…

相關文章