為什麼應該使用流
你可能看過這樣的程式碼。
var http = require('http');
var fs = require('fs')
var server = http.createServer(function (req, res) {
fs.readFile(__dirname + '/data.txt', function (err, dat
res.end(data);
});
});
server.listen(8000);
複製程式碼
這段程式碼中,伺服器每收到一次請求,就會先把data.txt讀入到記憶體中,然後再從記憶體取出返回給客戶端。尷尬的是,如果data.txt非常的大,而每次請求都需要先把它全部存到記憶體,再全部取出,不僅會消耗伺服器的記憶體,也可能造成使用者等待時間過長。
幸好,HTTP請求中的request物件和response物件都是流物件,於是我們可以換一種更好的方法:
var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
let stream = fs.createReadStream(__dirname + '/data.txt');//創造可讀流
stream.pipe(res);//將可讀流寫入response
});
server.listen(8000);
複製程式碼
pipe方法如同stream和response之間的一個管道,將data.txt檔案一小段一小段地傳送到客戶端,減小了伺服器的記憶體壓力。
比喻理解Stream
在node中,一共有五種型別的流:readable,writable,transform,duplex以及"classic"。其中最核心的是可讀流和可寫流。我們舉個栗子來生動形象地理解它們。
可讀流可理解為:從一個裝滿了水的水桶中一點一點把水抽取出來的過程
可寫流可理解為:把從可讀流抽出來的水一點一點倒入一個空的桶中的過程
也可以以經典的生產者和消費者的問題來理解Stream,生產者不斷在快取中製造產品,而消費者則不斷地從快取中消費產品
readableStream
可讀流(Readable streams)是對提供資料的 源頭 (source)的抽象 可讀流的流程如圖所示
資源的資料流並不是直接流向消費者,而是先 push 到快取池,快取池有一個水位標記 highWatermark,超過這個標記閾值,push 的時候會返回 false,從而控制讀取資料流的速度,如同水管上的閥門,當水管麵裝滿了水,就暫時關上閥門,不再從資源裡“抽水”出來。什麼場景下會出現這種情況呢?
-
消費者主動執行了 .pause()
-
消費速度比資料 push 到快取池的生產速度慢
可讀流有兩種模式,flowing和pause
-
flowing模式下 可讀流可自動從資源讀取資料
-
pause模式下 需要顯式呼叫stream.read()方法來讀取資料
快取池就像一個空的水桶,消費者通過管口接水,同時,資源池就像一個水泵,不斷地往水桶中泵水,而 highWaterMark 是水桶的浮標,達到閾值就停止蓄水。下面是一個簡單的flowing模式 Demo:
const Readable = require('stream').Readable
class MyReadable extends Readable{
constructor(dataSource, options){
super(options)
this.dataSource = dataSource
}
//_read表示需要從MyReadable類內部呼叫該方法
_read(){
const data = this.dataSource.makeData()
this.push(data)
}
}
//模擬資源池
const dataSource = {
data: new Array('abcdefghijklmnopqrstuvwxyz'),
makeData: function(){
if(!this.data.length) return null
return this.data.pop()
}
}
const myReadable = new MyReadable(dataSource);
myReadable.setEncoding('utf8');
myReadable.on('data', (chunk) => {
console.log(chunk);
});
複製程式碼
另外一種模式是pause模式,這種模式下可讀流有三種狀態
- readable._readableState.flowing = null 目前沒有資料消費者,所以不會從資源庫中讀取資料
- readable._readableState.flowing = false 暫停從資源庫讀取資料,但 不會 暫停資料生成,主動觸發了 readable.pause() 方法, readable.unpipe() 方法, 或者接收 “背壓”(back pressure)可達到此狀態
- readable._readableState.flowing = true 正在從資源庫中讀取資料,監聽 'data' 事件,呼叫 readable.pipe() 方法,或者呼叫 readable.resume() 方法可達到此狀態 一個簡單的切換狀態的demo:
const myReadable = new MyReadable(dataSource);
myReadable.setEncoding('utf8');
myReadable.on('data', (chunk) => {
console.log(`Received ${chunk.length} bytes of data.`);
myReadable.pause()
console.log('pausing for 1 second')
setTimeout(()=>{
console.log('now restart')
myReadable.resume()
}, 1000)
});
複製程式碼
pause模式的流程圖如下
資源池會不斷地往快取池輸送資料,直到 highWaterMark 閾值,消費者需要主動呼叫 .read([size]) 函式才會從快取池取出,並且可以帶上 size 引數,用多少就取多少:
const myReadable = new MyReadable(dataSource);
myReadable.setEncoding('utf8');
myReadable.on('readable', () => {
let chunk;
while (null !== (chunk = myReadable.read(5))) {//每次讀5個位元組
console.log(`Received ${chunk.length} bytes of data.`);
}
});
複製程式碼
這裡值得注意的是,readable事件的回撥函式沒有引數。因為 'readable' 事件將在流中有資料可供讀取時就會觸發,而在pause模式下讀取資料需要顯式呼叫read()才會消費資料 輸出為:
Received 5 bytes of data.
Received 5 bytes of data.
Received 5 bytes of data.
Received 5 bytes of data.
Received 5 bytes of data.
Received 1 bytes of data.
複製程式碼
readableStream一些需要注意的事件
- 'data' 事件會在流將資料傳遞給消費者時觸發'end' 事件將在流中再沒有資料可供消費時觸發
- 'end' 事件將在流中再沒有資料可供消費時觸發
- 'readable'(從字面上看:“可以讀的”)事件將在流中有資料可供讀取時觸發。在某些情況下,為 'readable' 事件新增回撥將會導致一些資料被讀取到內部快取中。'readable' 事件表明 流有了新的動態:要麼是有了新的資料,要麼是到了流的尾部。 對於前者, stream.read() 將返回可用的資料。而對於後者, stream.read() 將返回 null。
- 'setEncoding'設定編碼會使得該流資料返回指定編碼的字串而不是Buffer物件。
- 'pipe' 事件放到後面詳談。
writableStream
Writable streams 是 destination 的一種抽象,一個writable流指的是隻能流進不能流出的流:
readableStream.pipe(writableStream)
複製程式碼
資料流過來的時候,會直接寫入到資源池,當寫入速度比較緩慢或者寫入暫停時,資料流會進入佇列池快取起來,當生產者寫入速度過快,把佇列池裝滿了之後,就會出現「背壓」(backpressure),這個時候是需要告訴生產者暫停生產的,當佇列釋放之後,Writable Stream 會給生產者傳送一個 drain 訊息,讓它恢復生產.
writable.write() 方法向流中寫入資料,並在資料處理完成後呼叫callback。在確認了 chunk 後,如果內部緩衝區的大小小於建立流時設定的 highWaterMark 閾值,函式將返回 true 。 如果返回值為 false (即佇列池已經裝滿),應該停止向流中寫入資料,直到 'drain' 事件被觸發。
構造一個可寫流需要重寫_write方法
const Writable = require('stream').writable
class MyWritableStream extends Writable{
constructor(options){
super(options)
}
_write(chunk, encoding, callback){
console.log(chunk)
}
}
複製程式碼
一個寫入資料10000次的demo,其中可以加深對write方法和drain方法的認識
function writeOneMillionTimes(writer, data, encoding, callback) {
let i = 10000;
write();
function write() {
let ok = true;
while(i-- > 0 && ok) {
// 寫入結束時回撥
if(i===0){
writer.write(data, encoding, callback)//當最後一次寫入資料即將結束時,再呼叫callback
}else{
ok = writer.write(data, encoding)//寫資料還沒有結束,不能呼叫callback
}
}
if (i > 0) {
// 這裡提前停下了,'drain' 事件觸發後才可以繼續寫入
console.log('drain', i);
writer.once('drain', write);
}
}
}
const Writable = require('stream').Writable;
class MyWritableStream extends Writable{
constructor(options){
super(options)
}
_write(chunk, encoding, callback){
setTimeout(()=>{
callback(null)
},0)
}
}
let writer = new MyWritableStream()
writeOneMillionTimes(writer, 'simple', 'utf8', () => {
console.log('end');
});
複製程式碼
輸出是
drain 7268
drain 4536
drain 1804
end
複製程式碼
輸出結果說明程式遇到了三次「背壓」,如果我們沒有在上面繫結 writer.once('drain'),那麼最後的結果就是 Stream 將第一次獲取的資料消耗完就結束了程式,即只輸出drain 7268
pipe
readable.pipe(writable);
複製程式碼
readable 通過 pipe(管道)傳輸給 writable
Readable.prototype.pipe = function(writable, options) {
this.on('data', (chunk) => {
let ok = writable.write(chunk);
if(!ok) this.pause();// 背壓,暫停
});
writable.on('drain', () => {
// 恢復
this.resume();
});
// 告訴 writable 有流要匯入
writable.emit('pipe', this);
// 支援鏈式呼叫
return writable;
};
複製程式碼
核心有5點:
- emit(pipe),通知寫入
- write(),新資料過來,寫入
- pause(),消費者消費速度慢,暫停寫入
- resume(),消費者完成消費,繼續寫入
- return writable,支援鏈式呼叫