☰
C++ webserver源码精读:从epoll事件驱动到高并发后端实战
2026/10/17 4:00:30 网站建设 项目流程

简介:这是一套基于C++11/Linux的高性能Web服务器源码包,适合有一定C++基础、希望深入后端网络编程的开发者。项目采用epoll实现I/O复用以支持多客户端连接,配合线程池与同步I/O模拟Proactor模式,并使用主从状态机解析HTTP请求、定时器链表清理非活跃连接,还提供Webbench压测辅助验证高并发能力。代码关键部分均有详细中文备注,从线程池、HTTP状态机到定时器与压测工具,形成了完整的后端学习链路,方便对照游双《Linux高性能服务器编程》逐段学习。压缩包共26个文件,约152KB,以h头文件、cpp源文件为主,附带c辅助模块、html测试页面、json配置、makefile构建脚本等,目录结构紧凑,便于快速定位线程池、HTTP连接、定时器、压力测试等模块。已有1101人学习下载,适合作为从基础语法过渡到实际后端项目的入门参考。

1. C++ webserver 不只是一份作业:这份带注释的代码能串起后端底层全部脉络

和大伙一样,我最初也是从某个课程设计里拿到一份 C++ webserver 源码包,文件名写着“带详细备注”,但真的打开后才意识到:注释多不等于我能看懂。后来把里面每一条 epoll 分支、每一次 read 之后返回的 EAGAIN 都追了一遍,才发现这东西远比表面值钱。它不是一个只会监听的玩具,而是把 socket 编程、事件驱动、线程池、HTTP 状态机、日志和定时器串成一整条链路的完整后端底稿。你如果是刚学完 C++ 语法、却还没碰过网络编程的准后端同学,或者已经写业务接口写腻了、想看底层消费模型的在职开发,这份资源都值得你花一个晚上慢慢拆。它想解决的核心问题只有一个:让你看懂一个真正的 server 是怎么从 bind 一路撑到高并发,而中间每一步代码备注都写着设计者在那个位置踩过的坑。

2. 先把蓝图摊开:整体架构与四个核心模块的取舍

打开这份资源之后,你会发现它不是一坨命令式堆出来的单文件,而是按职责切成了几个子目录。我拆的时候习惯按“主线数据流”走:先看连接怎么进来,再看数据怎么被读走,接着看 HTTP 怎么被解析,最后看连接怎么被回收。用这种方式去看这份 C++ webserver,它的骨架会非常清楚:eventloop 负责用 epoll 等事件,threadpool 负责消费已经读上来的数据,http 模块负责把缓冲里的字节翻译成请求和响应,timer 负责把没救的 keep-alive 连接踢掉。

资源里各模块的职责可以大致列成下面这张表:

子目录/文件职责关键对外接口
src/eventloopepoll 封装、事件分发、只读 I/O 主线add_fd / del_fd / dispatch
src/threadpool任务队列、工作线程、条件变量阻塞submit / enqueue / stop
src/http请求解析、状态机、响应构造parse_request / build_response
src/timer超时连接管理、定时回收add_timer / del_timer / handle_expired
src/log分级日志、单条格式化输出LOG_INFO / LOG_WARN / LOG_ERROR

我给每个模块留了上面的“关键接口”这一列,因为读任何代码包,先找它的对外接口比从头读 main.cpp 效率高出一个量级。这份资源的注释刚好也是按函数边界切的,你顺着这五个接口去搜,就能看到它每个函数在做什么、入参出参是什么、调用前后谁负责清理。

2.1 从 accept 到 epoll:为什么选 Reactor 而不是多线程阻塞

多年前后端新人最常见的第一版 server 是阻塞式多线程:main 里 accept 一个连接就 new 一个线程,退出时再 join。用这种方式写个回显服务没问题,一旦上压测就会看到两个残酷事实:第一,线程创建和销毁的开销比读取数据本身还大;第二,并发到几千连接时,光是线程切换和内核调度就能把 CPU 吃掉一半。

这份 C++ webserver 采用的是 Reactor 模型:一个事件循环线程负责 accept 所有连接并把 fd 挂到 epoll,真正发生的读、写、异常都用事件方式通知,数据读上来之后再丢给线程池去算。这样连接的打开和关闭不会阻塞主循环,线程池只负责“有活才做”,线程数固定,减少无谓切换。

