Node系列——Node中的异常处理。

元气小坏坏 提交于 2020-03-27 17:14:57

3 月,跳不动了?>>>

1、对异常错误的理解

    异常错误应该被分为两种情况:操作失败程序员失误

1.1、操作失败

    这是正确编写的程序在运行时产生的错误。它并不是程序的Bug,反而经常是其它问题。

    例如:系统本身(内存不足或者打开文件数过多),系统配置(没有到达远程主机的路由),网络问题(端口挂起),远程服务(500错误,连接失败)。具体情况如下:

连接不到服务器
无法解析主机名
无效的用户输入
请求超时
服务器返回500
套接字被挂起
系统内存不足

1.2、程序员失误

    这是程序里的Bug。这些错误往往可以在调试阶段通过修改代码避免。它们永远都没法被有效的处理,而是应该在程序员变编程的时候注意,例如:

读取 undefined 的一个属性
调用异步函数没有指定回调
该传对象的时候传了一个字符串
该传IP地址的时候传了一个对象

1.3、二者的差别对比

    人们把操作失败和程序员的失误都称为“错误”,但其实它们很不一样。操作失败是所有正确的程序应该处理的错误情形,只要被妥善处理它们不一定会预示 着Bug或是严重的问题。“文件找不到”是一个操作失败,但是它并不一定意味着哪里出错了。它可能只是代表着程序如果想用一个文件得事先创建它。

    与之相反,程序员失误是彻彻底底的Bug。这些情形下你会犯错:忘记验证用户输入,敲错了变量名,诸如此类。这样的错误根本就没法被处理,如果可以,那就意味着你用处理错误的代码代替了出错的代码。

    这样的区分很重要:操作失败是程序正常操作的一部分。而由程序员的失误则是Bug

    有的时候,你会在一个Root问题里同时遇到操作失败和程序员的失误。HTTP服务器访问了未定义的变量时奔溃了,这是程序员的失误。当前连接着的客户端会在程序崩溃的同时看到一个ECONNRESET错误,在NodeJS里通常会被报成“Socket Hang-up”。对客户端来说,这是一个不相关的操作失败, 那是因为正确的客户端必须处理服务器宕机或者网络中断的情况。

    类似的,如果不处理好操作失败, 这本身就是一个失误。举个例子,如果程序想要连接服务器,但是得到一个ECONNREFUSED错误,而这个程序没有监听套接字上的error事件,然后程序崩溃了,这是程序员的失误。连接断开是操作失败(因为这是任何一个正确的程序在系统的网络或者其它模块出问题时都会经历的),如果它不被正确处理,那它就是一个失误。

    理解操作失败和程序员失误的不同, 是搞清怎么传递异常和处理异常的基础。明白了这点再继续往下读

    注:如果想有更好的理解,请读参考文档1

2、domain域模块介绍

2.1、domain的原理和使用

    请大家读完这篇文章    Node.js 异步异常的处理与domain模块解析     

2.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.3、domain的总结

    ①domain就是来捕获同步和异步的异常的。

    ②我们通过中间件的形式,引入domain来处理异步中的异常。当然,domain虽然捕捉到了异常,但是还是由于异常而导致的堆栈丢失会导致内存泄漏,所以出现这种情况的时候还是需要重启这个进程的,有兴趣的同学可以去看看domain-middleware这个domain中间件。

    ③domain嵌套:我们可能会外层有domain的情况下,内层还有其他的domain,使用情景可以在官网文档中找到

3、node中的异常分类

    按照可预测和不可预测分

    按照同步和异步分

    按照监听和请求分

4、我的项目中的异常处理解决完整方案

    经过以上三个部分的介绍,我们就有了我们的解决方案:

    ①捕获操作失败并作记录和处理,程序员失误应在调试阶段解决。

    ②对可预想到的操作失败要做有效的处理(用户提示,日志记录)保证程序的健壮性。

    ③对不可预料的错误要做日志记录,邮件提示,错误分析,进程守护。

4.1、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.2、同步异常处理

    普通的同步异常可预知的,用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.3、错误事件监听

    对一些操作要加上事件监听,然后做记录并相应的处理。

    例如监听数据库连接断开的异常

//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.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.5、用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.6、进程级别的错误捕获

    4.5中虽然用了中间件捕获所有的请求,但是服务器的创建我没有包在domain中,大家可以把服务器创建也写在domain中,这就是2.3中提到的domain嵌套,但我的解决办法是,在服务器中进行进程级别的错误捕获。

//进程级别的异常捕获
process.on('uncaughtException', function (err) {
    console.log('进程级别的错误:'+err.message);
});

        NodeJS异常处理uncaughtException篇 

4.7、进程守护

   上面做了各个级别的错误处理,但还是要给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.8、非常重要的一点思考

   因为博客文字限制,请大家移步看这段文字

   Node系列——Node系列中异常捕获的一个重要思考  

    总结:

    以上的这些话都表明,node中的异常可能会引起内存的变化。正确的做法是:针对发生异常的请求返回一个错误代码 - 出错的Worker不再接受新的请求 - 退出关闭Worker进程。

    我的程序中是这样处理的,domain中间件捕获到的都是请求的错误,捕获异常之后分析一下。而进程级别捕获捕获到的异常要做记录,邮件提醒,分析,退出worker,重启服务。

5、异常错误返回处理

    我们异常处理返回的都是一个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"

6、其他

Node下自定义错误类型    

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

这篇文章可以读读,写了很多基础的东西

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


7、参考文章 

1、翻译 - NodeJS错误处理最佳实践  这篇文章请读完,详细的阐释了程序员失误,操作失败

2、Node.js十大常见的开发者错误    大家最好看一看

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

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

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


免责说明

1、本博客中的文章摘自网上的众多博客,仅作为自己知识的补充和整理,并分享给其他需要的coder,不会用于商用。

2、因为很多博客的地址看完没有及时做保存,所以很多不会在这里标明出处。


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!