記得剛剛開始學(xué)Node.js時(shí)自己嘗試著寫了一個(gè)簡(jiǎn)單的http服務(wù)器遥巴,跟以前接觸過的php相比感覺更自由姻锁,編起碼來也更爽了肮雨。但是某天發(fā)現(xiàn)稍微一個(gè)很小的錯(cuò)誤就導(dǎo)致整個(gè)http進(jìn)程掛掉了体斩,頓時(shí)有種不靠譜的感覺啊梭稚,跟php比起來感覺Node.js容錯(cuò)能力確實(shí)弱了很多,起碼一個(gè)php文件出錯(cuò)也不會(huì)導(dǎo)致所有的服務(wù)都掛掉絮吵。
后來接觸到Node.js web開發(fā)框架后感覺也不是那么輕易就讓整個(gè)進(jìn)程都掛掉的哨毁,于是便想研究下Node.js究竟是如何來處理各種異常從而避免整個(gè)進(jìn)程掛掉的。
當(dāng)我們的程序運(yùn)行在Node.js進(jìn)程里不小心拋出一個(gè)異常時(shí)便會(huì)觸發(fā)process對(duì)象的_fatalException方法源武,并將異常對(duì)象err傳進(jìn)去扼褪,_fatalException方法主要做以下一些處理:
當(dāng)process對(duì)象上有綁定domain時(shí)便調(diào)用domain對(duì)象的_errorHandler方法來處理,
if (process.domain && process.domain._errorHandler)
caught = process.domain._errorHandler(er)
_errorHandler會(huì)返回一個(gè)布爾值來通知當(dāng)前程序domain是否有對(duì)該異常進(jìn)行處理粱栖,如果domain沒有做處理话浇,此時(shí)process對(duì)象便會(huì)觸發(fā)一個(gè)綁定到process上的uncaughtException事件來處理該異常,并且同樣會(huì)返回一個(gè)布爾值來通知當(dāng)前程序是否有對(duì)異常進(jìn)行處理闹究。
if (!caught)
caught = process.emit('uncaughtException', er);
走到這個(gè)地步時(shí)如果異常還沒被正常的處理那么此時(shí)process就有點(diǎn)不高興了幔崖,既然你們都不處理那我就準(zhǔn)備讓你們?nèi)繏斓舭桑?確實(shí)太狠了點(diǎn)啊),這個(gè)時(shí)候悲劇即將發(fā)生渣淤。赏寇。。
if (!caught) {
try {
if (!process._exiting) {
process._exiting = true;
process.emit('exit', 1);
}
} catch (er) {
}
}
如果異常都被妥妥的處理掉了那么Node.js進(jìn)程便會(huì)處理當(dāng)前事件的收尾的工作价认,比如調(diào)用process.nextTick傳進(jìn)去的回調(diào)函數(shù)在這個(gè)時(shí)候就準(zhǔn)備被調(diào)用了嗅定,然后繼續(xù)執(zhí)行事件隊(duì)列里的下一個(gè)事件
t = setImmediate(process._tickCallback)
總結(jié)下來Node.js中異常處理流程大概就是這樣的:
![nodejs](http://img3.tbcdn.cn/L1/461/1/b_20019_1398447516_496377847.png)
這整個(gè)過程中有個(gè)很重要的處理環(huán)節(jié)沒有加上去,那就是上面提到的domain對(duì)象用踩。
首先簡(jiǎn)單介紹下domain對(duì)象的使用場(chǎng)景以及基本使用方法:
當(dāng)我們開啟一個(gè)Node.js的http服務(wù)器時(shí)不可避免的會(huì)出現(xiàn)各種我們沒有預(yù)期到的異常渠退,并且我們預(yù)先寫好的try catch也無法捕捉。這時(shí)最關(guān)鍵的是如何保證整個(gè)服務(wù)進(jìn)程不會(huì)掛掉脐彩,并且能夠很友好的反饋給瀏覽器端的用戶碎乃。盡管process對(duì)象提供了一個(gè)uncaughtException事件方法讓我們可以處理異常并且保證當(dāng)前的服務(wù)進(jìn)程不會(huì)掛掉,但由于丟失了當(dāng)前的上下文惠奸,說得直接點(diǎn)就是丟失了response對(duì)象很難向用戶及時(shí)并且友好的輸出錯(cuò)誤提示梅誓,此時(shí)便陷入了用戶會(huì)一直傻傻的等待服務(wù)器超時(shí)(早就關(guān)閉網(wǎng)站了)的尷尬場(chǎng)景。
有了domain模塊我們便可以很方便的處理上面描述的場(chǎng)景了,剛剛開始接觸domain這個(gè)模塊時(shí)真不知道是個(gè)啥東西梗掰,名字都叫的怪怪的删豺。后來去翻了先官網(wǎng)上有關(guān)domain的文檔才知道這貨到底有啥作用,我們就依照官網(wǎng)的示例來說明domain如何處理上述場(chǎng)景:
http.createServer(function(req, res) {
var reqd = domain.create();
reqd.add(req);
reqd.add(res);
reqd.on('error', function(er) {
console.error('Error', er, req.url);
try {
res.writeHead(500);
res.end('Error occurred, sorry.');
} catch (er) {
res.end('Error sending 500', er.message, req.url);
}
});
當(dāng)res對(duì)象調(diào)用各種方法產(chǎn)生異常時(shí)愧怜,之前創(chuàng)建好的domain對(duì)象reqd便會(huì)收到通知,從而觸發(fā)我們預(yù)先設(shè)置好的處理方法來即使并且友好的輸出給用戶妈拌,避免超時(shí)這種糟糕的用戶體驗(yàn)拥坛!對(duì)于domain對(duì)象其他的方法大家可以直接翻看Node.js官網(wǎng)文檔的介紹,我這里就不啰嗦了~
下面我們著重的來研究下domain對(duì)象為何如此神奇尘分?
當(dāng)我們r(jià)equire('domain')對(duì)象時(shí)便對(duì)event模塊的EventEmitter對(duì)象產(chǎn)生了影響
EventEmitter.usingDomains = true;
緊跟著對(duì)process的domain屬性進(jìn)行了覆蓋
Object.defineProperty(process, 'domain', {
enumerable: true,
get: function() {
return _domain[0];
},
set: function(arg) {
return _domain[0] = arg;
}
});
domain模塊本身維護(hù)著一個(gè)存放domain對(duì)象的數(shù)組 _domain猜惋,再接著就是告訴process對(duì)象要使用到domain了
process._setupDomainUse(_domain, _domain_flag);
調(diào)用這個(gè)方法后影響到的地方可不少,之前我們說過Node.js每個(gè)事件都會(huì)調(diào)用一下_tickCallback來處理之前調(diào)用process.nextTick保存到事件隊(duì)列里的回調(diào)函數(shù)培愁,現(xiàn)在Node.js不調(diào)用了這個(gè)了著摔,換成了調(diào)用_tickDomainCallback方法來代替_tickCallback。繼續(xù)我們的domain模塊定续,當(dāng)創(chuàng)建一個(gè)新的domain對(duì)象時(shí)便初始化了的它的members屬性來存放該domain要守護(hù)的對(duì)象谍咆,對(duì)照著上述的代碼
var reqd = domain.create();
此時(shí)reqd.members=[], 于是我們調(diào)用add方法將req已經(jīng)res對(duì)象都添加到domain中,由domain來幫他們處理各種錯(cuò)誤私股。
reqd.add(req);
reqd.add(res);
接著告訴domain當(dāng)req或者res操作出異常時(shí)應(yīng)該如何處理
reqd.on('error', function(er) {
console.error('Error', er, req.url);
try {
res.writeHead(500);
res.end('Error occurred, sorry.');
} catch (er) {
console.error('Error sending 500', er, req.url);
}
});
其實(shí)就上面那樣還是沒法捕獲到異常摹察,甚至都無法響應(yīng),因?yàn)槲覀冞€沒調(diào)用res.write或者res.end方法來向用戶輸出內(nèi)容倡鲸,就算我們加上
reqd.add(req);
reqd.add(res);
res.test('end');
依然無法像我們預(yù)期想象的那樣進(jìn)入異常處理回調(diào)方法里供嚎,別忘了將可能發(fā)生異常的代碼放入domain.run中來執(zhí)行,就像這樣的:
var domain=require('domain'),
http=require('http');
http.createServer(function(req, res) {
var reqd = domain.create();
reqd.add(req);
reqd.add(res);
reqd.on('error', function(er) {
console.error('Error', er, req.url);
try {
res.writeHead(500);
res.end('Error occurred, sorry.'+ er.message);
} catch (er) {
console.error('Error sending 500', er, req.url);
}
});
reqd.run(function(){
res.test();
res.end('end');
});
}).listen(1337);
此時(shí)一切都已就緒峭状,萬事俱備只欠東風(fēng)了克滴,就等著各種異常來臨了。ok, 此時(shí)由于res的某個(gè)操作(比如調(diào)用不存在的test方法)導(dǎo)致了一個(gè)異常的產(chǎn)生优床。根據(jù)最開始描述的處理流程劝赔,這個(gè)異常會(huì)被Node.js進(jìn)程傳到process._fatalException中進(jìn)行處理,如果process上綁定有domain對(duì)象則會(huì)調(diào)用domain的_errorHandler方法來處理異常胆敞,那_errorHandler究竟如火如荼處理異常的呢望忆?在討論這個(gè)問題之前我們先回到上面的reqd.run方法中。調(diào)用domain對(duì)象的run方法時(shí)會(huì)先進(jìn)入enter里做如下處理:
exports.active = process.domain = this;
stack.push(this);
_domain_flag[0] = stack.length;
將當(dāng)期的domain對(duì)象設(shè)置成active并且綁定到process上,stack是一個(gè)保存domain對(duì)象的堆棧竿秆,用于domain嵌套使用的情況启摄,其中_domain_flag是一個(gè)用于js與c++進(jìn)行通信的對(duì)象。緊接著再執(zhí)行我們的業(yè)務(wù)代碼比如res.test()操作幽钢,此時(shí)便拋出了一個(gè)方法不存在的異常歉备。由于進(jìn)入enter方法后我們把當(dāng)前domain對(duì)象綁定到了process上,所以異常就交給domain的_errorHandler方法來處理了匪燕,回到之前的問題蕾羊,_errorHandler是如何處理異常的喧笔?
首先嘗試著讓之前綁定到domain上的error事件回調(diào)函數(shù)來處理該異常并清空當(dāng)前process的domain屬性,之所以所嘗試是因?yàn)榛卣{(diào)函數(shù)里可能又會(huì)拋出新的異常龟再,當(dāng)然了理想情況就是回調(diào)函數(shù)能夠很好的處理掉異常并且不拋出新的異常书闸,此時(shí)整個(gè)異常處理流程完美結(jié)束。如果有新的異常拋出利凑,先將對(duì)stack堆棧進(jìn)行出棧操作剔除已經(jīng)使用過的當(dāng)期domain對(duì)象浆劲,然后再看看棧里邊是否還存在domain對(duì)象,有的話就用棧訂上的domain又回到process._fatalException里繼續(xù)處理剛剛回調(diào)函數(shù)拋出的新異常哀澈。stack為空的話此時(shí)已經(jīng)沒有domain對(duì)象可以來處理異常牌借,至次本次異常處理以失敗結(jié)束然后繼續(xù)交給最開始講到的uncaughtException事件來處理。當(dāng)然了調(diào)用domain.run時(shí)并沒有拋出異常割按,那么domain也需要進(jìn)行出棧操作膨报,來抵消enter方法時(shí)的入棧操作以保持stack堆棧的平衡。
其實(shí)上面的reqd.add(res)和reqd.add(req)是可以不要的适荣,為什么可以不要呢现柠?在什么情況下需要什么情況下又不需要?ok弛矛,我們?cè)偕钊胙芯恳幌耫omain.add是如何工作的晒旅。官網(wǎng)中文檔有介紹domain.add接收emitter類型的參數(shù),也就是EventEmitter | Timer emitter or timer汪诉。為什么要這樣呢废恋,看下面的一段代碼
var EventEmitter = require('events').EventEmitter;
var e = new EventEmitter();
var timer = setTimeout(function () {
e.emit('data');
}, 1000);
function next() {
e.once('data', function () {
throw new Error('something wrong here');
});
}
var d = domain.create();
d.on('error', function () {
console.log('cache by domain');
});
d.run(next);
此時(shí)next函數(shù)里邊綁定到e對(duì)象上的data事件被觸發(fā)時(shí)domain對(duì)象是無法處理的,原因很明顯扒寄,data回調(diào)函數(shù)的運(yùn)行已經(jīng)處理domain.run方法之外鱼鼓。那我就要這個(gè)domain來處理錯(cuò)誤怎么辦呢,此時(shí)domain.add方法就派上用場(chǎng)了该编,我們只需要簡(jiǎn)單的調(diào)用一下d.add(e)或者d.add(timer)就可以解決這個(gè)問題迄本。domain.add方法為什么可以解決又是如何解決的呢?繼續(xù)往下看课竣。
當(dāng)調(diào)用domain.add(e)時(shí)嘉赎,如果上綁定有domain先移除再綁定新的domain,并將e對(duì)象加入新domain的members中于樟,從而保持著對(duì)e對(duì)象的引用公条。不管是timer對(duì)象還是event對(duì)象在觸發(fā)回調(diào)函數(shù)時(shí)都會(huì)先判斷是否有綁定domain對(duì)象
if (this.domain && this !== process)
this.domain.enter();
callback();
if (this.domain && this !== process)
this.domain.exit();
這些操作和domain.run方法相似,先執(zhí)行enter將domain對(duì)象綁定到process上迂曲,然后再執(zhí)行回調(diào)當(dāng)有異常發(fā)生時(shí)process會(huì)將異常傳到domain上處理靶橱,最后再調(diào)用exit方法將該domain移出stack堆棧。所以上面的代碼中必須得調(diào)用下d.add(e)或者d.add(timer)才會(huì)讓domain對(duì)象捕獲到回調(diào)中的異常。
整個(gè)Node.js異常處理就講到這里了关霸,其實(shí)在process._fatalException方法中調(diào)用domain來處理異常之前還進(jìn)行了一個(gè)異常處理操作
var caught = _errorHandler(er);
這個(gè)處理主要涉及到Node.js的異步隊(duì)列AsyncQueue在這里暫不做討論传黄,以后再做進(jìn)一步的研究,文章有點(diǎn)長(zhǎng)感謝能堅(jiān)持看到結(jié)尾的同學(xué)們队寇,不要吝嗇你們的贊哦~