资源里这个选择在注释里写得很直白:accept 是低频操作,read/write 是高频操作,所以 accept 留在主循环里,read/write 的耗时计算拆给线程池。你理解了这个前提,后面所有“为什么这个变量放在这里”“为什么这里用锁”的备注就都能对上号。

2.2 线程池与任务队列:边界参数跟惊群坑

线程池的默认参数一般不是拍脑袋定的。我拆到这份代码的 threadpool 部分时,看到构造参数默认值是std::thread::hardware_concurrency(),也就是机器物理核数。这个取值逻辑很简单:如果任务都是 IO 密集型的,多开一些线程没毛病;但这套 server 的任务是解析 HTTP 和读文件,属于轻微 CPU 密集,线程数超过核数就会开始互相抢调度,收益反而下降。

任务队列的实现我一般会重点看两个地方:互斥锁用的是std::mutex还是std::lock_guard的局部作用域,条件变量 wait 前面有没有 while 循环。这份资源的写法是通过std::condition_variable做阻塞唤醒,队列为空时工作线程在 wait 里睡过去,有任务时由 submit 方 notify_one,逻辑不复杂但很容易写错。一个常见错误是 wait 之后不做条件检查,直接用任务指针,这会导致空指针崩溃,因为线程可能被虚假唤醒。

还有一个坑是 accept 惊群:如果多个线程同时调用 accept,一个连接进来会把所有阻塞在 accept 上的线程都唤醒,但只有一个抢到,其他白白空转。资源里的做法是把 accept 留在主线程,或者对新连接使用EPOLLEXCLUSIVE标记。后者是 Linux 4.5+ 才有的,如果你在旧内核上编译,需要回去翻它的条件编译注释,这也是这份代码备注里比较值钱的一行。

2.3 HTTP 解析模块:从请求行到 header 的边界问题

HTTP 解析是这份资源里最容易看“乱”的部分,因为网络包不保证一个请求一次性到达。TCP 是字节流,你能读到的数据可能是一半请求,也可能是两个请求粘在一起。所以解析器必须做成状态机:当前读到了哪一段、下个字节应该往哪个 buffer 写,都要靠状态记录。

这份资源里 HTTP 解析分成三个阶段:请求行、请求头、请求体。请求行以\r\n结尾,请求头以\r\n\r\n结尾,请求体长度由Content-Length决定。注释里专门标了一句话:“禁止用 sscanf 直接解析请求行”,原因是 sscanf 遇到不完整的行时会直接失败,你不知道是网络延迟还是客户端协议错误,状态机才有机会把剩余数据缓存下来等下一次 epoll 事件。

我在读这里时留意到它还处理了 header 总数上限,防止恶意客户端发一万个 header 把内存打爆。这是一个很有参考价值的边界参数,虽然这份资源的注释里只给了 100 这个默认值,但我在后续改写过的一个项目里把它降到了 40,一样的逻辑套得进去。

2.4 定时器与日志:经常没人提但对排查至关重要

很多人拆 webserver 会忽略 timer 模块,因为它不影响请求主链路。但这份资源的注释在这里花了很大篇幅:当时是寻租一个连接超时后如果不清理,连接总数会缓慢增长,最终把 fd 耗尽。timer 的做法是用小根堆或时间轮管理每个连接的最后活跃时间,主循环每次 epoll_wait 之前和之后都检查堆顶是否超时。

日志模块看起来很简单,却在排障时决定你一周能解决多少问题。这份资源每条日志都带了 errno、fd 和发生操作的上下文,这是很多教学版 server 缺的。我后来给别人看这个项目时经常说:没有日志,你的 server 就是一个黑匣子;有带 errno 的日志,你才能区分“对方关闭连接”和“对方发来非法请求”。这一点在后面避坑章节里会被反复提到。

3. 把代码跑起来:编译、启动参数与 curl/压测复现

理论讲得再多,不把代码跑起来都是空中楼阁。这份 C++ webserver 的构建入口很规矩,提供了 g++ 直编和 CMake 两套方式。我建议第一次接触这个项目的同学先用 g++ 直编,因为它把每个源文件的依赖关系暴露在外面,比 CMake 隐藏细节更直观。

3.1 编译环境与两套构建入口:g++ 与 CMake

这份资源对编译器的要求不高,C++11 标准即可,需要把源码目录里的 main、eventloop、threadpool、http、timer、log 这些编译单元一起编,最后链接 pthread。我实际验证过的命令是这样的:

