阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

简单总结nodejs处理tcp连接的核心流程

33次阅读
没有评论

共计 4012 个字符,预计需要花费 11 分钟才能阅读完成。

导读 这篇文章主要介绍了 nodejs 处理 tcp 连接的核心流程, 本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

前几天和一个小伙伴交流了一下 nodejs 中 epoll 和处理请求的一些知识,今天简单来聊一下 nodejs 处理请求的逻辑。我们从 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;
}

我们看到,当我们 createServer 的时候,到 Libuv 层就是传统的网络编程的逻辑。这时候我们的服务就启动了。在 poll io 阶段,我们的监听型的文件描述符和上下文(感兴趣的事件、回调等)就会注册到 epoll 中。正常来说就阻塞在 epoll。那么这时候有一个 tcp 连接到来,会怎样呢?epoll 首先遍历触发了事件的 fd,然后执行 fd 上下文中的回调,即 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 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);
  }
 }
}

从 uv__server_io,我们知道 Libuv 在一个循环中不断 accept 新的 fd,然后执行回调,正常来说,回调会消费 fd,如此循环,直到没有连接可处理了。接下来,我们重点看看回调里是如何消费 fd 的,大量的循环会不会消耗过多时间导致 Libuv 的事件循环被阻塞一会。tcp 的回调是 c ++ 层的 OnConnection。

// 有连接时触发的回调
template 
void ConnectionWrap::OnConnection(uv_stream_t* handle,
                          int status) {
 // 拿到 Libuv 结构体对应的 c ++ 层对象                          
 WrapType* wrap_data = static_cast(handle->data);
 CHECK_EQ(&wrap_data->handle_, reinterpret_cast(handle));
 
 Environment* env = wrap_data->env();
 HandleScope handle_scope(env->isolate());
 Context::Scope context_scope(env->context());
 
 // 和客户端通信的对象
 Local client_handle;
 
 if (status == 0) {
  // Instantiate the client javascript object and handle.
  // 新建一个 js 层使用对象
  Local 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(&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 argv[] = { Integer::New(env->isolate(), status), client_handle };
 wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv);
}

代码看起来很复杂,我们只需要关注 uv_accept。uv_accept 的参数,第一个是服务器对应的 handle,第二个是表示和客户端通信的对象。

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;
}

uv_accept 主要就是两个逻辑,把和客户端通信的 fd 设置到 client 中,并标记已经消费,从而驱动刚才讲的 while 循环继续执行。对于上层来说,就是拿到了一个和客户端的对象,在 Libuv 层是结构体,在 c ++ 层是一个 c ++ 对象,在 js 层是一个 js 对象,他们三个是一层层封装且关联起来的,最核心的是 Libuv 的 client 结构体中的 fd,这是和客户端通信的底层门票。最后回调 js 层,那就是执行 net.js 的 onconnection。onconnection 又封装了一个 Socket 对象用于表示和客户端通信,他持有 c ++ 层的对象,c++ 层对象又持有 Libuv 的结构体,Libuv 结构体又持有 fd。

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

到此这篇关于 nodejs 处理 tcp 连接的核心流程的文章就介绍到这了,感谢大家的支持。

阿里云 2 核 2G 服务器 3M 带宽 61 元 1 年,有高配

腾讯云新客低至 82 元 / 年,老客户 99 元 / 年

代金券:在阿里云专用满减优惠券

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-07-25发表,共计4012字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中