Node系列——Node中的異常處理。

一、對異常錯誤的理解

    異常錯誤應該被分爲兩種狀況:操做失敗程序員失誤html

1.一、操做失敗

    這是正確編寫的程序在運行時產生的錯誤。它並非程序的Bug,反而常常是其它問題。node

    例如:系統自己(內存不足或者打開文件數過多),系統配置(沒有到達遠程主機的路由),網絡問題(端口掛起),遠程服務(500錯誤,鏈接失敗)。具體狀況以下:
git

鏈接不到服務器
沒法解析主機名
無效的用戶輸入
請求超時
服務器返回500
套接字被掛起
系統內存不足

1.二、程序員失誤

    這是程序裏的Bug。這些錯誤每每能夠在調試階段經過修改代碼避免。它們永遠都無法被有效的處理,而是應該在程序員變編程的時候注意,例如:
程序員

讀取 undefined 的一個屬性
調用異步函數沒有指定回調
該傳對象的時候傳了一個字符串
該傳IP地址的時候傳了一個對象

1.三、兩者的差異對比

    人們把操做失敗和程序員的失誤都稱爲「錯誤」,但其實它們很不同。操做失敗是全部正確的程序應該處理的錯誤情形,只要被妥善處理它們不必定會預示 着Bug或是嚴重的問題。「文件找不到」是一個操做失敗,可是它並不必定意味着哪裏出錯了。它可能只是表明着程序若是想用一個文件得事先建立它。github

    與之相反,程序員失誤是不折不扣的Bug。這些情形下你會犯錯:忘記驗證用戶輸入,敲錯了變量名,諸如此類。這樣的錯誤根本就無法被處理,若是能夠,那就意味着你用處理錯誤的代碼代替了出錯的代碼。mongodb

    這樣的區分很重要:操做失敗是程序正常操做的一部分。而由程序員的失誤則是Bug數據庫

    有的時候,你會在一個Root問題裏同時遇到操做失敗和程序員的失誤。HTTP服務器訪問了未定義的變量時奔潰了,這是程序員的失誤。當前鏈接着的客戶端會在程序崩潰的同時看到一個ECONNRESET錯誤,在NodeJS裏一般會被報成「Socket Hang-up」。對客戶端來講,這是一個不相關的操做失敗, 那是由於正確的客戶端必須處理服務器宕機或者網絡中斷的狀況。express

    相似的,若是不處理好操做失敗, 這自己就是一個失誤。舉個例子,若是程序想要鏈接服務器,可是獲得一個ECONNREFUSED錯誤,而這個程序沒有監聽套接字上的error事件,而後程序崩潰了,這是程序員的失誤。鏈接斷開是操做失敗(由於這是任何一個正確的程序在系統的網絡或者其它模塊出問題時都會經歷的),若是它不被正確處理,那它就是一個失誤。編程

    理解操做失敗和程序員失誤的不一樣, 是搞清怎麼傳遞異常和處理異常的基礎。明白了這點再繼續往下讀json

    注:若是想有更好的理解,請讀參考文檔1

二、domain域模塊介紹

2.一、domain的原理和使用

    請你們讀完這篇文章    Node.js 異步異常的處理與domain模塊解析     

2.二、domain的API簡介

domain.create(): 返回一個domain對象
domain.run(fn): 在domain上下文中執行一個函數,並隱式綁定全部事件,定時器和低級的請求。
domain.members: 已加入domain對象的域定時器和事件發射器的數組。
domain.add(emitter): 顯式的增長事件
domain.remove(emitter): 刪除事件
domain.bind(callback): 以return爲封裝callback函數
domain.intercept(callback): 同domain.bind,但只返回第一個參數
domain.enter(): 進入一個異步調用的上下文,綁定到domain
domain.exit(): 退出當前的domain,切換到不一樣的鏈的異步調用的上下文中。對應domain.enter()
domain.dispose(): 釋放一個domain對象,讓node進程回收這部分資源

    更多的請你們看官網學習

2.三、domain的總結

    ①domain就是來捕獲同步和異步的異常的。

    ②咱們經過中間件的形式,引入domain來處理異步中的異常。固然,domain雖然捕捉到了異常,可是仍是因爲異常而致使的堆棧丟失會致使內存泄漏,因此出現這種狀況的時候仍是須要重啓這個進程的,有興趣的同窗能夠去看看domain-middleware這個domain中間件。

    ③domain嵌套:咱們可能會外層有domain的狀況下,內層還有其餘的domain,使用情景能夠在官網文檔中找到

三、node中的異常分類

    按照可預測和不可預測分

    按照同步和異步分

    按照監聽和請求分