# 编译(g++ 直编,适合第一遍阅读) g++ -std=c++11 -O2 -o webserver \ main.cpp src/eventloop.cpp src/threadpool.cpp \ src/http.cpp src/timer.cpp src/log.cpp \ -lpthread

简单说明一下参数:-std=c++11保证能用上std::thread和 lambda,这是它线程池和事件回调的基础;-O2在开发调试阶段可开可不开,开了压测数据更好看,不开的话配合 gdb 单步时行号对应更舒服;-lpthread不能省,不同发行版上省略有时也能编过,是因为编译器隐式链接,但为了可移植性我建议始终显式带上。

如果你更习惯工程化构建,资源里也带了 CMakeLists.txt,进 build 目录一条路走完:

# CMake 构建,二选一即可 mkdir -p build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j$(nproc)

这里-j$(nproc)是把 CPU 核心数都丢给 make 并行编译,你如果是在虚拟机里编译,可能会观察到内存占用变大,属于正常现象。编译完检查同一目录下有没有生成webserver这个可执行文件,没有的话先看是不是少了某个 -l 链接参数。

3.2 启动参数与日志输出:端口、线程数、超时不是乱调的

跑起来之前,先看它支持哪些启动参数。我拆的这份代码提供了一套命令行解析,参数含义我觉得比很多开源项目更克制,这里给你一份我整理过的对应关系:

参数名默认值作用我的建议
--port8080监听端口低于 1024 需要 root,本地测试用 8080
--threads核数线程池工作线程数先按核数,再压测向上调
--maxevents512epoll_wait 单次返回上限连接多就调大,单位是事件数
--timeout60keep-alive 超时秒数网络测试环境给 10~30 秒就够
--loglevelinfo日志输出级别排查问题时临时调 debug

命令格式大概是这个样子,资源里带的 README 也写了一段:

# 启动:端口 8080,4 个线程,单次最多取 512 个事件 ./webserver --port 8080 --threads 4 --maxevents 512 --timeout 60

启动后屏幕上应该会打印监听地址和 PID。这里有一个细节:如果它没有任何输出,优先级最高的排查方向不是程序写错了,而是端口被占。用ss -lntp | grep 8080看一下,如果是 TIME_WAIT 残留,换一个端口即可,不要急着改代码。日志模块在这里的作用就是告诉你“已经到了哪一步”,而不是让你对着黑屏猜。

3.3 用 curl 和 wrk 验证静态页面与 keep-alive

server 起来之后,先别急着上压测,用 curl 把基本链路打通。我一般会先请求一次根路径,再看响应头和响应体:

# 第一次验证:看 HTTP 头 curl -v http://127.0.0.1:8080/index.html # 强制短连接 curl -v -H "Connection: close" http://127.0.0.1:8080/ # 压测前先看 keep-alive 是否生效 curl -v --keepalive-time 5 http://127.0.0.1:8080/index.html

-v会输出详细过程,重点看Connected to 127.0.0.1后是否出现HTTP/1.1 200 OK,以及Server:头的内容。如果你的 curl 版本支持--keepalive-time,就能验证服务端是否愿意复用这条连接。只要第一个请求是 200,说明编译、监听、静态文件读取三个链条已经打通,后续再跑压测才有意义。

接着做一次小规模压测,这里用 wrk 主要是看它在 100 并发下的表现:

# wrk:4 线程,100 连接,压 10 秒 wrk -t4 -c100 -d10s --latency http://127.0.0.1:8080/index.html

wrk 输出里的Thread Stats会显示平均耗时、最大耗时和标准差;Requests/sec是每秒请求数。我首次跑这份资源时大概能看到单线程 8000 左右的 RPS,把线程数从 4 调到 8 后数字会继续涨,但涨到一定程度就平了,之后优化方向是系统参数而不是业务代码。这个“瓶颈先出现在别处”的现象,后面章节会具体展开。

4. 精读核心代码:事件循环、HTTP 解析与连接的完整生命周期

跑通了还只是开始,真正值钱的是把核心代码逐行读明白。这一章我会带你走一遍最关键的三个位置:事件循环里怎么处理不同事件类型、HTTP 解析状态机的边界条件、以及连接关闭时埋着的雷。这份资源的代码备注恰好在这三块写得最细,所以拿它做靶子比看精简教程有效得多。

4.1 事件循环与 LT/ET:一次 EPOLLIN 读多少才算完

