本文转载自微信公众号「编程杂技」,作者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 char* name;
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。我们输出的正是这个错误码。