共计 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
代码看起来很复杂,我们只需要关注 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 连接的核心流程的文章就介绍到这了,感谢大家的支持。
![post-qrcode](https://www.xgss.net/wp-content/uploads/2023/07/二维码-微信公众号256x256.jpg)