聊聊Nodejs的錯(cuò)誤處理
本文轉(zhuǎn)載自微信公眾號(hào)「編程雜技」,作者theanarkh。轉(zhuǎn)載本文請(qǐng)聯(lián)系編程雜技公眾號(hào)。
本文以連接錯(cuò)誤ECONNREFUSED為例,看看nodejs對(duì)錯(cuò)誤處理的過(guò)程。
假設(shè)我們有以下代碼
- const net = require('net');
- net.connect({port: 9999})
如果本機(jī)上沒(méi)有監(jiān)聽(tīng)9999端口,那么我們會(huì)得到以下輸出。
- 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)
我們簡(jiǎn)單看一下connect的調(diào)用流程。
- const req = new TCPConnectWrap();
- req.oncomplete = afterConnect;
- req.address = address;
- req.port = port;
- req.localAddress = localAddress;
- req.localPort = localPort;
- // 開(kāi)始三次握手建立連接
- err = self._handle.connect(req, address, port);
接著我們看一下C++層connect的邏輯
- err = req_wrap->Dispatch(uv_tcp_connect,
- &wrap->handle_,
- reinterpret_cast(&addr),
- AfterConnect);
C++層直接調(diào)用Libuv的uv_tcp_connect,并且設(shè)置回調(diào)是AfterConnect。接著我們看libuv的實(shí)現(xiàn)。
- do {
- errno = 0;
- // 非阻塞調(diào)用
- r = connect(uv__stream_fd(handle), addr, addrlen);
- } while (r == -1 && errno == EINTR);
- // 連接錯(cuò)誤,判斷錯(cuò)誤碼
- if (r == -1 && errno != 0) {
- // 還在連接中,不是錯(cuò)誤,等待連接完成,事件變成可讀
- 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,等待可寫(xiě)事件
- handle->connect_req = req;
- uv__io_start(handle->loop, &handle->io_watcher, POLLOUT);
我們看到Libuv以異步的方式調(diào)用操作系統(tǒng),然后把request掛載到handle中,并且注冊(cè)等待可寫(xiě)事件,當(dāng)連接失敗的時(shí)候,就會(huì)執(zhí)行uv__stream_io回調(diào),我們看一下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);
獲取錯(cuò)誤信息后回調(diào)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);
接著調(diào)用JS層的oncomplete回調(diào)。
- const ex = exceptionWithHostPort(status,
- 'connect',
- req.address,
- req.port,
- details);
- if (details) {
- ex.localAddress = req.localAddress;
- ex.localPort = req.localPort;
- }
- // 銷(xiāo)毀socket
- self.destroy(ex);
exceptionWithHostPort構(gòu)造錯(cuò)誤信息,然后銷(xiāo)毀socket并且以ex為參數(shù)觸發(fā)error事件。我們看看uvExceptionWithHostPort的實(shí)現(xiàn)。
- 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;
- }
- // 獲取調(diào)用棧信息但不包括當(dāng)前調(diào)用的函數(shù)uvExceptionWithHostPort,注入stack字段到ex中
- Error.captureStackTrace(ex, excludedStackFn || uvExceptionWithHostPort);
- return ex;
- }
我們看到錯(cuò)誤信息主要通過(guò)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;
- }
繼續(xù)往下看,uvErrmapGet調(diào)用了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中獲取錯(cuò)誤信息
- size_t errors_len = arraysize(per_process::uv_errors_map);
- // 賦值
- for (size_t i = 0; i < errors_len; ++i) {
- // map的鍵是 uv_errors_map每個(gè)元素中的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);
- }
我們看到錯(cuò)誤信息存在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
- };
UV_ERRNO_MAP宏展開(kāi)后如下
- {UV_E2BIG, "E2BIG", "argument list too long"},
- {UV_EACCES, "EACCES", "permission denied"},
- {UV_EADDRINUSE, "EADDRINUSE", "address already in use"},
- ……
所以導(dǎo)出到JS層的結(jié)果如下
- {
- // 鍵是一個(gè)數(shù)字,由Libuv定義,其實(shí)是封裝了操作系統(tǒng)的定義
- UV_ECONNREFUSED: ["ECONNREFUSED", "connection refused"],
- UV_ECONNRESET: ["ECONNRESET", "connection reset by peer"]
- ...
- }
Node.js最后會(huì)組裝這些信息返回給調(diào)用方。這就是我們輸出的錯(cuò)誤信息。那么為什么會(huì)是ECONNREFUSED呢?我們看一下操作系統(tǒng)對(duì)于該錯(cuò)誤碼的邏輯。
- static void tcp_reset(struct sock *sk)
- {
- switch (sk->sk_state) {
- case TCP_SYN_SENT:
- sk->sk_err = ECONNREFUSED;
- break;
- // ...
- }
- }
當(dāng)操作系統(tǒng)收到一個(gè)發(fā)給該socket的rst包的時(shí)候會(huì)執(zhí)行tcp_reset,我們看到當(dāng)socket處于發(fā)送syn包等待ack的時(shí)候,如果收到一個(gè)fin包,則會(huì)設(shè)置錯(cuò)誤碼為ECONNREFUSED。我們輸出的正是這個(gè)錯(cuò)誤碼。