ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零复刻 TinyWebServer —— 环境准备与最小 TCP 服务端

从零复刻 TinyWebServer —— 环境准备与最小 TCP 服务端 第 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/cc0.3 项目启动步骤0.3.1 安装依赖sudo apt update sudo apt install -y build-essential libmysqlclient-dev mysql-server0.3.2 启动 MySQLsudo systemctl enable --now mysql sudo systemctl status mysql如果systemctl不可用可以使用sudo service mysql start sudo service mysql status看到下面的信息说明 MySQL 已经启动Active: active (running) Status: Server is operational0.3.3 初始化数据库进入 MySQL 管理终端sudo mysql执行以下 SQLCREATE DATABASE IF NOT EXISTS qgydb DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS webuserlocalhost IDENTIFIED BY webpass123; ALTER USER webuserlocalhost IDENTIFIED BY webpass123; GRANT ALL PRIVILEGES ON qgydb.* TO webuserlocalhost; FLUSH PRIVILEGES; USE qgydb; CREATE TABLE IF NOT EXISTS user( username varchar(50), passwd varchar(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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 本章目标本章完成复刻项目的第一个基础模块线程同步机制封装。具体目标如下建立与参考项目一致的目录结构。编写可用于 C11 项目的 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创建makefileCXX ? g CXXFLAGS ? -g -Wall -Wextra -stdc11 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使用-stdc11因为封装代码使用了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。如果有线程正在等待则唤醒其中一个线程。构造函数允许指定初始值默认值为 0class 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
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进