天天看點

Node.js之異常處理

   記得剛剛開始學node.js時自己嘗試着寫了一個簡單的http伺服器,跟以前接觸過的php相比感覺更自由,編起碼來也更爽了。但是某天發現稍微一個很小的錯誤就導緻整個http程序挂掉了,頓時有種不靠譜的感覺啊,跟php比起來感覺node.js容錯能力确實弱了很多,起碼一個php檔案出錯也不會導緻所有的服務都挂掉。

      後來接觸到node.js web開發架構後感覺也不是那麼輕易就讓整個程序都挂掉的,于是便想研究下node.js究竟是如何來處理各種異常進而避免整個程序挂掉的。

     當我們的程式運作在node.js程序裡不小心抛出一個異常時便會觸發process對象的_fatalexception方法,并将異常對象err傳進去,_fatalexception方法主要做以下一些處理:

當process對象上有綁定domain時便調用domain對象的_errorhandler方法來處理,

_errorhandler會傳回一個布爾值來通知目前程式domain是否有對該異常進行處理,如果domain沒有做處理,此時process對象便會觸發一個綁定到process上的uncaughtexception事件來處理該異常,并且同樣會傳回一個布爾值來通知目前程式是否有對異常進行處理。

走到這個地步時如果異常還沒被正常的處理那麼此時process就有點不高興了,既然你們都不處理那我就準備讓你們全部挂掉吧!(确實太狠了點啊),這個時候悲劇即将發生。。。

如果異常都被妥妥的處理掉了那麼node.js程序便會處理目前事件的收尾的工作,比如調用process.nexttick傳進去的回調函數在這個時候就準備被調用了,然後繼續執行事件隊列裡的下一個事件

總結下來node.js中異常處理流程大概就是這樣的:

Node.js之異常處理

這整個過程中有個很重要的處理環節沒有加上去,那就是上面提到的domain對象。

首先簡單介紹下domain對象的使用場景以及基本使用方法:

當我們開啟一個node.js的http伺服器時不可避免的會出現各種我們沒有預期到的異常,并且我們預先寫好的try catch也無法捕捉。這時最關鍵的是如何保證整個服務程序不會挂掉,并且能夠很友好的回報給浏覽器端的使用者。盡管process對象提供了一個uncaughtexception事件方法讓我們可以處理異常并且保證目前的服務程序不會挂掉,但由于丢失了目前的上下文,說得直接點就是丢失了response對象很難向使用者及時并且友好的輸出錯誤提示,此時便陷入了使用者會一直傻傻的等待伺服器逾時(早就關閉網站了)的尴尬場景。

有了domain子產品我們便可以很友善的處理上面描述的場景了,剛剛開始接觸domain這個子產品時真不知道是個啥東西,名字都叫的怪怪的。後來去翻了先官網上有關domain的文檔才知道這貨到底有啥作用,我們就依照官網的示例來說明domain如何處理上述場景:

當res對象調用各種方法産生異常時,之前建立好的domain對象reqd便會收到通知,進而觸發我們預先設定好的處理方法來即使并且友好的輸出給使用者,避免逾時這種糟糕的使用者體驗!對于domain對象其他的方法大家可以直接翻看node.js官網文檔的介紹,我這裡就不啰嗦了~

下面我們着重的來研究下domain對象為何如此神奇?

當我們require('domain')對象時便對event子產品的eventemitter對象産生了影響

緊跟着對process的domain屬性進行了覆寫

domain子產品本身維護着一個存放domain對象的數組 _domain,再接着就是告訴process對象要使用到domain了

調用這個方法後影響到的地方可不少,之前我們說過node.js每個事件都會調用一下_tickcallback來處理之前調用process.nexttick儲存到事件隊列裡的回調函數,現在node.js不調用了這個了,換成了調用_tickdomaincallback方法來代替_tickcallback。繼續我們的domain子產品,當建立一個新的domain對象時便初始化了的它的members屬性來存放該domain要守護的對象,對照着上述的代碼

此時reqd.members=[], 于是我們調用add方法将req已經res對象都添加到domain中,由domain來幫他們處理各種錯誤。

接着告訴domain當req或者res操作出異常時應該如何處理

