NodeJs中的stream(流)- 基礎(chǔ)篇

一、什么是Stream(流)

流(stream)在 Node.js 中是處理流數(shù)據(jù)的抽象接口(abstract interface)。 stream 模塊提供了基礎(chǔ)的 API 。使用這些 API 可以很容易地來構(gòu)建實(shí)現(xiàn)流接口的對(duì)象。

流是可讀的、可寫的,或是可讀寫的。

二、NodeJs中的Stream的幾種類型

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

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

NodeJs中關(guān)于流的操作被封裝到了Stream模塊中,這個(gè)模塊也被多個(gè)核心模塊所引用。

const stream = require('stream');

在 NodeJS 中對(duì)文件的處理多數(shù)使用流來完成

  • 普通文件
  • 設(shè)備文件(stdin、stdout)
  • 網(wǎng)絡(luò)文件(http、net)

注:在NodeJs中所有的Stream(流)都是EventEmitter的實(shí)例

Example:

1.將1.txt的文件內(nèi)容讀取為流數(shù)據(jù)

const fs = require('fs');

// 創(chuàng)建一個(gè)可讀流(生產(chǎn)者)
let rs = fs.createReadStream('./1.txt'); 

通過fs模塊提供的createReadStream()可以輕松創(chuàng)建一個(gè)可讀的文件流。但我們并有直接使用Stream模塊,因?yàn)閒s模塊內(nèi)部已經(jīng)引用了Stream模塊并做了封裝。所以說 流(stream)在 Node.js 中是處理流數(shù)據(jù)的抽象接口,提供了基礎(chǔ)Api來構(gòu)建實(shí)現(xiàn)流接口的對(duì)象。

var rs = fs.createReadStream(path,[options]);

1.path 讀取文件的路徑

2.options

  • flags打開文件的操作, 默認(rèn)為'r'
  • mode 權(quán)限位 0o666
  • encoding默認(rèn)為null
  • start開始讀取的索引位置
  • end結(jié)束讀取的索引位置(包括結(jié)束位置)
  • highWaterMark讀取緩存區(qū)默認(rèn)的大小64kb

Node.js 提供了多種流對(duì)象。 例如:

  • HTTP 請(qǐng)求 (request response)
  • process.stdout 就都是流的實(shí)例。

2.創(chuàng)建可寫流(消費(fèi)者)處理可讀流

將1.txt的可讀流 寫入到2.txt文件中 這時(shí)我們需要一個(gè)可寫流

const fs = require('fs');
// 創(chuàng)建一個(gè)可寫流
let ws = fs.createWriteStream('./2.txt');
// 通過pipe讓可讀流流入到可寫流 寫入文件
rs.pipe(ws); 
var ws = fs.createWriteStream(path,[options]);

1.path 讀取文件的路徑

2.options

  • flags打開文件的操作, 默認(rèn)為'w'
  • mode 權(quán)限位 0o666
  • encoding默認(rèn)為utf8
  • autoClose:true是否自動(dòng)關(guān)閉文件
  • highWaterMark讀取緩存區(qū)默認(rèn)的大小16kb

pipe 它是Readable流的方法,相當(dāng)于一個(gè)"管道",數(shù)據(jù)必須從上游 pipe 到下游,也就是從一個(gè) readable 流 pipe 到 writable 流。
后續(xù)將深入將介紹pipe。

stream-1.png

如上圖,我們把文件比作裝水的桶,而水就是文件里的內(nèi)容,我們用一根管子(pipe)連接兩個(gè)桶使得水從一個(gè)桶流入另一個(gè)桶,這樣就慢慢的實(shí)現(xiàn)了大文件的傳輸過程。

三、為什么應(yīng)該使用 Stream

當(dāng)有用戶在線看視頻,假定我們通過HTTP請(qǐng)求返回給用戶視頻內(nèi)容

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
    fs.readFile(videoPath, (err, data) => {
        res.end(data);
    });
}).listen(8080);

但這樣有兩個(gè)明顯的問題

1.視頻文件需要全部讀取完,才能返回給用戶,這樣等待時(shí)間會(huì)很長(zhǎng)
2.視頻文件一次全放入內(nèi)存中,內(nèi)存吃不消

