聊聊Nodejs的错误处理

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

[[392980]]

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

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

假设我们有以下代码

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

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

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

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

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

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

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

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

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

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

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

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

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

接着调用JS层的oncomplete回调。

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

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

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

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

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

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

void GetErrMap(const FunctionCallbackInfo& args) {   
  Environment* env = Environment::GetCurrent(args);   
  Isolate* isolate = env->isolate();   
  Localcontext = env->context();   
   
  Local 
  // 从per_process::uv_errors_map中获取错误信息   
  size_t errors_len = arraysize(per_process::uv_errors_map);   
  // 赋值   
  for (size_t i = 0; i < errors_len; ++i) {   
     // map的键是 uv_errors_map每个元素中的value,值是name和message 
    const auto& error = per_process::uv_errors_map[i];   
    Localarr[] = {OneByteString(isolate, error.name),   
                          OneByteString(isolate, error.message)};  
    if (err_map   
            ->Set(context,   
                  Integer::New(isolate, error.value),   
                  Array::New(isolate, arr, arraysize(arr)))   
            .IsEmpty()) {   
      return;   
    }   
  }   
   
  args.GetReturnValue().Set(err_map);   

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

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

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

UV_ERRNO_MAP宏展开后如下

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

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

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

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

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

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

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2021-04-29 09:02:44

语言Go 处理

2024-03-27 08:18:02

Spring映射HTML

2014-11-17 10:05:12

Go语言

2023-12-26 22:05:53

并发代码goroutines

2022-11-16 08:41:43

2010-06-01 16:14:04

2023-10-28 16:30:19

Golang开发

2009-08-05 16:04:50

2009-06-19 16:20:14

ASP.NET错误处理

2023-10-08 20:31:18

React

2016-09-07 20:28:17

MySQL存储数据库

2017-03-08 08:57:04

JavaScript错误堆栈

2016-08-19 10:41:42

Swift 2错误

2017-04-06 14:40:29

JavaScript错误处理堆栈追踪

2011-05-25 10:26:42

ora-02069错误

2015-08-19 14:11:56

SQL Server错误处理

2025-02-10 09:49:00

2011-03-22 10:10:21

CentOSNagios安装

2020-08-20 10:16:56

Golang错误处理数据

2024-09-23 08:10:00

.NET开发
点赞
收藏

51CTO技术栈公众号