Nodejs 實踐 -- Stream 流

蜉蝣生物發表於2018-03-19

什麼時候使用流

當處理大檔案讀取、壓縮、歸檔、媒體檔案和巨大的日誌檔案時,資料都會被讀入記憶體,記憶體很快就會被使用完,這將會給程式帶來很大的問題。

如果在進行這些操作的時候,配合一個合適的緩衝區,一次讀取固定的長度,就會使用更少的記憶體,這就是流式的API。

Stream 可用的API 類


一、使用內建的流來實現靜態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), 當資料轉換完成後執行回撥,允許轉換流非同步解析資料。

示例待補。



相關文章