从零构建高性能Web服务器:epoll、线程池与HTTP协议解析实战
1. 项目概述为什么从零构建一个Web服务器是程序员的“必修课”如果你是一名后端开发者或者对网络编程有浓厚兴趣那么“自己动手写一个Web服务器”这个想法大概率在你的脑海里出现过。这听起来像是一个庞大而复杂的工程但实际上它远没有想象中那么遥不可及。今天我们就来深入拆解一个典型的WebServer开源项目教程这不仅是学习网络编程的绝佳实践更是你深入理解HTTP协议、I/O模型、并发处理乃至操作系统底层原理的“活教材”。市面上有很多优秀的开源Web服务器比如Nginx、Apache但直接阅读它们的源码对新手来说犹如天书。而一个教学性质的WebServer项目其价值就在于它剥离了生产级项目的极致优化和复杂特性保留了最核心的骨架让你能清晰地看到从Socket监听、解析HTTP请求到返回响应的完整链路。这个教程的目标是引导你从零开始用C/C这是最常见的实现语言能让你接触到最底层的系统调用构建一个支持基本静态文件服务、处理GET/POST请求的轻量级Web服务器。通过这个过程你会真正明白当你在浏览器地址栏输入一个URL按下回车后背后到底发生了什么。这不仅仅是完成一个项目更是一次对计算机科学核心知识的串联式复习。无论你是想夯实基础的学生还是希望突破瓶颈、理解框架背后原理的职场开发者这个实践都能带来巨大的提升。接下来我将以一个典型的、结构清晰的教学项目为蓝本带你走完全程并分享那些官方文档里不会写的“踩坑”经验和性能调优思路。2. 项目核心架构与设计思路拆解在动手写代码之前我们必须先想清楚这个Web服务器要怎么工作。一个最简单的Web服务器其核心生命周期可以概括为“监听-接受-读取-解析-处理-响应-关闭”。但如何高效地处理可能同时到来的成千上万个连接就是架构设计的精髓所在。2.1 核心工作流程与I/O模型选型最朴素的实现方式是使用同步阻塞I/O模型。主线程在一个死循环中调用accept()等待客户端连接一旦有连接进来就为其创建一个新线程或进程来处理这个连接的读写。这种方式编程简单直观但缺点极其明显每个连接都需要一个独立的线程/进程系统资源消耗巨大上下文切换开销成为性能瓶颈根本无法应对高并发场景。这通常只存在于最基础的教学示例中。因此现代高性能Web服务器无一例外地采用了I/O多路复用技术。这是本项目的核心。I/O多路复用允许一个线程同时监视多个文件描述符包括监听socket和已连接的客户端socket的状态当其中任何一个描述符就绪可读、可写或出现异常时线程才会去处理从而避免了为每个连接创建独立线程的巨大开销。在Linux上我们有三种主要的I/O多路复用机制select、poll和epoll。对于我们的学习项目我强烈推荐直接使用epoll。虽然select和poll更古老、更通用跨平台但epoll在Linux上是性能最高的解决方案。它采用事件驱动的方式避免了select/poll中线性扫描所有文件描述符的O(n)复杂度在连接数巨大而活跃连接数较少时性能优势是指数级的。理解epoll的两种工作模式水平触发LT和边缘触发ET以及如何编写相应的非阻塞I/O代码是本次实践的关键收获之一。设计思路选择本项目将采用epoll 非阻塞I/O 线程池的架构。主线程负责epoll事件循环监听新连接和已连接套接字的读写事件。当有新的HTTP请求数据可读时主线程并不直接处理业务逻辑而是将解析出的请求对象或原始的连接句柄封装成一个任务投递到一个预先创建好的线程池中。工作线程从任务队列中取出任务执行具体的业务处理如读取静态文件、处理表单提交等生成响应数据再通过事件通知机制写回客户端。这种“事件驱动线程池”的模式既利用了epoll高并发的优势又通过线程池避免了在事件回调中执行耗时操作阻塞整个事件循环是业界非常成熟的模式。2.2 项目目录结构规划一个清晰的项目结构是良好开端的一半。参考常见的开源项目我们可以这样组织webserver-tutorial/ ├── bin/ # 存放编译生成的可执行文件 ├── build/ # CMake构建目录推荐使用CMake管理编译 ├── conf/ # 配置文件目录 │ └── server.conf # 服务器配置文件端口、根目录、线程数等 ├── src/ # 源代码目录 │ ├── main.cpp # 程序入口服务器启动和主循环 │ ├── Epoll.cpp/.hpp # epoll相关操作的封装类 │ ├── ThreadPool.cpp/.hpp # 线程池实现 │ ├── HttpConn.cpp/.hpp # 封装一个HTTP连接的处理状态机、读写缓冲区 │ ├── HttpRequest.cpp/.hpp # HTTP请求解析器 │ ├── HttpResponse.cpp/.hpp # HTTP响应构造器 │ ├── Timer.cpp/.hpp # 定时器用于处理非活跃连接超时 │ └── util/ # 工具函数目录 │ ├── Logger.cpp/.hpp # 日志模块 │ └── Utils.cpp/.hpp # 字符串处理、文件读取等工具函数 ├── resources/ # 静态资源目录网站的html, css, js, 图片等 │ ├── index.html │ ├── style.css │ └── ... ├── tests/ # 单元测试 ├── CMakeLists.txt # CMake构建脚本 └── README.md # 项目说明文档这样的结构将不同职责的代码模块化比如将网络I/O、协议解析、业务逻辑、资源管理分离不仅便于开发和调试也更容易进行单元测试和后续的功能扩展比如添加SSL支持、支持WebSocket等。3. 核心模块实现与关键技术点剖析接下来我们深入到几个最核心的模块看看它们具体如何实现以及有哪些需要注意的细节。3.1 Epoll事件驱动模块封装epoll的使用有三个关键步骤epoll_create创建实例、epoll_ctl添加/修改/删除监控事件、epoll_wait等待事件发生。一个好的封装应该隐藏这些系统调用的细节提供更易用的接口。// Epoll.hpp 简化示例 class Epoll { public: Epoll(int maxEvents 1024); ~Epoll(); bool AddFd(int fd, uint32_t events); // 添加文件描述符到epoll bool ModFd(int fd, uint32_t events); // 修改监听事件 bool DelFd(int fd); // 从epoll中删除 int Wait(int timeoutMs -1); // 等待事件返回就绪事件数 struct epoll_event* GetEvents(); // 获取就绪事件数组 private: int epollFd_; std::vectorstruct epoll_event events_; // 用于epoll_wait返回 };关键点与避坑指南文件描述符设置为非阻塞这是与epoll配合使用的铁律。无论是监听socket还是accept返回的客户端socket在添加到epoll之前都必须使用fcntl(fd, F_SETFL, O_NONBLOCK)将其设置为非阻塞模式。否则在边缘触发ET模式下如果一次没有读完数据程序可能会永远阻塞在后续的读操作上在水平触发LT模式下虽然不会阻塞但会持续触发可读事件造成CPU空转。ET与LT模式的选择边缘触发ET效率更高因为它只在状态变化时通知一次。但这要求程序员必须一次性将缓冲区中的数据全部读完或写完直到系统调用返回EAGAIN或EWOULDBLOCK错误。这对代码逻辑要求更严谨。水平触发LT则更“宽容”只要缓冲区还有数据可读或可写就会持续通知。对于新手可以从LT模式开始它更简单不易出错。但为了追求极致性能和学习价值本项目鼓励使用ET模式。监听socket的事件监听socket只需要监听可读事件EPOLLIN。当epoll_wait返回并指示监听socket可读时意味着有新的连接到达此时应循环调用accept()直到返回EAGAIN以确保在ET模式下处理完所有等待的连接。客户端socket的事件通常需要监听可读事件EPOLLIN和可写事件EPOLLOUT。但不要一开始就监听可写事件因为socket的写缓冲区在大部分时间都是可写的这会导致epoll_wait频繁返回造成CPU空转。正确的做法是只在有数据需要发送给客户端时才通过ModFd为该socket添加上EPOLLOUT事件当数据全部写入后立即将EPOLLOUT事件移除。3.2 HTTP协议解析器实现HTTP协议解析是Web服务器的“大脑”。我们需要从TCP字节流中识别出HTTP请求行、请求头、请求体并解析出方法GET/POST、URL、协议版本、头部字段等信息。这里最经典的实现方式是使用状态机。一个简单的请求解析状态机可能包含以下状态CHECK_STATE_REQUESTLINE: 正在解析请求行。CHECK_STATE_HEADER: 正在解析请求头。CHECK_STATE_CONTENT: 正在解析请求体对于POST请求。解析过程是逐行读取数据。从socket读到的数据先存入该连接对应的读缓冲区。解析器从缓冲区中按行取出数据进行分析。请求行解析示例GET /index.html HTTP/1.1我们需要解析出方法GET请求资源路径/index.html以及协议版本HTTP/1.1。这里要特别注意URL的编码解码如空格被编码为%20以及查询字符串?keyvalue的处理。请求头解析头部字段以冒号分隔如Host: localhost:8080。我们需要用一个std::mapstd::string, std::string来存储它们。空行标志着请求头的结束。对于GET请求解析到此结束。对于POST请求还需要根据Content-Length或Transfer-Encoding头部来确定请求体的长度和格式并继续从缓冲区读取对应长度的数据。避坑经验缓冲区管理必须为每个连接维护独立的读/写缓冲区。读缓冲区用于累积从socket读取的原始数据直到解析出一个完整的请求。写缓冲区用于暂存待发送给客户端的响应数据。缓冲区大小要合理如4KB、8KB并实现自动扩容机制。粘包与拆包TCP是字节流协议没有消息边界。一次recv调用可能读到半个HTTP请求也可能读到多个HTTP请求。解析器必须能够处理这种情况。我们的状态机设计天然支持“断点续传”如果本次数据不足以完成当前状态的解析就保持状态等待下次数据到来继续解析。安全性必须对解析出的路径进行严格检查防止目录遍历攻击。比如如果用户请求../../../etc/passwd你的服务器不能傻傻地返回系统文件。需要将请求路径限制在配置的网站根目录内。3.3 线程池设计与任务调度线程池的目的是避免频繁创建和销毁线程复用线程资源并平衡任务负载。我们的线程池需要以下几个组件任务队列一个线程安全的队列使用互斥锁std::mutex和条件变量std::condition_variable保护用于存放待处理的任务。工作线程组一组在启动时就创建好的线程它们循环地从任务队列中取出任务并执行。任务抽象通常用一个函数对象std::functionvoid()来表示一个任务。当主线程epoll事件循环线程检测到一个HTTP连接已经接收完一个完整的请求数据包后它不应该自己处理这个请求因为处理可能涉及磁盘I/O等耗时操作而是应该构造一个任务。这个任务至少包含对该连接对象的引用或智能指针以及需要处理的请求数据。然后将这个任务push到线程池的任务队列中。线程池的核心逻辑void ThreadPool::worker() { while (!stop_) { std::functionvoid() task; { std::unique_lockstd::mutex lock(queueMutex_); // 等待条件变量当任务队列非空或线程池停止时被唤醒 condition_.wait(lock, [this]() { return stop_ || !tasks_.empty(); }); if (stop_ tasks_.empty()) return; task std::move(tasks_.front()); tasks_.pop(); } task(); // 执行任务 } }关键设计考量任务队列的线程安全必须用锁保护对队列的push和pop操作。条件变量用于在队列为空时让工作线程休眠避免忙等待消耗CPU。任务窃取高级的线程池会实现“工作窃取”算法每个工作线程有自己的本地队列当本地队列为空时可以去其他线程的队列里“偷”任务这能更好地平衡负载。但对于我们的学习项目一个全局队列已经足够。线程数量线程池的线程数设置是一个权衡。通常设置为CPU核心数或者CPU核心数1。过多的线程会增加上下文切换开销过少则无法充分利用多核。可以在配置文件中提供这个参数。3.4 定时器管理非活跃连接一个健壮的服务器必须能清理那些长时间不活跃的“僵尸连接”以释放文件描述符等资源。我们可以为每个客户端连接关联一个定时器。常见的实现有几种升序链表简单但效率低每次检查需要遍历。时间轮像时钟一样将定时器散列到不同的槽位效率高实现稍复杂。最小堆优先队列最常用的方案。将定时器按照超时时间组织成一个小根堆堆顶就是最早要超时的连接。主循环定期检查堆顶元素是否超时。我们采用最小堆方案。每个连接对象HttpConn记录其最后一次活跃的时间比如最后一次收到数据的时间。定时器模块维护一个最小堆堆中元素是(超时时间戳, 连接标识)。工作流程当连接建立或有数据到达时更新该连接的活跃时间并调整其在堆中的位置或先删除旧定时器再插入新的。在主事件循环中定期比如每次epoll_wait返回后调用定时器的tick()函数。该函数检查堆顶将所有已超时的连接从堆中弹出并关闭这些连接调用close(fd)并从epoll中删除。这种“惰性删除”的方式非常高效只在需要的时候进行检查和清理。4. 完整开发流程与实操步骤现在让我们把各个模块串联起来看看如何一步步构建并运行这个Web服务器。4.1 环境准备与依赖安装首先你需要一个Linux开发环境Windows可以用WSL2macOS也可以但epoll是Linux特有的macOS对应的是kqueue原理类似。确保安装了GCC/G版本建议7以上、CMake和Make。# 在Ubuntu/Debian上 sudo apt update sudo apt install build-essential cmake项目不依赖复杂的第三方库核心是Linux系统调用和C标准库。但为了更好的输出可以引入一个简单的日志库如spdlog或者我们自己实现一个。这里为了简化我们先自己实现一个控制台日志。4.2 从零开始主循环骨架搭建我们从最核心的主函数和事件循环开始。创建一个main.cpp#include Epoll.h #include ThreadPool.h #include HttpConn.h #include util/Logger.h #include netinet/in.h #include arpa/inet.h #include unistd.h #include signal.h #include fcntl.h // 全局变量用于信号处理 static bool stop false; void handle_signal(int sig) { stop true; LOG_INFO(Receive signal %d, stopping server..., sig); } int main(int argc, char* argv[]) { // 1. 忽略SIGPIPE信号防止向已关闭的socket写数据导致程序退出 signal(SIGPIPE, SIG_IGN); signal(SIGINT, handle_signal); // 处理CtrlC signal(SIGTERM, handle_signal); // 2. 读取配置文件这里简化使用默认值 int port 8080; const char* rootDir ./resources; int threadNum 4; // 3. 创建监听socket int listenFd socket(AF_INET, SOCK_STREAM, 0); if(listenFd 0) { LOG_ERROR(Create socket error!); return 1; } // 设置端口复用防止“Address already in use”错误 int opt 1; setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in serverAddr; memset(serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family AF_INET; serverAddr.sin_addr.s_addr htonl(INADDR_ANY); // 监听所有网卡 serverAddr.sin_port htons(port); if(bind(listenFd, (struct sockaddr*)serverAddr, sizeof(serverAddr)) 0) { LOG_ERROR(Bind port %d error!, port); close(listenFd); return 1; } if(listen(listenFd, 1024) 0) { // 第二个参数是backlog等待连接队列的最大长度 LOG_ERROR(Listen error!); close(listenFd); return 1; } LOG_INFO(Server started on port %d, port); // 4. 设置监听socket为非阻塞 int oldFlag fcntl(listenFd, F_GETFL); fcntl(listenFd, F_SETFL, oldFlag | O_NONBLOCK); // 5. 初始化线程池和epoll ThreadPool threadPool(threadNum); Epoll epollManager; epollManager.AddFd(listenFd, EPOLLIN | EPOLLET); // 监听socket用ET模式 // 6. 主事件循环 while(!stop) { int eventCnt epollManager.Wait(1000); // 等待1秒 if(eventCnt 0 errno ! EINTR) { LOG_ERROR(Epoll wait error!); break; } // 处理定时器关闭超时连接这里省略定时器具体调用 // timerManager.tick(); for(int i 0; i eventCnt; i) { int fd epollManager.GetEvents()[i].data.fd; uint32_t events epollManager.GetEvents()[i].events; if(fd listenFd) { // 处理新连接 handleNewConnection(listenFd, epollManager); } else if(events (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) { // 对端关闭连接或发生错误 closeConn(fd, epollManager); } else if(events EPOLLIN) { // 可读事件有HTTP请求数据到达 handleRead(fd, epollManager, threadPool); } else if(events EPOLLOUT) { // 可写事件可以发送数据给客户端 handleWrite(fd, epollManager); } else { LOG_WARN(Unexpected event: %d, events); } } } // 7. 清理资源 close(listenFd); LOG_INFO(Server exited gracefully.); return 0; }上面的代码勾勒出了主循环的骨架。你需要实现handleNewConnection、closeConn、handleRead、handleWrite这几个关键函数以及相应的HttpConn类来管理每个连接的状态和数据。4.3 核心功能实现静态文件服务与请求处理handleRead函数是业务逻辑的入口。当epoll通知某个客户端socket可读时我们需要从该socket中读取数据并交给HTTP解析器。void handleRead(int clientFd, Epoll epollManager, ThreadPool threadPool) { // 假设我们有一个全局的映射通过fd找到对应的HttpConn对象 // 这里简化处理实际项目中应该用map或数组管理 auto conn getHttpConnByFd(clientFd); if(!conn) { // 可能是新连接还未创建对象 conn std::make_sharedHttpConn(); conn-init(clientFd, someConfig); // 初始化连接设置根目录等 storeHttpConn(clientFd, conn); } // 读取数据到连接的读缓冲区 ssize_t readBytes conn-readData(); if(readBytes 0) { // 读取错误或对端关闭连接 closeConn(clientFd, epollManager); return; } // 解析读缓冲区中的数据 HTTP_CODE parseResult conn-parseRequest(); if(parseResult NO_REQUEST) { // 请求不完整继续等待数据 // 在ET模式下需要重新监听可读事件吗不需要因为数据没读完状态没变。 // 在LT模式下如果不处理完epoll会持续通知。 return; } else if(parseResult GET_REQUEST) { // 成功解析到一个完整的GET请求 // 将处理任务提交到线程池 threadPool.enqueue([conn, epollManager]() { // 在工作线程中处理请求 conn-processRequest(); // 这里可能涉及读取文件 // 处理完成后需要通知主线程这个连接可以写数据了 // 通常通过修改epoll监听事件为EPOLLOUT来实现 epollManager.ModFd(conn-getFd(), EPOLLOUT | EPOLLET); }); } else { // 解析出错返回400 Bad Request等错误 conn-makeErrorResponse(400); epollManager.ModFd(clientFd, EPOLLOUT | EPOLLET); } }在HttpConn::processRequest()中对于GET请求我们需要根据解析出的URL路径在配置的网站根目录下找到对应的文件如./resources/index.html读取文件内容并构造一个正确的HTTP响应。构造HTTP响应示例void HttpConn::makeResponse(const std::string filePath) { // 1. 状态行 responseBuffer_ HTTP/1.1 200 OK\r\n; // 2. 头部 responseBuffer_ Server: MyWebServer/1.0\r\n; responseBuffer_ Content-Type: text/html\r\n; // 需要根据文件后缀判断MIME类型 responseBuffer_ Connection: keep-alive\r\n; // 支持长连接 // 3. 空行 responseBuffer_ \r\n; // 4. 消息体文件内容 // 读取filePath文件内容追加到responseBuffer_ // ... (文件读取操作) responseBuffer_ fileContent; }对于POST请求你需要在processRequest中解析请求体例如表单数据application/x-www-form-urlencoded或JSONapplication/json并根据业务逻辑进行处理最后同样构造响应。4.4 编译、运行与测试使用CMake来管理编译过程是最佳实践。在项目根目录创建CMakeLists.txtcmake_minimum_required(VERSION 3.10) project(WebServerTutorial) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 添加可执行文件 add_executable(webserver src/main.cpp src/Epoll.cpp src/ThreadPool.cpp src/HttpConn.cpp src/HttpRequest.cpp src/HttpResponse.cpp src/Timer.cpp src/util/Logger.cpp ) # 设置编译选项 target_compile_options(webserver PRIVATE -Wall -Wextra -O2) # 链接线程库 find_package(Threads REQUIRED) target_link_libraries(webserver Threads::Threads)编译和运行mkdir build cd build cmake .. make ./webserver打开浏览器访问http://localhost:8080/index.html你应该能看到resources目录下的index.html页面内容。压力测试可以使用abApache Bench或wrk工具进行简单的并发测试。# 安装ab sudo apt install apache2-utils # 发起1000个请求并发10个 ab -n 1000 -c 10 http://localhost:8080/观察服务器的响应时间、吞吐量以及内存/CPU使用情况。这是检验你服务器稳定性和性能的第一步。5. 进阶优化与扩展方向一个能跑起来的基础Web服务器已经完成了。但要让其更健壮、更高效还有大量的优化和扩展工作可以做。5.1 性能优化关键点缓冲区优化为每个连接分配固定大小的缓冲区如8KB可能造成浪费或不足。可以实现一个动态增长的缓冲区类并考虑使用内存池来减少频繁的malloc/free或new/delete操作带来的内存碎片和性能开销。文件发送优化发送静态文件时避免先将整个文件读入内存再发送。对于大文件这会导致内存暴增。应该使用sendfile()系统调用它可以直接在内核空间将文件数据从磁盘拷贝到网卡缓冲区实现“零拷贝”极大提升性能。#include sys/sendfile.h ssize_t sent sendfile(clientFd, fileFd, nullptr, fileSize);日志性能频繁的日志输出尤其是同步输出到控制台或文件会成为性能瓶颈。可以将日志改为异步写入即日志调用只将日志信息放入一个内存队列由专门的日志线程负责将其写入文件。定时器效率当连接数非常大时使用最小堆管理定时器每次调整更新连接活跃时间的时间复杂度是O(log n)。可以考虑更高效的数据结构如时间轮或者将定时器检查的频率降低比如每5秒检查一次以换取整体吞吐量。5.2 功能扩展思路支持HTTPS集成OpenSSL库在监听socket之外再创建一个SSL监听socket。处理连接时需要进行SSL握手、加密解密等操作。这会显著增加复杂度但却是生产环境必备。支持HTTP/1.1 Pipeline与长连接你已经通过Connection: keep-alive头部初步支持了长连接。更深入的是支持HTTP Pipelining即客户端可以在同一个连接上连续发送多个请求而不必等待响应服务器必须按顺序返回响应。这要求你的请求解析和响应发送逻辑能够处理多个请求/响应的交错。实现简单的反向代理功能让你的服务器能够将请求转发到后端其他服务器并将响应返回给客户端。这需要解析Host头部并根据配置规则进行转发。添加配置文件解析目前端口、线程数等参数是硬编码的。实现一个配置文件解析模块如读取YAML或JSON让服务器行为可通过配置文件灵活调整。集成数据库为处理动态请求可以集成一个轻量级数据库如SQLite或者连接MySQL/PostgreSQL。在工作线程中执行数据库查询生成动态HTML或JSON响应。5.3 常见问题与调试技巧在开发过程中你一定会遇到各种奇怪的问题。这里记录一些常见坑点和调试方法“Address already in use”错误即使程序退出操作系统也会保留处于TIME_WAIT状态的socket一段时间通常2分钟。在bind之前设置SO_REUSEADDR套接字选项可以解决这个问题见前面代码示例。服务器CPU占用100%这通常是因为epoll工作在LT模式且某个socket的读/写事件就绪后你没有一次性处理完所有数据导致epoll_wait立即返回形成忙等待循环。在ET模式下也可能因为逻辑错误导致循环读取/写入。使用top命令查看CPU占用并用gdb或printf大法定位是哪个循环出了问题。内存缓慢增长内存泄漏最可能的原因是连接对象HttpConn在关闭后没有正确释放或者任务队列中的任务对象没有释放。使用valgrind工具进行内存检查是定位这类问题的利器。valgrind --leak-checkfull ./webserver并发测试时连接被重置Connection reset by peer可能是服务器同时关闭了同一个连接两次或者在发送数据时对方已经关闭了连接。确保关闭连接close(fd)和从epoll删除DelFd的操作是原子的并且做好错误处理忽略send或write返回的EPIPE错误我们已经通过signal(SIGPIPE, SIG_IGN)忽略了。使用网络调试工具netstat或ss查看服务器监听端口和当前连接状态。ss -tlnp | grep 8080tcpdump或Wireshark抓取网络包直观地看到HTTP请求和响应的原始数据对于调试协议解析错误无比有用。sudo tcpdump -i any port 8080 -Astrace跟踪进程的系统调用看看程序卡在哪个read、write或epoll_wait上。strace -f ./webserver构建一个Web服务器的旅程就像在微观世界里亲手搭建互联网的基石。从最初对Socket的陌生到熟练运用epoll处理成千上万的并发连接从对HTTP协议文本的茫然到能精准实现一个状态机解析器从面对并发bug的手足无措到能从容使用线程池和锁进行任务调度——这个过程带给你的不仅仅是代码能力的提升更是对计算机系统如何协同工作的深刻理解。这个项目代码可能只有一两千行但它所涵盖的知识密度远超同等行数的业务代码。我建议你在实现基本功能后不要就此停止尝试去实现前面提到的某一个扩展功能或者用wrk压测后根据性能瓶颈图去做针对性的优化。当你看到自己写的服务器能够稳定地承受一定的压力测试时那种成就感是无可替代的。最后将你的代码放到GitHub上写一份清晰的README这不仅是学习的总结也会是你技术履历中一个扎实的亮点。

相关新闻