什麼時候使用流
當處理大檔案讀取、壓縮、歸檔、媒體檔案和巨大的日誌檔案時,資料都會被讀入記憶體,記憶體很快就會被使用完,這將會給程式帶來很大的問題。
如果在進行這些操作的時候,配合一個合適的緩衝區,一次讀取固定的長度,就會使用更少的記憶體,這就是流式的API。
Stream 可用的API 類
- Readable - 可讀的流 (例如
fs.createReadStream()
). - Writable - 可寫的流 (例如
fs.createWriteStream()
). - Duplex - 可讀寫的流 (例如
net.Socket
). - Transform - 在讀寫過程中可以修改和變換資料的 Duplex 流 (例如
zlib.createDeflate()
).
一、使用內建的流來實現靜態web伺服器
Node 的檔案系統和網路操作的核心模組 fs 和 net 都提供了流介面。使用流來處理 I/O 問題會相當簡單。
使用Node 核心模組,實現簡單的靜態伺服器:
const http = require('http');
const fs = require('fs');
const server = http.createServer(function(req,res){
fs.readFile(__dirname + '/index.html', function(err,data){
if(err){
res.statusCode = 500;
res.end(String(err))
return;
}
res.end(data)
})
})
server.listen(3000)
複製程式碼
雖然上述程式碼是用來非阻塞的 readFile, 一旦讀取的檔案非常大或非常多的檔案訪問,將會很快耗完記憶體,因此需要使用fs.createReadStream 方法進行改進:
const http = require('http');
const fs = require('fs');
const server = http.createServer(function(req,res){
// 資料通過流的方式,從html 檔案輸出到 http 的請求響應
fs.createReadStream(__dirname + '/index.html').pipe(res);
})
server.listen(3000)
複製程式碼
上述程式碼提供一個緩衝器來傳送到客戶端,如果客戶端連線較慢,網路流將會傳送訊號暫停I/O資源直到客戶端準備好接受更多資料。
使用流實現一個簡單的靜態檔案伺服器:
const http = require('http');
const fs = require('fs');
const server = http.createServer(function(req,res){
let filename = req.url
if(filename === '/'){
filename = '/index.html'
}
fs.createReadStream(__dirname + filename ).pipe(res);
})
server.listen(3000)
複製程式碼
使用gzip壓縮的靜態伺服器
const http = require('http');
const fs = require('fs');
const zlib = require('zlib')
const server = http.createServer(function(req,res){
res.writeHead(200, { 'content-encoding': 'gzip' })
fs.createReadStream(__dirname + '/index.html' )
.pipe(zlib.createGzip())
.pipe(res);
})
server.listen(3000)
複製程式碼
二、Readable 可讀流
stream 繼承自 events, 因此有事件中的 on、emit 方法。
1、事件
- readable --- 在可以從流中讀取資料塊的時候發出。
- data --- 資料正在傳遞時,觸發該事件(以chunk資料塊為物件)
- end --- 當資料讀取結束時觸發
- close --- 當底層資源(如檔案) 關閉時觸發。
- error --- 在接收資料中出錯時觸發。
2、方法
- read([size]) --- 從流中讀資料.資料可以是String、Buffer、null(下面程式碼會有),當指定size,那麼只讀僅限於那個位元組數
- setEncoding(encoding) --- 設定read()請求讀取返回String時使用的編碼
- pause() --- 暫停從該物件發出的data事件
- resume() --- 恢復從該物件發出的data事件
- pipe(destination,[options]) --- 把讀取的資料塊傳遞給一個 Writable 的目的地。當資料傳送完畢,觸發'end'事件時,會同時觸發目標(可寫流)的'end'事件,導致目標不再可寫
- unpipe([destination]) ---- 從Writale目的地斷開這一物件。
繼承可讀流的注意事項:
- readable.read 方法會返回的資料塊,都是由 readable.push 方法加入到內部可讀佇列中的。
- 所有繼承可讀流的子類,必須實現
readable._read()
方法去獲得底層的資料資源,並僅能由Readable物件內部方法呼叫,不應該被使用者程式直接呼叫。在readable._read()
實現中,只有還有資料可讀取,就應該呼叫readable.push(chunk)
方法把資料加入到內部的可讀佇列,由readable.read
方法讀取供應用程式使用。 - 一旦 例項監聽了 data 事件,則 readable._read() 的返回值將丟失。
例項:實現一個可讀流
const { Readable } = require('stream');
const util = require('util');
util.inherits(MyReadStream, Readable)
function MyReadStream(arr){
this.source = arr;
Readable.call(this);
}
MyReadStream.prototype._read = function(){
if(this.source.length){
this.push(this.source[0])
this.source.splice(0,1)
}else{
this.push(null)
}
}
let myStream = new MyReadStream(['php','js','java'])
myStream.on('readable',function(){
let output_buf = myStream.read();
console.log(output_buf,'output') // null
})
myStream.on('data',function(res){
console.log(res.toString(),'data')
})
myStream.on('end',function(){
console.log('end')
})
複製程式碼
在上述程式碼中,在 readable
事件中呼叫 read
方法,來讀取一段字串,並監聽 data
事件來輸出讀取的資料。
三、Writable 可寫流
Writable 流介面是對寫入資料的目標的抽象。
1、方法
write(chunk,[encoding],[callback]) --- 將資料寫入流。chunk(資料塊)中包含要寫入的資料,encoding指定字串的編碼,callback指定當資料已經完全重新整理時執行的一個回撥函式。如果成功寫入,write()返回true.
end([chunk],[encoding],[callback]) ---與write()相同,它把Writable物件設為不再接受資料的狀態,併傳送finish事件。
2、事件
drain -- 在write()呼叫返回false後,當準備好開始寫更多資料時,發出此事件通知監視器。
finish -- 當end()在Writable物件上呼叫,所以資料被重新整理,並不會有更多的資料被接受時觸發
pipe -- 當pipe()方法在Readable流上呼叫,已新增此writable為目的地時發出
unpipe -- 當unpipe()方法被呼叫,以刪除Writable為目的地時發出。
繼承可寫流的注意事項:
writable.write()
方法向流中寫入資料,並在資料處理完成後呼叫callback
。如果有錯誤發生,callback
不一定以這個錯誤作為第一個引數並被呼叫。要確保可靠地檢測到寫入錯誤,應該監聽'error'
事件。- 所有可寫流實現必須提供一個
writable._write()
方法將資料傳送到底層資源。
例項:實現一個標準輸入到標準輸出的可寫流,並判斷如果輸入的字元包含a, 則報錯並退出
const { Writable } = require('stream');
const util = require('util');
util.inherits(MyWriteStream, Writable)
function MyWriteStream(options){
Writable.call(this, options);
}
MyWriteStream.prototype._write = function(chunk, encoding, callback){
if(chunk.toString().indexOf('a') > -1){
process.stdout.write("新寫入的:"+ chunk)
callback(null)
}else{
callback(new Error('no a'))
}
}
let myStream = new MyWriteStream();
myStream.write('abc\n')
process.stdin.pipe(myStream)複製程式碼
注意:必須呼叫callback
方法來表示寫入成功或失敗。如果出現錯誤,callback
第一個引數必須是Error
物件,成功時引數為null
。
四、雙工流 -- 可讀可寫的流
繼承 stream.Duplex
即可實現一個雙工流
示例:實現一個改變標準輸入內容的顏色,再從標準輸出列印出來
const { Duplex } = require('stream');
const util = require('util');
util.inherits(MyDuplexStream, Duplex)
function MyDuplexStream(options){
Duplex.call(this, options);
this.wating = false;
}
MyDuplexStream.prototype._write = function(chunk, encoding, callback){
this.wating = false;
// 把資料推動到內部佇列
this.push('\u001b[32m' + chunk + '\u001b[39m');
callback()
}
MyDuplexStream.prototype._read = function(chunk, encoding, callback){
if(!this.wating){
// 在等待資料時展示一個提示
this.push('等待輸入> ')
this.wating = true;
}
}
let myStream = new MyDuplexStream();
// 獲取標準輸入,用管道傳給雙工流,單後返回給標準輸出
process.stdin.pipe(myStream).pipe(process.stdout)
複製程式碼
五、轉換流
轉換流很像雙工流,也實現了 Readable 和 Writable 的介面。不同的是,轉換流是轉換資料,還是用 _transform 實現的。這個方法有三個引數,thunk資料塊、encoding編碼、callback回撥(很像_write), 當資料轉換完成後執行回撥,允許轉換流非同步解析資料。
示例待補。