在前两篇中大致介绍了使用Reactor线程模型和epoll机制实现的高性能网络组件的思路。在我们产品中使用的思路与其类似,不过是基于select机制实现的。特别是从上一篇文章中知道了,main eventloop thread用于监听端口,处理accept事件。sub eventloop thread用于处理IO读写事件。本文一起来看以下几个问题如何解决:


  • sub eventloop没有读写事件,可能阻塞在dispatcher内的epoll_wait函数上,此时main eventloop接收了新连接,如何通知sub eventloop进行注册到自己的dispatcher上?


  • 连接事件和IO读写事件如何区分?下面,我们将看看如何解决这些问题。


1、主线程注册listen fd

     main eventloop 首先会将自己的listen fd包装成channel,注册到epoll_dispatcher上面,代码如下:

//开启监听void tcp_server_start(struct TCPserver *tcpServer) {    struct acceptor *acceptor = tcpServer->acceptor;    struct event_loop *eventLoop = tcpServer->eventLoop;    //开启多个线程    thread_pool_start(tcpServer->threadPool);    //acceptor主线程, 同时把tcpServer作为参数传给channel对象   struct channel *channel=channel_new(acceptor->listen_fd,EVENT_READ,                        handle_connection_established, NULL,                       tcpServer);   event_loop_add_channel_event(eventLoop, channel->fd, channel);    return;}
       从上面的代码可以看到,main eventloop在调用thread_pool_start启动其他线程后,会将自己的listen socket包装为一个新的channel,然后添加到自己的线程链表中。event_loop_add_channl_event函数就是由main函数所在的线程进行处理并注册到main-eventloop的epoll_dispatcher上面交给epoll去监听socket fd列表。

    其中,我们尤为关注的是回调函数handle_connection_established函数,这里主要是调用accept函数,从listen的socket上获得新连接,获得的新连接包装成一个事件添加到子线程的eventloop中。
int handle_connection_established(void *data) {    //省略其余代码  int connected_fd = accept(listenfd,(struct sockaddr *)&client_addr, &client_len);  make_nonblocking(connected_fd); //从线程池中选取一个eventloop,将accept后的socket交给新的eventloop处理  struct event_loop *eventLoop =thread_pool_get_loop(tcpServer->threadPool); //省略其余代码......  return 0;}

    我们看event_loop_add_channel_event函数,如何将新得到的socket封装成channel注册到子线程中。这里需要注意,此时的运行线程还在main函数中,因此将先获得main eventloop线程的锁,然后将socket设置为非阻塞。

pthread_mutex_lock(&eventLoop->mutex);//获取main-eventloop的锁assert(eventLoop->is_handle_pending == 0);event_loop_channel_buffer_nolock(eventLoop, fd, channel1, type);pthread_mutex_unlock(&eventLoop->mutex);// 释放锁if (!isInSameThread(eventLoop)){    event_loop_wakeup(eventLoop);} else {    event_loop_handle_pending_channel(eventLoop); // 执行这里}
     event_loop_channel_buffer_nolock函数将当前新连接的socket包装成channel,然后放到子线程的eventloop中的链表上,此时子线程还没处理。因为这里还是main函数线程在执行,因此将进入event_loop_handle_pending_channel函数。该函数的作用就是最终将socket注册到自己的epoll上进行事件监听。到这里第一次main eventloop的初始化工作结束,然后进入event_loop_run死循环中。
     如果main-eventloop收到了连接事件,同样会将新连接包装成channel,并将该事件放到sub eventloop的事件列表,然后调用event_loop_wakeup去唤醒子线程,这里就涉及到如何唤醒子线程了。


2、main eventloop通知sub eventloop注册

     由于一开始创建main eventloop和sub eventloop的时候,创建了一对socket_pair,类似于管道。此时假设需要唤醒子线程,只需要写入一个字节。这样子线程就会收到读写事件,从epoll wait处进行返回。

void event_loop_wakeup(struct event_loop *eventLoop) {    char one = 'a';    ssize_t n = write(eventLoop->socketPair[0], &one, sizeof one);     if (n != sizeof one) {        LOG_ERR("wakeup event loop thread failed");    }}
    子线程被唤醒,此时从epoll wait处收到读事件,将通过回调函数handle_wakeup处理一下。
int handleWakeup(void *data) {    struct event_loop *eventLoop = (struct event_loop *) data;    char one;    ssize_t n = read(eventLoop->socketPair[1], &one, sizeof one);     if (n != sizeof one) {        LOG_ERR("handleWakeup  failed");    }}
       处理完该读写事件,该线程将继续判断事件链表是否为空,如果不为空,则循环处理事件链表上的事件,将新连接的socket fd注册到自己的dispatcher上,利用epoll机制来得到IO读写事件。

     最后,我们再回到epoll_dispatcher的dispatch方法看看:
int epoll_dispatch(struct event_loop *eventLoop, struct timeval *timeval) {    epoll_dispatcher_data *epollDispatcherData = (epoll_dispatcher_data *) eventLoop->event_dispatcher_data;    int i, n;    n = epoll_wait(epollDispatcherData->efd, epollDispatcherData->events, MAXEVENTS, -1);    for (i = 0; i < n; i++) {        if ((epollDispatcherData->events[i].events & EPOLLERR) || (epollDispatcherData->events[i].events & EPOLLHUP)) {            fprintf(stderr, "epoll error\n");            close(epollDispatcherData->events[i].data.fd);            continue;        }        //连接事件/读事件       if (epollDispatcherData->events[i].events & EPOLLIN) {            channel_event_activate(eventLoop, epollDispatcherData->events[i].data.fd, EVENT_READ);        }        // 写事件       if (epollDispatcherData->events[i].events & EPOLLOUT) {            channel_event_activate(eventLoop, epollDispatcherData->events[i].data.fd, EVENT_WRITE);        }    }    return 0;}
        通过这个方法,可以看到,如果是listen fd,那么新连接是EPOLLIN事件,此时会回调到初始化注册的handle_connection_established回调函数上。如果是connect socket,那么这个EPOLLIN注册的是读写事件,将会处理socket的数据读写。


3、总结

    我们将整个流程大致画一个流程图,如下所示:

记一个高性能网络组件的实现(下)_java

      首先,main Eventloop首先将listen fd封装成一个channel,然后注册到自己的dispatcher上,当有epoll in事件的时候,其中回调函数handle_connection_established将会被调用。该函数内部将调用accept获得一个新的socket,然后将该socket fd封装为新的channel,然后从sub Eventloop上选择一个eventloop线程,并将新的channel作为事件添加到该eventloop的事件列表上。             同时,当main eventloop唤醒sub eventloop,主要是向其初始化创建的socket pair上写入一个字符,从而促发sub eventloop收到epoll in事件,然后唤醒了去处理自己的事件链表,当发现了链表不为空,则存在事件需要处理。因此,此时将该socket fd调用epoll dispatcher中的add回调方法注册到自己的dispatcher上面。