Reactor、Connection、Accept、EventLoop模块关系梳理
总体逻辑总结
- 主Reactor负责监听新连接,监听到的新连接分发给子Reactor
- 子Reactor则通过EventLoop和Poller处理具体的读写异常事件
- Channel负责将套接字的事件封装并注册到EventLoop中
- Buffer和Connection负责处理数据,负责数据的接收发送以及协议解析
- TimerQueue管理定时任务,确保EventLoop可以执行定时任务,如果连接超时则进行释放
主Reactor与子Reactor之间交互(主Reactor接收新连接并通知子Reactor)
- Acceptor使用监听套接字等待新的连接请求
- 新连接到来后,Acceptor创建一个新的Socket来处理该连接
- Acceptor为新的Socket创建一个Channel,同时将其注册到EventLoop 中 &
- EventLoop(主Reactor)将新连接的处理任务,分发到子Reactor中,并向子Reactor的eventfd中写入数据,唤醒子Reactor处理新连接的到来
子Reactor处理连接逻辑
- 子Reactor的EventLoop通过poller等待关心的事件发生
- 当poller监控的事件发生时,通知该连接对应的Channel
- Channel则执行注册的回调函数,处理具体的读写操作
- Connection模块则处理数据的接收、发送以及协议解析
定时任务处理逻辑
- EventLoop中包含有TimerQueue,专门负责管理定时任务
- 定时任务到期后,TimerQueue通知EventLoop模块执行任务,处理掉超时连接
各个模块主要作用总结
- 主Reactor:运行在主线程中,负责监听新连接,通过eventfd来通知子Reactor处理新连接
- 从Reactor:独立线程中运行,通过EventLoop和Poller处理具体事件
- EventLoop:Reactor的核心,通过Poller监控事件,Channnel则负责具体处理事件
- Poller:负责管理某个Reactor中所有的CHannel
- Channel:封装文件描述符以及对应处理方法,通过回调函数来处理I/O操作
主从Reactor服务器通信逻辑分析
总体概述(下面展开说明具体步骤)
- 初始化TcpServer:设置通信端口,创建Acceptor 和 LoopThreadPoll
- 启动TcpServer:创建从Reactor线程池,同时启动主Reactor的事件循环
- 接收新连接:Accept接收新连接,然后调用TcpServer::NewConnection函数
- 分发新连接:Tcpserver::NewConnection创建新的Connection对象,并将新创建的连接分发给从Reactor中
- 初始化:通过连接模块中的Established函数,设置连接状态,并启动读事件监控
- 处理读事件:当有数据可读时,epoll通知相应的EventLoop,然后调用Connection::HandleRead处理数据
1. 初始化TcpServer
- 构造函数接收到服务器设置的端口号后
- 初始化Acceptor 和 LoopThreadPool对象
- 设置主Reactor接收客户端连接后的执行逻辑,通过_accept.SetAcceptCallback设置连接回调函数为TcpServer::NewConnection(也就是把Accept类中的_accept_callback回调函数设置成了TCPServer对象的新连接处理函数)
- 调用_acceptor.Listen()函数启动监听套接字的读事件监控
Acceptor::Acceptor
- Acceptor构造函数中,会创建一个服务器套接字,然后初始化一个Channel对象
- 然后利用_channel.SetReadCallback设置读事件回调函数
2. 启动TcpServer(创建主从Reactor)
- _poll.Create():创建从Reactor线程池
- _baseloop.Start():启动主Reactor的事件循环机制
从属Reactor线程池:创建线程池后,为每一个线程创建LoopThread对象后,然后启动线程
LoopThread中利用ThreadEntry方法创建EventLoop对象,然后启动事件循环,该方法也是从Reactor线程的入口函数(为防止多线程争夺资源,此处需要用锁)
EventLoop::Start:启动事件循环,通过poller获取就绪事件,然后调Channel::HandleEvent函数处理这些事件,然后执行任务队列的所有任务
Poll方法则是利用epoll_wait系统调用,获取就绪事件,然后将这些事件存储在active列表中
3. 接收新连接
- 客户端发起对服务端的连接,新连接到达后,Accept则触发读事件,然后调用之前设置的回调函数HandleRead方法处理新连接
- HandleRead则通过accept获取新的连接,同时调用_accept_callback回调函数处理新连接(也就是执行了新连接到来后的处理逻辑)
accept系统调用获取新连接
4. 分发新连接
- NewConnection方法创建新Connection对象,然后通过LoopThreadPool::NextLoop函数,从池子里选择一个从Reactor的EventLoop对该连接进行管理
- 设置该连接的各种回调函数
5. 选择一个从Reactor处理连接
- NextLoop方法实现轮询调度,从线程池的中选择下一个EventLoop
6. 初始化连接Connection
(注意,初始化连接操作是在分发连接之前完成的,单独分析)
- 初始化各种变量过程不解释,重点分析Established函数的运行逻辑
- Established函数,将EstablishedInLoop方法包装秤一个任务,同时通过EventLoop::RunInLoop方法执行
RunInLoop函数,检查当前是否在EventLoop线程中,如果在当前线程中,则直接执行回调函数cb,如果不在线程中,则将回调函数放入任务队列中
QueueInLoop函数将回调函数添加到任务队列_tasks中,然后通过WeakUpEventFd方法唤醒EventLoop(也就是唤醒其他线程处理任务)
7. 处理连接
(也是在初始化连接步骤完成,同时结合第6步一同分析)
EnableRead函数将读事件添加到_events中,然后调用updata方法进行监控。首先是利用updata函数将channel对象更新到EventLoop中,然后将channel对象添加到epoll中进行管理
8. 事件循环处理连接的读事件
- 当新连接有数据可读的时候,epoll会通知对应的EventLoop
- 然后调用CHannel的HandleEvent方法进行处理
Channel类作用分析
Channel类与Reactor中其他模块关系梳理
Reactor中EventLoop、Channel、Poller三者关系总结
- EventLoop类通过Poller类管理Channel的事件监控,当事件触发的时候调用Channel设置的回到函数
- Poller通过epoll监控文件描述符事件,也就是需要关心的读写事件,有事件响应后则就将事件分发给Channel进行处理
新连接到来后Channel创建过程分析
接收新连接:通过accept获取一个新的文件描述符
EventLoop将CHannel注册到Poller中,监控对应的事件
Poller中如果发现监控的事件发生时,则通知对应的Channel来处理事件
CHannel根据触发事件类型,调用相应回调函数进行处理
就绪事件处理逻辑
poller类检测到事件发生后,并将其设置为就绪事件
EventLoop获取并处理就绪事件
channel 处理已就绪事件,与上面逻辑相同
eventfd系统调用(线程通知)
- 参数
- initval:文件描述符初始值,通常都是设置为0
- flags:下列参数支持 按位或操作
- EFD_SEMAPHORE:启动信号量
- EFD_NONBLOCK:文件描述符非阻塞状态
- EFD_CLOEXEC:在执行exec()的时候关闭文件描述符
- 返回值
- 成功:新的文件描述符
- 失败:返回-1
eventfd实现线程通知实验代码
#include <sys/eventfd.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <pthread.h>
int efd;
void *thread_func(void *arg) {
uint64_t u;
ssize_t s;
printf("Thread: Waiting for event...\n");
s = read(efd, &u, sizeof(uint64_t));
if (s != sizeof(uint64_t)) {
perror("read");
return NULL;
}
printf("Thread: Received event, counter = %llu\n", (unsigned long long) u);
return NULL;
}
int main() {
pthread_t t;
efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (efd == -1) {
perror("eventfd");
return -1;
}
if (pthread_create(&t, NULL, thread_func, NULL) != 0) {
perror("pthread_create");
return -1;
}
sleep(1); // Simulate work
printf("Main: Triggering event\n");
uint64_t u = 1;
ssize_t s = write(efd, &u, sizeof(uint64_t));
if (s != sizeof(uint64_t)) {
perror("write");
return -1;
}
pthread_join(t, NULL);
close(efd);
return 0;
}
内部实现了解
- eventfd内部有一个计数器,当调用write的时候,计数器增加,当调用read的时候,计数器则会减少。如果计数器为0,则读操作会阻塞,直到有数据写入。
- 一个线程可以在eventfd上等待事件,当另外一个线程向eventfd中写入数据的时候,等待的线程会被唤醒,从而实现线程之间通信,也可以用于一个Reactor唤醒另一个Reactor
eventfd与Reactor模式
含义
- Linux内部提供的一种机制,主要用于进程或者线程之间的事件通知
主要作用总结
- 事件通知:在事件循环机制中,eventfd会创建一个文件描述符,然后向该文件描述符写入数据,触发事件通知
- 唤醒事件循环:当EventLoop在等待某件事件发生的时候,例如等待I/O或者定时器事件,如果需要从其他线程中向EventLoop中添加任务或者事件。此时就可通过eventfd创建的文件描述符中写入数据来唤醒EventLoop,从而可以及时处理新的任务。
- 线程间通信:通过该系统调用返回的文件描述,一个线程向该文件描述符中写入消息,通知运行在另一个线程的EventLoop执行某些操作
主从Reactor服务器中eventfd作用:确保EventLoop可以及时响应新连接或者其他需要立即处理的任务,通过eventfd的唤醒机制。(可以简单理解为主Reactor唤醒从Reactor的一种方式,主要还是怕从Reactor偷懒)