四、個人項目中的異常處理解決完整方案

    通過以上三個部分的介紹,咱們就有了咱們的解決方案:

    ①捕獲操做失敗並做記錄和處理,程序員失誤應在調試階段解決。

    ②對可預想到的操做失敗要作有效的處理(用戶提示,日誌記錄)保證程序的健壯性。

    ③對不可預料的錯誤要作日誌記錄,郵件提示,錯誤分析,進程守護。

4.一、404錯誤處理

// 初始化路由配置,必定要在404前面初始化
require('./config/router')(app);

// 404錯誤處理
app.use(function(req, res, next) {
    //var err = new Error('Not Found');
    //err.status = 404;
    //next(err);
    res.statusCode = 404;
    res.json({sucess:false, message: '請求路徑不存在',err:''});
});

4.二、同步異常處理

    普通的同步異常可預知的,用try-catch-final已經能夠很好的處理了。

function sync_error() {
    var r = Math.random() * 10;
    console.log("random num is " + r);
    if (r > 5) {
        throw new Error("Error: random num" + r + " > 5");
    }
}

setInterval(function () {
    try {
        sync_error();
    } catch (err) {
        console.log(err);
    }

}, 1000)

4.三、錯誤事件監聽

    對一些操做要加上事件監聽,而後作記錄並相應的處理。

    例如監聽數據庫鏈接斷開的異常

//mongodb鏈接監視
mongoose.connection.on('connected',function(){
    console.info("mongoose contected to"+config.DB);
});

mongoose.connection.on('error',function(err){
    console.info("mongoose contection error"+err);
});

mongoose.connection.on('disconnected',function(){
    console.info(config.DB+" mongoose disconnected");
});

process.on('SIGINT', function () {
    mongoose.connection.close(function () {
        console.info("mongoose disconnected through app termination");
        process.exit(0);
    });
});

    例如監聽https請求的監聽事件

var https = require('https');
var url=require('url');

module.exports = function(app){
   app.get('/get', function(req, res, next) {
       //node是後臺語言,因此在請求的時候不會遇到跨域問題
       //本身定義option有些麻煩,不如這樣處理
       var regUrl = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=23&code=33";
       var option = url.parse(regUrl);
       //options 能夠是一個對象或字符串。若是 options 是字符串,它會自動被 url.parse() 解析。
       var request=https.request(option,function(response){
            response.on('data',function(chunk){
                res.send("get請求的結果:"+chunk.toString());
            });

           response.on('end',function(chunk){
               response.setEncoding('utf8');
               console.log('get請求結束了。');
               console.log(chunk);
            });
        });
       //這點事請求的異常處理,能夠寫進日誌,也能夠跟上面同樣鏈式編程,這點的處理和中間件的怎麼處理
       request.on('error', function(e) {
           console.log('problem with request: ' + e.message);
           throw e;//能夠上拋到中間件處理
       });
       //請求結束
       request.end();

    });
};

    例如express中的服務器監聽

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }
  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */
function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}


4.四、異步異常捕獲

    異步異常的捕獲是經過回調函數中的err參數

function async(fn, callback) {
    // Code execution path breaks here.
    setTimeout(function () {
        try {
            callback(null, fn());
        } catch (err) {
            callback(err);
        }
    }, 0);
}

async(null, function (err, data) {
    if (err) {
        console.log('Error: %s', err.message);
    } else {
        // Do something.
    }
});

4.五、用domain捕獲同步和異步的不可預計的錯誤

    上面的方法對一些能夠預計到的錯誤均可以進行到捕獲,但還有一些不可預計的能夠經過domain捕獲,domain能夠寫在每一個js文件中,也能夠像下面這樣,用中間件攔截每個請求。

    domain的使用請參讀2.1

// 全部請求異常處理
app.use(function (req,res, next) {
    var serverDomain = domain.create();
    //監聽domain的錯誤事件
    serverDomain.on('error', function (err) {
        //logger.error(err);
        // todo:日誌記錄,返回錯誤的信息要詳細
        res.statusCode = 500;
        res.json({sucess:false, message: '服務器異常',err:err.message});
        serverDomain.dispose();
    });
    serverDomain.add(req);
    serverDomain.add(res);
    serverDomain.run(next);
});

4.六、進程級別的錯誤捕獲

    4.5中雖然用了中間件捕獲全部的請求,可是服務器的建立我沒有包在domain中,你們能夠把服務器建立也寫在domain中,這就是2.3中提到的domain嵌套,但個人解決辦法是,在服務器中進行進程級別的錯誤捕獲。

//進程級別的異常捕獲
process.on('uncaughtException', function (err) {
    console.log('進程級別的錯誤:'+err.message);
});

        NodeJS異常處理uncaughtException篇 

