第 1 篇:从零复刻 TinyWebServer —— 环境准备与最小 TCP 服务端
原项目:qinguoyi/TinyWebServer
复刻仓库:L2501031968/ccTinyWebServer
完整 20 章教程:仓库内
docs/TinyWebServer-Recreation.md
为什么写这个系列
TinyWebServer 是一个很好的 Linux C++ 网络编程学习项目。它用相对精简的代码覆盖了 Web 服务器的主要模块:
线程池 epoll 事件循环 HTTP 状态机 定时器 日志系统 数据库连接池 Reactor / Proactor但直接读原项目源码,对初学者有几个门槛:
- 模块多,一开始不知道从哪里读起。
- 代码之间耦合紧,改一处可能编译不过。
- 看懂某个模块,不等于能自己写出来。
所以这个系列不打算“讲一遍源码”,而是从零复刻:参考原项目,在自己的目录里一步一步重新实现,每一章完成一个能独立编译、运行和验证的小目标,再进入下一章。
原项目:qinguoyi/TinyWebServer
复刻仓库:L2501031968/ccTinyWebServer
本系列的目标不是逐行复制原项目,而是:
理解每个模块解决什么问题 自己动手写出可运行的版本 用 curl、浏览器、Webbench 验证结果 记录编译错误、调试过程和理解复刻仓库里同时保留:
完整 20 章 Markdown 教程 每章对应的源码 Keil / Makefile 工程 测试脚本和压测结果如果你只想看完整版,可以直接访问仓库的docs/TinyWebServer-Recreation.md。博客上的 6 篇是精华版,覆盖最关键的几个模块。
每章做什么
系列的大致顺序是:
第 1 章 同步原语:信号量、互斥锁、条件变量 第 2 章 最小 TCP 服务端:socket、bind、listen、accept 第 3 章 非阻塞套接字与 epoll 事件循环 第 4 章 HTTP 请求解析状态机 第 5 章 静态文件服务 第 6 章 完整静态文件响应:mmap + writev 第 7 章 线程池与任务队列 第 8 章 定时器与连接超时 第 9 章 日志系统 第 10 章 LT 与 ET 触发模式 第 11 章 Reactor 与模拟 Proactor 第 12 章 MySQL 连接池 第 13 章 POST 表单解析与用户注册登录 第 14 章 浏览器界面与端到端测试 第 15 章 Cookie 与 Session(扩展) 第 16 章 原项目页面跳转与静态资源 第 17 章 原版 CGI 页面跳转与 MIME 类型 第 18 章 HTTP Range 分段传输 第 19 章 原项目 Config 与启动参数 第 20 章 原项目功能验收与 Webbench 压力测试TinyWebServer 从零复刻
本笔记记录从零复刻 TinyWebServer 的过程。参考源码位于同工作区下的TinyWebServer,复刻代码位于ccTinyWebServer。
每个章节完成一个可以独立编译、运行和验证的小目标,再进入下一章节。
第 0 章 环境准备与项目启动
项目:
/home/cc/TinyWebServer
目的:记录从跑通项目到理解核心模块的过程。
创建时间:2026-09-17
当前阶段:环境准备和启动流程整理
0.1 学习目标
- 先把 TinyWebServer 编译、启动、访问跑通。
- 理解项目的目录结构。
- 理解线程池、epoll、HTTP 状态机、定时器、日志、数据库连接池这些核心模块。
- 在学习过程中持续完善这份博客文档。
0.2 当前环境
- 操作系统:Ubuntu 25.10
- 编译器:g++
- 数据库:MySQL 8.4.10
- 项目路径:
/home/cc/TinyWebServer - 家目录:
/home/cc
0.3 项目启动步骤
0.3.1 安装依赖
sudo apt update sudo apt install -y build-essential libmysqlclient-dev mysql-server0.3.2 启动 MySQL
sudo systemctl enable --now mysql sudo systemctl status mysql如果systemctl不可用,可以使用:
sudo service mysql start sudo service mysql status看到下面的信息说明 MySQL 已经启动:
Active: active (running) Status: "Server is operational"0.3.3 初始化数据库
进入 MySQL 管理终端:
sudo mysql执行以下 SQL:
CREATE DATABASE IF NOT EXISTS qgydb DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS 'webuser'@'localhost' IDENTIFIED BY 'webpass123'; ALTER USER 'webuser'@'localhost' IDENTIFIED BY 'webpass123'; GRANT ALL PRIVILEGES ON qgydb.* TO 'webuser'@'localhost'; FLUSH PRIVILEGES; USE qgydb; CREATE TABLE IF NOT EXISTS user( username varchar(50), passwd varchar(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO user(username, passwd) VALUES('name', 'passwd'); SELECT * FROM user; EXIT;当前配置:
数据库名:qgydb 数据库用户:webuser 数据库密码:webpass123 测试用户:name / passwd0.3.4 修改项目配置
编辑main.cpp,确认第 6 到第 8 行是:
string user = "webuser"; string passwd = "webpass123"; string databasename = "qgydb";0.3.5 编译项目
cd /home/cc/TinyWebServer make server也可以使用项目自带脚本:
cd /home/cc/TinyWebServer sh ./build.sh编译成功后,当前目录会出现server可执行文件。
0.3.6 启动 Web 服务器
cd /home/cc/TinyWebServer ./server -p 9006 -s 4 -t 4参数说明:
-p 9006 Web 服务端口 -s 4 数据库连接池中的连接数量 -t 4 线程池中的线程数量程序会一直停在前台运行。默认日志会写入当前目录下类似下面的文件:
YYYY_MM_DD_ServerLog0.3.7 浏览器访问
打开浏览器访问:
http://127.0.0.1:9006/或者使用命令行检查:
curl -i http://127.0.0.1:9006/看到HTTP/1.1 200 OK和judge.html的内容,就说明服务器已经跑通。
0.4 已完成记录
两张站点打开记录:
- [x] 安装编译工具和 MySQL 依赖
- [x] 启动 MySQL 服务
- [x] 创建数据库
qgydb - [x] 创建用户表
user - [x] 修改
main.cpp中的数据库配置 - [ ] 编译生成
server - [ ] 启动 Web 服务并访问首页
0.5 下一步学习记录
下一步可以按模块阅读代码,并在每个模块下补充自己的理解:
lock 线程同步机制封装 threadpool 半同步/半反应堆线程池 http HTTP 请求解析与响应 timer 非活动连接定时器 log 同步/异步日志系统 CGImysql 数据库连接池 webserver 主循环、事件监听和事件处理0.6 学习备注
这里可以继续记录编译错误、调试过程和自己的理解。
第 1 章 工程骨架与线程同步原语
1.1 本章目标
本章完成复刻项目的第一个基础模块:线程同步机制封装。
具体目标如下:
- 建立与参考项目一致的目录结构。
- 编写可用于 C++11 项目的 Makefile。
- 封装信号量、互斥锁和条件变量。
- 使用多线程测试程序验证三个同步原语。
本章暂时不实现 WebServer、HTTP、线程池、定时器、日志和数据库。
1.2 为什么先实现同步原语
线程池、日志系统和数据库连接池都不是单独存在的模块:
- 线程池需要互斥锁保护任务队列。
- 线程池需要信号量通知工作线程。
- 异步日志的阻塞队列需要互斥锁和条件变量。
- 数据库连接池需要互斥锁和信号量管理空闲连接。
因此,先封装这些同步原语,可以避免后续模块重复使用pthread_mutex_t、sem_t和pthread_cond_t的原始接口。
参考项目对应实现位于TinyWebServer/lock/locker.h。
1.3 建立工程目录
进入复刻目录:
cd /home/cc/ccTinyWebServer创建后续开发需要的目录:
mkdir -p lock http timer threadpool log CGImysql root tests docs本章会使用其中两个目录:
ccTinyWebServer/ ├── docs/ │ └── TinyWebServer-Recreation.md ├── lock/ │ └── locker.h ├── tests/ │ └── test_locker.cpp └── makefile其他目录暂时保持为空,等后续章节再逐步填充。
1.4 编写 Makefile
创建makefile:
CXX ?= g++ CXXFLAGS ?= -g -Wall -Wextra -std=c++11 all: test_locker test_locker: tests/test_locker.cpp lock/locker.h $(CXX) -o test_locker tests/test_locker.cpp $(CXXFLAGS) -pthread clean: rm -f test_locker server .PHONY: all clean这里有几个关键点:
CXXFLAGS使用-std=c++11,因为封装代码使用了nullptr和= delete。-pthread同时负责链接 pthread 库并设置线程相关编译选项。test_locker同时依赖源文件和头文件,头文件变化后会重新编译。- 编译命令前必须使用 Tab,不能使用空格。
1.5 封装信号量
信号量由sem类封装,底层接口是sem_init、sem_wait和sem_post。
sem::wait()对应 P 操作:
- 信号量的值大于 0 时,值减 1,然后继续执行。
- 信号量的值为 0 时,调用线程进入阻塞状态。
sem::post()对应 V 操作:
- 信号量的值加 1。
- 如果有线程正在等待,则唤醒其中一个线程。
构造函数允许指定初始值,默认值为 0:
class sem { public: explicit sem(unsigned int value = 0) { if (sem_init(&m_sem, 0, value) != 0) throw std::exception(); } ~sem() { sem_destroy(&m_sem); } sem(const sem &) = delete; sem &operator=(const sem &) = delete; bool wait() { return sem_wait(&m_sem) == 0; } bool post() { return sem_post(&m_sem) == 0; } private: sem_t m_sem; };sem_init的第二个参数0表示信号量只在当前进程的线程之间共享。
复制构造和复制赋值被删除,避免两个对象管理同一个sem_t,从而在析构时重复调用sem_destroy。
1.6 封装互斥锁
互斥锁由locker类封装,底层接口是pthread_mutex_init、pthread_mutex_lock和pthread_mutex_unlock。
class locker { public: locker() { if (pthread_mutex_init(&m_mutex, nullptr) != 0) throw std::exception(); } ~locker() { pthread_mutex_destroy(&m_mutex); } locker(const locker &) = delete; locker &operator=(const locker &) = delete; bool lock() { return pthread_mutex_lock