深入node stream

pagecao發表於2018-10-15

要說node最令人印象深刻的模組,第一肯定是events,而第二肯定就是stream模組了,今天這篇文章就來跟大家聊聊stream的實現,主要是以stream_readable為例來講解,並以fs模組中的createReadStream為例來說明node內部是如何使用stream的。

Stream.Readable

從lib/stream.js中我們可以看到node的stream主要分為了四種,讀流,寫流以及讀寫兼具的全雙工流,還有可以修改和變換資料的 Transform流 ,不過讀流和寫流的實現邏輯和思路在大架構上是類似的,至於雙全工流我們可以通過檔案lib/_stream_duplex.js看到他其實就是通過:

function Duplex(options) {
	...
	Readable.call(this, options);
	Writable.call(this, options);
	...
}
複製程式碼

使函式擁有了讀流和寫流所有的方法和屬性而成,而Transform流其實是 Duplex流重寫了_read和_write方法的派生流,所以本文通過介紹讀流的實現方式來理解整個stream的實現方式。

首先,可以從lib/stream.js中發現stream的真實位置來源於lib/internal/streams/legacy.js,從這個檔案中我們可以看到stream也繼承於events,並且這裡還提供了一個pipe方法,這個方法我們後面講例子的時候再講。而從lib/_stream_readable.js檔案中可以看出,雖然提供了能組成整套流程的方法,但是最核心的Readable.prototype._read中卻是這樣:

this.emit('error', new Error('_read() is not implemented'));
複製程式碼

會直接爆出錯誤,為什麼呢?註釋中已經給名了原因,該函式是一個抽象方法,是不能被直接呼叫的,只能通過其他實際的讀流來繼承該類再重寫這個方法後使用,可以將Stream.Readable理解為一個抽象類,所以我們直接通過一個fs中的使用用例來說明。

fs.createReadStream

下面我們直接從lib/fs.js的fs.createReadStream方法來看Stream.Readable,這個方法中返回了一個ReadStream例項,而ReadStream是通過util.inherits(ReadStream, Readable);關聯上了Stream.Readable,在 new ReadStream的時候會通過new ReadableState建立大量控制流操作的屬性,後面我們都會講到。現在讓我們來看一個最簡單的例子:

var stream = fs.createReadStream('sample.txt');
stream.on('data', (chunk) => {
	console.log('讀取檔案資料:', chunk);
});
複製程式碼

這個例子中,在建立了讀流以後,就只需要註冊events事件,在有資料的時候來觸發就行了,這是怎麼實現的呢?原來在Stream.Readable中重寫了events的on方法:

Readable.prototype.on = function(ev, fn) {
	const res = Stream.prototype.on.call(this, ev, fn);

	if (ev === 'data') {
		if (this._readableState.flowing !== false)
			this.resume();
	} else if (ev === 'readable') {
		const state = this._readableState;
		if (!state.endEmitted && !state.readableListening) {
			state.readableListening = state.needReadable = true;
			state.emittedReadable = false;
			if (!state.reading) {
				process.nextTick(nReadingNextTick, this);
			} else if (state.length) {
				emitReadable(this);
			}
		}
	}

	return res;
};
複製程式碼

其中當註冊data和readable事件的時候,我們先以例子中data事件舉例往下分析,這裡呼叫了ReadStream.prototype.resume函式,這個函式會在最後通過process.nextTick呼叫函式resume_,所以我們可以直接來看看這個函式的程式碼:

if (!state.reading) {
	debug('resume read 0');
	stream.read(0);
}

state.resumeScheduled = false;
state.awaitDrain = 0;
stream.emit('resume');
flow(stream);
if (state.flowing && !state.reading)
	stream.read(0);
複製程式碼

其中的state就是ReadableState的例項,當非同步讀取未完成時reading屬性為true,不過我們的程式碼剛開始為初始值false所以這裡會直接呼叫stream.read(0);。現在讓我們來看一下Readable.prototype.read的程式碼,這個方法在這次呼叫時,會直接執行以下的程式碼:

....//忽略做判斷以及不執行的程式碼
state.reading = true;
state.sync = true;
if (state.length === 0)
  state.needReadable = true;
this._read(state.highWaterMark);
state.sync = false;
...//忽略做判斷以及不執行的程式碼
if (ret === null) {
	state.needReadable = true;
	n = 0;
} 
複製程式碼

這裡主要就是執行重寫了的ReadStream.prototype._read,這裡我們可以看到這個方法第一句程式碼:

if (typeof this.fd !== 'number') {
	return this.once('open', function() {
		this._read(n);
	});
}
複製程式碼

這是為了保證檔案已經被開啟了,讓我們回到ReadStream建構函式中,這裡有這樣一段程式碼:

if (typeof this.fd !== 'number')
	this.open();
複製程式碼

當我們只傳了檔案路徑的時候,會直接呼叫ReadStream.prototype.open這個函式實際就是使用了fs.open開啟檔案路徑,然後在成功回撥後通過:

self.emit('open', fd);
// start the flow of data.
self.read();
複製程式碼

觸發open事件,並呼叫read方法。瞭解了這個以後,我們接著回來看_read方法:

//分配buffer,校正讀取長度
...
//已經不能繼續讀取了
if (toRead <= 0)
	return this.push(null);