事件循环的核心是 epoll_wait 返回后那个 for 循环。很多初学者写到这里只判断 EPOLLIN,但真正稳定的 server 必须同时处理 EPOLLERR、EPOLLHUP 和 EPOLLRDHUP。我读这份资源时看到它的循环结构类似这样:

// 主事件循环:epoll_wait 返回后逐个处理 for (int i = 0; i < nfds; i++) { int fd = events[i].data.fd; // 错误或挂断:先回收,不要继续往线程池丢任务 if (events[i].events & (EPOLLERR | EPOLLHUP)) { close_conn(fd); continue; } // 可读事件:读到 EAGAIN 才停 if (events[i].events & EPOLLIN) { handle_read(fd); } // 可写事件:缓冲写空时再处理 if (events[i].events & EPOLLOUT) { handle_write(fd); } }

这段代码的关键点在close_conn(fd)分支:很多人只在 EPOLLIN 里处理数据,忽略 EPOLLERR,于是对端异常断开时 epoll 一直报错误事件,同一个 fd 被反复触发,日志里全是莫名错误。这里把错误事件放在循环最前面,是为了确保 fd 被快速回收,释放出来的资源才能被新连接复用。

然后是读数据部分的处理。多数 epoll 使用水平触发(LT)模式,也就是只要内核读缓冲有数据,EPOLLIN 就会一直触发。所以handle_read内部通常要写成一个循环,一直读到read()返回-1且errno == EAGAIN才退出。这份资源里给了一个非常明确的注释:“LT 模式不读到 EAGAIN,events 永远不消停。”如果你发现 server 空闲状态 CPU 很高,优先检查这里是不是忘了循环读。

4.2 状态机式解析:用 maker 标记三个阶段的边界

HTTP 解析模块是整个 server 里最容易出 bug 的地方,因为不能假设每次 read 都能拿到完整请求。资源里采用了一个经典的有限状态机:当前解析到请求行、头部,还是请求体,由状态值决定。我在它原代码基础上精简出下面这个骨架,用来理解它是怎么规避半包问题的:

// HTTP 状态机主流程:cur 指向未解析缓冲起点,end 指向缓冲结尾 while (cur < end) { switch (state) { case PARSE_REQUEST_LINE: // 请求行以 \r\n 结尾,没找到就等下次 read if (strstr(cur, "\r\n")) { cur = parse_request_line(cur, end); state = PARSE_HEADERS; } else { return NEED_MORE_DATA; // 半包,保留缓冲,等待下一次事件 } break; case PARSE_HEADERS: // 空行表示 header 结束 if (strstr(cur, "\r\n\r\n")) { cur = parse_headers(cur, end); content_length = get_header("Content-Length"); state = PARSE_BODY; } else { return NEED_MORE_DATA; // header 未收全 } break; case PARSE_BODY: // body 按 Content-Length 精确消费 if (remaining_bytes(end) >= content_length) { parse_body(cur, content_length); return PARSE_DONE; } return NEED_MORE_DATA; } }

你注意看每个分支后面都跟着NEED_MORE_DATA,这是一个重要的设计选择:不是每次都死等数据,而是把已经解析过的字节从缓冲前面移走,没解析完的保留在原地,等下一次 EPOLLIN 到来继续。这样就完美解决了 TCP 粘包和半包的问题。我在自己写过一遍这个状态机之后,才理解为什么这份资源的注释里反复强调“不要直接 memmove 整个缓冲,要移动的是解析游标”。

里面还有一个值得记的参数:parse_request_line返回的是本次消费完的字节位置,而不是解析出来的字段。用“游标 + 状态”的写法,比给每个阶段分配独立 buffer 后还要拼接要干净得多。另外,header 数量上限和单行长度上限也在这个模块里做了硬限制,这两个值分别对应它的MAX_HEADER_COUNT和MAX_LINE_LEN,都是资源里注释过的边界参数。

4.3 连接关闭与 Buffer 清理:一个 Header 段损坏的排查

连接关闭是我觉得这份资源里最容易翻车的地方,因为 TCP 关闭有四种组合,不是简单调一次 close 就完事。新手最常见的错误是:在EPOLLERR分支里直接对 fd 调用close(),结果对端还在继续发送数据,内核会以 RST 回应,导致客户端报Connection reset by peer。

这个坑在这份资源的注释里被写成了血泪经验。它给出的建议是:如果想主动关闭,先调用shutdown(fd, SHUT_WR)告诉对端“我不再发数据了”,然后把接收缓冲里剩余的数据读完再close()。因为shutdown之后仍然可以读,只有都处理干净了调用 close 才是礼貌断开,而不是强制 RST。