4.七、進程守護

   上面作了各個級別的錯誤處理,但仍是要給node加上進程守護。你能夠本身寫本身的進程守護,也能夠用 pm2  forver 這些第三方工具進行監控。些模塊有很好的日誌,部署,監控功能,我以後也會有博客專門介紹使用。

    網上有人說用一個第三方插件就會多一份風險,因此我用了本身寫的。

/**
 * Created by shiguoqing on 2015/8/24.
 */
var fork = require('child_process').fork;
var cpus = require('os').cpus();
//保存被子進程實例數組
var workers = [];
//這裏的被子進程理論上能夠無限多
var appsPath = ['./bin/server'];
var createWorker = function(appPath){
    //保存fork返回的進程實例
    var worker = fork(appPath);
    //監聽子進程exit事件
    worker.on('exit',function(){
        console.log('worker:' + worker.pid + 'exited');
        delete workers[worker.pid];
        createWorker(appPath);
    });
    workers[worker.pid] = worker;
    console.log('Create worker:' + worker.pid);
};
//啓動全部子進程
for (var i = appsPath.length - 1; i >= 0; i--) {
    createWorker(appsPath[i]);
}
//父進程退出時殺死全部子進程
process.on('exit',function(){
    for(var pid in workers){
        workers[pid].kill();
    }
});

    如今這個只能保證子進程重啓,可是父進程若是掛了就完蛋了,因此你能夠把上面的代碼寫成,父進程本身掛了能夠重啓本身。個人程序是以後要在外面加上pm2

Todo:父進程本身掛了重啓本身的代碼

4.八、很是重要的一點思考

   由於博客文字限制,請你們移步看這段文字

   Node系列——Node系列中異常捕獲的一個重要思考  

    總結:

    以上的這些話都代表,node中的異常可能會引發內存的變化。正確的作法是:針對發生異常的請求返回一個錯誤代碼 - 出錯的Worker再也不接受新的請求 - 退出關閉Worker進程。

    個人程序中是這樣處理的,domain中間件捕獲到的都是請求的錯誤,捕獲異常以後分析一下。而進程級別捕獲捕獲到的異常要作記錄,郵件提醒,分析,退出worker,重啓服務。

五、異常錯誤返回處理

    咱們異常處理返回的都是一個json對象。這樣容易擴展。

{sucess:false, message: '服務器異常',err:err.message,code:'500'}

    這種restful的api最好返回狀態碼,先後臺經過約定去讀取錯誤信息,前面的本身拋出的錯誤最好也拋出自定義的狀態碼。

100 "continue"
101 "switching protocols"
102 "processing"
200 "ok"
201 "created"
202 "accepted"
203 "non-authoritative information"
204 "no content"
205 "reset content"
206 "partial content"
207 "multi-status"
300 "multiple choices"
301 "moved permanently"
302 "moved temporarily"
303 "see other"
304 "not modified"
305 "use proxy"
307 "temporary redirect"
400 "bad request"
401 "unauthorized"
402 "payment required"
403 "forbidden"
404 "not found"
405 "method not allowed"
406 "not acceptable"
407 "proxy authentication required"
408 "request time-out"
409 "conflict"
410 "gone"
411 "length required"
412 "precondition failed"
413 "request entity too large"
414 "request-uri too large"
415 "unsupported media type"
416 "requested range not satisfiable"
417 "expectation failed"
418 "i'm a teapot"
422 "unprocessable entity"
423 "locked"
424 "failed dependency"
425 "unordered collection"
426 "upgrade required"
428 "precondition required"
429 "too many requests"
431 "request header fields too large"
500 "internal server error"
501 "not implemented"
502 "bad gateway"
503 "service unavailable"
504 "gateway time-out"
505 "http version not supported"
506 "variant also negotiates"
507 "insufficient storage"
509 "bandwidth limit exceeded"
510 "not extended"
511 "network authentication required"

六、其餘

Node下自定義錯誤類型    

 https://cnodejs.org/topic/52090bc944e76d216af25f6f

這篇文章能夠讀讀,寫了不少基礎的東西

http://blog.csdn.net/cike110120/article/details/12916573


七、參考文章 

一、翻譯 - NodeJS錯誤處理最佳實踐  這篇文章請讀完,詳細的闡釋了程序員失誤,操做失敗

二、Node.js十大常見的開發者錯誤    你們最好看一看

http://blog.fens.me/nodejs-core-domain/

http://www.cnblogs.com/cbscan/articles/3826461.html

https://cnodejs.org/topic/516b64596d38277306407936


免責說明

一、本博客中的文章摘自網上的衆多博客,僅做爲本身知識的補充和整理,並分享給其餘須要的coder,不會用於商用。

二、由於不少博客的地址看完沒有及時作保存,因此不少不會在這裏標明出處。

相關文章
相關標籤/搜索