iT邦幫忙

2021 iThome 鐵人賽

DAY 21
0
自我挑戰組

從C到JS的同步非同步探索系列 第 21

[Day 21] Node 註冊事件 2

前言

昨天我們聊到, C++ 連接層中的 TCP 物件被 JS 調用, 拿來註冊事件及回調函數, 今天讓我們繼續看下去。

C++ 連接層

https://github.com/nodejs/node/blob/master/src/tcp_wrap.cc

env->SetProtoMethod(t, "open", Open);
env->SetProtoMethod(t, "bind", Bind);
env->SetProtoMethod(t, "listen", Listen);
env->SetProtoMethod(t, "connect", Connect);
env->SetProtoMethod(t, "bind6", Bind6);
env->SetProtoMethod(t, "connect6", Connect6);
env->SetProtoMethod(t, "getsockname",
                    GetSockOrPeerName<TCPWrap, uv_tcp_getsockname>);
env->SetProtoMethod(t, "getpeername",
                    GetSockOrPeerName<TCPWrap, uv_tcp_getpeername>);
env->SetProtoMethod(t, "setNoDelay", SetNoDelay);
env->SetProtoMethod(t, "setKeepAlive", SetKeepAlive);

在 TCP 的初始化就可看到這段程式碼, SetProtoMethod 在 V8 中的作用是給物件天加上 prototype , 用 JS 的說法就是給物件加上 method 。

我們隨意看一個 Listen method 在幹嘛

void TCPWrap::Listen(const FunctionCallbackInfo<Value>& args) {
  TCPWrap* wrap;
  ASSIGN_OR_RETURN_UNWRAP(&wrap,
                          args.Holder(),
                          args.GetReturnValue().Set(UV_EBADF));
  Environment* env = wrap->env();
  int backlog;
  if (!args[0]->Int32Value(env->context()).To(&backlog)) return;
  int err = uv_listen(reinterpret_cast<uv_stream_t*>(&wrap->handle_),
                      backlog,
                      OnConnection);
  args.GetReturnValue().Set(err);
}

跳過設定, 這段程式碼的意思就是, 把回調函數 (這個方法的參數) 用 uv_listen 往下傳。

所以看看 uv_listen 把參數(回調函數)傳去哪了

int uv_listen(uv_stream_t* stream, int backlog, uv_connection_cb cb) {
  int err;

  switch (stream->type) {
  case UV_TCP:
    err = uv_tcp_listen((uv_tcp_t*)stream, backlog, cb);
    break;

  case UV_NAMED_PIPE:
    err = uv_pipe_listen((uv_pipe_t*)stream, backlog, cb);
    break;

  default:
    err = UV_EINVAL;
  }

  if (err == 0)
    uv__handle_start(stream);

  return err;
}

假設類型是 TCP 直接查看 uv_tcp_listen

有兩個選擇

  1. node/deps/uv/src/unix/tcp.c
  2. node/deps/uv/src/win/tcp.c

分別代表兩個作業系統

我們只看 windows , 節省時間

int uv_tcp_listen(uv_tcp_t* handle, int backlog, uv_connection_cb cb) {
  unsigned int i, simultaneous_accepts;
  uv_tcp_accept_t* req;
  int err;

  assert(backlog > 0);

  if (handle->flags & UV_HANDLE_LISTENING) {
    handle->stream.serv.connection_cb = cb;
  }

  if (handle->flags & UV_HANDLE_READING) {
    return WSAEISCONN;
  }

  if (handle->delayed_error) {
    return handle->delayed_error;
  }

  if (!(handle->flags & UV_HANDLE_BOUND)) {
    err = uv_tcp_try_bind(handle,
                          (const struct sockaddr*) &uv_addr_ip4_any_,
                          sizeof(uv_addr_ip4_any_),
                          0);
    if (err)
      return err;
    if (handle->delayed_error)
      return handle->delayed_error;
  }

// 後略

略過檢查與設定, 在uv_tcp_listen 中察看 uv_tcp_try_bind

static int uv_tcp_try_bind(uv_tcp_t* handle,
                           const struct sockaddr* addr,
                           unsigned int addrlen,
                           unsigned int flags) {
  DWORD err;
  int r;

  if (handle->socket == INVALID_SOCKET) {
    SOCKET sock;

    /* Cannot set IPv6-only mode on non-IPv6 socket. */
    if ((flags & UV_TCP_IPV6ONLY) && addr->sa_family != AF_INET6)
      return ERROR_INVALID_PARAMETER;

    sock = socket(addr->sa_family, SOCK_STREAM, 0);
    if (sock == INVALID_SOCKET) {
      return WSAGetLastError();
    }

    err = uv_tcp_set_socket(handle->loop, handle, sock, addr->sa_family, 0);
    if (err) {
      closesocket(sock);
      return err;
    }
  }
// 後略

略過創建 socket 直接查看 uv_tcp_set_socket

static int uv_tcp_set_socket(uv_loop_t* loop,
                             uv_tcp_t* handle,
                             SOCKET socket,
                             int family,
                             int imported) {
  DWORD yes = 1;
  int non_ifs_lsp;
  int err;

  if (handle->socket != INVALID_SOCKET)
    return UV_EBUSY;

  /* Set the socket to nonblocking mode */
  if (ioctlsocket(socket, FIONBIO, &yes) == SOCKET_ERROR) {
    return WSAGetLastError();
  }

  /* Make the socket non-inheritable */
  if (!SetHandleInformation((HANDLE) socket, HANDLE_FLAG_INHERIT, 0))
    return GetLastError();

  /* Associate it with the I/O completion port. Use uv_handle_t pointer as
   * completion key. */
  if (CreateIoCompletionPort((HANDLE)socket,
                             loop->iocp,
                             (ULONG_PTR)socket,
                             0) == NULL) {
    if (imported) {
      handle->flags |= UV_HANDLE_EMULATE_IOCP;
    } else {
      return GetLastError();
    }
  }
// 後略

略過設定 , 終於看到了一個熟悉的 method CreateIoCompletionPort

這就是我前幾天聊的 IOCP 註冊方法, 至此我們對其中一種事件的註冊有了大概的了解。

總結

Node 註冊事件的其中一種步驟

  1. 觸發 JS 中引入的 C++ 連接層方法
  2. C++ 連接層會檢查與打包 事件和回調函數
  3. C++ 連接層創建接收事件的 socket
  4. 引用 IOCP 方法把事件用非同步 IO 的方式進行註冊, 且同時綁定事件發生後的回調函數。

( 據說 unix 中會使用 epoll 不過我對這件事沒做太多確認 )

明天進度

明天開始介紹 libuv 如何 schedule 吧

明天見 !


上一篇
[Day 20] Node 註冊事件 1
下一篇
[Day 22] Node Event loop 1
系列文
從C到JS的同步非同步探索30

尚未有邦友留言

立即登入留言