导读 | 这篇文章主要介绍了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 < 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); } } }
从uv__server_io,我们知道Libuv在一个循环中不断accept新的fd,然后执行回调,正常来说,回调会消费fd,如此循环,直到没有连接可处理了。接下来,我们重点看看回调里是如何消费fd的,大量的循环会不会消耗过多时间导致Libuv的事件循环被阻塞一会。tcp的回调是c++层的OnConnection。
// 有连接时触发的回调 templatevoid 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连接的核心流程的文章就介绍到这了,感谢大家的支持。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/125538.html