目录
- 一、项目概述
-
- [1.1 项目简介](#1.1 项目简介)
- [1.2 技术栈](#1.2 技术栈)
- [1.3 项目亮点](#1.3 项目亮点)
- [1.4 技术架构图](#1.4 技术架构图)
- [1.5 请求处理流程](#1.5 请求处理流程)
- [1.6 项目结构](#1.6 项目结构)
- [1.7 编译与运行](#1.7 编译与运行)
- 1.8性能测试
- 二、技术架构
-
- [2.1 简单概述](#2.1 简单概述)
- [2.2 架构图](#2.2 架构图)
- [2.3 模块职责](#2.3 模块职责)
- [2.4 四个关键设计决策](#2.4 四个关键设计决策)
- [2.5一个 HTTP 请求的完整流程](#2.5一个 HTTP 请求的完整流程)
- 三、核心模块详解
-
- [3.1 线程池 ------ ThreadPool](#3.1 线程池 —— ThreadPool)
- [3.2 事件循环 ------ EventLoop](#3.2 事件循环 —— EventLoop)
- [3.3 缓冲区 ------ Buffer](#3.3 缓冲区 —— Buffer)
- [3.4 TCP 连接 ------ TcpConnection](#3.4 TCP 连接 —— TcpConnection)
- [3.5 HTTP 协议解析 ------ HttpParser](#3.5 HTTP 协议解析 —— HttpParser)
- [3.6 服务器主类 ------ HttpServer](#3.6 服务器主类 —— HttpServer)
- 模块依赖关系
- 四、请求处理全流程
-
- [4.1 流程总览](#4.1 流程总览)
- [4.2 分布详解](#4.2 分布详解)
- [4.3 流程时序图](#4.3 流程时序图)
- [4.4 核心设计要点](#4.4 核心设计要点)
- [4.5 如果改成主从 Reactor](#4.5 如果改成主从 Reactor)
- 五、踩坑记录
-
- [5.1 编译阶段](#5.1 编译阶段)
- [5.2 运行时问题](#5.2 运行时问题)
- [5.3 设计与理解](#5.3 设计与理解)
- 六、性能测试
-
- 6.1测试环境
- [6.2 安装测试工具](#6.2 安装测试工具)
- [6.3 开始测试](#6.3 开始测试)
- [6.4 性能瓶颈](#6.4 性能瓶颈)
- [6.5 注意事项](#6.5 注意事项)
- 七、总结与展望
-
- [7.1 项目回顾](#7.1 项目回顾)
- [7.2 技术栈总结](#7.2 技术栈总结)
- [7.3 核心收获](#7.3 核心收获)
- [7.4 优化方向](#7.4 优化方向)
一、项目概述
1.1 项目简介
Mini HTTP Server 是一个基于 C++17 实现的高性能 HTTP 静态文件服务器 ,使用 Reactor + 线程池的并发模型,能够在单台机器上高效处理数千个并发连接。
核心功能:
- 支持http/1.1 GET 请求
- 返回静态文件,比如HTML、CSS、JS、图片等
- 自动识别MIME类型
- 支持自定义404页面
1.2 技术栈
| 技术 | 说明 |
|---|---|
| 语言 | c++17 |
| IO多路复用 | epoll(边缘触发模式) |
| 并发模型 | Reactor + 线程池 |
| 线程池 | 自实现,支持std::future异步获取返回值 |
| HTTP解析 | 自实现,基于状态机 |
| 构建工具 | CMake |
| 操作系统 | Linux(WSL) |
| 测试工具 | curl、WebBench |
1.3 项目亮点
- 模块化设计:将事件循环、连接管理、协议解析、线程池拆分为独立模块,职责清晰,易于扩展
- 跨线程安全调度:通过 eventfd 实现线程池与 I/O 线程之间的安全通信,确保 socket 的所有操作都在 I/O 线程执行
- 应用层缓冲区:自定义 Buffer 类,使用 readv 减少系统调用,支持高效处理粘包/半包问题
- 优雅关闭:确保已提交任务不丢失,待发送数据写完后再关闭连接
- 可扩展架构:替换 HttpParser 即可支持其他协议(如 Redis RESP 协议)
1.4 技术架构图
#mermaid-svg-umkkZX4VOz1rDSIl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-umkkZX4VOz1rDSIl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-umkkZX4VOz1rDSIl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-umkkZX4VOz1rDSIl .error-icon{fill:#552222;}#mermaid-svg-umkkZX4VOz1rDSIl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-umkkZX4VOz1rDSIl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-umkkZX4VOz1rDSIl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-umkkZX4VOz1rDSIl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-umkkZX4VOz1rDSIl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-umkkZX4VOz1rDSIl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-umkkZX4VOz1rDSIl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-umkkZX4VOz1rDSIl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-umkkZX4VOz1rDSIl .marker.cross{stroke:#333333;}#mermaid-svg-umkkZX4VOz1rDSIl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-umkkZX4VOz1rDSIl p{margin:0;}#mermaid-svg-umkkZX4VOz1rDSIl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-umkkZX4VOz1rDSIl .cluster-label text{fill:#333;}#mermaid-svg-umkkZX4VOz1rDSIl .cluster-label span{color:#333;}#mermaid-svg-umkkZX4VOz1rDSIl .cluster-label span p{background-color:transparent;}#mermaid-svg-umkkZX4VOz1rDSIl .label text,#mermaid-svg-umkkZX4VOz1rDSIl span{fill:#333;color:#333;}#mermaid-svg-umkkZX4VOz1rDSIl .node rect,#mermaid-svg-umkkZX4VOz1rDSIl .node circle,#mermaid-svg-umkkZX4VOz1rDSIl .node ellipse,#mermaid-svg-umkkZX4VOz1rDSIl .node polygon,#mermaid-svg-umkkZX4VOz1rDSIl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-umkkZX4VOz1rDSIl .rough-node .label text,#mermaid-svg-umkkZX4VOz1rDSIl .node .label text,#mermaid-svg-umkkZX4VOz1rDSIl .image-shape .label,#mermaid-svg-umkkZX4VOz1rDSIl .icon-shape .label{text-anchor:middle;}#mermaid-svg-umkkZX4VOz1rDSIl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-umkkZX4VOz1rDSIl .rough-node .label,#mermaid-svg-umkkZX4VOz1rDSIl .node .label,#mermaid-svg-umkkZX4VOz1rDSIl .image-shape .label,#mermaid-svg-umkkZX4VOz1rDSIl .icon-shape .label{text-align:center;}#mermaid-svg-umkkZX4VOz1rDSIl .node.clickable{cursor:pointer;}#mermaid-svg-umkkZX4VOz1rDSIl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-umkkZX4VOz1rDSIl .arrowheadPath{fill:#333333;}#mermaid-svg-umkkZX4VOz1rDSIl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-umkkZX4VOz1rDSIl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-umkkZX4VOz1rDSIl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-umkkZX4VOz1rDSIl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-umkkZX4VOz1rDSIl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-umkkZX4VOz1rDSIl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-umkkZX4VOz1rDSIl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-umkkZX4VOz1rDSIl .cluster text{fill:#333;}#mermaid-svg-umkkZX4VOz1rDSIl .cluster span{color:#333;}#mermaid-svg-umkkZX4VOz1rDSIl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-umkkZX4VOz1rDSIl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-umkkZX4VOz1rDSIl rect.text{fill:none;stroke-width:0;}#mermaid-svg-umkkZX4VOz1rDSIl .icon-shape,#mermaid-svg-umkkZX4VOz1rDSIl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-umkkZX4VOz1rDSIl .icon-shape p,#mermaid-svg-umkkZX4VOz1rDSIl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-umkkZX4VOz1rDSIl .icon-shape .label rect,#mermaid-svg-umkkZX4VOz1rDSIl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-umkkZX4VOz1rDSIl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-umkkZX4VOz1rDSIl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-umkkZX4VOz1rDSIl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HttpServer 内部结构
accept/read
提交任务
处理业务
返回结果
发送响应
write
返回数据
HTTP 请求
浏览器 / curl
EventLoop
主 I/O 线程
epoll_wait
ThreadPool
4个工作线程
任务队列
TcpConnection
fd + Buffer
HttpParser
请求解析 + 响应构造
1.5 请求处理流程
一个完整的 HTTP 请求处理流程如下:
#mermaid-svg-a9a4KO92U47kAYMm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-a9a4KO92U47kAYMm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-a9a4KO92U47kAYMm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-a9a4KO92U47kAYMm .error-icon{fill:#552222;}#mermaid-svg-a9a4KO92U47kAYMm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-a9a4KO92U47kAYMm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-a9a4KO92U47kAYMm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-a9a4KO92U47kAYMm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-a9a4KO92U47kAYMm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-a9a4KO92U47kAYMm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-a9a4KO92U47kAYMm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-a9a4KO92U47kAYMm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-a9a4KO92U47kAYMm .marker.cross{stroke:#333333;}#mermaid-svg-a9a4KO92U47kAYMm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-a9a4KO92U47kAYMm p{margin:0;}#mermaid-svg-a9a4KO92U47kAYMm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-a9a4KO92U47kAYMm .cluster-label text{fill:#333;}#mermaid-svg-a9a4KO92U47kAYMm .cluster-label span{color:#333;}#mermaid-svg-a9a4KO92U47kAYMm .cluster-label span p{background-color:transparent;}#mermaid-svg-a9a4KO92U47kAYMm .label text,#mermaid-svg-a9a4KO92U47kAYMm span{fill:#333;color:#333;}#mermaid-svg-a9a4KO92U47kAYMm .node rect,#mermaid-svg-a9a4KO92U47kAYMm .node circle,#mermaid-svg-a9a4KO92U47kAYMm .node ellipse,#mermaid-svg-a9a4KO92U47kAYMm .node polygon,#mermaid-svg-a9a4KO92U47kAYMm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-a9a4KO92U47kAYMm .rough-node .label text,#mermaid-svg-a9a4KO92U47kAYMm .node .label text,#mermaid-svg-a9a4KO92U47kAYMm .image-shape .label,#mermaid-svg-a9a4KO92U47kAYMm .icon-shape .label{text-anchor:middle;}#mermaid-svg-a9a4KO92U47kAYMm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-a9a4KO92U47kAYMm .rough-node .label,#mermaid-svg-a9a4KO92U47kAYMm .node .label,#mermaid-svg-a9a4KO92U47kAYMm .image-shape .label,#mermaid-svg-a9a4KO92U47kAYMm .icon-shape .label{text-align:center;}#mermaid-svg-a9a4KO92U47kAYMm .node.clickable{cursor:pointer;}#mermaid-svg-a9a4KO92U47kAYMm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-a9a4KO92U47kAYMm .arrowheadPath{fill:#333333;}#mermaid-svg-a9a4KO92U47kAYMm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-a9a4KO92U47kAYMm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-a9a4KO92U47kAYMm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-a9a4KO92U47kAYMm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-a9a4KO92U47kAYMm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-a9a4KO92U47kAYMm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-a9a4KO92U47kAYMm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-a9a4KO92U47kAYMm .cluster text{fill:#333;}#mermaid-svg-a9a4KO92U47kAYMm .cluster span{color:#333;}#mermaid-svg-a9a4KO92U47kAYMm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-a9a4KO92U47kAYMm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-a9a4KO92U47kAYMm rect.text{fill:none;stroke-width:0;}#mermaid-svg-a9a4KO92U47kAYMm .icon-shape,#mermaid-svg-a9a4KO92U47kAYMm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-a9a4KO92U47kAYMm .icon-shape p,#mermaid-svg-a9a4KO92U47kAYMm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-a9a4KO92U47kAYMm .icon-shape .label rect,#mermaid-svg-a9a4KO92U47kAYMm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-a9a4KO92U47kAYMm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-a9a4KO92U47kAYMm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-a9a4KO92U47kAYMm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端发起 HTTP GET 请求
epoll_wait 检测到连接
accept 接受连接,
创建 TcpConnection
注册到 epoll, 监听 EPOLLIN
客户端发送数据, epoll 返回
TcpConnection::
handleRead 读取数据
messageCallback 取出数据,
提交线程池
线程池工作线程执行
processRequest
解析 HTTP 请求
读取本地文件
构造 HTTP 响应
conn->send 回 EventLoop
runInLoop 唤醒 epoll_wait
注册 EPOLLOUT, 等待可写
handleWrite 发送响应数据
关闭连接
关键设计决策 :所有 socket 操作(read/write/close)都在 EventLoop 线程中执行,线程池只处理业务逻辑(解析请求、读取文件、构造响应)。
1.6 项目结构
text
mini-http-server/
├── CMakeLists.txt # 构建配置
├── src/
│ ├── main.cpp # 程序入口
│ ├── http_server.hpp # 服务器主类(组装所有组件)
│ ├── event_loop.hpp # epoll 事件循环
│ ├── tcp_connection.hpp # TCP 连接对象(读写 + 缓冲区)
│ ├── buffer.hpp # 应用层缓冲区
│ ├── http_parser.hpp # HTTP 请求解析 + 响应构造 + MIME 映射
│ └── thread_pool.hpp # 线程池
├── www/ # 静态文件目录
│ ├── index.html
│ └── 404.html
└── out/build/ # 编译输出目录
1.7 编译与运行
bash
# 编译
cd out/build
cmake ../..
make
# 运行(回到项目根目录)
cd ../..
./out/build/http_server
# 输出
HTTP server started: http://localhost:8080
# 测试
curl http://localhost:8080/
# 浏览器打开
http://localhost:8080/
1.8性能测试
使用WebBench进行压力测试:
bash
webbench -c 1000 -t 30 http://localhost:8080/
| 并发连接数 | QPS | 成功率 |
|---|---|---|
| 100 | 330+ | 99% |
| 500 | 260+ | 99% |
| 1000 | 300+ | 84% |
性能瓶颈分析:
- 当前使用的是HTTP/1.0 短连接模式,每次请求后立即关闭,频繁进行TCP的握手和挥手,消耗大量资源
- 线程池只设置了16个工作线程,多个任务并发下严重阻塞
- WSL2虚拟网卡有额外的性能开销
可能的优化方向
- 支持HTTP/1.1 Keep--Alive 长连接,复用TCP连接
- 增加文件缓存,避免每次请求读取磁盘
二、技术架构
2.1 简单概述
此项目使用单 Reactor + 线程池的并发模型。主线程运行 epoll 事件循环,统一管理所有 TCP 连接的网络 I/O。耗时操作(如 HTTP 解析、文件读取、响应构造)全部交给线程池异步处理。核心实现:读写分离------I/O 线程绝不会阻塞,计算任务绝不会占用 I/O 线程。
2.2 架构图
#mermaid-svg-HXFlKIIu1BFpHf7G{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HXFlKIIu1BFpHf7G .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HXFlKIIu1BFpHf7G .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HXFlKIIu1BFpHf7G .error-icon{fill:#552222;}#mermaid-svg-HXFlKIIu1BFpHf7G .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HXFlKIIu1BFpHf7G .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HXFlKIIu1BFpHf7G .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HXFlKIIu1BFpHf7G .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HXFlKIIu1BFpHf7G .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HXFlKIIu1BFpHf7G .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HXFlKIIu1BFpHf7G .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HXFlKIIu1BFpHf7G .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HXFlKIIu1BFpHf7G .marker.cross{stroke:#333333;}#mermaid-svg-HXFlKIIu1BFpHf7G svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HXFlKIIu1BFpHf7G p{margin:0;}#mermaid-svg-HXFlKIIu1BFpHf7G .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HXFlKIIu1BFpHf7G .cluster-label text{fill:#333;}#mermaid-svg-HXFlKIIu1BFpHf7G .cluster-label span{color:#333;}#mermaid-svg-HXFlKIIu1BFpHf7G .cluster-label span p{background-color:transparent;}#mermaid-svg-HXFlKIIu1BFpHf7G .label text,#mermaid-svg-HXFlKIIu1BFpHf7G span{fill:#333;color:#333;}#mermaid-svg-HXFlKIIu1BFpHf7G .node rect,#mermaid-svg-HXFlKIIu1BFpHf7G .node circle,#mermaid-svg-HXFlKIIu1BFpHf7G .node ellipse,#mermaid-svg-HXFlKIIu1BFpHf7G .node polygon,#mermaid-svg-HXFlKIIu1BFpHf7G .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HXFlKIIu1BFpHf7G .rough-node .label text,#mermaid-svg-HXFlKIIu1BFpHf7G .node .label text,#mermaid-svg-HXFlKIIu1BFpHf7G .image-shape .label,#mermaid-svg-HXFlKIIu1BFpHf7G .icon-shape .label{text-anchor:middle;}#mermaid-svg-HXFlKIIu1BFpHf7G .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HXFlKIIu1BFpHf7G .rough-node .label,#mermaid-svg-HXFlKIIu1BFpHf7G .node .label,#mermaid-svg-HXFlKIIu1BFpHf7G .image-shape .label,#mermaid-svg-HXFlKIIu1BFpHf7G .icon-shape .label{text-align:center;}#mermaid-svg-HXFlKIIu1BFpHf7G .node.clickable{cursor:pointer;}#mermaid-svg-HXFlKIIu1BFpHf7G .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HXFlKIIu1BFpHf7G .arrowheadPath{fill:#333333;}#mermaid-svg-HXFlKIIu1BFpHf7G .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HXFlKIIu1BFpHf7G .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HXFlKIIu1BFpHf7G .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HXFlKIIu1BFpHf7G .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HXFlKIIu1BFpHf7G .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HXFlKIIu1BFpHf7G .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HXFlKIIu1BFpHf7G .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HXFlKIIu1BFpHf7G .cluster text{fill:#333;}#mermaid-svg-HXFlKIIu1BFpHf7G .cluster span{color:#333;}#mermaid-svg-HXFlKIIu1BFpHf7G div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HXFlKIIu1BFpHf7G .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HXFlKIIu1BFpHf7G rect.text{fill:none;stroke-width:0;}#mermaid-svg-HXFlKIIu1BFpHf7G .icon-shape,#mermaid-svg-HXFlKIIu1BFpHf7G .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HXFlKIIu1BFpHf7G .icon-shape p,#mermaid-svg-HXFlKIIu1BFpHf7G .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HXFlKIIu1BFpHf7G .icon-shape .label rect,#mermaid-svg-HXFlKIIu1BFpHf7G .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HXFlKIIu1BFpHf7G .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HXFlKIIu1BFpHf7G .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HXFlKIIu1BFpHf7G :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP 请求
accept
epoll_wait
提交任务
浏览器
HttpServer
EventLoop
ThreadPool
TcpConnection
processRequest
2.3 模块职责
| 模块 | 文件 | 职责 | 依赖 |
|---|---|---|---|
| Buffer | buffer.hpp | 应用层缓冲区,解决粘包半包,高效读写 | 无 |
| ThreadPoll | thread_pool.hpp | 线程池,异步执行任务,支持获取返回值 | 标准库 |
| EventLoop | event_loop.hpp | 封装 epoll,驱动所有 I/O 事件,支持跨线程唤醒 | 操作系统 |
| HttpParser | http_parser.hpp | 解析 HTTP 请求行/头部,构造 HTTP 响应 | 无 |
| TcpConnection | tcp_connection.hpp | 管理一个客户端连接的生命周期和数据收发 | Buffer + EventLoop |
| HttpServer | http_server.hpp | 组装所有模块,处理连接和业务调度 | 所有模块 |
| main | main.cpp | 入口,解析命令行参数,启动服务器 | HttpServer |
依赖层级:
text
main
└── HttpServer
├── ThreadPool
├── HttpParser
├── TcpConnection
│ ├── Buffer
│ └── EventLoop
└── EventLoop
2.4 四个关键设计决策
决策一:epoll + 边缘触发(ET),不用 select/poll
select 有 1024 个 fd 上限,poll 虽然没有上限,但两者都需要每次调用时把整个 fd 集合从用户态拷贝到内核态,返回后还要 O(n) 遍历所有 fd 找出就绪的。
epoll 通过 epoll_ctl 在内核中维护事件表,epoll_wait 只返回就绪的 fd,复杂度 O(1)。边缘触发(ET) 模式下,内核只在状态变化时通知一次,倒逼程序必须循环读直到返回 EAGAIN。这样做有两个好处:
- 减少 epoll_wait 的返回次数
- 避免同一个事件反复通知浪费 CPU
epoll必须配合非阻塞 I/O,否则程序会卡在 read/write 上。
决策二:线程池固定大小,不对每个连接建立线程
每来一个连接创建一个线程的方式最简单,但一万个线程光是栈空间就要 80GB(按照 8MB/线程来算),上下文切换开销更是不可接受。
本项目的线程池在启动时创建固定数量工作线程(默认 16 个),所有连接共享这些线程。任务提交通过 submit() 入队,condition_variable 实现线程的休眠和唤醒,线程空闲时不消耗 CPU。
决策三:单 Reactor,暂不用主从模式
单 Reactor 模式中,一个线程同时负责 accept 和所有已连接 socket 的读写。优点是代码简洁、没有多线程竞争 epoll 的问题。
主从 Reactor 是进一步的优化:主 Reactor 只做 accept,把连接分发给多个子 Reactor 各自管理。适合需要处理数万连接的场景。本项目作为学习项目,单 Reactor 就可以说明核心原理,代码也简明,架构复杂度可控。
决策四:业务逻辑异步化,I/O 线程永不阻塞
这是整个架构最重要的设计原则。I/O 线程唯一的职责是尽快完成数据收发,任何可能耗时的操作------包括 HTTP 协议解析、文件 I/O、字符串拼接------全部封装成任务丢给线程池。
这个原则贯穿代码始终:handleRead 读到数据后不做处理,直接 pool_.submit();线程池处理完要发响应时,不直接操作 socket,而是通过 runInLoop() 把写操作投递回 EventLoop 线程。这样 socket 的所有操作都集中在一个线程里,天然线程安全。
2.5一个 HTTP 请求的完整流程
以下追踪一次 curl http://localhost:8080/ 从进到出的完整路径:
| 步骤 | 在哪里 | 发生了什么 |
|---|---|---|
| 第一步 | EventLoop::run() | epoll_wait 检测到 listenFd 可读 |
| 第二步 | HttpServer::handleAccept() | accept4() 获取 connFd,创建 TcpConnection,注册到 epoll |
| 第三步 | EventLoop::run() | epoll_wait 再次返回,这次是 connFd 可读 |
| 第四步 | TcpConnection::handleRead() | Buffer::readFromFd() 用 readv 读取全部数据 |
| 第五步 | messageCallback | 从 Buffer 取出原始数据,pool_.submit() 提交任务 |
| 第六步 | ThreadPool::worker_loop() | 工作线程被唤醒,取出任务并执行 processRequest() |
| 第七步 | HttpServer::processRequest() | 解析 HTTP 请求 → 拼接文件路径 → readFile() 读取文件 → 构造 HttpResponse |
| 第八步 | TcpConnection::send() | 调用 loop_->runInLoop(),把 sendInLoop 函数投递回 I/O 线程 |
| 第九步 | EventLoop::run() | wakeupFd 被唤醒,执行 pendingFunctors 中的 sendInLoop,注册 EPOLLOUT |
| 第十步 | EventLoop::run() | epoll_wait 检测到 connFd 可写 |
| 第十一步 | TcpConnection::handleWrite() | write 把响应数据写入 socket,检查 closeAfterWrite_ 标志 |
| 第十二步 | TcpConnection::doClose() | 从 epoll 移除 fd,关闭连接 |
关键节点:第八步的 runInLoop 是整个流程中最精妙的一环。线程池、工作线程和 I/O 线程是不同线程,不能直接操作 socket。runInLoop 把回调放入 pendingFunctors_,然后向 eventfd 写入一字节,epoll_wait 立刻返回,在 I/O 线程的安全环境中执行写操作。这就是跨线程任务投递 + 唤醒机制的本质。
- 线程池:主要用于第五步和第六步作为任务队列和线程管理的调度员。
- 工作线程:主要出现在第六、七、八步,只在处理业务时使用。
- I/O 线程:除开六、七、八步,其他步骤都是 I/O 线程在工作,负责所有网络 I/O。
三、核心模块详解
本项目一共有六个核心模块,按照底层到上层的依赖关系来排列:
| 模块 | 文件 | 职责 |
|---|---|---|
| 线程池 | thread_pool.hpp | 管理工作线程,异步执行任务 |
| 事件循环 | event_loop.hpp | epoll封装,事件驱动核心 |
| 缓冲区 | buffer.hpp | 应用层读写缓冲区 |
| TCP连接 | tcp_connection.hpp | 封装单个连接的状态和I/O |
| HTTP解析 | http_parser.hpp | 解析HTTP请求,构造响应 |
| 服务器主类 | http_server.hpp | 组装所有模块,处理业务逻辑 |
3.1 线程池 ------ ThreadPool
设计目标
频繁创建和销毁 std::thread 开销巨大。线程池预先创建一组工作线程,通过任务队列解耦任务提交与执行,实现线程复用。
核心数据结构
cpp
std::vector<std::thread> workers_; // 工作线程组
std::queue<std::function<void()>> tasks_; // 任务队列
std::mutex mutex_; // 保护任务队列
std::condition_variable cv_; // 线程等待/唤醒
bool stop_; // 停止标志
工作流程
#mermaid-svg-pW7HSEARWfTgSs2T{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pW7HSEARWfTgSs2T .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pW7HSEARWfTgSs2T .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pW7HSEARWfTgSs2T .error-icon{fill:#552222;}#mermaid-svg-pW7HSEARWfTgSs2T .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pW7HSEARWfTgSs2T .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pW7HSEARWfTgSs2T .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pW7HSEARWfTgSs2T .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pW7HSEARWfTgSs2T .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pW7HSEARWfTgSs2T .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pW7HSEARWfTgSs2T .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pW7HSEARWfTgSs2T .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pW7HSEARWfTgSs2T .marker.cross{stroke:#333333;}#mermaid-svg-pW7HSEARWfTgSs2T svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pW7HSEARWfTgSs2T p{margin:0;}#mermaid-svg-pW7HSEARWfTgSs2T .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pW7HSEARWfTgSs2T .cluster-label text{fill:#333;}#mermaid-svg-pW7HSEARWfTgSs2T .cluster-label span{color:#333;}#mermaid-svg-pW7HSEARWfTgSs2T .cluster-label span p{background-color:transparent;}#mermaid-svg-pW7HSEARWfTgSs2T .label text,#mermaid-svg-pW7HSEARWfTgSs2T span{fill:#333;color:#333;}#mermaid-svg-pW7HSEARWfTgSs2T .node rect,#mermaid-svg-pW7HSEARWfTgSs2T .node circle,#mermaid-svg-pW7HSEARWfTgSs2T .node ellipse,#mermaid-svg-pW7HSEARWfTgSs2T .node polygon,#mermaid-svg-pW7HSEARWfTgSs2T .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pW7HSEARWfTgSs2T .rough-node .label text,#mermaid-svg-pW7HSEARWfTgSs2T .node .label text,#mermaid-svg-pW7HSEARWfTgSs2T .image-shape .label,#mermaid-svg-pW7HSEARWfTgSs2T .icon-shape .label{text-anchor:middle;}#mermaid-svg-pW7HSEARWfTgSs2T .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pW7HSEARWfTgSs2T .rough-node .label,#mermaid-svg-pW7HSEARWfTgSs2T .node .label,#mermaid-svg-pW7HSEARWfTgSs2T .image-shape .label,#mermaid-svg-pW7HSEARWfTgSs2T .icon-shape .label{text-align:center;}#mermaid-svg-pW7HSEARWfTgSs2T .node.clickable{cursor:pointer;}#mermaid-svg-pW7HSEARWfTgSs2T .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pW7HSEARWfTgSs2T .arrowheadPath{fill:#333333;}#mermaid-svg-pW7HSEARWfTgSs2T .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pW7HSEARWfTgSs2T .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pW7HSEARWfTgSs2T .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pW7HSEARWfTgSs2T .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pW7HSEARWfTgSs2T .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pW7HSEARWfTgSs2T .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pW7HSEARWfTgSs2T .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pW7HSEARWfTgSs2T .cluster text{fill:#333;}#mermaid-svg-pW7HSEARWfTgSs2T .cluster span{color:#333;}#mermaid-svg-pW7HSEARWfTgSs2T div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pW7HSEARWfTgSs2T .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pW7HSEARWfTgSs2T rect.text{fill:none;stroke-width:0;}#mermaid-svg-pW7HSEARWfTgSs2T .icon-shape,#mermaid-svg-pW7HSEARWfTgSs2T .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pW7HSEARWfTgSs2T .icon-shape p,#mermaid-svg-pW7HSEARWfTgSs2T .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pW7HSEARWfTgSs2T .icon-shape .label rect,#mermaid-svg-pW7HSEARWfTgSs2T .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pW7HSEARWfTgSs2T .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pW7HSEARWfTgSs2T .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pW7HSEARWfTgSs2T :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
submit(task)
加锁,放入队列
cv_.notify_one()
工作线程被唤醒
取出 task 执行
队列为空?
cv_.wait() 休眠
关键实现:支持返回值的任务提交
cpp
template <typename F, typename... Args>
auto submit(F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>> {
using return_type = std::invoke_result_t<F, Args...>;
// packaged_task 只能移动不能拷贝,用 shared_ptr 包装
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> result = task->get_future();
{
std::lock_guard<std::mutex> lock(mutex_);
if (stop_) throw std::runtime_error("ThreadPool 已停止");
tasks_.emplace([task] { (*task)(); });
}
cv_.notify_one();
return result;
}
设计要点
| 技术点 | 说明 |
|---|---|
| std::packaged_task | 将可调用对象包装为 std::future 可获取结果的任务 |
| 完美转发 | std::forward 保持参数左右值属性,避免不必要的拷贝 |
| 条件变量 + 谓词 | cv_.wait(lock, this{ return stop_ || !tasks_.empty(); }) 防止虚假唤醒 |
| 优雅关闭 | 析构时先设 stop_,唤醒所有线程,再逐一线程 join() |
3.2 事件循环 ------ EventLoop
设计目标
封装 Linux epoll,实现事件驱动模型。所有 I/O 操作都在事件循环线程中执行,保证线程安全。
核心数据结构
cpp
int epfd_; // epoll 文件描述符
int wakeupFd_; // eventfd,用于跨线程唤醒
std::unordered_map<int, EventCallback> callbacks_; // fd → 回调函数映射
std::vector<std::function<void()>> pendingFunctors_; // 待执行任务队列
std::mutex mutex_; // 保护 pendingFunctors_
主循环实现
cpp
void run() {
std::vector<epoll_event> events(128);
while (!quit_) {
int n = epoll_wait(epfd_, events.data(), events.size(), 1000);
for (int i = 0; i < n; ++i) {
int fd = events[i].data.fd;
if (fd == wakeupFd_) { // 跨线程唤醒
uint64_t val;
read(wakeupFd_, &val, sizeof(val));
executePendingFunctors();
continue;
}
auto it = callbacks_.find(fd); // 执行对应回调
if (it != callbacks_.end()) it->second();
}
}
}
跨线程唤醒机制
这是本项目最精巧的设计之一。线程池工作线程处理完业务后,需要把"发送响应"这个操作交给事件循环线程执行。通过 eventfd 实现:
cpp
void runInLoop(std::function<void()> fn) {
{
std::lock_guard<std::mutex> lock(mutex_);
pendingFunctors_.push_back(std::move(fn));
}
wakeup(); // 向 eventfd 写入数据,唤醒 epoll_wait
}
void wakeup() {
uint64_t one = 1;
write(wakeupFd_, &one, sizeof(one));
}
| 关键技术 | 作用 |
|---|---|
| eventfd | 创建一个可被 epoll 监控的文件描述符 |
| runInLoop | 线程安全的任务投递接口 |
| 唤醒 + 执行 | 写入 eventfd → epoll_wait 返回 → 执行 pendingFunctors |
3.3 缓冲区 ------ Buffer
设计目标
非阻塞 I/O 下,一次 read/write 可能只处理部分数据。Buffer 提供应用层缓冲,解决粘包和半包问题。
核心设计
cpp
std::vector<char> buffer_; // 底层存储
size_t readIndex_; // 读指针
size_t writeIndex_; // 写指针
Buffer内部维护一块连续的内存,通过读写指针区分已读和可写区域:
- 0 → 已读
- readIdx → 待读
- writeIdx → 可写
高效读取:readv分散读
cpp
ssize_t readFromFd(int fd) {
char extrabuf[65536]; // 栈上临时空间
struct iovec vec[2];
vec[0].iov_base = begin() + writeIndex_;
vec[0].iov_len = writableBytes();
vec[1].iov_base = extrabuf;
vec[1].iov_len = sizeof(extrabuf);
ssize_t n = readv(fd, vec, 2);
// 如果数据量超出剩余空间,多出的部分在 extrabuf 里,再 append 回去
if (n > writableBytes()) {
writeIndex_ = buffer_.size();
append(extrabuf, n - writableBytes());
}
}
设计要点
| 技术点 | 说明 |
|---|---|
| readv分散读 | 一次系统调用,减少数据拷贝 |
| 线上临时空间 | 避免频繁扩容 vector |
| 读写指针 | 避免数据迁移,提高效率 |
3.4 TCP 连接 ------ TcpConnection
设计目标
封装单个 TCP 连接的所有状态:文件描述符、读写缓冲区、回调函数。通过 enable_shared_from_this 安全地在回调中传递自身。
核心结构
cpp
int fd_; // socket 文件描述符
EventLoop* loop_; // 所属事件循环
Buffer inputBuffer_; // 读缓冲区
Buffer outputBuffer_; // 写缓冲区
bool writing_ = false; // 是否正在关注写事件
bool closeAfterWrite_ = false; // 写完数据后关闭
MessageCallback messageCb_; // 收到数据时的回调
CloseCallback closeCb_; // 连接关闭时的回调
数据读取流程
cpp
void handleRead() {
int n = inputBuffer_.readFromFd(fd_); // 非阻塞读取
if (n > 0) {
messageCb_(shared_from_this(), &inputBuffer_); // 通知上层
} else if (n == 0) {
handleClose(); // 对端关闭
}
}
安全发送,写完后关闭
这是本项目踩过的一个坑。如果先调用 send() 再立即 forceClose(),会删掉刚注册的 EPOLLOUT 事件,导致数据永远发不出去。解决方案:
cpp
void forceCloseInLoop() {
if (outputBuffer_.readableBytes() > 0) {
closeAfterWrite_ = true; // 等数据写完再真正关闭
return;
}
doClose();
}
void handleWrite() {
// 写数据...
if (outputBuffer_.readableBytes() == 0 && closeAfterWrite_) {
doClose(); // 数据写完了,安全关闭
}
}
3.5 HTTP 协议解析 ------ HttpParser
设计目标
解析HTTP/1.1请求,构造标准 HTTP 响应。
请求结构体
cpp
struct HttpRequest {
std::string method; // GET、POST
std::string path; // /index.html
std::string version; // HTTP/1.1
std::unordered_map<std::string, std::string> headers;
bool parse(const std::string& raw); // 从原始报文解析
};
解析过程
cpp
GET /index.html HTTP/1.1\r\n ← 请求行
Host: localhost\r\n ← 头部
Connection: keep-alive\r\n
\r\n ← 空行分隔
cpp
bool parse(const std::string& raw) {
std::istringstream stream(raw);
std::string line;
// 1. 解析请求行
std::getline(stream, line);
std::istringstream lineStream(line);
lineStream >> method >> path >> version;
// 2. 解析头部(逐行读取,按 : 分割 key 和 value)
while (std::getline(stream, line) && line != "\r") {
size_t colon = line.find(':');
headers[key] = value;
}
}
MIME 类型映射
cpp
inline std::string getMimeType(const std::string& path) {
static std::unordered_map<std::string, std::string> mime = {
{".html", "text/html"}, {".png", "image/png"}, ...
};
size_t dot = path.rfind('.');
return mime.count(ext) ? mime[ext] : "application/octet-stream";
}
3.6 服务器主类 ------ HttpServer
设计目标
组装所有模块,对外提供 start() 接口。处理连接接受、请求分发、响应生成。
核心成员
cpp
int listenFd_; // 监听 socket
EventLoop loop_; // 事件循环
ThreadPool pool_; // 线程池
std::string docRoot_; // 静态文件根目录
std::unordered_map<int, shared_ptr<TcpConnection>> connections_; // 连接集合
启动流程
cpp
void start() {
// 1. 创建 socket、bind、listen
listenFd_ = socket(...);
setNonBlocking(listenFd_);
bind(listenFd_, ...);
listen(listenFd_, ...);
// 2. 注册监听事件
loop_.updateEvent(listenFd_, EPOLLIN | EPOLLET,
[this]() { handleAccept(); });
// 3. 进入事件循环(阻塞)
loop_.run();
}
请求处理流程
cpp
void handleAccept() {
int connFd = accept4(listenFd_, ..., SOCK_NONBLOCK | SOCK_CLOEXEC);
auto conn = std::make_shared<TcpConnection>(connFd, &loop_);
conn->setMessageCallback([this](auto conn, Buffer* buf) {
std::string raw = buf->retrieveAll();
pool_.submit([this, conn, raw]() {
processRequest(conn, raw); // 在线程池中处理
});
});
}
void processRequest(weak_ptr<TcpConnection> weakConn, const string& raw) {
auto conn = weakConn.lock();
HttpRequest request;
request.parse(raw); // 解析 HTTP 请求
// 安全检查:防止目录穿越
if (request.path.find("..") != string::npos) { 返回 403; }
if (request.path == "/") request.path = "/index.html"; // 默认首页
// 读取文件,构造响应
string content = readFile(docRoot_ + request.path);
response.headers["Content-Type"] = getMimeType(path);
conn->send(response.toString()); // 发送响应
conn->forceClose(); // 关闭连接
}
模块依赖关系
#mermaid-svg-TsF1WFJRtJLZvbd1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TsF1WFJRtJLZvbd1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TsF1WFJRtJLZvbd1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TsF1WFJRtJLZvbd1 .error-icon{fill:#552222;}#mermaid-svg-TsF1WFJRtJLZvbd1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TsF1WFJRtJLZvbd1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TsF1WFJRtJLZvbd1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TsF1WFJRtJLZvbd1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TsF1WFJRtJLZvbd1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TsF1WFJRtJLZvbd1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TsF1WFJRtJLZvbd1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TsF1WFJRtJLZvbd1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TsF1WFJRtJLZvbd1 .marker.cross{stroke:#333333;}#mermaid-svg-TsF1WFJRtJLZvbd1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TsF1WFJRtJLZvbd1 p{margin:0;}#mermaid-svg-TsF1WFJRtJLZvbd1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TsF1WFJRtJLZvbd1 .cluster-label text{fill:#333;}#mermaid-svg-TsF1WFJRtJLZvbd1 .cluster-label span{color:#333;}#mermaid-svg-TsF1WFJRtJLZvbd1 .cluster-label span p{background-color:transparent;}#mermaid-svg-TsF1WFJRtJLZvbd1 .label text,#mermaid-svg-TsF1WFJRtJLZvbd1 span{fill:#333;color:#333;}#mermaid-svg-TsF1WFJRtJLZvbd1 .node rect,#mermaid-svg-TsF1WFJRtJLZvbd1 .node circle,#mermaid-svg-TsF1WFJRtJLZvbd1 .node ellipse,#mermaid-svg-TsF1WFJRtJLZvbd1 .node polygon,#mermaid-svg-TsF1WFJRtJLZvbd1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TsF1WFJRtJLZvbd1 .rough-node .label text,#mermaid-svg-TsF1WFJRtJLZvbd1 .node .label text,#mermaid-svg-TsF1WFJRtJLZvbd1 .image-shape .label,#mermaid-svg-TsF1WFJRtJLZvbd1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-TsF1WFJRtJLZvbd1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TsF1WFJRtJLZvbd1 .rough-node .label,#mermaid-svg-TsF1WFJRtJLZvbd1 .node .label,#mermaid-svg-TsF1WFJRtJLZvbd1 .image-shape .label,#mermaid-svg-TsF1WFJRtJLZvbd1 .icon-shape .label{text-align:center;}#mermaid-svg-TsF1WFJRtJLZvbd1 .node.clickable{cursor:pointer;}#mermaid-svg-TsF1WFJRtJLZvbd1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TsF1WFJRtJLZvbd1 .arrowheadPath{fill:#333333;}#mermaid-svg-TsF1WFJRtJLZvbd1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TsF1WFJRtJLZvbd1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TsF1WFJRtJLZvbd1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TsF1WFJRtJLZvbd1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TsF1WFJRtJLZvbd1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TsF1WFJRtJLZvbd1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TsF1WFJRtJLZvbd1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TsF1WFJRtJLZvbd1 .cluster text{fill:#333;}#mermaid-svg-TsF1WFJRtJLZvbd1 .cluster span{color:#333;}#mermaid-svg-TsF1WFJRtJLZvbd1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TsF1WFJRtJLZvbd1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TsF1WFJRtJLZvbd1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-TsF1WFJRtJLZvbd1 .icon-shape,#mermaid-svg-TsF1WFJRtJLZvbd1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TsF1WFJRtJLZvbd1 .icon-shape p,#mermaid-svg-TsF1WFJRtJLZvbd1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TsF1WFJRtJLZvbd1 .icon-shape .label rect,#mermaid-svg-TsF1WFJRtJLZvbd1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TsF1WFJRtJLZvbd1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TsF1WFJRtJLZvbd1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TsF1WFJRtJLZvbd1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} main.cpp
HttpServer
ThreadPool
EventLoop
HttpParser
TcpConnection
Buffer
从底向上:Buffer → ThreadPool / EventLoop → TcpConnection → HttpParser → HttpServer → main。
四、请求处理全流程
将前面拆开的所有模块串联起来,处理一个完整的HTTP请求。
4.1 流程总览
浏览器发起请求 ↓ 1 EventLoop 检测到 ListenFd 可读 ↓ 2 HttpServer::handleAccept() 接受新连接 ↓ 3 创建 TCPConnection,注册到 epoll ↓ 4 EventLoop 检测到 connFd 可读 ↓ 5 TCPConnection::handleRead() 读取数据到 Buffer ↓ 6 回调 messageCallback,数据交给线程池 ↓ 7 线程池工作,线程池执行 processRequest() ↓ 8 解析 HTTP 请求,读取文件,构造响应 ↓ 9 conn -->send(),通过 runInLoop 回到 EventLoop 线程 ↓ 10 EventLoop 检测到 connFd 可写 ↓ 11 TCPConnection::handleWrite() 发送响应 ↓ 12 关闭连接,清理资源
4.2 分布详解
第一步:EventLoop 检测到 listenFd 可读
服务器启动后,EventLoop::run() 中的 epoll_wait 阻塞等待事件。此时整个程序就停在这一行:
cpp
int n = epoll_wait(epfd_, events.data(), events.size(), 1000);
当浏览器发起连接时,TCP 三次握手完成,listenFd_ 变为可读,epoll_wait 返回。
第二步:HttpServer::handleAccept() 接受新连接
epoll_wait 返回后,EventLoop 根据 fd 找到之前注册的回调:
cpp
// http_server.hpp 中注册的
loop_.updateEvent(listenFd_, EPOLLIN | EPOLLET,
[this]() { handleAccept(); });
handleAccept() 在一个循环中不断调用 accept4,直到没有更多连接:
cpp
void handleAccept() {
while (true) {
int connFd = accept4(listenFd_, ..., SOCK_NONBLOCK | SOCK_CLOEXEC);
if (connFd < 0) {
if (errno == EAGAIN) break; // 没有更多连接,退出
continue;
}
auto conn = std::make_shared<TcpConnection>(connFd, &loop_);
// ... 设置回调,保存连接
}
}
accept4 的 SOCK_NONBLOCK 标志一步到位,新连接直接设为非阻塞,省去一次 fcntl 调用。
第三步:创建 TcpConnection,注册到 epoll
TcpConnection 的构造函数里做了三件事:
cpp
TcpConnection(int fd, EventLoop* loop)
: fd_(fd), loop_(loop), writing_(false), closeAfterWrite_(false) {
setNonBlocking(fd_); // 确保非阻塞
loop_->updateEvent(fd_, EPOLLIN | EPOLLET, // 注册到 epoll
[this]() { handleRead(); });
}
此时 connFd 已加入 epoll 的监控列表,等待客户端发送 HTTP 请求。
第四步:EventLoop 检测到 connFd 可读
浏览器发送 HTTP 请求后,connFd 变为可读,epoll_wait 再次返回。EventLoop 找到该 fd 对应的回调并执行。
第五步:TcpConnection::handleRead() 读取数据到 Buffer
cpp
void handleRead() {
int n = inputBuffer_.readFromFd(fd_);
if (n > 0) {
if (messageCb_) {
messageCb_(shared_from_this(), &inputBuffer_);
}
} else if (n == 0) {
handleClose();
}
}
Buffer::readFromFd 内部使用 readv 分散读:
cpp
ssize_t readFromFd(int fd) {
char extrabuf[65536];
struct iovec vec[2];
vec[0].iov_base = begin() + writeIndex_; // 缓冲区剩余空间
vec[0].iov_len = writable();
vec[1].iov_base = extrabuf; // 栈上临时空间
vec[1].iov_len = sizeof(extrabuf);
ssize_t n = readv(fd, vec, 2);
// ...
}
小请求直接读入内置缓冲区,大请求自动扩容,一次系统调用完成。
第六步:回调 messageCallback,数据交给线程池
messageCallback 在 handleAccept 中被设置,它取出数据并提交给线程池:
cpp
conn->setMessageCallback(
[this](shared_ptr<TcpConnection> conn, Buffer* buf) {
string raw = buf->retrieveAll();
weak_ptr<TcpConnection> weakConn = conn;
pool_.submit([this, weakConn, raw = move(raw)]() {
processRequest(weakConn, raw);
});
});
设计要点:
- retrieveAll() 取出数据并重置缓冲区
- weak_ptr 防止线程池延长连接生命周期
- raw 用 move 传递,避免拷贝
第七步:线程池工作线程执行 processRequest()
ThreadPool::submit 把任务包装成 packaged_task,放入队列,唤醒工作线程:
cpp
auto submit(F&& f, Args&&... args) -> future<...> {
auto task = make_shared<packaged_task<...>>(bind(...));
{
lock_guard<mutex> lock(mutex_);
tasks_.emplace([task] { (*task)(); });
}
cv_.notify_one();
return result;
}
工作线程取出任务,开始执行 processRequest。
第八步:解析 HTTP 请求,读取文件,构造响应
processRequest 在工作线程中执行:
cpp
void processRequest(weak_ptr<TcpConnection> weakConn, const string& raw) {
auto conn = weakConn.lock(); // 安全检查
if (!conn) return;
HttpRequest request;
request.parse(raw);
// 安全防护
if (request.path.find("..") != string::npos) {
// 返回 403
}
// 路径映射
if (request.path == "/") request.path = "/index.html";
string filePath = docRoot_ + request.path;
// 读取文件
string content = readFile(filePath);
// 构造响应
HttpResponse response;
response.body = move(content);
response.headers["Content-Type"] = getMimeType(filePath);
// ...
}
第九步:conn->send() 通过 runInLoop 回到 EventLoop 线程
processRequest 最后调用:
cpp
conn->send(response.toString());
conn->forceClose();
send() 通过 runInLoop 切换到 EventLoop 线程:
cpp
void send(const string& data) {
loop_->runInLoop([this, data]() {
sendInLoop(data);
});
}
runInLoop 把 sendInLoop 放入待执行队列,然后向 eventfd 写数据唤醒 epoll_wait:
cpp
void runInLoop(function<void()> fn) {
{
lock_guard<mutex> lock(mutex_);
pendingFunctors_.push_back(move(fn));
}
wakeup(); // 向 eventfd 写 1 字节
}
epoll_wait 被唤醒后,执行 executePendingFunctors,sendInLoop 得以执行。sendInLoop 把数据放入 outputBuffer_,并向 epoll 注册可写事件:
cpp
void sendInLoop(const string& data) {
outputBuffer_.append(data);
if (!writing_) {
writing_ = true;
loop_->updateEvent(fd_, EPOLLIN | EPOLLOUT | EPOLLET,
[this]() { handleWrite(); });
}
}
第十步:EventLoop 检测到 connFd 可写
当内核发送缓冲区有空闲时,connFd 变为可写,epoll_wait 返回,调用 handleWrite()。
第十一步:TcpConnection::handleWrite() 发送响应
cpp
void handleWrite() {
if (outputBuffer_.readableBytes() > 0) {
ssize_t n = write(fd_, outputBuffer_.peek(),
outputBuffer_.readableBytes());
if (n > 0) outputBuffer_.retrieveAllNoCopy();
if (outputBuffer_.readableBytes() > 0) return; // 没写完等下次
}
// 写完了
if (closeAfterWrite_) {
doClose();
return;
}
writing_ = false;
loop_->updateEvent(fd_, EPOLLIN | EPOLLET, [this]() { handleRead(); });
}
如果一次没写完(outputBuffer_ 还有数据),就继续等下次 EPOLLOUT 事件;写完了检查是否需要延迟关闭。
第十二步:关闭连接,清理资源
forceClose() 在 processRequest 中紧接着 send() 被调用:
cpp
void forceClose() {
loop_->runInLoop([this]() { forceCloseInLoop(); });
}
void forceCloseInLoop() {
if (outputBuffer_.readableBytes() > 0) {
closeAfterWrite_ = true; // 延迟关闭
return;
}
doClose();
}
void doClose() {
loop_->removeEvent(fd_);
if (closeCb_) closeCb_(shared_from_this());
}
延迟关闭机制:如果数据还没发完,先设置 closeAfterWrite_ 标记,等 handleWrite 把数据发完后再执行真正的 doClose()。TcpConnection 析构时自动 close(fd_),资源全部释放。
4.3 流程时序图
业务逻辑 ThreadPool TcpConnection EventLoop connFd listenFd 浏览器 业务逻辑 ThreadPool TcpConnection EventLoop connFd listenFd 浏览器 #mermaid-svg-yrmbj8a1cwM21Pmb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yrmbj8a1cwM21Pmb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yrmbj8a1cwM21Pmb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yrmbj8a1cwM21Pmb .error-icon{fill:#552222;}#mermaid-svg-yrmbj8a1cwM21Pmb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yrmbj8a1cwM21Pmb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yrmbj8a1cwM21Pmb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yrmbj8a1cwM21Pmb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yrmbj8a1cwM21Pmb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yrmbj8a1cwM21Pmb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yrmbj8a1cwM21Pmb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yrmbj8a1cwM21Pmb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yrmbj8a1cwM21Pmb .marker.cross{stroke:#333333;}#mermaid-svg-yrmbj8a1cwM21Pmb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yrmbj8a1cwM21Pmb p{margin:0;}#mermaid-svg-yrmbj8a1cwM21Pmb .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yrmbj8a1cwM21Pmb text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-yrmbj8a1cwM21Pmb .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yrmbj8a1cwM21Pmb .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-yrmbj8a1cwM21Pmb .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-yrmbj8a1cwM21Pmb .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-yrmbj8a1cwM21Pmb #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-yrmbj8a1cwM21Pmb .sequenceNumber{fill:white;}#mermaid-svg-yrmbj8a1cwM21Pmb #sequencenumber{fill:#333;}#mermaid-svg-yrmbj8a1cwM21Pmb #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-yrmbj8a1cwM21Pmb .messageText{fill:#333;stroke:none;}#mermaid-svg-yrmbj8a1cwM21Pmb .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yrmbj8a1cwM21Pmb .labelText,#mermaid-svg-yrmbj8a1cwM21Pmb .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-yrmbj8a1cwM21Pmb .loopText,#mermaid-svg-yrmbj8a1cwM21Pmb .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-yrmbj8a1cwM21Pmb .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yrmbj8a1cwM21Pmb .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-yrmbj8a1cwM21Pmb .noteText,#mermaid-svg-yrmbj8a1cwM21Pmb .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-yrmbj8a1cwM21Pmb .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yrmbj8a1cwM21Pmb .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yrmbj8a1cwM21Pmb .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yrmbj8a1cwM21Pmb .actorPopupMenu{position:absolute;}#mermaid-svg-yrmbj8a1cwM21Pmb .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-yrmbj8a1cwM21Pmb .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yrmbj8a1cwM21Pmb .actor-man circle,#mermaid-svg-yrmbj8a1cwM21Pmb line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-yrmbj8a1cwM21Pmb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TCP SYN TCP ACK epoll 可读事件 handleAccept() new TcpConnection 注册回调 HTTP GET 可读事件 handleRead() readv() 读取数据 提交任务 processRequest() 解析 HTTP 请求 读取文件 构造响应 返回 send() runInLoop() wakeup() 注册 EPOLLOUT 可写事件 handleWrite() HTTP 200 doClose() TCP FIN
4.4 核心设计要点
| 设计点 | 实现方式 | 解决的问题 |
|---|---|---|
| I/O与计算分离 | Reactor 线程负责 I/O,线程池负责业务 | 避免业务阻塞网络线程,提升并发能力 |
| 跨线程调度 | runInLoop + eventfd | 线程处理结果安全返回到 I/O 线程写 socket |
| 延迟关闭 | closeAfterWrite_标志 | 发送完缓冲区的数据再关闭,防止数据丢失 |
| 非阻塞 I/O + ET 模式 | SOCK_NONBLOCK + EPOLLET | 减少epoll事件触发次数,提升性能 |
| 应用层缓冲区 | 自定义Buffer类 + readv | 处理 TCP 沾包,减少系统调用 |
| 智能指针管理生命周期 | shared_ptr + weak_ptr | 确保连接对象在回调执行期间不被释放 |
4.5 如果改成主从 Reactor
当前项目是单 Reactor + 线程池,所有连接的 I/O 都在一个 EventLoop 里。如果有大量连接,epoll_wait 和事件处理会成为瓶颈。
改成主从 Reactor 的改动点:
cpp
// 主 Reactor 只负责 accept
mainLoop_.updateEvent(listenFd_, EPOLLIN, []() {
int connFd = accept4(...);
int idx = next_++ % subLoops_.size(); // 轮询
auto conn = make_shared<TcpConnection>(connFd, subLoops_[idx].get());
// ...
});
// 每个从 Reactor 跑在独立线程
for (auto& loop : subLoops_) {
threads_.emplace_back([&]() { loop->run(); });
}
这样多核 CPU 的利用率更高,单机可以承载更多并发连接。这是本项目的重要优化方向,值得在博客的"展望"部分提一笔。
五、踩坑记录
这个项目从第一行代码到最终跑通,踩了不少坑。在此记录下来,是对自己排查问题能力的总结,也希望能今后能减少错误的发生。
5.1 编译阶段
坑1:WSL 环境下缺少编译工具
现象 :进入 WSL 后直接 cmake 或 make,提示 command not found。
原因 :WSL 是精简的 Linux 环境,默认不带 C++ 开发工具链。
解决:
bash
sudo apt update
sudo apt install build-essential cmake -y
教训:新环境第一步永远是检查工具链是否齐全。
坑2:CMakeLists.txt 中忘记链接线程库
现象 :编译时报 undefined reference to pthread_create。
原因 :C++ 的 std::thread 底层依赖 pthread 库,需要在 CMake 中显式链接。
解决:在 CMakeLists.txt 中加入:
cpp
find_package(Threads REQUIRED)
target_link_libraries(http_server Threads::Threads)
教训:使用多线程的程序,编译时必须链接 pthread。
5.2 运行时问题
坑3:中文乱码
现象:服务器启动后终端输出乱码:
text
HTTP���������: http://localhost:8080
������: 127.0.0.1:40872
原因 :WSL 默认终端不支持 UTF-8 中文显示,代码里的 cout 中文输出无法正常渲染。
解决:
cpp
// 将中文改为英文
std::cout << "HTTP server started: http://localhost:" << port_ << std::endl;
std::cout << "New connection: " << inet_ntoa(peer.sin_addr) << std::endl;
std::cout << "Connection closed" << std::endl;
教训:写跨平台或命令行工具时,输出信息尽量用英文,避免编码问题。
坑4:curl 连接成功但收不到响应
现象 :服务器显示 "New connection",但 curl 一直卡住,最后只能 Ctrl+C 退出。
排查过程 :这是这个项目遇到的最难排查的问题,前后经历了多轮定位。
第一次怀疑点:线程池死锁
一开始怀疑是线程池中发生了死锁。线程池的工作线程执行 processRequest,然后调用 conn->send(),如果 send() 内部要获取某个锁,而该锁被其他线程持有,就会死锁。
排查方法:在 processRequest 开头加输出,确认线程池有没有执行到这个函数。
结果:processRequest 确实被执行了,排除线程池死锁。
第二次怀疑点:epoll_wait 的 events 数组问题
仔细审查 EventLoop::run() 的代码,发现:
cpp
std::vector<epoll_event> events; // 空的 vector!
int n = epoll_wait(epfd_, events.data(), events.size(), 1000);
events 没有初始大小,events.size() 为 0,epoll_wait 的 maxevents 参数为 0,epoll 永远不会返回任何事件。
修复:
cpp
std::vector<epoll_event> events(128); // 必须指定初始大小
结果:修复后 handleRead 能执行了,processRequest 也能执行了,但 curl 还是收不到响应。
第三次怀疑点:加上全链路日志,终于定位真凶
在 tcp_connection.hpp 的每个关键方法中加了调试输出:
cpp
void sendInLoop() { write(1, "sendInLoop called\n", 18); ... }
void handleWrite() { write(1, "handleWrite called\n", 19); ... }
void forceCloseInLoop() { write(1, "forceCloseInLoop called\n", 24); ... }
最终看到日志:
text
sendInLoop called
forceCloseInLoop called
Connection closed
关键发现 :handleWrite called 从未出现。
根因分析 :sendInLoop 注册了 EPOLLOUT 事件,但紧接着 forceCloseInLoop 调用了 removeEvent,把刚注册的可写事件直接删掉了。handleWrite 永远不会被触发,响应数据永远不会写回客户端。
解决方案:实现延迟关闭机制。
cpp
void forceCloseInLoop() {
// 如果输出缓冲区还有数据没发完,标记"写完再关"
if (outputBuffer_.readableBytes() > 0) {
closeAfterWrite_ = true;
return; // 不立即关闭
}
doClose();
}
void handleWrite() {
// 发送数据...
// 数据全部写完后
if (closeAfterWrite_) {
doClose(); // 此时才真正关闭连接
return;
}
// 恢复正常状态
}
总结:这个 bug 的根因是时序竞态------关闭操作在写操作生效前执行了。这是异步编程中的经典坑,教训是:
- 异步操作之间存在依赖时,必须确保执行顺序
- 加全链路调试日志是排查异步问题的最有效手段
- 遇到"卡住"不要盲目猜,从数据流的第一环节开始逐级验证
坑5:runInLoop 缺少唤醒机制
现象 :线程池处理完业务逻辑,调用 conn->send(),但响应永远不会发出。
原因 :send() 通过 runInLoop 把发送任务投递到 EventLoop 的待执行队列。但如果 epoll_wait 正在阻塞等待,没有人告诉它"有新任务来了",任务就永远不会执行。
解决:使用 eventfd 实现跨线程唤醒。
cpp
class EventLoop {
int wakeupFd_; // eventfd 文件描述符
EventLoop() {
wakeupFd_ = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
// 把 wakeupFd 注册到 epoll
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = wakeupFd_;
epoll_ctl(epfd_, EPOLL_CTL_ADD, wakeupFd_, &ev);
}
void wakeup() {
uint64_t one = 1;
write(wakeupFd_, &one, sizeof(one)); // 写入数据,触发 epoll_wait 返回
}
void run() {
// ...
if (fd == wakeupFd_) {
uint64_t val;
read(wakeupFd_, &val, sizeof(val)); // 读掉数据
executePendingFunctors(); // 执行待处理任务
}
}
};
工作原理:
- 线程池工作线程调用 runInLoop(fn) → fn 放入队列 → wakeup() 向 eventfd 写入数据
- epoll_wait 检测到 wakeupFd 可读,立即返回
- EventLoop 读掉 eventfd 数据,执行队列中的所有待处理函数
教训:跨线程通信时,不仅要把数据传过去,还要主动通知对方来取。eventfd 是实现跨线程唤醒 epoll 的标准手段。
5.3 设计与理解
坑6:Buffer::readFromFd 为什么要用 readv
疑问 :直接用 read 不行吗?为什么要用 readv 分散读?
解答:readv 一次系统调用可以读到多个缓冲区,能减少内存分配和拷贝:
- 小请求:数据直接读入 Buffer 内部空间,栈上临时空间用不到
- 大请求:Buffer 空间不够时,剩余部分读到栈上空间,然后一次性扩容追加
这是一个巧妙的性能优化,Muduo 网络库也是这么做的。
坑7:shared_from_this 和 weak_ptr 的配合
疑问:为什么回调里要这样写:
cpp
std::weak_ptr<TcpConnection> weakConn = conn;
pool_.submit([this, weakConn, raw = move(raw)]() {
auto conn = weakConn.lock();
if (!conn) return;
processRequest(conn, raw);
});
而不是直接捕获 shared_ptr?
解答 :防止延长对象生命周期。
如果直接捕获 shared_ptr,即使客户端已经断开连接,TcpConnection 对象也会因为线程池持有引用而不会被释放。用 weak_ptr:
- 不增加引用计数,不影响对象释放
- 使用时通过 lock() 检查对象是否还存在
- 如果连接已关闭(客户端断开),lock() 返回空,任务直接退出
这是 C++ 异步编程中避免内存泄漏的惯用法。
坑8:为什么 socket 操作必须在 EventLoop 线程
疑问 :线程池里直接 write(connFd, data, len) 不行吗?
解答:不安全。Linux 的 epoll 不是完全线程安全的。多个线程同时操作同一个 epoll 实例和 socket,会导致:
- epoll_ctl 修改事件时发生竞态条件
- 数据的读写顺序无法保证
- 连接的状态管理(writing_、closeAfterWrite_)完全乱掉
正确做法:所有 I/O 操作通过 runInLoop 统一到 EventLoop 线程执行,业务逻辑交给线程池。这就是 Reactor + 线程池 半同步半异步模型 的核心思想。
六、性能测试
一个好的项目不仅要"能跑",还要用数据来证明。所以需要对 HTTP 服务器进行压力测试,并分析测试结果。
6.1测试环境
| 项目 | 配置 |
|---|---|
| 操作系统 | Windows 11 + WSL2 (Ubuntu) |
| 编译器 | GCC |
| 线程池 | 16个工作线程 |
| 测试工具 | wrk 或 ab |
| 测试文件 | index.html |
6.2 安装测试工具
bash
sudo apt install wrk -y
sudo apt install apache2-utils -y # ab 备用
6.3 开始测试
启动服务器
bash
# Release 模式编译
cd out/build
cmake .. -DCMAKE_BUILD_TYPE=Release
make
cd ../..
# 运行
./out/build/http_server
执行压测
bash
# wrk:2个测试线程、100个并发连接、持续10秒
wrk -t2 -c100 -d10s http://localhost:8080/index.html
典型输出实例
cpp
Running 10s test @ http://localhost:8080/index.html
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.23ms 2.45ms 45.6ms 92.30%
Req/Sec 8.45k 1.23k 12.34k 78.50%
168234 requests in 10.00s, 34.56MB read
Requests/sec: 16823.40
两个重要指标:Requests/sec(QPS) 和 Latency(延迟)。
6.4 性能瓶颈
通过分析,当前项目还能优化的点
| 瓶颈 | 说明 | 优化方向 |
|---|---|---|
| 文件发送 | read + write 两次拷贝 | 改用 sendfile 零拷贝 |
| 内存分配 | 频繁 malloc/free | 实现内存池 |
| 锁竞争 | 线程池共享一个任务队列 | 改用任务窃取队列 |
6.5 注意事项
| 问题 | 说明 |
|---|---|
| WSL2 网络 | WSL2 网络走虚拟化,性能低于原生 Linux |
| 端口耗尽 | 高并发测试后大量 TIME_WAIT,等几分钟再测 |
| 编译模式 | 务必用 Release模式编译,Debug 性能可能差数倍 |
| 文件缓存 | 多测几次取稳定值,第一次可能因磁盘读取偏慢 |
七、总结与展望
7.1 项目回顾
这个项目用 C++ 从零实现了一个基于 Reactor 模型 + 线程池的高性能 HTTP 静态文件服务器,核心代码约 500 行,支持:
- 处理 HTTP GET 请求,返回静态文件
- 自动识别 MIME 类型(HTML、CSS、JS、图片等)
- 自定义 404 页面
- 并发处理多个客户端请求
7.2 技术栈总结
| 层次 | 技术 | 作用 |
|---|---|---|
| I/O 模型 | epoll + 非阻塞 I/O + 边缘触发(ET) | 单线程监控大量连接,事件驱动 |
| 并发模型 | Reactor + 线程池 | I/O 与计算分离,提升吞吐量 |
| 同步机制 | eventfd + mutex + condition_variable | 跨线程安全投递任务 |
| 内存管理 | shared_ptr + weak_ptr + RAII | 自动管理连接生命周期,防止悬空指针 |
| 协议解析 | 手动实现 HTTP/1.1 请求解析 | 理解应用层协议本质 |
| 缓冲区 | 自定义 Buffer + readv 分散读 | 处理 TCP 粘包,减少系统调用 |
7.3 核心收获
1.理解了 Reactor模型的本质
Reactor 不是某个库,也不是什么很难的技术,它就是:
text
while (true) {
等待事件发生;
根据事件类型调用对应的回调函数;
}
核心就是事件驱动,把主动轮询变成被动通知。
2.线程池与 I/O 线程的配合是最难的点
线程安全的边界要划清楚:
- I/O 线程操作 socket,不能阻塞
- 工作线程处理业务,不能直接操作 socket
- 两者通过 runInLoop 机制衔接
3.C++ 现代特性落到了实处
- shared_ptr + enable_shared_from_this:在回调中安全使用对象
- weak_ptr:打破循环引用,检测对象存活
- packaged_task + future:异步获取任务返回值
- function + lambda:灵活的回调机制
- 完美转发(forward):避免不必要的拷贝
- RAII:通过析构函数自动 close(fd)、join(thread)
7.4 优化方向
短期内:
支持 HTTP/1.1 Keep-Alive
- 响应后不关闭连接,等待下一个请求
- 需要增加超时定时器,踢出空闲连接
增加超时踢除机制
- 用定时器(时间堆或时间轮)检测空闲连接
- 超过一定时间(如 60 秒)没收到数据,主动关闭
压力测试并记录数据
- 用 webbench 或 ab 在不同并发数下测试
- 对比单线程与线程池的 QPS 差异