其實就上面那樣還是沒法捕獲到異常,甚至都無法響應,因為我們還沒調用res.write或者res.end方法來向使用者輸出内容,就算我們加上

依然無法像我們預期想象的那樣進入異常處理回調方法裡,别忘了将可能發生異常的代碼放入domain.run中來執行,就像這樣的:

此時一切都已就緒,萬事俱備隻欠東風了,就等着各種異常來臨了。ok, 此時由于res的某個操作(比如調用不存在的test方法)導緻了一個異常的産生。根據最開始描述的處理流程,這個異常會被node.js程序傳到process._fatalexception中進行處理,如果process上綁定有domain對象則會調用domain的_errorhandler方法來處理異常,那_errorhandler究竟如火如荼處理異常的呢?在讨論這個問題之前我們先回到上面的reqd.run方法中。調用domain對象的run方法時會先進入enter裡做如下處理:

将當期的domain對象設定成active并且綁定到process上,stack是一個儲存domain對象的堆棧,用于domain嵌套使用的情況,其中_domain_flag是一個用于js與c++進行通信的對象。緊接着再執行我們的業務代碼比如res.test()操作,此時便抛出了一個方法不存在的異常。由于進入enter方法後我們把目前domain對象綁定到了process上,是以異常就交給domain的_errorhandler方法來處理了,回到之前的問題,_errorhandler是如何處理異常的?

首先嘗試着讓之前綁定到domain上的error事件回調函數來處理該異常并清空目前process的domain屬性,之是以所嘗試是因為回調函數裡可能又會抛出新的異常,當然了理想情況就是回調函數能夠很好的處理掉異常并且不抛出新的異常,此時整個異常處理流程完美結束。如果有新的異常抛出,先将對stack堆棧進行出棧操作剔除已經使用過的當期domain對象,然後再看看棧裡邊是否還存在domain對象,有的話就用棧訂上的domain又回到process._fatalexception裡繼續處理剛剛回調函數抛出的新異常。stack為空的話此時已經沒有domain對象可以來處理異常,至次本次異常處理以失敗結束然後繼續交給最開始講到的uncaughtexception事件來處理。當然了調用domain.run時并沒有抛出異常,那麼domain也需要進行出棧操作,來抵消enter方法時的入棧操作以保持stack堆棧的平衡。

其實上面的reqd.add(res)和reqd.add(req)是可以不要的,為什麼可以不要呢?在什麼情況下需要什麼情況下又不需要?ok,我們再深入研究一下domain.add是如何工作的。官網中文檔有介紹domain.add接收emitter類型的參數,也就是eventemitter | timer emitter or timer。為什麼要這樣呢,看下面的一段代碼

此時next函數裡邊綁定到e對象上的data事件被觸發時domain對象是無法處理的,原因很明顯,data回調函數的運作已經處理domain.run方法之外。那我就要這個domain來處理錯誤怎麼辦呢,此時domain.add方法就派上用場了,我們隻需要簡單的調用一下d.add(e)或者d.add(timer)就可以解決這個問題。domain.add方法為什麼可以解決又是如何解決的呢?繼續往下看。

當調用domain.add(e)時,如果上綁定有domain先移除再綁定新的domain,并将e對象加入新domain的members中,進而保持着對e對象的引用。不管是timer對象還是event對象在觸發回調函數時都會先判斷是否有綁定domain對象

這些操作和domain.run方法相似,先執行enter将domain對象綁定到process上,然後再執行回調當有異常發生時process會将異常傳到domain上處理,最後再調用exit方法将該domain移出stack堆棧。是以上面的代碼中必須得調用下d.add(e)或者d.add(timer)才會讓domain對象捕獲到回調中的異常。

整個node.js異常處理就講到這裡了,其實在process._fatalexception方法中調用domain來處理異常之前還進行了一個異常處理操作

這個處理主要涉及到node.js的異步隊列asyncqueue在這裡暫不做讨論,以後再做進一步的研究,文章有點長感謝能堅持看到結尾的同學們,不要吝啬你們的贊哦~

該文章來自于阿裡巴巴技術協會(ata)

作者:淘傑