用流可以將視頻文件一點(diǎn)一點(diǎn)讀到內(nèi)存中,再一點(diǎn)一點(diǎn)返回給用戶,讀一部分,寫一部分。(利用了 HTTP 協(xié)議的 Transfer-Encoding: chunked 分段傳輸特性),用戶體驗(yàn)得到優(yōu)化,同時(shí)對(duì)內(nèi)存的開銷明顯下降

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
    fs.createReadStream(videoPath).pipe(res);
}).listen(8080);

四、可讀流(Readable Stream)

可讀流(Readable streams)是對(duì)提供數(shù)據(jù)的源頭(source)的抽象。

例如:

  • HTTP responses, on the client
  • HTTP requests, on the server
  • fs read streams
  • TCP sockets
  • process.stdin

所有的 Readable 都實(shí)現(xiàn)了 stream.Readable 類定義的接口。

可讀流的兩種模式(flowing 和 paused)

1.在 flowing 模式下, 可讀流自動(dòng)從系統(tǒng)底層讀取數(shù)據(jù),并通過 EventEmitter 接口的事件盡快將數(shù)據(jù)提供給應(yīng)用。

2.在 paused 模式下,必須顯式調(diào)用 stream.read()方法來從流中讀取數(shù)據(jù)片段。

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

  • 監(jiān)聽'data'事件
  • 調(diào)用stream.resume()方法
  • 調(diào)用stream.pipe()方法將數(shù)據(jù)發(fā)送到Writable

流動(dòng)模式flowing

流切換到流動(dòng)模式 監(jiān)聽data事件

const rs = fs.createReadStream('./1.txt');
const ws = fs.createWriteStream('./2.txt');
rs.on('data', chunk => {
    ws.write(chunk);
});
ws.on('end', () => {
    ws.end();
});

如果寫入的速度跟不上讀取的速度,有可能導(dǎo)致數(shù)據(jù)丟失。正常的情況應(yīng)該是,寫完一段,再讀取下一段,如果沒有寫完的話,就讓讀取流先暫停,等寫完再繼續(xù)。

var fs = require('fs');
// 讀取highWaterMark(3字節(jié))數(shù)據(jù),讀完之后填充緩存區(qū),然后觸發(fā)data事件
var rs = fs.createReadStream(sourcePath, {
    highWaterMark: 3 
});
var ws = fs.createWriteStream(destPath, {
    highWaterMark: 3
});

rs.on('data', function(chunk) { // 當(dāng)有數(shù)據(jù)流出時(shí),寫入數(shù)據(jù)
    if (ws.write(chunk) === false) { // 如果沒有寫完,暫停讀取流
        rs.pause();
    }
});

ws.on('drain', function() { // 緩沖區(qū)清空觸發(fā)drain事件 這時(shí)再繼續(xù)讀取
    rs.resume();
});

rs.on('end', function() { // 當(dāng)沒有數(shù)據(jù)時(shí),關(guān)閉數(shù)據(jù)流
    ws.end();
});

或者使用更直接的pipe

fs.createReadStream(sourcePath).pipe(fs.createWriteStream(destPath));

暫停模式paused

1.在流沒有 pipe() 時(shí),調(diào)用 pause() 方法可以將流暫停
2.pipe() 時(shí),需要移除所有 data 事件的監(jiān)聽,再調(diào)用 unpipe() 方法

read(size)

流在暫停模式下需要程序顯式調(diào)用 read() 方法才能得到數(shù)據(jù)。read() 方法會(huì)從內(nèi)部緩沖區(qū)中拉取并返回若干數(shù)據(jù),當(dāng)沒有更多可用數(shù)據(jù)時(shí),會(huì)返回null。read()不會(huì)觸發(fā)'data'事件。

使用 read() 方法讀取數(shù)據(jù)時(shí),如果傳入了 size 參數(shù),那么它會(huì)返回指定字節(jié)的數(shù)據(jù);當(dāng)指定的size字節(jié)不可用時(shí),則返回null。如果沒有指定size參數(shù),那么會(huì)返回內(nèi)部緩沖區(qū)中的所有數(shù)據(jù)。
NodeJS 為我們提供了一個(gè) readable 的事件,事件在可讀流準(zhǔn)備好數(shù)據(jù)的時(shí)候觸發(fā),也就是先監(jiān)聽這個(gè)事件,收到通知又?jǐn)?shù)據(jù)了我們?cè)偃プx取就好了:

const fs = require('fs');
rs = fs.createReadStream(sourcePath);

