Nodejs資料流(Stream)手冊
1、介紹
本文介紹了使用 node.js streams 開發程式的基本方法。
"We should have some ways of connecting programs like garden hose--screw inanother segment when it becomes necessary to massage data inanother way. This is the way of IO also."Doug McIlroy. October 11, 1964
最早接觸Stream是從早期的unix開始的數十年的實踐證明Stream 思想可以很簡單的開發出一些龐大的系統。在unix裡,Stream是通過 |實現的;在node中,作為內建的stream模組,很多核心模組和三方模組都使用到。和unix一樣,node Stream主要的操作也是.pipe(),使用者可以使用反壓力機制來控制讀和寫的平衡。
Stream 可以為開發人員提供可以重複使用統一的介面,通過抽象的Stream介面來控制Stream之間的讀寫平衡。
2、為什麼使用Stream
node中的I/O是非同步,因此對磁碟和網路的讀寫需要通過回呼函數來讀取資料,下面是一個檔案下載伺服器的簡單代碼:
var http = require('http');var fs = require('fs');var server = http.createServer(function (req, res) { fs.readFile(__dirname + '/data.txt', function (err, data) { res.end(data); });});server.listen(8000);
這些代碼可以實現需要的功能,但是服務在傳送檔案資料之前需要緩衝整個檔案資料到記憶體,如果"data.txt"檔案很大且並發量很大的話,會浪費很多記憶體。因為使用者需要等到整個檔案快取到記憶體才能接受的檔案資料,這樣導致使用者體驗相當不好。不過還好(req, res)兩個參數都是Stream,這樣我們可以用fs.createReadStream()代替fs.readFile():
var http = require('http');var fs = require('fs');var server = http.createServer(function (req, res) { var stream = fs.createReadStream(__dirname + '/data.txt'); stream.pipe(res);});server.listen(8000);
.pipe()方法監聽fs.createReadStream()的'data' 和'end'事件,這樣"data.txt"檔案就不需要緩衝整個檔案,當用戶端串連完成之後馬上可以發送一個資料區塊到用戶端。使用.pipe()另一個好處是可以解決當用戶端延遲非常大時導致的讀寫不平衡問題。如果想壓縮檔再發送,可以使用三方模組實現:
var http = require('http');var fs = require('fs');var oppressor = require('oppressor');var server = http.createServer(function (req, res) { var stream = fs.createReadStream(__dirname + '/data.txt'); stream.pipe(oppressor(req)).pipe(res);});server.listen(8000);
這樣檔案就會對支援gzip和deflate的瀏覽器進行壓縮。oppressor 模組會處理所有的content-encoding。
Stream使開發程式變得簡單。
3、基礎概念
有五種基本的Stream: readable, writable, transform, duplex, and”classic”.
3-1、pipe
所有類型的Stream收是使用 .pipe() 來建立一個輸入輸出對,接收一個可讀流src並將其資料輸出到可寫流dst,如下:
src.pipe(dst)
.pipe( dst )方法為返回dst流,這樣就可以接連使用多個.pipe(),如下:
a.pipe( b ).pipe( c ).pipe( d )
功能與下面的代碼相同:
a.pipe( b );b.pipe( c );c.pipe( d );
3-2、readable streams
通過調用Readable streams的 .pipe()方法可以把Readable streams的資料寫入一個Writable , Transform, 或者Duplex stream。
readableStream.pipe( dst )
1>、建立 readable stream
這裡我們建立一個readable stream!
var Readable = require('stream').Readable;var rs = new Readable;rs.push('beep ');rs.push('boop\n');rs.push(null);rs.pipe(process.stdout);$ node read0.jsbeep boop
rs.push( null ) 通知數據接收者資料已經發送完畢.
注意到我們在將所有資料內容壓入可讀流之前並沒有調用rs.pipe(process.stdout);,但是我們壓入的所有資料內容還是完全的輸出了,這是因為可讀流在接收者沒有讀取資料之前,會緩衝所有壓入的資料。但是在很多情況下, 更好的方法是只有資料接收著請求資料的時候,才壓入資料到可讀流而不是緩衝整個資料。下面我們重寫 一下._read()函數:
var Readable = require('stream').Readable;var rs = Readable();var c = 97;rs._read = function () { rs.push(String.fromCharCode(c++)); if (c > 'z'.charCodeAt(0)) rs.push(null);};rs.pipe(process.stdout);
$ node read1.jsabcdefghijklmnopqrstuvwxyz
上面的代碼通過重寫_read()方法實現了只有在資料接受者請求資料才向可讀流中壓入資料。_read()方法也可以接收一個size參數表示資料請求著請求的資料大小,但是可讀流可以根據需要忽略這個參數。
注意我們也可以用util.inherits()繼承可讀流。為了說明只有在資料接受者請求資料時_read()方法才被調用,我們在向可讀流壓入資料時做一個延時,如下:
var Readable = require('stream').Readable;var rs = Readable();var c = 97 - 1;rs._read = function () { if (c >= 'z'.charCodeAt(0)) return rs.push(null); setTimeout(function () { rs.push(String.fromCharCode(++c)); }, 100);};rs.pipe(process.stdout);process.on('exit', function () { console.error('\n_read() called ' + (c - 97) + ' times');});process.stdout.on('error', process.exit);
用下面的命令運行程式我們發現_read()方法只調用了5次:
$ node read2.js | head -c5abcde_read() called 5 times
使用計時器的原因是系統需要時間來發送訊號來通知程式關閉管道。使用process.stdout.on('error', fn) 是為了處理系統因為header命令關閉管道而發送SIGPIPE訊號,因為這樣會導致process.stdout觸發EPIPE事件。如果想建立一個的可以壓入任意形式資料的可讀流,只要在建立流的時候設定參數objectMode為true即可,例如:Readable({ objectMode: true })。
2>、讀取readable stream資料
大部分情況下我們只要簡單的使用pipe方法將可讀流的資料重新導向到另外形式的流,但是在某些情況下也許直接從可讀流中讀取資料更有用。如下:
process.stdin.on('readable', function () { var buf = process.stdin.read(); console.dir(buf);});$ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume0.js null
當可讀流中有資料可讀取時,流會觸發'readable' 事件,這樣就可以調用.read()方法來讀取相關資料,當可讀流中沒有資料可讀取時,.read() 會返回null,這樣就可以結束.read() 的調用, 等待下一次'readable' 事件的觸發。下面是一個使用.read(n)從標準輸入每次讀取3個位元組的例子:
process.stdin.on('readable', function () { var buf = process.stdin.read(3); console.dir(buf);});
如下運行程式發現,輸出結果並不完全!
$ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume1.js
這是應為額外的資料資料留在流的內部緩衝區裡了,而我們需要通知流我們要讀取更多的資料.read(0)可以達到這個目的。
process.stdin.on('readable', function () { var buf = process.stdin.read(3); console.dir(buf); process.stdin.read(0);});
這次運行結果如下:
$ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume2.js
我們可以使用 .unshift() 將資料重新押迴流資料隊列的頭部,這樣可以接續讀取押回的資料。如下面的代碼,會按行輸出標準輸入的內容:
var offset = 0;process.stdin.on('readable', function () { var buf = process.stdin.read(); if (!buf) return; for (; offset < buf.length; offset++) { if (buf[offset] === 0x0a) { console.dir(buf.slice(0, offset).toString()); buf = buf.slice(offset + 1); offset = 0; process.stdin.unshift(buf); return; } } process.stdin.unshift(buf);});$ tail -n +50000 /usr/share/dict/american-english | head -n10 | node lines.js 'hearties''heartiest''heartily''heartiness''heartiness\'s''heartland''heartland\'s''heartlands''heartless''heartlessly'
當然,有很多模組可以實現這個功能,如:split 。
3-3、writable streams
writable streams只可以作為.pipe()函數的目的參數。如下代碼:
src.pipe( writableStream );
1>、建立 writable stream
重寫 ._write(chunk, enc, next) 方法就可以接受一個readable stream的資料。
var Writable = require('stream').Writable;var ws = Writable();ws._write = function (chunk, enc, next) { console.dir(chunk); next();};process.stdin.pipe(ws);$ (echo beep; sleep 1; echo boop) | node write0.js
第一個參數chunk是資料輸入者寫入的資料。第二個參數end是資料的編碼格式。第三個參數next(err)通過回呼函數通知數據寫入者可以寫入更多的時間。如果readable stream寫入的是字串,那麼字串會預設轉換為Buffer,如果在建立流的時候設定Writable({ decodeStrings: false })參數,那麼不會做轉換。如果readable stream寫入的資料時對象,那麼需要這樣建立writable stream
Writable({ objectMode: true })
2>、寫資料到 writable stream
調用writable stream的.write(data)方法即可完成資料寫入。
process.stdout.write('beep boop\n');
調用.end()方法通知writable stream 資料已經寫入完成。
var fs = require('fs');var ws = fs.createWriteStream('message.txt');ws.write('beep ');setTimeout(function () { ws.end('boop\n');}, 1000);$ node writing1.js $ cat message.txtbeep boop
如果需要設定writable stream的緩衝區的大小,那麼在建立流的時候,需要設定opts.highWaterMark,這樣如果緩衝區裡的資料超過opts.highWaterMark,.write(data)方法會返回false。當緩衝區可寫的時候,writable stream會觸發'drain' 事件。
3-4、classic streams
Classic streams比較老的介面了,最早出現在node 0.4版本中,但是瞭解一下其運行原理還是十分有好
處的。當一個流被註冊了"data" 事件的回到函數,那麼流就會工作在老版本模式下,即會使用老的API。
1>、classic readable streams
Classic readable streams事件就是一個事件觸發程序,如果Classic readable streams有資料可讀取,那麼其觸發 "data" 事件,等到資料讀取完畢時,會觸發"end" 事件。.pipe() 方法通過檢查stream.readable 的值確定流是否有資料可讀。下面是一個使用Classic readable streams列印A-J字母的例子:
var Stream = require('stream');var stream = new Stream;stream.readable = true;var c = 64;var iv = setInterval(function () { if (++c >= 75) { clearInterval(iv); stream.emit('end'); } else stream.emit('data', String.fromCharCode(c));}, 100);stream.pipe(process.stdout);$ node classic0.jsABCDEFGHIJ
如果要從classic readable stream中讀取資料,註冊"data" 和"end"兩個事件的回呼函數即可,代碼如下:
process.stdin.on('data', function (buf) { console.log(buf);});process.stdin.on('end', function () { console.log('__END__');});$ (echo beep; sleep 1; echo boop) | node classic1.js __END__
需要注意的是如果你使用這種方式讀取資料,那麼會失去使用新介面帶來的好處。比如你在往一個 延遲非常大的流寫資料時,需要注意讀取資料和寫資料的平衡問題,否則會導致大量資料緩衝在記憶體中,導致浪費大量記憶體。一般這時候強烈建議使用流的.pipe()方法,這樣就不用自己監聽”data” 和”end”事件了,也不用擔心讀寫不平衡的問題了。當然你也可以用 through代替自己監聽”data” 和”end” 事件,如下面的代碼:
var through = require('through');process.stdin.pipe(through(write, end));function write (buf) { console.log(buf);}function end () { console.log('__END__');}$ (echo beep; sleep 1; echo boop) | node through.js __END__
或者也可以使用concat-stream來緩衝整個流的內容:
var concat = require('concat-stream');process.stdin.pipe(concat(function (body) { console.log(JSON.parse(body));}));$ echo '{"beep":"boop"}' | node concat.js { beep: 'boop' }
當然如果你非要自己監聽"data" 和"end"事件,那麼你可以在寫資料的流不可寫的時候使用.pause()方法暫停Classic readable streams繼續觸發”data” 事件。等到寫資料的流可寫的時候再使用.resume() 方法通知流繼續觸發"data" 事件繼續讀取
資料。
2>、classic writable streams
Classic writable streams 非常簡單。只有 .write(buf), .end(buf)和.destroy()三個方法。.end(buf) 方法的buf參數是可選的,如果選擇該參數,相當於stream.write(buf); stream.end() 這樣的操作,需要注意的是當流的緩衝區寫滿即流不可寫時.write(buf)方法會返回false,如果流再次可寫時,流會觸發drain事件。
4、transform
transform是一個對讀入資料過濾然輸出的流。
5、duplex
duplex stream是一個可讀也可寫的雙向流,如下面的a就是一個duplex stream:
a.pipe(b).pipe(a)