TinyReactor 是一个基于 Reactor 模式 实现的轻量级 C++ Web 服务器项目。
目标不是简单复刻,而是以工程化方式从零手写,在实践中学习 Linux 系统编程、网络编程、并发编程等核心知识。
- 语言:C++17
- 构建:CMake
- 开发环境:Docker(Ubuntu 22.04)
- 编译器:g++ 11.4.0
- 数据库:MySQL 8.0
本项目全程在 Docker 容器中开发,保证跨平台环境一致。
# 启动容器
docker compose up -d
# 进入开发容器
docker exec -it tinyreactor_dev bash
# 编译
cd /workspace/build
cmake ..
make
# 运行
./server## 目录结构
```text
TinyReactor/
├── include/ # 头文件
│ ├── buffer.h
│ ├── blockdeque.h
│ ├── log.h
│ ├── threadpool.h
│ ├── heaptimer.h
│ ├── sqlconnpool.h
│ ├── sqlconnRAII.h
│ ├── httprequest.h
│ ├── httpresponse.h
│ ├── httpconn.h
│ ├── userservice.h
│ ├── epoller.h
│ └── webserver.h
├── src/ # 实现文件
│ ├── buffer.cpp
│ ├── log.cpp
│ ├── heaptimer.cpp
│ ├── sqlconnpool.cpp
│ ├── httprequest.cpp
│ ├── httpresponse.cpp
│ ├── httpconn.cpp
│ ├── userservice.cpp
│ ├── epoller.cpp
│ ├── webserver.cpp
│ └── main.cpp
├── resources/ # 静态资源目录
├── log/ # 运行时日志文件
├── CMakeLists.txt
├── Dockerfile
└── docker-compose.yml
基于 std::vector<char> 实现的动态字节缓冲区,用 readPos_ 和 writePos_ 两个位置标记把连续内存逻辑上划分为三段:已读区、可读区、可写区。
核心设计:
Append()往尾部追加数据,空间不足时自动扩容或整理碎片(MakeSpace)Retrieve()消费数据,本质是移动readPos_,不做内存拷贝ReadFd()使用readv分散读,栈上临时缓冲区兜底,一次 syscall 读尽数据WriteFd()把可读区数据直接write到 fd
在 Log 模块中作为格式化工作台复用,每条日志拼好后整体取走,Buffer 清空备用。
模板类,std::deque<T> 加锁封装,实现生产者/消费者模型。
核心设计:
- 两个
condition_variable分别管理"不空"和"不满"两个等待条件,职责清晰 push_back()用unique_lock+wait,队满自动阻塞pop()返回std::optional<T>(C++17),队空阻塞,队列关闭返回std::nullopt,比输出参数写法更现代Close()先加锁置关闭标志,再notify_all唤醒所有阻塞线程安全退出- 显式禁用拷贝构造和拷贝赋值(内部持有 mutex,不可拷贝)
单例模式,支持同步和异步两种写入模式,对业务线程无感知。
核心设计:
- 异步模式:业务线程调
write()只把格式化好的字符串推进BlockDeque,立即返回;后台写线程AsyncWrite_()循环消费队列写文件,两者完全解耦 - 同步模式:
maxQueueSize = 0时不创建队列和写线程,直接fputs写文件 write()内部用Buffer拼装完整日志行(时间戳 + 级别前缀 + 用户消息),vsnprintf返回值做钳制处理,防止越界- 时间函数使用
localtime_r(线程安全),而非标准库的localtime - 按天自动切割日志文件,单文件超过 50000 行时按编号新建
- 对外暴露四个宏
LOG_DEBUG / LOG_INFO / LOG_WARN / LOG_ERROR,用do { } while(0)包裹保证在任意语法位置安全展开
日志格式:
2026-04-22 08:50:44.472649 [info] : server starting, port = 1316
基于生产者/消费者模型实现的固定大小线程池,所有工作线程共享一个任务队列。
核心设计:
- 用
struct Pool把锁、条件变量、关闭标志、任务队列打包,通过std::shared_ptr<Pool>在线程池对象和所有工作线程之间共享,保证线程池析构后工作线程仍能安全访问数据直到真正退出 - 工作线程逻辑直接写在构造函数的 lambda 里:持锁等待 → 有任务则取出解锁执行 → 执行完重新加锁回到等待;锁只保护队列操作,执行任务期间不持锁,保证并发
AddTask()用万能引用F&&+std::forward完美转发,支持 lambda、函数指针等所有可调用对象,零拷贝传入队列- 工作线程用
detach独立运行,生命周期由shared_ptr引用计数保证 - 纯头文件实现(
threadpool.h),无需.cpp
基于最小堆实现的定时器,管理所有连接的超时,超时的连接自动触发回调关闭。
核心设计:
- 用
std::vector<TimerNode>数组模拟最小堆,堆顶永远是最快过期的连接,tick()只需看堆顶,不用遍历全部 - 用
std::unordered_map<int, size_t>哈希表记录 fd 到堆中下标的映射,update()刷新某个连接的超时时间时 O(1) 定位,不需要遍历 add()新连接进来挂号登记,update()有数据来了把闹钟往后拨,tick()定期检查超时触发回调,del_()支持删除堆中任意位置节点(正常完成的连接主动删除)getNextTick()返回距离下一个超时的毫秒数,直接传给epoll_wait的超时参数,让事件循环到时间自动醒来执行tick()- 节点删除用"换到末尾再 pop_back"的方式,避免数组中间删除的移位开销,换上来的节点做 siftDown/siftUp 重新调整位置
基于信号量和互斥锁实现的数据库连接池,预先建立固定数量的 MySQL 连接复用,避免每次请求都重新建连的开销。
核心设计:
- 单例模式,全局唯一一个连接池实例
- 用
std::queue<MYSQL*>存放空闲连接,sem_t信号量记录可用连接数量,取连接时sem_wait自动阻塞,还连接时sem_post自动唤醒等待线程 GetConn()处理信号量被系统信号打断的情况(EINTR重试),以及队列意外为空时回滚信号量防止计数错乱ClosePool()关闭所有连接后在锁外调mysql_library_end(),不把全局清理操作放在锁保护范围内
配套 SqlConnRAII:RAII 包装器,构造时自动调 GetConn() 取连接,析构时自动调 FreeConn() 还连接,禁用拷贝和移动,保证连接不会被重复归还或泄漏。任何代码路径退出作用域都能安全归还连接。
HTTP 模块已经完成静态 GET、POST 表单解析、登录注册路由和 MySQL 业务接入。
核心设计:
HttpRequest负责解析请求行、请求头、请求体,支持application/x-www-form-urlencoded表单解析HttpResponse负责生成状态行、响应头、Content-Type、Content-Length,并通过mmap映射静态文件HttpConn负责单个连接的读写流程,串联Read -> Parse -> HandleRequest -> MakeResponse -> WriteUserService负责登录 / 注册业务,通过SqlConnRAII从连接池取连接并访问 MySQL- 已通过
socketpair测试完整链路:POST 请求 -> HttpConn -> UserService -> MySQL -> welcome/error 页面响应
对 Linux epoll 做轻量 RAII 封装,作为 WebServer 的底层事件通知器。
核心设计:
- 构造时通过
epoll_create1(0)创建 epoll 实例,析构时自动close AddFd / ModFd / DelFd分别封装EPOLL_CTL_ADD / MOD / DELWait()封装epoll_wait,返回本轮就绪事件数量GetEventFd()和GetEvents()用于获取就绪 fd 以及对应事件类型- Epoller 只负责事件通知,不负责 accept、读写 socket、HTTP 解析、连接超时或线程调度
已通过 socketpair 最小测试验证:向一端 fd 写入数据,另一端 fd 触发 EPOLLIN,Wait / GetEventFd / GetEvents 能正确返回。
WebServer 负责把前面所有模块组装成完整服务器,完成监听端口、接收连接、事件分发、线程池处理、定时器超时清理和 HTTP 响应写回。
核心设计:
socket / bind / listen初始化监听 socket,并设置SO_REUSEADDR- 监听 fd 和客户端 fd 都设置为非阻塞,配合 epoll ET 模式使用
Epoller负责事件通知,WebServer根据EPOLLIN / EPOLLOUT / EPOLLERR分发处理HttpConn负责单连接的Read / Process / WriteThreadPool执行读写和请求处理任务,主线程专注事件循环HeapTimer管理连接超时,连接正常关闭时主动移除 timer- 支持静态 GET、POST 登录注册、keep-alive 和连接关闭
已通过真实 TCP 请求验证:GET /、GET /login.html、Connection: keep-alive、POST /register.html、POST /login.html。
| 顺序 | 模块 | 状态 |
|---|---|---|
| 1 | Buffer | ✅ 完成 |
| 2 | Log | ✅ 完成 |
| 3 | ThreadPool | ✅ 完成 |
| 4 | HeapTimer | ✅ 完成 |
| 5 | MySQL 连接池 | ✅ 完成 |
| 6 | HTTP 请求解析 | ✅ 完成 |
| 7 | HTTP 响应封装 | ✅ 完成 |
| 8 | HTTP 连接处理 | ✅ 完成 |
| 9 | 登录注册业务 | ✅ 完成 |
| 10 | Epoller | ✅ 完成 |
| 11 | WebServer | ✅ 完成 |
GET /:返回首页静态资源GET /login.html:返回登录页面Connection: keep-alive:同一 TCP 连接连续请求成功POST /register.html:注册链路接入 MySQLPOST /login.html:登录成功返回 welcome 页面
整体架构思路参考 markparticle/WebServer,代码基于 C++17 自行实现,结合工程规范做了调整和改进。