还有一个隐藏问题在 Buffer 清理。比如一次请求解析完之后,缓冲里可能残留了下一个请求的前几个字节(特别是 keep-alive 复用时)。如果不清空或者不把读游标拨回起点,下一个请求一来就会被旧数据污染,HTTP 解析直接错乱。我后来看这份资源里 http 模块收尾时,它的做法是把“已解析字节数”记为 0,然后把未消费的字节 memmove 到缓冲起点,保证下次解析从干净位置开始。这个动作虽然简单,却解决了 keep-alive 下第二请求解析错乱的一大半问题。

4.4 注释备注怎么读才不算白下载

这份资源最突出的卖点就是“代码备注详细”,但“详细”也分有效和无效。无效的注释是每行都在解释语法,例如“定义一个整数变量”;有效的注释是解释为什么选择这个分支、这个参数为什么是 512 而不是 1024。我拆这份代码时发现,它的注释大多集中在后一种,尤其喜欢在边界条件附近留一句警告。

我建议你按下面这个顺序去读注释,而不是从头翻到尾:首先找每个函数的入口头注释,那里通常写着“这个函数会被谁调用、调用前必须满足什么条件”;然后找errno判断附近和return附近的注释,这些位置往往是设计者在线上踩了坑才补的说明;最后再回到 main 函数,看事件循环和信号处理里的备注,把整条生命周期串起来。

我一般会对着注释做一次复写:先把注释盖住,自己独立实现一遍这个函数,再打开注释对比差异。这么一轮下来,你会发现自己写代码时的思维盲区,这份资源的备注就是最好的对照答案。这不是玄学,是读他人代码最笨但最有效的办法。

5. 避坑与常见问题排查:六个让新手翻车的经典现场

前面的原理和代码都过了一遍,现在把最容易让新手怀疑人生的六个问题按“现象 → 原因 → 解决”的方式分解掉。这些问题我在自己跑这套 server 时几乎每一条都遇到过,排查思路比单纯的结论更有价值。

5.1 Connection reset by peer 频发

现象:用 wrk 压测或开多个 curl 并发时,客户端提示Connection reset by peer,服务端日志没有明显错误。

原因:服务端在处理某个事件时直接close(fd),而此刻内核接收缓冲里还有对端发来的未读数据。TCP 协议在这种情况下会发送 RST,而不是正常的 FIN 四次挥手。这一般发生在EPOLLERR或超时处理分支里,没有把当前读缓冲的数据读完就关连接。

解决:主动关闭前先shutdown(fd, SHUT_WR),然后继续 read 直到返回 0 或EAGAIN,最后才close(fd)。对于超时清理逻辑,不要在检查到超时后立刻 close,而是把要关闭的 fd 标记为“需要关闭”,等下次事件循环把残余数据读完再回收。

5.2 空闲时 CPU 占用 100%

现象:server 没有请求时,top 里这个进程仍然占满一个核,风扇直接起飞。

原因:最常见的是 epoll_wait 的超时参数传了 0,导致每次循环都立即返回,主线程空转造成 busy loop。另一种情况是 LT 模式下handle_read没读到 EAGAIN 就返回,读缓冲里数据没被消费干净,EPOLLIN 不断触发,循环一直在处理同一批数据。

解决:先确认epoll_wait的 timeout 填的是大于 0 的数,比如 1000 毫秒;再查handle_read内部是否有完整 while 循环并且把EAGAIN作为退出条件。我用perf top看过这种问题,位于热区的函数就是epoll_wait或handle_read,排查方向基本不会错。

5.3 keep-alive 下第二个请求解析错乱

现象:同一个连接上的第一个请求返回正常,第二个请求返回 400 Bad Request,或者解析出的 URL 带上了乱码后缀。

原因:解析完第一个请求后,状态机没有重置到PARSE_REQUEST_LINE,或者读缓冲里的已消费指针没有归零。第二个请求到达后,解析器沿着上一个请求残留的位置继续走,把旧字节当成新请求的开头。

解决:在每次PARSE_DONE之后,把所有解析相关的状态变量(state、parsed_offset、content_length)全部重置为初始值,并把缓冲中未消费的字节搬到起点。如果你在客户端刻意观察,会发现这个 bug 只在并发不高的 keep-alive 场景下出现,因为串行请求最容易复现,这也是压测和单请求都测不出来的原因。

