TinyWebServer源码解析——Reactor和Proactor双模式web组件
项目地址
https://github.com/qinguoyi/TinyWebServer.git
尊重开源精神,保障作者权益,内容均为原创,如有雷同,纯属巧合
本文件是将TinyWebSever其他组件串联起来的文件,笔者水平有限,仅能挖出这些技术栈,以供讨论,仅个人观点,如有错误,还望海涵!
定义了一个WebServer类
通过整个服务器程序的主控逻辑与核心调度模块,实现了一个高性能、可配置、支持 Reactor/Proactor 双模式的微型 Web 服务器
封装内容:
- 构造函数初始化所有成员为安全初值
- 析构函数释放所有动态资源
- void init()函数接收用户配置参数
- void trig_mode()函数设置epoll触发模式
- void log_write()函数初始化日志系统
- void sql_pool()函数创建MYSQL连接池
- void thread_pool()函数创建线程池,后续HTTP请求对象提交给线程池异步处理
- void eventListen()函数完成网络监听准备
- void eventLoop()函数主事件循环
- bool dealclientdata()函数处理新客户端连接
- bool dealwithsignal()函数从信号管道读取信号
- void dealwithread()函数处理可读事件
- void dealwithwrite()函数处理可写事件
- void timer()函数为新连接创建定时器
核心函数:
voidinit(int port , string user, string passWord, string databaseName,int log_write ,int opt_linger,int trigmode,int sql_num,int thread_num,int close_log,int actor_model);- init()函数初始化端口,数据库,日志,触发模式,线程数,其否启用Reactor模式
- void trig_mode()设置listenfd和connfd的epoll触发模式
- log_write()初始化日志系统(单例 Log)
- sql_pool():创建 MySQL 连接池,并初始化用户表缓存
- thread_pool()创建
threadpool<http_conn>,用于异步处理请求 - eventListen()创建监听 socket、epoll、信号管道、设置信号处理
- void eventLoop()网络事件循环,基于epoll_wait实现高性能I/O多路复用
| 事件类型 | 处理函数 | 说明 |
|---|---|---|
新连接 (m_listenfd) | dealclientdata() | 接受新客户端,初始化 http_conn 和定时器 |
信号通知 (m_pipefd[0]) | dealwithsignal() | 处理 SIGALRM(定时)和 SIGTERM(退出) |
可读事件 (EPOLLIN) | dealwithread() | 读取 HTTP 请求 |
可写事件 (EPOLLOUT) | dealwithwrite() | 发送 HTTP 响应 |
- m_actormodel模式处理
- m_actormode == 0 时(Proactor模式)
- 主线程完成完整I/O操作
- 将已读取/待发送的http_conn对象放入线程池,仅执行process()
- m_actormode == 1 时(Reactor模式)
- 主线程只负责事件通知
- 线程池的工作线程执行实际I/O+process
- m_actormode == 0 时(Proactor模式)
- 定时器管理:
- 使用基于链表的定时器
- 每个新连接创建一个定时器
- 每次读写成功则adjust_timer()延长超时时间
- 超时或出错就调用deal_timer()
| 类别 | 函数 | 功能 |
|---|---|---|
| 生命周期 | 构造 / 析构 | 资源初始化与释放 |
| 初始化 | init, trig_mode, log_write, sql_pool, thread_pool, eventListen | 配置与启动准备 |
| 主循环 | eventLoop | 驱动整个服务器运行 |
| 事件处理 | dealclientdata, dealwithsignal, dealwithread, dealwithwrite | 响应 epoll 事件 |
| 定时器 | timer | 为连接设置超时 |
| 工具 | addfd, modfd, show_error | 辅助 I/O 与错误处理 |
触发模式:
- LT(水平触发):epoll默认模式
- 只要fd处于就绪状态就会持续通知
- ET(边缘触发):
- 只当fd状态改变时,触发一次
- 需配合非阻塞I/O+循环读写
- 循环读写一般使用while循环,不用for循环是为了避免虚假唤醒
- 参数组合
// trigmode 参数决定组合: 0: LISTEN_LT + CONN_LT (最安全) 1: LISTEN_LT + CONN_ET (常用) 2: LISTEN_ET + CONN_LT 3: LISTEN_ET + CONN_ET (最高性能,但 listenfd 也需循环 accept) Reactor模式
- 创建socket
- 绑定地址与端口
- 开始监听
- epoll
- 事件处理
- 当事件循环发现fd就绪后,执行相应的义务逻辑
- 常见类型
事件循环
while(!stop_server){int n =epoll_wait(epollfd, events, MAX_EVENTS,-1);for(int i =0; i < n; i++){int fd = events[i].data.fd;if(fd == listenfd){// 处理新连接 }elseif(events[i].events & EPOLLIN){// 处理可读事件 }elseif(events[i].events & EPOLLOUT){// 处理可写事件 }}}网络监听
int listenfd =socket(AF_INET, SOCK_STREAM,0);bind(listenfd,(structsockaddr*)&addr,sizeof(addr));listen(listenfd,5);// 5 是 backlog,等待队列长度
| 事件类型 | 触发条件 | 处理动作 |
|---|---|---|
| 新连接 | listenfd 可读 | 调用 accept() 获取 connfd,初始化连接对象 |
| 可读事件(EPOLLIN) | 客户端发送了数据 | 调用 read() 读取 HTTP 请求 |
| 可写事件(EPOLLOUT) | socket 发送缓冲区有空位 | 调用 write() 发送 HTTP 响应 |
| 错误/关闭 | 对端关闭连接或出错 | 清理资源,关闭 fd |
- 信号是异步的
- 创建一个 socketpair 或 pipe(两个 fd,一端写,一端读)。
- 注册读端到
epoll。 - 在信号处理函数中,只向写端写一个字节(如
'1')。 - 主循环的
epoll_wait会检测到读端可读,从而在主线程安全上下文中处理信号。 - 将异步信号转换为同步I/O
- 可读事件和可写事件
- 可读事件:
- fd的接收缓冲区有数据可读
- 调用read()或recv()读取数据
- 可写事件:
- fd的发送缓冲区由空闲空间,可以写入数据
- 调用write()或send()发送数据
- 可读事件:
信号管道
// 初始化 socketpair(PF_UNIX, SOCK_STREAM,0, m_pipefd);// 信号处理函数voidaddsig(int sig,void(handler)(int)){structsigaction sa; sa.sa_handler = handler;sigaction(sig,&sa,NULL);}// SIGALRM 处理函数 voidalarm_handler(int sig){send(m_pipefd[1],"1",1,0);// 只写1字节! }// 主循环中 if(sockfd == m_pipefd[0]){dealwithsignal();// 安全地读取并处理信号 }新连接
信号
connfd 可读
connfd 可写
yes
yes
启动服务器
创建 listenfd 并监听
创建 epoll + 信号管道
注册 listenfd 和 pipefd 到 epoll
进入 eventLoop
epoll_wait 返回事件?
dealclientdata: accept + 初始化 connfd
dealwithsignal: 读管道,设置 timeout/stop
dealwithread: 读 HTTP 请求
dealwithwrite: 发 HTTP 响应
将 connfd 加入 epoll 监听
提交任务到线程池处理请求
发送响应,若未发完则重注册 EPOLLOUT
生成响应内容
是否timeout
关闭超时连接
是否stop_server
退出循环,清理资源