fs.read(this.fd, pool, pool.used, toRead, this.pos, (err, bytesRead) => {
//錯誤處理
	if(err){
	...
	}else{
		var b = null;
		if (bytesRead > 0) {
			this.bytesRead += bytesRead;
			b = thisPool.slice(start, start + bytesRead);
		}
		this.push(b);
	}
}
//記錄本次讀取後的檔案的偏移
if (this.pos !== undefined)
	this.pos += toRead;
複製程式碼

在第一次通過_read成功讀取了資料後會呼叫Readable.prototype.push,這個方法呼叫addChunk方法,在這個方法中有這樣一段程式碼:

if (state.flowing && state.length === 0 && !state.sync) {
	stream.emit('data', chunk);
	stream.read(0);
}
複製程式碼

state.flowing在之前說的ReadStream.prototype.resume中已經設定為true了,而state.length為初始值即為0,因為我們是非同步呼叫所以state.sync也為false,這個if語句成立後會直接觸發data事件,並將讀取的資料傳遞給回撥方法。然後通過stream.read(0);不斷的重複讀取,直到讀取不到資料時,在_read的方法中通過this.push(null);呼叫方法onEofChunk將讀操作停止掉。而如果以下面的例子會發生什麼不同的情況呢?

var stream = fs.createReadStream('sample.txt');
stream.on('readable', () => {
	console.log('檔案資料可讀');
});
複製程式碼

只註冊了readable事件的情況呢?我們回過頭去看之前Readable.prototype.on的方法中對readable事件的處理,通過分析可以瞭解到會呼叫process.nextTick方法執行函式nReadingNextTick,這個函式只有一句話self.read(0);,到了這裡讀取的邏輯跟之前的邏輯就基本一樣了,但是在Readable.prototype.push中發生了變化,因為state.flowing為false,所以就會執行程式碼:

state.length += state.objectMode ? 1 : chunk.length;
if (addToFront)
  state.buffer.unshift(chunk);
else
  state.buffer.push(chunk);

if (state.needReadable)
  emitReadable(stream);
複製程式碼

會將讀取到的buffer存到ReadableState的buffer屬性中,並通過emitReadable觸發readble事件的回撥。在執行完這些之後,函式也會通過process.nextTick註冊tick任務,執行如下程式碼:

var len = state.length;
while (!state.reading && !state.flowing && !state.ended &&
     state.length < state.highWaterMark) {
	stream.read(0);
	if (len === state.length)
  		break;
	else
  		len = state.length;
}
複製程式碼

可以看到當state.flowing為true的時候並不會執行,所以在上面我們並沒有介紹這個方法,這個方法會通過不斷觸發read一直執行到state中的buffer快取區快取的資料達到highWaterMark所設定的閾值或沒有讀到新資料為止。那假如在這個時候,我們再註冊data事件會怎麼樣呢?這個時候就會觸發我們之前說的resume_函式,而在其中之前說到的最開頭的呼叫stream.read(0)因為ReadableState.length超過了閾值,所以並不會執行_read方法,而是直接跳過,但是在後面的flow(stream)中通過flow(stream);呼叫執行程式碼while (state.flowing && stream.read() !== null);,而這裡的stream.read()會執行以下語句:

n = parseInt(n, 10);
...
n = howMuchToRead(n, state);
...
if (n > 0)
	ret = fromList(n, state);
...
if (ret !== null)
	this.emit('data', ret);
return ret;
複製程式碼

其中因為read呼叫沒傳入任何引數,所以parseInt的結果為NaN,從howMuchToRead中可以看到:

if (n !== n) {
	if (state.flowing && state.length)
  		return state.buffer.head.data.length;
	else
  		return state.length;
}
複製程式碼

會返回之前儲存在state中的buffer的size,然後通過fromList(n, state)從state儲存的buffer中取出資料返回,並觸發data事件的回撥。不過一般而言,如果選擇使用註冊readable事件,則是選擇使用手動的方式來讀取內容了。比如:

var stream = fs.createReadStream('sample.txt',{encoding:'utf8'});
stream.on('readable', () => {
	console.log('檔案資料為:' + stream.read() );
});
複製程式碼

總結

從上面的描述大家可以瞭解到流中因為屬性多,判斷多,不同的實現有不同的觸發機制所以顯得比較混亂,希望通過上文的講解能讓大家對node的stream的設計思路有所瞭解,本文篇幅有限所以其他比如寫流的實現和pipe的實現大家就可以自己去研究一下了,如果有興趣還能模仿fs的流生成過程,用node的stream派生出自己的stream來做一些嘗試,這裡給大家一個例子參考:

var stream = require('stream');
var util = require('util');
util.inherits(Coustomer, stream.Readable);
function Coustomer(options) {
    stream.Readable.call(this, options);
    this._str = "";
}
Coustomer.prototype._read = function() {
    if(this._str.length < 10){
        this._str += "a";
        this.push(this._str);
    }else{
        this.push(null);
    }
};
var coustoomer = new Coustomer();

coustoomer.on('data', function(data){
    console.log("讀到資料: " + data.toString());//no maybe
});
coustoomer.on('end', function(data){
    console.log("結束");
});
複製程式碼

相關文章