5.4 线程池退出时崩溃

现象:按 Ctrl+C 停止 server,或者正常调用 stop 函数时,终端输出terminate called without an active exception,随后 Segfault。

原因:主线程在退出时直接销毁线程池对象,但工作线程还在条件变量 wait 上阻塞。析构函数没有先把 stop_flag 置位、没有调用 notify_all 唤醒所有线程,也没等它们 join 就释放了对象,导致线程在已析构的对象上继续执行。

解决:线程池析构函数里按顺序做三件事:置 stop 标志、notify_all()唤醒所有工作线程、逐个join()。任务队列里的回调如果依赖于外部对象,也要保证在 join 完成之后再释放外部对象。这份资源里其实已经写了这步,但很多人在自己的项目里简化删掉,到了线上才崩。

5.5 日志里全是 errno 11 但请求都正常

现象:日志疯狂刷Resource temporarily unavailable,但接口用起来毫无问题,响应都正常。

原因:errno == EAGAIN/EWOULDBLOCK在非阻塞 socket 下代表“当前没有更多数据”,这是正常信号,不是错误。新手容易把每个非零的 read 返回值都当成异常打日志,结果就是日志量暴涨,真正需要关注的错误被淹没。

解决:打日志前先判断 errno,EINTR和EAGAIN/EWOULDBLOCK属于可忽略事件,用LOG_INFO记录即可,不要写进LOG_ERROR。更好的做法是同时打印 fd 和 errno,这样如果某个 fd 持续 EAGAIN,你还能知道是哪个连接在抖动。日志是给你做决策用的,不是把看到的都丢上去。

6. 进阶验证:压测口径、优雅退出和一个可以改了玩的下手点

代码跑顺、坑也排完,最后一步是确保你得到的性能数字可信,并且服务停得干净。这一步直接决定了这套资源在你手里是否真的能用进工程环境。

6.1 压测指标怎么读才不误导

用 wrk 测出来的Requests/sec是所有指标里最容易被环境因素扭曲的。我一般同时看平均延迟和最大延迟:如果平均延迟很低但最大延迟翻了几十倍,说明有偶发性的锁竞争或内核上下文切换;如果 RPS 很低而 CPU 没吃满,先怀疑是不是 wrk 所在机器和 server 在同一台物理机上互相抢核。最好把压测客户端放到另一台机或容器里跑。这份资源在默认参数下,单机回环压测通常能得到一个令人满意的数字,但拿到真实机房跨机器测,数字会明显下降,这属于正常现象,不需要怀疑代码写错。

6.2 给项目加一个优雅退出

资源里原本可能只是简单处理 Ctrl+C,我建议你给它补上 SIGINT/SIGTERM 的优雅退出。这个改动不大,但能显著提升可靠性,也让后面接系统服务管理更体面:

// 全局退出标志位 volatile sig_atomic_t g_should_stop = 0; // 信号处理函数:只置位,不做清理 void handle_signal(int) { g_should_stop = 1; } // main 里注册信号后,主循环每轮检查 signal(SIGINT, handle_signal); signal(SIGTERM, handle_signal); // 事件循环外层 while (!g_should_stop) { int nfds = epoll_wait(epfd, events, maxevents, timeout); // ...处理事件 } // 退出后统一清理:先停止线程池、再关定时器、最后 close listen_fd

这段代码里要注意:信号处理函数里绝对不能调用 printf 或 malloc,因为信号可能在任何位置打断主线程,这么做有死锁或内存损坏风险。置一个原子标志位,等主循环到下个迭代再去检查,是最安全的做法。加完这个之后,按 Ctrl+C 你会看到日志打完当前请求再退出,而不是半路丢连接。

6.3 下一个值得改的模块

如果还想把这份资源改出花,我认为优先改三处:一是把日志做成异步线程 + 双缓冲,减少写日志对主线程的阻塞;二是给静态文件响应加上 sendfile 系统调用,让大文件传输不再拷进用户态再拷出;三是给 http 模块支持 chunked 编码,这样就能转发流式响应。前两个改动对性能提升立竿见影,第三个则能让你处理更接近真实世界的接口形态。

从那以后,我每次拿到新的 C++ server 源码,都会先花十分钟把它的信号处理和线程池退出路径读一遍,再看业务模块。这个习惯帮我在三次换项目时都没被遗留的僵尸进程坑过。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询