Node.js TCP 连接处理核心流程解析

作者:后端茶话

时间:2026-10-24

浏览:552

本文深入解析 Node.js 处理 TCP 连接的核心流程,从 createServer 启动到 Libuv 层注册 epoll 监听。重点分析 uv__server_io 循环接受新连接、uv_accept 绑定文件描述符,以及 C++ 层与 JS 层 Socket 对象的封装关联,揭示从底层 fd 到上层应用对象的完整传递机制。

文章目录

Node.js TCP 连接处理核心流程解析

此前与开发者探讨了 Node.js 中 epoll 机制及请求处理的原理,本文将简要梳理 Node.js 处理网络请求的具体逻辑。分析将从 listen 函数的执行开始。


int uv_tcp_listen(uv_tcp_t* tcp, int backlog, uv_connection_cb cb) {
 // 设置处理的请求的策略,见下面的分析
 if (single_accept == -1) {
  const char* val = getenv("UV_TCP_SINGLE_ACCEPT");
  single_accept = (val != NULL && atoi(val) != 0); /* Off by default. */
 }
 if (single_accept)
  tcp->flags |= UV_HANDLE_TCP_SINGLE_ACCEPT;
 // 执行bind或设置标记
 err = maybe_new_socket(tcp, AF_INET, flags);
 // 开始监听
 if (listen(tcp->io_watcher.fd, backlog))
  return UV__ERR(errno);
 // 设置回调
 tcp->connection_cb = cb;
 tcp->flags |= UV_HANDLE_BOUND;
 // 设置io观察者的回调,由epoll监听到连接到来时执行
 tcp->io_watcher.cb = uv__server_io;
 // 插入观察者队列,这时候还没有增加到epoll,poll io阶段再遍历观察者队列进行处理(epoll_ctl)
 uv__io_start(tcp->loop, &tcp->io_watcher, POLLIN);

 return 0;
}

Libuv 层 epoll 监听与回调注册

当调用 createServer 创建服务时,底层会进入 Libuv 层的标准网络编程逻辑,此时服务正式运行。在 poll io 阶段,监听类型的文件描述符及其上下文(包含感兴趣的事件和回调等)会被注册到 epoll 中。正常情况下,程序会阻塞等待 epoll 事件。当有新的 TCP 连接到达时,epoll 首先遍历触发事件的文件描述符,随后执行该文件描述符上下文中的回调函数,即 uvserver_io。接下来我们查看 uvserver_io 的具体实现。


void uv__server_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
 // 循环处理,uv__stream_fd(stream)为服务器对应的fd
 while (uv__stream_fd(stream) != -1) {
  // 通过accept拿到和客户端通信的fd,我们看到这个fd和服务器的fd是不一样的
  err = uv__accept(uv__stream_fd(stream));
  // uv__stream_fd(stream)对应的fd是非阻塞的,返回这个错说明没有连接可用accept了,直接返回
  if (err < 0) {
   if (err == UV_EAGAIN || err == UV__ERR(EWOULDBLOCK))
    return;
  }
  // 记录下来
  stream->accepted_fd = err;
  // 执行回调
  stream->connection_cb(stream, 0);
  /*
   stream->accepted_fd为-1说明在回调connection_cb里已经消费了accepted_fd,
   否则先注销服务器在epoll中的fd的读事件,等待消费后再注册,即不再处理请求了
  */
  if (stream->accepted_fd != -1) {
   uv__io_stop(loop, &stream->io_watcher, POLLIN);
   return;
  }
 /*
   ok,accepted_fd已经被消费了,我们是否还要继续accept新的fd,
   如果设置了UV_HANDLE_TCP_SINGLE_ACCEPT,表示每次只处理一个连接,然后
   睡眠一会,给机会给其他进程accept(多进程架构时)。如果不是多进程架构,又设置这个,
   就会导致处理连接被延迟了一下
 */
  if (stream->type == UV_TCP &&
    (stream->flags & UV_HANDLE_TCP_SINGLE_ACCEPT)) {
   struct timespec timeout = { 0, 1 };
   nanosleep(&timeout, NULL);
  }
 }
}

