zl程序教程

您现在的位置是:首页 >  Javascript

当前栏目

聊聊Nodejs的错误处理

2023-03-09 22:10:22 时间

本文转载自微信公众号「编程杂技」,作者theanarkh。转载本文请联系编程杂技公众号。  

本文以连接错误ECONNREFUSED为例,看看nodejs对错误处理的过程。

假设我们有以下代码

  1. const net = require('net'); 
  2.  
  3. net.connect({port: 9999}) 

如果本机上没有监听9999端口,那么我们会得到以下输出。

  1. events.js:170   
  2.        throw er; // Unhandled 'error' event   
  3.        ^   
  4.     
  5.  Error: connect ECONNREFUSED 127.0.0.1:9999   
  6.      at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1088:14)   
  7.  Emitted 'error' event at:   
  8.      at emitErrorNT (internal/streams/destroy.js:91:8)   
  9.      at emitErrorAndCloseNT (internal/streams/destroy.js:59:3)   
  10.      at processTicksAndRejections (internal/process/task_queues.js:81:17)  

我们简单看一下connect的调用流程。

  1. const req = new TCPConnectWrap();   
  2. req.oncomplete = afterConnect;   
  3. req.address = address;   
  4. req.port = port;   
  5. req.localAddress = localAddress;   
  6. req.localPort = localPort;   
  7. // 开始三次握手建立连接   
  8. err = self._handle.connect(req, address, port);  

接着我们看一下C++层connect的逻辑

  1. err = req_wrap->Dispatch(uv_tcp_connect,   
  2.                 &wrap->handle_,   
  3.                 reinterpret_cast(&addr),   
  4.                 AfterConnect);  

C++层直接调用Libuv的uv_tcp_connect,并且设置回调是AfterConnect。接着我们看libuv的实现。

  1. do {   
  2.     errno = 0;   
  3.     // 非阻塞调用   
  4.     r = connect(uv__stream_fd(handle), addr, addrlen);   
  5.   } while (r == -1 && errno == EINTR);   
  6.   // 连接错误,判断错误码   
  7.   if (r == -1 && errno != 0) {   
  8.     // 还在连接中,不是错误,等待连接完成,事件变成可读   
  9.     if (errno == EINPROGRESS)   
  10.       ; /* not an error */   
  11.     else if (errno == ECONNREFUSED)   
  12.       // 连接被拒绝   
  13.       handle->delayed_error = UV__ERR(ECONNREFUSED);   
  14.     else   
  15.       return UV__ERR(errno);   
  16.   }   
  17.   uv__req_init(handle->loop, req, UV_CONNECT);   
  18.   req->cb = cb;   
  19.   req->handle = (uv_stream_t*) handle;   
  20.   QUEUE_INIT(&req->queue);   
  21.   // 挂载到handle,等待可写事件   
  22.   handle->connect_req = req;   
  23. uv__io_start(handle->loop, &handle->io_watcher, POLLOUT);

我们看到Libuv以异步的方式调用操作系统,然后把request挂载到handle中,并且注册等待可写事件,当连接失败的时候,就会执行uv__stream_io回调,我们看一下Libuv的处理(uv__stream_io)。

  1. getsockopt(uv__stream_fd(stream),   
  2.                SOL_SOCKET,   
  3.                SO_ERROR,   
  4.                &error,   
  5.                &errorsize);   
  6. error = UV__ERR(error);   
  7. if (req->cb)   
  8.     req->cb(req, error); 

获取错误信息后回调C++层的AfterConnect。

  1. Localargv[5] = {   
  2.    Integer::New(env->isolate(), status),   
  3.    wrap->object(),   
  4.    req_wrap->object(),   
  5.    Boolean::New(env->isolate(), readable),   
  6.    Boolean::New(env->isolate(), writable)   
  7.  };   
  8.    
  9.  req_wrap->MakeCallback(env->oncomplete_string(), arraysize(argv), argv);   

接着调用JS层的oncomplete回调。

  1. const ex = exceptionWithHostPort(status,   
  2.                                  'connect',   
  3.                                  req.address,   
  4.                                  req.port,   
  5.                                  details);   
  6. if (details) {   
  7.   ex.localAddress = req.localAddress;   
  8.   ex.localPort = req.localPort;   
  9. }   
  10. // 销毁socket   
  11. self.destroy(ex); 

