Stream 是Node.js中最重要的組件和模式之一,在構建較復雜的系統時,通常將其拆解為功能獨立的若干部分。這些部分的接口遵循一定的規范,通過某種方式相連,以共同完成較復雜的任務。nodejs的核心模塊,基本上都是stream的的實例,比如process.stdout、http.clientRequest
什么是流?
- 流是一組有序的,有起點和終點的字節數據傳輸手段
- 它不關心文件的整體內容,只關注是否從文件中讀到了數據,以及讀到數據之后的處理
- 流是一個抽象接口,被 Node 中的很多對象所實現。比如HTTP 服務器request和response對象都是流。
簡單的理解,流就是將大塊的東西,分小塊依次處理。就像你需要從水龍頭上接一杯水,那么當你擰開水龍頭,水管就會一點點的源源不斷的流出來給你。
那么流這種方式在程序當中又有什么優勢呢?先看如下代碼:
let fs = require('fs');
fs.readFile('./1.txt', 'utf8', function(err, data){
// 1.txt 已經讀取完成
console.log(data);
fs.writeFile('/2.txt', data); // 將內容寫入2.txt中
});
以上兩個方法是實現的功能是將1.txt文件讀取到內存當中,再將它寫入到2.txt文件中。但是如果文件過大就會出現問題了,內存容易爆掉。那么這里比較合適的方式應該是讀寫交替進行,也就是使用流的方式讀寫文件,這樣不管文件有多大,都不會一下子耗盡內存,可以安全的執行完。
如下:
let fs = require('fs');
let readStream = fs.createReadStream('./1.txt');
let writeStream = fs.createWriteStream('./2.txt');
readStream.on('data', function(chunk) { // 當有數據流出時,寫入數據,chunk的類型為Buffer
writeStream.write(chunk);
});
readStream.on('end', function() { // 當沒有數據時,關閉數據流
writeStream.end();
});
流的四種類型
在nodejs中,有四種stream類型:
- Readable - 可讀的流,用來讀取數據 (例如 fs.createReadStream()).
- Writable - 可寫的流,用來寫數據 (例如 fs.createWriteStream()).
- Duplex - 可讀寫的流(雙工流),可讀+可寫 (例如 net.Socket).
- Transform - 轉換流,在讀寫過程中可以修改和變換數據的 Duplex 流 (比如 zlib.createDeflate()(數據壓縮/解壓)).
1、可讀流(Readable streams)
nodejs中常見的可讀流有:fs.createReadStream()、http.IncomingRequest、process.stdin
可讀流createReadStream用法如下:
// 創建可讀流
let rs = fs.createReadStream(path,[options]);
// 設置編碼格式
rs.setEncoding('utf8');
// 監聽open事件,打開文件時觸發
rs.on('open', function () {
console.log(err);
});
//流切換到流動模式,數據會被盡可能快的讀出
rs.on("data",function(data){
console.log(data); //讀取到的數據
});
// 該事件會在讀完數據后被觸發
rs.on("end",function(data){
console.log("數據已經讀取完畢");
});
//如果讀取文件出錯了,會觸發error事件
rs.on("error",function(err){
console.log("something is wrong during processing");
})
//文件關閉觸發
rs.on('close', function () {
console.log('文件關閉');
});
1、path讀取文件的路徑
2、options
- flags打開文件要做的操作,默認為'r'
- encoding默認為null
- start開始讀取的索引位置
- end結束讀取的索引位置(包括結束位置)
- highWaterMark讀取緩存區默認的大小64kb
如果指定utf8編碼highWaterMark要大于3個字節
2、可寫流(Writable streams)
可寫流createReadStream
實現了stream.Readable接口的對象,將對象數據讀取為流數據,當監聽data事件后,開始發射數據
let fs = require("fs");
// 創建一個可以寫入的流,寫入到文件 1.txt 中
let ws= fs.createWriteStream('1.txt');
let data = '寫入流數據';
// 使用 utf8 編碼寫入數據
ws.write(data,'UTF8');
// 表明接下來沒有數據要被寫入 Writable 通過傳入可選的 chunk 和 encoding 參數,可以在關閉流之前再寫入一段數據 如果傳入了可選的 callback 函數,它將作為 'finish' 事件的回調函數
ws.end("最后寫入的數據","utf8",function(){
console.log(" 我是'finish' 事件的回調函數")
});
// 在調用了 stream.end() 方法,且緩沖區數據都已經傳給底層系統之后, 'finish' 事件將被觸發。
ws.on('finish', function() {
console.log("寫入完成。");
});
// 寫入時發生錯誤觸發
ws.on('error', function(err){
console.log(err.stack);
});
// 創建可寫流
let ws = fs.createWriteStream(path,[options]);
1、path讀取文件的路徑
2、options
- flags打開文件要做的操作,默認為'w'
- encoding默認為utf8
- highWaterMark寫入緩存區的默認大小16kb
管道流pipe用法
將數據的滯留量限制到一個可接受的水平,以使得不同速度的來源和目標不會淹沒可用內存。
linux經典的管道的概念,前者的輸出是后者的輸入
pipe是一種最簡單直接的方法連接兩個stream,內部實現了數據傳遞的整個過程,在開發的時候不需要關注內部數據的流動
用法:
var from = fs.createReadStream('./1.txt');
var to = fs.createWriteStream('./2.txt');
from.pipe(to); // 就是從1.txt中讀一點就往2.txt中寫一點
3、雙工流(Duplex streams)
Duplex實際上就是繼承了Readable和Writable。
有了雙工流,我們可以在同一個對象上同時實現可讀和可寫,就好像同時繼承這兩個接口。 重要的是雙工流的可讀性和可寫性操作完全獨立于彼此。這僅僅是將兩個特性組合成一個對象
const {Duplex} = require('stream');
const inoutStream = new Duplex({
write(chunk, encoding, callback) {
console.log(chunk.toString());
callback();
},
read(size) {
this.push((++this.index)+'');
if (this.index > 3) {
this.push(null);
}
}
});
inoutStream.index = 0;
process.stdin.pipe(inoutStream).pipe(process.stdout);
最常見的Duplex stream應該就是net.Socket
實例了。
4、轉換流(Transform streams)
轉換流的輸出是從輸入中計算出來的,Transform stream是Duplex stream的特例。也就是說,Transform stream也同時可讀可寫,它可以用來修改或轉換數據。然它跟Duplex stream的區別在于,Transform stream的輸出與輸入是存在相關性的。你可以認為轉換流就是一個函數,這個函數的輸入是一個可寫流,輸出是一個可讀流。
對于轉換流,我們不必實現read或write的方法,我們只需要實現一個transform方法,將兩者結合起來。它有write方法的意思,我們也可以用它來push數據。
例如:希望將輸入的內容轉化成大寫在輸出出來
const {Transform} = require('stream');
const upperCase = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase()); // 將輸入的內容放入到可讀流中
callback();
}
});
// 希望將輸入的內容轉化成大寫在輸出出來
process.stdin.pipe(upperCase).pipe(process.stdout);
常見的Transform stream包括zlib、crypto,這里有個簡單例子:文件的gzip壓縮。
let fs = require('fs');
let zlib = require('zlib');
let gzip = zlib.createGzip();
// 將1.txt文件的內容,打包壓縮成compress.txt.gz
let inFile = fs.createReadStream('./file/1.txt');
let outGz = fs.createWriteStream('./file/compress.txt.gz');
inFile .pipe(gzip).pipe(outGz);