连接接受与 C++ 层回调机制

通过 uv__server_io 的代码可知,Libuv 在一个循环中持续调用 accept 接收新的文件描述符,并执行相应回调。通常,回调函数会消费这些文件描述符,如此循环往复,直到没有更多连接需要处理。随后,我们需要重点考察回调函数内部是如何消费文件描述符的,以及这种大量循环是否会导致 Libuv 事件循环被阻塞。TCP 连接的回调逻辑位于 C++ 层的 OnConnection 函数中。


// 有连接时触发的回调
template <typename WrapType, typename UVType>
void ConnectionWrap<WrapType, UVType>::OnConnection(uv_stream_t* handle,
                          int status) {
 // 拿到Libuv结构体对应的c++层对象                          
 WrapType* wrap_data = static_cast<WrapType*>(handle->data);
 CHECK_EQ(&wrap_data->handle_, reinterpret_cast<UVType*>(handle));

 Environment* env = wrap_data->env();
 HandleScope handle_scope(env->isolate());
 Context::Scope context_scope(env->context());

 // 和客户端通信的对象
 Local<Value> client_handle;

 if (status == 0) {
  // Instantiate the client javascript object and handle.
  // 新建一个js层使用对象
  Local<Object> client_obj;
  if (!WrapType::Instantiate(env, wrap_data, WrapType::SOCKET)
       .ToLocal(&client_obj))
   return;

  // Unwrap the client javascript object.
  WrapType* wrap;
  // 把js层使用的对象client_obj所对应的c++层对象存到wrap中
  ASSIGN_OR_RETURN_UNWRAP(&wrap, client_obj);
  // 拿到对应的handle
  uv_stream_t* client = reinterpret_cast<uv_stream_t*>(&wrap->handle_);
  // 从handleaccpet到的fd中拿一个保存到client,client就可以和客户端通信了
  if (uv_accept(handle, client))
   return;
  client_handle = client_obj;
 } else {
  client_handle = Undefined(env->isolate());
 }
 // 回调js,client_handle相当于在js层执行new TCP
 Local<Value> argv[] = { Integer::New(env->isolate(), status), client_handle };
 wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv);
}

尽管相关代码较为复杂,我们只需关注 uv_accept 函数的调用。该函数接收两个参数:第一个是服务器对应的句柄,第二个是用于与客户端通信的对象句柄。


int uv_accept(uv_stream_t* server, uv_stream_t* client) {
 int err;

 switch (client->type) {
  case UV_NAMED_PIPE:
  case UV_TCP:
   // 把fd设置到client中
   err = uv__stream_open(client,
              server->accepted_fd,
              UV_HANDLE_READABLE | UV_HANDLE_WRITABLE);
   break;
 // ...
 }

 client->flags |= UV_HANDLE_BOUND;
 // 标记已经消费了fd
 server->accepted_fd = -1;
 return err;
}

文件描述符封装与 JS Socket 对象创建

uv_accept 主要执行两项逻辑:将客户端通信的文件描述符设置到 client 对象中,并标记该文件描述符已被消费,从而驱动前述的 while 循环继续执行。对于上层应用而言,这相当于获取了一个与客户端通信的对象。在 Libuv 层,这体现为一个结构体;在 C++ 层,对应一个 C++ 对象;在 JS 层,则是一个 JS 对象。这三者层层封装且相互关联,其核心在于 Libuv 的 client 结构体中持有的文件描述符,这是与客户端通信的基础。最后,回调传递至 JS 层,执行 net.js 中的 onconnection 函数。onconnection 进一步封装了一个 Socket 对象以表示与客户端的通信,该 Socket 对象持有 C++ 层对象,C++ 层对象持有 Libuv 结构体,而 Libuv 结构体最终持有文件描述符。


const socket = new Socket({
  handle: clientHandle,
  allowHalfOpen: self.allowHalfOpen,
  pauseOnCreate: self.pauseOnConnect,
  readable: true,
  writable: true
 });

至此,关于 Node.js 处理 TCP 连接核心流程的介绍已完成。如需了解更多相关内容,建议搜索本站过往文章或继续浏览后续文章,感谢大家的支持。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章