// 當(dāng)你監(jiān)聽 readable事件的時(shí)候,會(huì)進(jìn)入暫停模式
rs.on('readable', () => {
    console.log(rs._readableState.length);
        // read如果不加參數(shù)表示讀取整個(gè)緩存區(qū)數(shù)據(jù)
        // 讀取一個(gè)字段,如果可讀流發(fā)現(xiàn)你要讀的字節(jié)小于等于緩存字節(jié)大小,則直接返回
        let ch = rs.read(1);
});

暫停模式 緩存區(qū)的數(shù)據(jù)以鏈表的形式保存在BufferList中

五、可寫流(Writable Stream)

可寫流是對(duì)數(shù)據(jù)流向設(shè)備的抽象,用來消費(fèi)上游流過來的數(shù)據(jù),通過可寫流程序可以把數(shù)據(jù)寫入設(shè)備,常見的是本地磁盤文件或者 TCP、HTTP 等網(wǎng)絡(luò)響應(yīng)。

Writable 的例子包括了:

  • HTTP requests, on the client
  • HTTP responses, on the server
  • fs write streams
  • zlib streams
  • crypto streams
  • TCP sockets
  • child process stdin
  • process.stdout, process.stderr

所有 Writable 流都實(shí)現(xiàn)了 stream.Writable 類定義的接口。

process.stdin.pipe(process.stdout);

process.stdout 是一個(gè)可寫流,程序把可讀流 process.stdin 傳過來的數(shù)據(jù)寫入的標(biāo)準(zhǔn)輸出設(shè)備。在了解了可讀流的基礎(chǔ)上理解可寫流非常簡(jiǎn)單,流就是有方向的數(shù)據(jù),其中可讀流是數(shù)據(jù)源,可寫流是目的地,中間的管道環(huán)節(jié)是雙向流。

可寫流使用

調(diào)用可寫流實(shí)例的 write() 方法就可以把數(shù)據(jù)寫入可寫流

const fs = require('fs');
const rs = fs.createReadStream(sourcePath);
const ws = fs.createWriteStream(destPath);

rs.setEncoding('utf-8'); // 設(shè)置編碼格式
rs.on('data', chunk => {
  ws.write(chunk); // 寫入數(shù)據(jù)
});

監(jiān)聽了可讀流的 data 事件就會(huì)使可讀流進(jìn)入流動(dòng)模式,我們?cè)诨卣{(diào)事件里調(diào)用了可寫流的 write() 方法,這樣數(shù)據(jù)就被寫入了可寫流抽象的設(shè)備destPath中。

write() 方法有三個(gè)參數(shù)

  • chunk {String| Buffer},表示要寫入的數(shù)據(jù)
  • encoding 當(dāng)寫入的數(shù)據(jù)是字符串的時(shí)候可以設(shè)置編碼
  • callback 數(shù)據(jù)被寫入之后的回調(diào)函數(shù)

'drain'事件

如果調(diào)用 stream.write(chunk) 方法返回 false,表示當(dāng)前緩存區(qū)已滿,流將在適當(dāng)?shù)臅r(shí)機(jī)(緩存區(qū)清空后)觸發(fā) 'drain

const fs = require('fs');
const rs = fs.createReadStream(sourcePath);
const ws = fs.createWriteStream(destPath);

rs.setEncoding('utf-8'); // 設(shè)置編碼格式
rs.on('data', chunk => {
  let flag = ws.write(chunk); // 寫入數(shù)據(jù)
  if (!flag) { // 如果緩存區(qū)已滿暫停讀取
      rs.pause();
  }
});

ws.on('drain', () => {
    rs.resume(); // 緩存區(qū)已清空 繼續(xù)讀取寫入
});

六、總結(jié)

stream(流)分為可讀流(flowing mode 和 paused mode)、可寫流、可讀寫流,Node.js 提供了多種流對(duì)象。 例如, HTTP 請(qǐng)求 和 process.stdout 就都是流的實(shí)例。stream 模塊提供了基礎(chǔ)的 API 。使用這些 API 可以很容易地來構(gòu)建實(shí)現(xiàn)流接口的對(duì)象。它們底層都調(diào)用了stream模塊并進(jìn)行封裝。

后續(xù)我們將繼續(xù)對(duì)stream深入解析以及Readable Writable pipe的實(shí)現(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,559評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,581評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,922評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,096評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,639評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,374評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,591評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,789評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評(píng)論 1 295
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,322評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,554評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容