☰
从零复刻 TinyWebServer —— 环境准备与最小 TCP 服务端
2026/9/27 6:18:27 网站建设 项目流程

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

原项目:qinguoyi/TinyWebServer

复刻仓库:L2501031968/ccTinyWebServer

完整 20 章教程:仓库内docs/TinyWebServer-Recreation.md

为什么写这个系列

TinyWebServer 是一个很好的 Linux C++ 网络编程学习项目。它用相对精简的代码覆盖了 Web 服务器的主要模块:

线程池 epoll 事件循环 HTTP 状态机 定时器 日志系统 数据库连接池 Reactor / Proactor

但直接读原项目源码,对初学者有几个门槛:

  1. 模块多,一开始不知道从哪里读起。
  2. 代码之间耦合紧,改一处可能编译不过。
  3. 看懂某个模块,不等于能自己写出来。

所以这个系列不打算“讲一遍源码”,而是从零复刻:参考原项目,在自己的目录里一步一步重新实现,每一章完成一个能独立编译、运行和验证的小目标,再进入下一章。

原项目: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 学习目标

  1. 先把 TinyWebServer 编译、启动、访问跑通。
  2. 理解项目的目录结构。
  3. 理解线程池、epoll、HTTP 状态机、定时器、日志、数据库连接池这些核心模块。
  4. 在学习过程中持续完善这份博客文档。

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-server
0.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 / passwd
0.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_ServerLog
0.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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询