exceptionWithHostPort构造错误信息,然后销毁socket并且以ex为参数触发error事件。我们看看uvExceptionWithHostPort的实现。

  1. function uvExceptionWithHostPort(err, syscall, address, port) {   
  2.   const [ code, uvmsg ] = uvErrmapGet(err) || uvUnmappedError;   
  3.   const message = `${syscall} ${code}: ${uvmsg}`;   
  4.   let details = '';   
  5.    
  6.   if (port && port > 0) {   
  7.     details = ` ${address}:${port}`;   
  8.   } else if (address) {   
  9.     details = ` ${address}`;   
  10.   }   
  11.   const tmpLimit = Error.stackTraceLimit;   
  12.   Error.stackTraceLimit = 0;   
  13.   const ex = new Error(`${message}${details}`);   
  14.   Error.stackTraceLimit = tmpLimit;   
  15.   ex.code = code;   
  16.   ex.errno = err;   
  17.   ex.syscall = syscall;   
  18.   ex.address = address;   
  19.   if (port) {   
  20.     ex.port = port;   
  21.   }   
  22.   // 获取调用栈信息但不包括当前调用的函数uvExceptionWithHostPort,注入stack字段到ex中   
  23.   Error.captureStackTrace(ex, excludedStackFn || uvExceptionWithHostPort);   
  24.   return ex;   
  25. }   

我们看到错误信息主要通过uvErrmapGet获取

  1. unction uvErrmapGet(name) {   
  2.    uvBinding = lazyUv();   
  3.    if (!uvBinding.errmap) {   
  4.      uvBinding.errmap = uvBinding.getErrorMap();   
  5.    }   
  6.    return uvBinding.errmap.get(name);   
  7.  }   
  8.     
  9.  function lazyUv() {   
  10.    if (!uvBinding) {   
  11.      uvBinding = internalBinding('uv');   
  12.    }   
  13.    return uvBinding;   
  14.  } 

继续往下看,uvErrmapGet调用了C++层的uv模块的getErrorMap。

  1. void GetErrMap(const FunctionCallbackInfo& args) {   
  2.   Environment* env = Environment::GetCurrent(args);   
  3.   Isolate* isolate = env->isolate();   
  4.   Localcontext = env->context();   
  5.    
  6.   Local 
  7.   // 从per_process::uv_errors_map中获取错误信息   
  8.   size_t errors_len = arraysize(per_process::uv_errors_map);   
  9.   // 赋值   
  10.   for (size_t i = 0; i < errors_len; ++i) {   
  11.      // map的键是 uv_errors_map每个元素中的value,值是name和message 
  12.     const auto& error = per_process::uv_errors_map[i];   
  13.     Localarr[] = {OneByteString(isolate, error.name),   
  14.                           OneByteString(isolate, error.message)};  
  15.     if (err_map   
  16.             ->Set(context,   
  17.                   Integer::New(isolate, error.value),   
  18.                   Array::New(isolate, arr, arraysize(arr)))   
  19.             .IsEmpty()) {   
  20.       return;   
  21.     }   
  22.   }   
  23.    
  24.   args.GetReturnValue().Set(err_map);   

我们看到错误信息存在per_process::uv_errors_map中,我们看一下uv_errors_map的定义。

  1. struct UVError { 
  2.   int value; 
  3.   const charname
  4.   const char* message; 
  5. }; 
  6.  
  7. static const struct UVError uv_errors_map[] = {   
  8. #define V(name, message) {UV_##name, #name, message},   
  9.     UV_ERRNO_MAP(V)   
  10. #undef V   
  11. };   

UV_ERRNO_MAP宏展开后如下

  1. {UV_E2BIG, "E2BIG""argument list too long"},   
  2. {UV_EACCES, "EACCES""permission denied"},   
  3. {UV_EADDRINUSE, "EADDRINUSE""address already in use"},   
  4. ……  

所以导出到JS层的结果如下

  1. {   
  2.   // 键是一个数字,由Libuv定义,其实是封装了操作系统的定义 
  3.   UV_ECONNREFUSED: ["ECONNREFUSED""connection refused"],     
  4.   UV_ECONNRESET: ["ECONNRESET""connection reset by peer"]    
  5.   ...    
  6. }  

Node.js最后会组装这些信息返回给调用方。这就是我们输出的错误信息。那么为什么会是ECONNREFUSED呢?我们看一下操作系统对于该错误码的逻辑。

  1. static void tcp_reset(struct sock *sk)   
  2. {   
  3.     switch (sk->sk_state) {   
  4.         case TCP_SYN_SENT:   
  5.             sk->sk_err = ECONNREFUSED;   
  6.             break;   
  7.          // ... 
  8.     }   
  9.    
  10. }  

当操作系统收到一个发给该socket的rst包的时候会执行tcp_reset,我们看到当socket处于发送syn包等待ack的时候,如果收到一个fin包,则会设置错误码为ECONNREFUSED。我们输出的正是这个错误码。