C++高性能Web服务器TinyWebServer源码解析与并发模型实践 1. 项目概述与核心价值如果你正在学习C网络编程或者想亲手搭建一个能扛住上万并发连接的Web服务器那么qinguoyi的TinyWebServer项目绝对是一个绕不开的“宝藏”。这个在GitHub上斩获近2万星的开源项目早已成为无数C后端开发者简历上的“标配”。但说实话很多朋友只是把它跑起来或者对着代码“照猫画虎”对于其内部精妙的设计和每一行代码背后的“所以然”往往一知半解。今天我就以一个过来人的身份带你深入这个项目的“五脏六腑”进行一次万字级别的源码解析。我们不止看它“是什么”更要深挖“为什么这么设计”。从线程池的同步机制到epoll的边沿触发与水平触发抉择再到Reactor与Proactor模型的模拟实现最后到日志系统和数据库连接池的工程化封装。我会结合自己多年踩坑的经验告诉你哪些是关键路径哪些是易错点以及如何将这些设计思想应用到你的实际项目中。2. 整体架构与核心设计思想拆解TinyWebServer之所以经典在于它在有限的代码量内清晰地展示了一个高性能服务器应有的核心模块和设计模式。它不是Nginx那样的工业级巨兽而是一个教学与实践意义并重的“麻雀”虽小但五脏俱全。2.1 核心架构图与数据流整个服务器的运行核心可以概括为“一个事件循环两大处理模型四个核心组件”。主线程通常就是main线程扮演着“交通指挥中心”的角色它通过epoll这个高效的I/O多路复用器监听所有客户端的连接请求listenfd和已连接套接字connfd上的读写事件。当事件发生时根据你选择的-a参数Proactor或Reactor服务器会采取不同的处理策略。事件和任务会被分发给一个预先创建好的线程池由池中的工作线程进行实际的业务处理如解析HTTP请求、访问数据库、组装HTTP响应等。为了管理海量的客户端连接项目引入了定时器组件定期清理那些长时间不活跃的“僵尸连接”释放资源。所有的运行状态无论是调试信息还是错误警报都被系统性地记录到日志系统中。而对于需要数据库操作的任务如用户登录校验工作线程会从数据库连接池中获取一个可用的连接用完即还避免了频繁创建和销毁数据库连接的巨大开销。数据流可以这样理解一个HTTP请求到达监听端口 - epoll通知主线程 - 主线程根据模型封装任务 - 任务被投递到线程池的任务队列 - 某个空闲工作线程取出任务 - 线程解析HTTP请求 - 若需数据库操作则从连接池借连接 - 处理完毕组装响应 - 通过epoll通知主线程可写 - 主线程将数据发回客户端。2.2 两种并发模型Reactor vs. 模拟Proactor这是本项目的一大亮点也是面试中高频被问到的点。项目通过-a参数支持两种模式这体现了设计上的灵活性。Reactor模式这是更“经典”的事件驱动模式。在Reactor中工作线程或处理程序只负责处理非I/O的“业务逻辑”。当epoll检测到某个socket可读时主线程仅仅通知工作线程“有数据可读了”。工作线程需要自己执行读取数据、解析请求、处理业务、发送响应这一整套I/O操作。你可以把主线程看作一个“前台接待”它只负责告诉你“有客人来了”至于端茶倒水、办理业务都得你自己工作线程来。模拟Proactor模式这是本项目对经典Proactor模式的一种模拟实现。在真正的Proactor如Windows IOCP中异步I/O操作如读、写由系统内核完成完成后内核通知应用程序“你要的操作我帮你搞定了数据在这儿”。TinyWebServer在用户态模拟了这一过程主线程在检测到可读事件后亲自将数据从socket内核缓冲区读到用户空间的缓冲区然后将这个“已经包含数据的请求”作为一个完整的任务打包扔给线程池。此时工作线程拿到手的就是一个“已经读好数据的请求包”它直接处理业务逻辑和生成响应即可。发送响应的过程也可能被类似处理主线程负责写。这样工作线程就完全从I/O操作中解放出来只关心CPU密集型的业务计算。选择与思考-a 0默认是Proactor-a 1是Reactor。Proactor模式下工作线程更纯粹但主线程负担加重且数据从内核到用户空间的拷贝发生在主线程可能成为瓶颈。Reactor模式更均衡但工作线程可能因I/O阻塞虽然用了非阻塞socket但读大文件时仍可能耗时。压力测试结果显示在特定场景下Proactor的QPS更高但这并不意味着它永远是最好的选择。理解两者的区别能帮助你在设计系统时做出更合适的选择。2.3 关键组件耦合与解耦设计项目的模块化做得相当清晰。WebServer类是总控中心它聚合了ThreadPool线程池、Epoll事件管理器、Timer定时器管理、SqlConnPool数据库连接池等核心模块的实例。而HttpConn类代表一个HTTP连接它本身不关心线程池和epoll只专注于HTTP协议的解析与响应生成。这种松耦合的设计使得每个模块都可以独立理解、测试和替换。例如你可以很容易地将日志系统从同步改为异步或者替换不同的数据库连接池实现而不会牵一发而动全身。3. 核心模块源码深度解析接下来我们进入重头戏逐个拆解核心模块的源码实现。我会用“代码片段 逐行解读 设计意图分析”的方式让你看得明白学得透彻。3.1 事件驱动核心Epoll的封装与ET/LT模式一切始于epoll。相关代码主要在http目录下的epoll.cpp/.h中。项目没有直接使用原始的epoll_ctl,epoll_wait而是进行了面向对象的封装。// epoll.h 中核心类的定义 class Epoll { public: Epoll(); ~Epoll(); int addFd(int fd, uint32_t events, bool oneShot false); // 添加fd到epoll int modFd(int fd, uint32_t events, bool oneShot false); // 修改fd事件 int delFd(int fd); // 从epoll删除fd int wait(int timeoutMs -1); // 等待事件返回就绪事件数 int getEventFd(int i) const; // 获取第i个就绪事件的fd uint32_t getEvents(int i) const; // 获取第i个就绪事件的事件类型 private: int epollFd_; // epoll实例的文件描述符 std::vectorstruct epoll_event events_; // 用于存放epoll_wait返回的就绪事件数组 };封装的好处显而易见资源管理RAII在构造函数中创建epollFd_在析构中关闭、隐藏底层系统调用细节、提供更易用的接口。events_向量在构造函数中被初始化为一个固定大小如1024避免了每次调用epoll_wait都重新分配内存。ET与LT的抉择这是另一个关键点通过-m参数配置。代码中addFd和modFd函数的events参数决定了模式。// 在webserver.cpp中根据配置设置事件类型 uint32_t event EPOLLIN | EPOLLRDHUP; // 基础事件可读、对端关闭连接 if (trigMode 1) { // ET模式 event | EPOLLET; // 添加边沿触发标志 } epoller_-addFd(clientFd, event | EPOLLONESHOT); // 通常结合EPOLLONESHOT使用LT水平触发默认如果文件描述符就绪epoll_wait会一直通知你直到你把它处理完比如把缓冲区数据读空。这有点像“铃响取快递”快递到了数据就绪就一直响铃通知你取走一件读一部分它还在响直到快递柜清空。编程简单不容易遗漏事件但可能带来不必要的重复通知效率稍低。ET边沿触发只在文件描述符状态发生变化时通知一次。比如数据从无到有空变不空时通知一次。这就像“闪光灯提醒”状态变化时闪一下通知一次之后哪怕数据还在快递没取完也不再提醒。ET模式必须使用非阻塞I/O并且必须一次性将数据读完或写完否则可能永远丢失这次事件。效率高但编程复杂容易出错。项目中对listenfd和connfd可以分别设置LT或ET共四种组合。压力测试数据表明不同组合对性能有可见影响。在实际使用中对于listenfdLT更安全对于高并发的connfdET通常能获得更好的性能但务必确保读写循环处理完整。3.2 并发基石半同步/半反应堆线程池详解线程池代码位于threadpool目录。这是“半同步/半反应堆”模式的具体实现。所谓“半同步/半反应堆”是指半反应堆主线程充当反应堆负责监听所有事件I/O多路复用。半同步工作线程池同步地处理从主线程接收到的任务。让我们看核心的ThreadPool模板类templatetypename T class ThreadPool { public: ThreadPool(int thread_number 8, int max_requests 10000); ~ThreadPool(); bool append(T* request); // 添加任务到队列 private: static void* worker(void* arg); // 工作线程静态函数 void run(); // 工作线程实际运行的函数 int thread_number_; // 线程池中线程数 int max_requests_; // 请求队列中最大请求数 std::vectorpthread_t threads_; // 描述线程池的数组 std::listT* workqueue_; // 请求队列 Locker queuelocker_; // 保护请求队列的互斥锁 Sem queuestat_; // 是否有任务需要处理的信号量 bool stop_; // 是否结束线程 };工作流程初始化构造函数中创建thread_number_个线程每个线程执行worker静态函数该函数调用run成员函数。任务投递主线程或任何线程调用append方法将任务指针T* request加入workqueue_。这个过程需要先加锁queuelocker_保护队列然后通过queuestat_信号量post()增加信号量值通知可能有线程在等待任务。任务获取与执行工作线程在run()函数中循环。循环内首先通过queuestat_.wait()等待信号量如果信号量为0则阻塞。当有任务被append时信号量增加一个工作线程被唤醒。它加锁从队列头部取出一个任务解锁然后处理这个任务对于HttpConn任务就是调用其process()方法。设计精妙之处生产者-消费者模型主线程是生产者工作线程是消费者。workqueue_是缓冲区queuelocker_保证队列操作的原子性queuestat_信号量则优雅地协调了生产与消费的速度避免了消费者忙等。模板化设计ThreadPool是一个模板类T是任务类型。这使它不局限于处理HTTP连接可以处理任何类型的任务只要该任务类型提供了统一的处理接口如process()方法。这极大地提高了代码的复用性。RAII管理线程析构函数中设置stop_true然后通过pthread_join等待所有线程结束确保资源正确释放。注意事项queuestat_信号量初始值为0这很关键。它意味着初始时没有任务所有工作线程都在wait()上阻塞。取任务时采用list的front()和pop_front()保证了FIFO先进先出的公平性。锁的粒度控制得很好加锁仅发生在操作队列push_back和front/pop_front的瞬间任务处理过程是不持锁的这最大程度减少了线程间竞争。3.3 HTTP连接处理状态机解析请求这是业务逻辑的核心代码在http目录的http_conn.cpp/.h中。每个HttpConn对象代表一个活跃的HTTP连接。它使用状态机来解析复杂的、可能分多次到达的HTTP请求报文。主从状态机模型 项目采用了一个经典的双层状态机结构主状态机CHECK_STATE定义当前解析处于请求行、请求头还是消息体的解析阶段。它有三个状态CHECK_STATE_REQUESTLINE、CHECK_STATE_HEADER、CHECK_STATE_CONTENT。从状态机LINE_STATUS负责从缓冲区中解析出一行内容以\r\n结尾。它也有三个状态LINE_OK解析出一行完整行、LINE_BAD行格式错误、LINE_OPEN行数据尚不完整。解析过程在process_read()函数中是一个大循环HttpConn::HTTP_CODE HttpConn::process_read() { LINE_STATUS line_status LINE_OK; HTTP_CODE ret NO_REQUEST; char* text 0; // 当在解析消息体且消息体完整或者解析出行内容时继续循环 while ((m_check_state CHECK_STATE_CONTENT line_status LINE_OK) || ((line_status parse_line()) LINE_OK)) { text get_line(); // 获取当前行起始指针 m_start_line m_checked_idx; // 更新下一行起始位置 switch (m_check_state) { case CHECK_STATE_REQUESTLINE: { ret parse_request_line(text); // 解析请求行 if (ret BAD_REQUEST) return BAD_REQUEST; break; } case CHECK_STATE_HEADER: { ret parse_headers(text); // 解析请求头 if (ret BAD_REQUEST) return BAD_REQUEST; else if (ret GET_REQUEST) return do_request(); // 如果是GET请求且无消息体直接处理 break; } case CHECK_STATE_CONTENT: { ret parse_content(text); // 解析消息体如POST数据 if (ret GET_REQUEST) return do_request(); // 解析完消息体处理请求 line_status LINE_OPEN; // 消息体可能未读完设置行状态为不完整退出循环等待更多数据 break; } default: return INTERNAL_ERROR; } } return NO_REQUEST; // 请求不完整需要继续读取数据 }parse_line()函数是从状态机的核心它遍历读缓冲区寻找\r\n。如果找到就将\r\n替换为\0\0并返回LINE_OK这样text就是一串以\0结尾的C字符串便于后续处理。如果缓冲区数据不够则返回LINE_OPEN。主状态机根据当前阶段调用不同的解析函数。parse_request_line解析GET /index.html HTTP/1.1这样的请求行提取方法、URL、协议版本。parse_headers解析Host:、Connection:等头部字段并特别关注Content-Length以判断是否有消息体以及消息体长度。对于POST请求Content-Length大于0主状态机会进入CHECK_STATE_CONTENT。parse_content并不复杂它主要是检查缓冲区中是否已经收到了Content-Length指定长度的消息体数据。如果收够了就返回GET_REQUEST触发do_request()进行实际处理如登录校验如果没收够就返回NO_REQUEST等待下次可读事件继续接收。为什么用状态机HTTP协议是基于文本的、行导向的协议并且请求可能被TCP拆分成多个包到达。状态机是处理这种“流式”协议最清晰、最健壮的方式。它能够优雅地处理数据不完整的情况返回NO_REQUEST也能准确地定位协议错误返回BAD_REQUEST。3.4 资源管理定时器与数据库连接池定时器timer目录用于处理非活动连接。每个客户端连接HttpConn在创建或每次有活动时都会更新其对应的定时器节点的时间戳。一个独立的线程或在主事件循环中定期例如每5秒执行一次tick()函数它遍历定时器链表通常按超时时间排序将当前时间与节点时间戳比较。如果某个连接的超时时间如15秒已到则认为其非活动关闭对应的socket释放HttpConn资源。这是一种惰性删除避免了为每个连接设置单独的超时检测线程带来的巨大开销。项目使用了升序链表管理定时器tick操作复杂度是O(N)。在连接数极大时可以考虑使用时间轮或最小堆来优化。数据库连接池CGImysql目录下的sql_connection_pool这是应对数据库访问瓶颈的经典手段。频繁创建和销毁数据库连接mysql_real_connect/mysql_close是极其昂贵的操作。连接池在服务器启动时一次性创建固定数量如8个的数据库连接放入一个队列connList中。class ConnectionPool { public: MYSQL* GetConnection(); // 获取一个连接 bool ReleaseConnection(MYSQL *conn); // 释放连接归还到池中 // ... private: std::listMYSQL* connList; // 连接队列 Sem reserve; // 信号量用于同步连接获取 Locker lock; // 锁保护连接队列 // ... };当工作线程需要访问数据库时调用GetConnection()。该函数首先通过reserve.wait()等待信号量表示有可用连接然后加锁从connList头部取出一个连接解锁后返回给工作线程使用。使用完毕后线程调用ReleaseConnection将连接加锁后放回connList尾部并调用reserve.post()增加信号量通知其他等待的线程。关键点这里使用了信号量来记录可用连接数量而不是在GetConnection里忙等或循环检查这是非常高效的做法。同时连接池通常还会实现连接的健康检查心跳但TinyWebServer的示例中简化了这部分。在生产环境中你需要考虑连接失效重连、最大等待时间等机制。3.5 运行记录同步/异步日志系统日志系统log目录是项目工程化的体现。它支持同步和异步通过-l参数选择两种写入方式。同步日志调用日志写入函数如LOG_DEBUG(“%s”, msg)时直接格式化日志内容并调用fputs写入文件。这可能会阻塞调用线程尤其是在磁盘IO慢的时候。异步日志这是更高效的模式。它引入了一个阻塞队列BlockQueue作为缓冲区。所有日志写入请求只是将格式化好的日志字符串push到这个队列中。有一个独立的日志线程在后台运行它不断地从队列中pop出日志字符串然后批量写入文件。这样工作线程或主线程在记录日志时耗时极短只是内存操作真正的IO压力由后台日志线程承担实现了写日志操作与主业务逻辑的解耦。异步日志的实现要点单例模式日志类通常被实现为单例全局只有一个实例便于各处调用。阻塞队列使用互斥锁和条件变量实现。当队列为空时日志线程在pop上阻塞当有日志入队时唤醒日志线程。日志格式化支持不同的日志级别DEBUG, INFO, WARN, ERROR并自动添加时间戳、线程ID可选等信息。日志滚动一个高级特性是按文件大小或日期切割日志文件防止单个文件过大。TinyWebServer的基础版本可能未实现但这是一个生产级日志库必备的功能。4. 项目构建、运行与压力测试实操理解了源码我们来看看如何把它跑起来并验证其性能。4.1 环境准备与数据库初始化假设你在Ubuntu 20.04环境下操作。# 1. 安装必备依赖 sudo apt-get update sudo apt-get install g cmake make mysql-server libmysqlclient-dev # 2. 克隆代码 git clone https://github.com/qinguoyi/TinyWebServer.git cd TinyWebServer # 3. 创建并初始化数据库 sudo mysql -u root -p # 输入你的MySQL root密码 mysql CREATE DATABASE yourdb; mysql USE yourdb; mysql CREATE TABLE user( - username char(50) NULL, - passwd char(50) NULL - )ENGINEInnoDB DEFAULT CHARSETutf8; mysql INSERT INTO user(username, passwd) VALUES(test, 123456); # 插入一个测试用户 mysql exit;注意确保MySQL服务正在运行sudo systemctl status mysql。新版MySQL可能默认使用auth_socket插件认证如果连接失败可能需要修改用户密码认证方式或使用sudo mysql直接登录。4.2 编译与配置修改修改main.cpp中的数据库连接信息使其匹配你的环境// 大约在main.cpp第30行左右 string user root; // 数据库用户名 string passwd your_password; // 数据库密码如果root无密码可能是空字符串 string databasename yourdb; // 数据库名就是上面创建的yourdb然后执行项目提供的编译脚本chmod x build.sh ./build.shbuild.sh脚本内容很简单就是g编译所有.cpp文件并链接pthread和mysqlclient库。编译成功后会生成server可执行文件。4.3 运行服务器与功能测试运行服务器你可以使用默认参数也可以使用个性化参数# 默认运行Proactor模型LTLT端口9006打开日志 ./server # 个性化运行示例使用Reactor模型ETLT端口8080关闭日志12个线程 ./server -p 8080 -a 1 -m 2 -c 1 -t 12服务器启动后打开浏览器访问http://你的服务器IP:9006。你应该能看到一个简单的测试页面可以尝试注册/登录点击相关链接使用你在数据库中插入的用户名(test)和密码(123456)登录。这触发了服务器访问数据库连接池、执行SQL查询、校验密码的完整流程。请求静态资源页面上的图片和视频链接会触发服务器解析GET请求、定位项目root目录下的文件、读取文件内容并组装HTTP响应返回。这里用到了mmap或writev等零拷贝或聚集写技术来高效发送文件。4.4 压力测试实战项目提供了Webbench的修改版进行压力测试。压力测试是衡量服务器性能的关键环节。# 进入压力测试目录 cd test_pressure # 编译webbench make # 运行压力测试模拟1000个客户端持续30秒访问服务器首页 ./webbench -c 1000 -t 30 http://127.0.0.1:9006/参数解读-c 1000: 并发客户端数为1000。-t 30: 测试持续时间为30秒。最后的URL是服务器地址和测试页面。结果分析你会看到类似如下的输出Webbench - Simple Web Benchmark 1.5 Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software. Benchmarking: GET http://127.0.0.1:9006/ 1000 clients, running 30 sec. Speed326410 pages/min, 1252343 bytes/sec. Requests: 163205 susceed, 0 failed.Speed: 每分钟处理的页面数QPS * 60。326410 pages/min约等于5440 QPS。Requests: 成功和失败的请求数。零失败是理想情况。影响性能的关键因素并发模型-a如之前所述Proactor通常QPS更高。触发模式-mET模式通常比LT模式性能更好尤其是对于高并发的活跃连接。线程数-t并非越多越好。需要匹配你的CPU核心数。通常建议设置为CPU逻辑核心数的1-2倍。可以通过nproc命令查看核心数。日志开关-c压力测试时务必关闭日志-c 1磁盘IO是性能杀手异步日志也仍有开销。关闭日志能反映服务器网络和处理逻辑的纯粹性能。数据库操作如果测试页面包含大量数据库请求性能瓶颈可能会转移到MySQL。可以测试纯静态文件请求来评估网络和I/O处理能力。5. 常见问题排查与性能调优指南在实际运行和借鉴本项目进行开发时你肯定会遇到各种问题。这里我总结了一些典型问题和调优思路。5.1 编译与运行问题问题现象可能原因解决方案fatal error: mysql/mysql.h: No such file or directory未安装MySQL开发库sudo apt-get install libmysqlclient-dev(Ubuntu/Debian)undefined reference tomysql_xxx‘编译时未链接MySQL客户端库检查build.sh或makefile确保有-lmysqlclient链接选项连接数据库失败服务器启动报错1. MySQL服务未运行2.main.cpp中数据库密码错误3. 用户权限不足1.sudo systemctl start mysql2. 检查并修改main.cpp中的密码3. 在MySQL中授予用户权限GRANT ALL ON yourdb.* TO rootlocalhost; FLUSH PRIVILEGES;服务器启动后浏览器无法访问1. 防火墙阻止端口2. 服务器监听地址错误1.sudo ufw allow 9006(Ubuntu)2. 检查main.cpp中server初始化是否监听INADDR_ANY(0.0.0.0)压力测试时webbench报错或找不到命令webbench未编译或不在PATH进入test_pressure目录执行make并在该目录下运行./webbench5.2 性能瓶颈分析与调优当QPS达不到预期时可以按以下思路排查CPU瓶颈使用top或htop命令观察服务器进程的CPU使用率。如果接近100%单核或所有核心都高负载可能是计算瓶颈。使用perf或gprof进行性能剖析找到最耗时的函数。热点可能在HTTP解析、字符串处理或业务逻辑上。调优建议优化关键路径代码如状态机解析、检查是否有不必要的拷贝如字符串、考虑使用更高效的数据结构。I/O瓶颈磁盘I/O如果开启日志且日志写入频繁可能成为瓶颈。务必在压测时关闭日志。对于生产环境确保日志写入的磁盘是高性能的如SSD并使用异步日志。网络I/O使用ET模式、调整内核网络参数如tcp_tw_reuse,somaxconn、使用sendfile或writev进行零拷贝文件发送可以提升网络吞吐。监控命令vmstat 1查看bi/bo块设备读写iostat -x 1查看磁盘使用率。上下文切换与锁竞争使用vmstat 1查看cs上下文切换次数。如果过高说明线程数可能过多或者锁竞争激烈。检查线程池大小线程数过多会导致大量上下文切换开销。建议设置为CPU核心数1到2倍核心数之间并通过压测找到最佳值。减少锁粒度仔细检查代码中的锁如日志系统的队列锁、连接池的锁。确保锁只保护必要的最小临界区并尽快释放。数据库瓶颈如果请求涉及数据库使用SHOW PROCESSLIST;查看MySQL当前连接和查询状态。确保user表上有合适的索引虽然本项目数据量小用不上。调整数据库连接池大小-s参数连接数过少会导致工作线程等待过多会增加MySQL负担。5.3 项目扩展与生产化思考TinyWebServer是一个优秀的教学和起点项目但要用于生产环境还需要考虑更多配置化将端口、线程数、数据库连接信息等硬编码在main.cpp中的参数应该提取到配置文件中如JSON、YAML或.ini便于运维。优雅退出实现信号处理如SIGINT,SIGTERM在收到退出信号时优雅地关闭监听socket、等待现有连接处理完毕、清理线程池和连接池再退出程序。更健壮的HTTP协议支持目前只支持GET和POST方法以及有限的头部。生产环境需要支持HEAD、PUT、DELETE等方法以及chunked传输编码、gzip压缩、HTTPS等。安全加固防止缓冲区溢出严格检查Content-Length、SQL注入使用参数化查询本项目在login检查中直接拼接字符串是有风险的、目录遍历攻击对请求的URL路径进行规范化检查防止../../../这样的路径。监控与告警集成更完善的日志和指标收集如请求数、响应时间、错误率并能够对接Prometheus、Grafana等监控系统。集群化与负载均衡单机性能总有上限。真正的生产系统需要多台服务器并通过Nginx等负载均衡器进行分发。回过头看TinyWebServer项目最宝贵的价值不在于它实现了多么强大的功能而在于它用相对简洁的代码清晰地串联起了Linux下C高性能服务器编程的几乎所有核心知识点I/O多路复用、并发模型、线程同步、网络协议解析、资源池化、异步处理。它像一张精心绘制的地图为你指明了学习网络编程的路径。我的建议是不要满足于让它跑起来。尝试去修改它把LT模式改成ET把Proactor改成Reactor自己实现一个时间轮定时器或者为它添加一个简单的配置文件解析功能。在动手改造的过程中你会对这些问题理解得无比深刻。当你能够清晰地回答出“为什么这里要用双状态机解析HTTP”、“信号量在这个线程池里到底起了什么作用”、“ET和LT在代码实现上到底有什么区别”这些问题时你才真正地把这个项目的精华吸收到了自己的知识体系里。