diff --git a/README.md b/README.md index 801591a..5f197ba 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ Enjoy it,it's gonna be really fun!!! - Complier: g++ 5.4.0 - Tools: CMake/VScode +## Docs + +更系统的源码与架构文档放在 `webserver/docs/` 目录下: + +- `webserver/docs/README.md` +- `webserver/docs/architecture-guide.md` +- `webserver/docs/wiki-code-architecture.md` +- `webserver/docs/change-notes.md` + ## Technical points * 基于Reactor模式构建网络服务器,编程风格偏向面向过程 diff --git a/webserver/docs/README.md b/webserver/docs/README.md new file mode 100644 index 0000000..f84751b --- /dev/null +++ b/webserver/docs/README.md @@ -0,0 +1,32 @@ +# WebServer Docs + +本文档目录基于当前仓库源码快照编写,重点覆盖三件事: + +1. 解释项目当前的实际架构与运行链路。 +2. 用源码视角拆解关键类、关键方法和线程模型。 +3. 记录当前代码中值得继续演进的重构方向。 + +说明: + +- 为了兼容大多数 IDE 的 Markdown 预览器,文档里的结构图统一使用纯文本/ASCII 图,而不是 Mermaid。 +- 这样即使没有额外插件,文档也可以稳定预览。 + +推荐阅读顺序: + +1. [architecture-guide.md](./architecture-guide.md) +2. [wiki-code-architecture.md](./wiki-code-architecture.md) +3. [change-notes.md](./change-notes.md) + +文档清单: + +| 文件 | 作用 | 适合谁看 | +| --- | --- | --- | +| [architecture-guide.md](./architecture-guide.md) | 系统架构说明,覆盖模块关系、线程模型、请求链路、数据流与设计取舍 | 第一次接手项目、准备做大改造的人 | +| [wiki-code-architecture.md](./wiki-code-architecture.md) | Wiki 风格源码导读,告诉你“从哪里开始看”“按什么顺序看”“怎么带着问题看” | 新同学、需要快速进入状态的人 | +| [change-notes.md](./change-notes.md) | 基于当前源码的修改说明与后续重构建议 | 准备继续重构、整理宏配置、收敛策略层的人 | + +阅读建议: + +- 如果你只想快速理解“浏览器访问 `/hello` 时代码怎么跑”,先看 [wiki-code-architecture.md](./wiki-code-architecture.md)。 +- 如果你要做结构重构或性能优化,先看 [architecture-guide.md](./architecture-guide.md),再看 [change-notes.md](./change-notes.md)。 +- 如果你准备整理编译期宏、降低业务层对宏的感知,重点看 [change-notes.md](./change-notes.md)。 diff --git a/webserver/docs/architecture-guide.md b/webserver/docs/architecture-guide.md new file mode 100644 index 0000000..671c5ba --- /dev/null +++ b/webserver/docs/architecture-guide.md @@ -0,0 +1,645 @@ +# WebServer Architecture Guide + +## 1. 先用一句话理解这个项目 + +这是一个基于 Reactor 模式的多线程 C++ WebServer: + +- 主线程负责监听和接受连接。 +- I/O 线程负责每个连接的读写事件。 +- 应用层目前实现了一个简单 HTTP 服务器。 +- 编译期开关决定 ET/LT、线性 Buffer / 环形 Buffer、队列/锁策略、连接复用策略等行为。 + +如果把这个项目压缩成一张图,可以先看下面这张总览图。 + +```text +runHttpServer.cpp + | + +-- EventLoop 主线程事件循环 + | + +-- HttpServer HTTP 语义层 + | + +-- TcpServer TCP 总控 + | + +-- Acceptor 监听与 accept + +-- EventLoopThreadPool + +-- TcpConnection 单连接 I/O 核心 + | + +-- Buffer / RingBuffer + +-- HttpContext + | + +-- HttpRequest + +-- HttpResponse + +EventLoop + | + +-- EventManager(epoll) + | + +-- Channel 回调分发 + | + +-- Acceptor::handleRead() + +-- TcpConnection::handleRead()/handleWrite() +``` + +## 2. 目录结构怎么读 + +`webserver/` 目录可以按职责分成 7 层: + +| 目录 | 作用 | 关键文件 | +| --- | --- | --- | +| `loop/` | 事件循环、线程唤醒、loop 线程封装 | `KEventLoop.*`、`KAsyncWaker.*`、`KEventLoopThread.*`、`KEventLoopThreadPool.*` | +| `poller/` | epoll 封装与 Channel 分发 | `KEventManager.*`、`KChannel.*` | +| `tcp/` | 监听、连接、socket、buffer、地址 | `KAcceptor.*`、`KTcpServer.*`、`KTcpConnection.*`、`KBuffer.*`、`KRingBuffer.*` | +| `http/` | HTTP 请求解析与响应组装 | `KHttpContext.*`、`KHttpRequest.h`、`KHttpResponse.*`、`KHttpServer.*` | +| `lock/` | 自旋锁与 lock-free 队列 | `KSpinLock.h`、`KLockFreeQueue.h` | +| `thread/` | 独立线程池实验实现 | `KThreadPool.*` | +| `utils/` | 回调类型、时间戳、基础工具 | `KCallbacks.h`、`KTimestamp.*`、`KTypes.h` | + +从阅读顺序上,最推荐: + +1. `runHttpServer.cpp` +2. `http/KHttpServer.*` +3. `tcp/KTcpServer.*` +4. `tcp/KTcpConnection.*` +5. `loop/KEventLoop.*` +6. `poller/KEventManager.*` 和 `poller/KChannel.*` +7. `http/KHttpContext.*` +8. `tcp/KBuffer.*` / `tcp/KRingBuffer.*` + +## 3. 启动阶段:程序是怎么跑起来的 + +入口文件是 `webserver/runHttpServer.cpp`。 + +它做的事情很少: + +1. 创建 `EventLoop loop`,作为主线程事件循环。 +2. 创建 `HttpServer server(&loop, InetAddress(8888), "httpserver")`。 +3. 设置线程数。 +4. 调用 `server.start()`。 +5. 调用 `loop.loop()` 进入事件循环。 + +这一步体现了项目的第一层设计哲学: +**main 几乎不放业务逻辑,只负责把各层对象拼起来。** + +启动时序如下: + +```text +runHttpServer.cpp + -> EventLoop 构造主事件循环 + -> HttpServer 构造 HTTP 服务器 + -> TcpServer 构造 TCP 总控 + -> Acceptor 创建监听 socket + Channel + -> HttpServer::start() + -> TcpServer::start() + -> EventLoopThreadPool::start() + -> EventLoop::runInLoop(Acceptor::listen) + -> EventLoop::loop() 进入主循环 +``` + +## 4. Reactor 核心:EventLoop、EventManager、Channel + +这三个类共同构成“事件通知 -> 事件分发 -> 回调执行”的骨架。 + +### 4.1 EventLoop:每个线程一个事件循环 + +文件: + +- `webserver/loop/KEventLoop.h` +- `webserver/loop/KEventLoop.cpp` + +职责: + +- 持有当前线程专属的 `EventManager` +- 驱动 `poll()` +- 保存活跃 `Channel` +- 执行跨线程投递的 functor +- 在必要时通过 `AsyncWaker` 唤醒阻塞中的 loop + +主循环长这样: + +```text +while (!quit_) { + activeChannels_.clear(); + pollReturnTime_ = eventmanager_->poll(...); + for each active channel: + channel->handleEvent(...) + doPendingFunctors(); +} +``` + +这是整个服务器最重要的一段控制流。 + +### 4.2 EventManager:epoll 的直接封装 + +文件: + +- `webserver/poller/KEventManager.h` +- `webserver/poller/KEventManager.cpp` + +职责: + +- 调用 `epoll_wait` +- 维护 `fd -> Channel*` 映射 +- 负责 `EPOLL_CTL_ADD / MOD / DEL` +- 把内核事件填充回 `activeChannels` + +它不拥有 `Channel`,只保存裸指针索引。 +这意味着对象所有权仍然在上层,比如 `TcpConnection` 或 `Acceptor`。 + +### 4.3 Channel:fd 与回调之间的桥 + +文件: + +- `webserver/poller/KChannel.h` +- `webserver/poller/KChannel.cpp` + +职责: + +- 绑定一个 fd +- 记录自己关心的事件掩码 +- 保存 4 类回调: + - read + - write + - error + - close +- 在 `handleEvent()` 中根据 `revents_` 分发 + +`Channel` 本身不拥有 fd,这一点非常关键: + +- fd 的生命周期由 `Socket` 或其他上层对象管理 +- `Channel` 只是“这个 fd 在事件循环中的代表” + +## 5. 线程模型:为什么是主线程 accept,I/O 线程处理连接 + +线程模型由下面两个类承载: + +- `webserver/loop/KEventLoopThread.*` +- `webserver/loop/KEventLoopThreadPool.*` + +### 5.1 EventLoopThread + +`EventLoopThread::startLoop()` 会启动一个新线程,在那个线程里局部创建 `EventLoop loop`,然后通过条件变量把 `loop_` 指针返回给外部。 + +这是一个典型模式: + +- `EventLoop` 必须在所属线程里创建 +- 但外部又需要拿到它的地址进行调度 + +### 5.2 EventLoopThreadPool + +`EventLoopThreadPool` 的职责非常单纯: + +- 按需创建多个 `EventLoopThread` +- 保存所有子 `EventLoop*` +- 通过 round-robin 分发连接 + +也就是说,本项目并不是“线程池里跑任务”,而是“线程池里每个线程跑一个事件循环”。 + +这一点和普通的计算型线程池完全不同。 + +## 6. 网络层:从监听到连接对象 + +### 6.1 InetAddress、Socket、KSocketsOps + +这三者负责最底层的 socket 细节。 + +#### InetAddress + +文件: + +- `webserver/tcp/KInetAddress.h` +- `webserver/tcp/KInetAddress.cpp` + +作用: + +- 封装 `sockaddr_in` +- 提供端口构造、IP:Port 字符串转换 + +#### Socket + +文件: + +- `webserver/tcp/KSocket.h` +- `webserver/tcp/KSocket.cpp` + +作用: + +- 封装 fd +- 在析构时关闭 fd +- 提供 `bind/listen/accept/shutdownWrite/setTcpNoDelay/setKeepAlive` + +#### KSocketsOps + +文件: + +- `webserver/tcp/KSocketsOps.h` +- `webserver/tcp/KSocketsOps.cpp` + +作用: + +- 提供纯函数式 socket 操作 +- 屏蔽 `sockaddr` 类型转换 +- 统一 `accept4`、`bind`、`listen`、`getsockname` 等调用 + +设计上可以把它理解为“无状态系统调用工具箱”。 + +### 6.2 Acceptor:监听 socket 的包装 + +文件: + +- `webserver/tcp/KAcceptor.h` +- `webserver/tcp/KAcceptor.cpp` + +职责: + +- 创建监听 socket +- 创建监听 fd 对应的 `Channel` +- 在 `listen()` 时把可读事件注册到 loop +- 在 `handleRead()` 中 `accept` 新连接 +- 通过 `NewConnectionCallback` 把新连接继续上抛 + +注意这里的重点: + +- `Acceptor` 只负责“把连接接进来” +- 它不管理连接对象的生命周期 + +### 6.3 TcpServer:连接总控 + +文件: + +- `webserver/tcp/KTcpServer.h` +- `webserver/tcp/KTcpServer.cpp` + +职责: + +- 拥有 `Acceptor` +- 拥有 `EventLoopThreadPool` +- 维护 `connections_` 字典 +- 收到新连接后构造 `TcpConnection` +- 设置连接层回调 +- 在连接关闭时把它从 map 中移除 + +`TcpServer` 是 TCP 层真正的“协调者”。 + +它不直接处理请求数据,但负责把连接放到正确的 I/O 线程里,并维持连接对象生命周期。 + +新连接分发链路如下: + +```text +Base EventLoop + -> Acceptor::handleRead() + -> accept() + -> TcpServer::newConnection(sockfd, peerAddr) + -> EventLoopThreadPool::getNextLoop() + -> 选择一个 ioLoop + -> 构造 TcpConnection + -> 设置 connection / message / close 回调 + -> ioLoop->runInLoop(TcpConnection::connectEstablished) +``` + +## 7. 连接层:TcpConnection 才是真正处理 I/O 的地方 + +文件: + +- `webserver/tcp/KTcpConnection.h` +- `webserver/tcp/KTcpConnection.cpp` + +这是项目里最核心的业务对象之一。 + +### 7.1 为什么它用 `shared_ptr` + +`TcpConnection` 是少数显式继承 `enable_shared_from_this` 的类。 +原因是: + +- 连接对象同时被 `TcpServer::connections_` 管理 +- 也会被回调、延迟任务、关闭流程临时持有 +- 关闭与销毁并不是一个同步点 + +如果不用引用计数,很容易在异步关闭过程中悬空。 + +### 7.2 连接状态机 + +源码里的状态: + +- `kConnecting` +- `kConnected` +- `kDisconnecting` +- `kDisconnected` + +典型流转: + +```text +kConnecting + -> connectEstablished() + -> kConnected + -> shutdown() + -> kDisconnecting + -> handleClose()/connectDestroyed() + -> kDisconnected +``` + +### 7.3 读路径 + +读事件发生时,链路是: + +1. `Channel::handleEvent()` +2. `TcpConnection::handleRead()` +3. `inputBuffer_.readFd()` 或 `readFdET()` +4. 调用上层 `messageCallback_` + +对 HTTP 来说,这个 `messageCallback_` 最终就是 `HttpServer::onMessage()`。 + +### 7.4 写路径 + +写路径分两种: + +#### 直接发送 + +`sendInLoop()` 会先尝试直接 `write()`: + +- 如果一次写完,就不需要关注可写事件 +- 如果没写完,就把剩余数据放进 `outputBuffer_` + +#### 延迟发送 + +当 `outputBuffer_` 有积压数据时: + +- 打开 `channel_->enableWriting()` +- 等待下次可写事件 +- 在 `handleWrite()` 中继续 flush + +这种设计避免了数据乱序,也避免了在不可写时忙等。 + +### 7.5 文件发送 + +`hpSendFile()` 使用 `sendfile()` 发送文件。 + +在当前实现里,`/file` 路径会: + +1. 先发送 HTTP 头 +2. 再调用 `sendfile` + +这是一个比较直接的 zero-copy 实现路径。 + +## 8. Buffer 层:为什么有两个实现 + +### 8.1 KBuffer:线性 Buffer + +文件: + +- `webserver/tcp/KBuffer.h` +- `webserver/tcp/KBuffer.cpp` + +特点: + +- 底层是 `std::vector` +- 可读区是连续内存 +- 必要时会做挪动或扩容 + +优点: + +- 实现简单 +- 解析逻辑自然 + +缺点: + +- 长连接高并发下可能有更多数据搬移 + +### 8.2 KRingBuffer:环形 Buffer + +文件: + +- `webserver/tcp/KRingBuffer.h` +- `webserver/tcp/KRingBuffer.cpp` + +特点: + +- 底层是手工管理的循环数组 +- 读写空间可能分裂成两段 +- 借助 `readv/writev` 处理不连续内存 + +优点: + +- 减少数据搬移 + +缺点: + +- 上层解析逻辑会复杂不少 +- 当前源码里因此出现了一些专门兼容 ringbuffer 的分支 + +## 9. HTTP 层:从字节流到响应报文 + +### 9.1 HttpContext:请求解析状态机 + +文件: + +- `webserver/http/KHttpContext.h` +- `webserver/http/KHttpContext.cpp` + +状态: + +- `kExpectRequestLine` +- `kExpectHeaders` +- `kExpectBody` +- `kGotAll` + +职责: + +- 从 `Buffer` 中读取一行一行的数据 +- 解析请求行 +- 解析 Header +- 在完成后把结果写入 `HttpRequest` + +这类设计的好处是: +`HttpContext` 不关心 socket 和线程,只关心“给我一个可读缓存,我把它解析成请求对象”。 + +### 9.2 HttpRequest + +文件: + +- `webserver/http/KHttpRequest.h` + +职责: + +- 保存方法、版本、路径、查询串、接收时间、请求头 + +这是一个非常轻量的值对象。 + +### 9.3 HttpResponse + +文件: + +- `webserver/http/KHttpResponse.h` +- `webserver/http/KHttpResponse.cpp` + +职责: + +- 保存状态码、头、body、是否关闭连接 +- 把响应序列化进 `Buffer` + +### 9.4 HttpServer + +文件: + +- `webserver/http/KHttpServer.h` +- `webserver/http/KHttpServer.cpp` + +职责: + +- 在 TCP 层之上装配 HTTP 语义 +- 连接建立时创建 `HttpContext` +- 收到字节流时交给 `HttpContext` 解析 +- 根据 `HttpRequest` 生成 `HttpResponse` + +当前支持的典型路径: + +- `/hello` +- `/good` +- `/` +- `/favicon.ico` +- `/file` + +`/file` 是当前实现里的一个特殊路径: +它会显式读取 `./index.html`,先拼 HTTP 头,再调用 `sendfile()` 发送文件内容。 + +## 10. 跨线程任务:AsyncWaker 为什么重要 + +文件: + +- `webserver/loop/KAsyncWaker.h` +- `webserver/loop/KAsyncWaker.cpp` + +`EventLoop` 的一个关键问题是: + +- `epoll_wait()` 正在阻塞 +- 另一个线程想投递一个 functor +- 如果不唤醒 loop,回调可能要等一个 poll 周期 + +这里的做法是: + +1. 用 `eventfd` 创建一个专门的唤醒 fd +2. 给这个 fd 绑定一个 `Channel` +3. 其他线程 `write(eventfd)` +4. loop 线程读掉它,并继续处理 `pendingFunctors_` + +这就是 `queueInLoop()` 能在多线程下工作的关键。 + +## 11. 回调设计:为什么 KCallbacks.h 很关键 + +文件: + +- `webserver/utils/KCallbacks.h` + +这里定义了: + +- `ConnectionCallback` +- `MessageCallback` +- `WriteCompleteCallback` +- `CloseCallback` +- `RecycleCallback` + +这套回调类型让: + +- `TcpServer` +- `TcpConnection` +- `HttpServer` + +之间形成了很清晰的层次关系: + +- TCP 层负责 I/O 与生命周期 +- HTTP 层只通过回调接入 + +## 12. 编译期开关对架构的影响 + +这是当前项目非常重要的一部分。 + +| 开关 | 当前意义 | 对架构的影响 | +| --- | --- | --- | +| `USE_EPOLL_LT` | LT/ET 选择 | 改变 accept/read/write 处理方式 | +| `USE_RINGBUFFER` | 线性/环形 buffer 选择 | 改变 buffer 接口语义与解析路径 | +| `USE_LOCKFREEQUEUE` | functor 队列实现 | 改变 `EventLoop` 内部并发模型 | +| `USE_SPINLOCK` | 锁选择 | 改变 `EventLoop` / `TcpServer` 的临界区实现 | +| `USE_RECYCLE` | 连接复用 | 改变 `TcpConnection` 生命周期 | +| `USE_STD_COUT` | 日志打印 | 影响运行期开销与依赖关系 | + +这些开关让项目具备实验性和对比性,但也提高了维护复杂度。 + +## 13. 当前设计的优点 + +### 优点 1:主链路很清晰 + +从 `main -> HttpServer -> TcpServer -> TcpConnection -> HttpContext` 这条链路非常清楚,适合教学和演示。 + +### 优点 2:对象职责边界基本合理 + +- `Acceptor` 只 accept +- `TcpServer` 只管理连接 +- `TcpConnection` 只做单连接 I/O +- `HttpContext` 只解析请求 + +### 优点 3:性能导向明显 + +源码里能明显看到作者在做这些优化尝试: + +- ET 模式 +- ringbuffer +- lock-free queue +- `sendfile` +- 连接复用 + +## 14. 当前设计的局限 + +### 局限 1:宏分支已经渗入业务层 + +这会导致: + +- 代码阅读成本高 +- 单元测试组合复杂 +- 改一个策略容易波及主链路 + +### 局限 2:部分对象所有权还不够现代化 + +例如: + +- `AsyncWaker` 里使用裸指针保存 `Channel` +- 老式 `std::bind` 与裸指针结合较多 + +### 局限 3:日志系统仍是 stdout 风格 + +这对高并发服务不够友好。 + +### 局限 4:HTTP 层与文件发送逻辑耦合较深 + +当前 `/file` 路径直接在 `HttpServer::onRequest()` 中处理文件打开、文件长度、发送头和文件体,扩展性一般。 + +## 15. 如果你要继续扩展这个项目,最推荐的方向 + +### 方向 1:先做结构整理,再做功能扩展 + +优先整理: + +- 配置入口 +- 策略层 +- 日志层 +- buffer 统一接口 + +### 方向 2:把 HTTP 层继续抽薄 + +可考虑把: + +- 静态文件处理 +- 路由分发 +- 响应序列化 + +进一步独立成单独模块。 + +### 方向 3:补测试与压测脚本 + +当前项目更像“可运行的高性能网络实验平台”,而不是“工业级带完整回归体系的框架”。 +继续走下去,测试和 benchmark 维度必须补齐。 + +## 16. 一句话总结 + +这个项目最值得学习的地方,不是它实现了一个简单 HTTP server,而是它把 **Reactor、事件循环、连接管理、跨线程唤醒、编译期策略切换** 这些核心思想都非常集中地放在了一起。 + +如果你想从源码层理解一个 C++ 网络服务器是怎么跑起来的,这个项目非常适合阅读; +如果你想把它继续演进成更稳定、更可维护的框架,那么下一步的关键工作就是:**把策略层从业务层剥离出来**。 diff --git a/webserver/docs/change-notes.md b/webserver/docs/change-notes.md new file mode 100644 index 0000000..6b1a4e1 --- /dev/null +++ b/webserver/docs/change-notes.md @@ -0,0 +1,304 @@ +# WebServer Change Notes + +## 1. 文档目的 + +这份文档不是简单重复 README,而是从“维护者要怎么继续改”这个角度,梳理当前源码的结构现状、痛点位置、影响面以及后续推荐的重构路线。 + +这里有一个很重要的前提: + +- 本文以当前仓库里的实际源码为准。 +- 文中提到的“建议改动”“后续重构方向”是基于当前代码分析出来的可落地方案,不应误读为“已经全部完成”。 + +## 2. 当前代码基线 + +当前实现是一个典型的 Reactor 风格 WebServer,主干链路如下: + +```text +runHttpServer.cpp + -> EventLoop + -> HttpServer + -> TcpServer + -> Acceptor / EventLoopThreadPool / TcpConnection + -> HttpContext / HttpRequest / HttpResponse +``` + +编译期开关对行为影响较大,主要包括: + +| 开关 | 作用 | 影响范围 | +| --- | --- | --- | +| `USE_EPOLL_LT` | 决定 LT/ET 模式 | `KAcceptor.cpp`、`KTcpConnection.cpp`、`KBuffer.cpp`、`KRingBuffer.cpp`、`KChannel.h` | +| `USE_RINGBUFFER` | 决定使用线性 Buffer 还是环形 Buffer | `KTcpConnection.h`、`KHttpContext.cpp`、`KHttpResponse.cpp`、`KBuffer/KRingBuffer` | +| `USE_LOCKFREEQUEUE` | 决定 `EventLoop` 的 pending functor 队列实现 | `KEventLoop.h/.cpp` | +| `USE_SPINLOCK` | 决定 `EventLoop` 与 `TcpServer` 的某些临界区锁实现 | `KEventLoop.h/.cpp`、`KTcpServer.h/.cpp` | +| `USE_RECYCLE` | 决定是否复用 `TcpConnection` | `KTcpServer.h/.cpp`、`KTcpConnection.cpp` | +| `USE_STD_COUT` | 控制日志输出 | 多个 `.cpp` 文件 | + +这些开关都有效,但也带来了一个明显问题:**宏分支已经渗透到业务层和对象生命周期里**。 + +## 3. 当前源码的主要维护痛点 + +### 3.1 编译期开关泄漏到业务层 + +最明显的例子有: + +- `webserver/tcp/KTcpConnection.cpp` +- `webserver/tcp/KAcceptor.cpp` +- `webserver/http/KHttpContext.cpp` +- `webserver/loop/KEventLoop.cpp` +- `webserver/tcp/KTcpServer.h` + +问题不在于“用了宏”,而在于: + +1. 宏直接改变业务流程。 +2. 同一个类同时承担业务职责和策略选择职责。 +3. 阅读代码时必须不停在“当前行为”和“其他编译配置下行为”之间切换。 + +### 3.2 Buffer 选择对上层代码有侵入 + +当前 `Buffer` 与 `RingBuffer` 通过预处理器让“同名类 `Buffer`”在不同构建下指向不同头文件。这种做法短期有效,但会带来几个问题: + +- 上层代码必须知道当前是否启用了 `USE_RINGBUFFER`。 +- `HttpContext` 为了兼容 ringbuffer,单独维护了一套请求解析流程。 +- `TcpConnection::send()` 在 ringbuffer 路径和线性 buffer 路径上出现不同分支。 + +影响文件: + +- `webserver/tcp/KTcpConnection.h` +- `webserver/http/KHttpContext.cpp` +- `webserver/http/KHttpResponse.cpp` + +### 3.3 `EventLoop` 的 pending functor 实现耦合了队列和锁策略 + +当前 `EventLoop` 同时在处理: + +- 回调任务语义 +- 锁 free 队列选择 +- 自旋锁 / 互斥锁选择 + +这让 `KEventLoop.h/.cpp` 的宏分支比较重,不利于后续扩展新的并发策略。 + +### 3.4 `TcpConnection` 回收逻辑分散在两个类里 + +当前连接复用链路横跨: + +- `KTcpServer::newConnection()` +- `KTcpServer::recycleCallback()` +- `TcpConnection::connectDestroyed()` + +这意味着一个“是否复用连接”的开关会同时影响: + +- 连接池管理 +- 生命周期释放顺序 +- 回调绑定方式 +- 资源清理时机 + +这类逻辑更适合单独抽成 lifecycle / recycler 策略层。 + +### 3.5 日志仍是 `std::cout` + +源码里大量日志直接使用: + +- `std::cout` +- `std::cerr` + +问题包括: + +- 业务线程直接承担 IO 输出开销 +- 头文件中引入 `iostream` +- 日志格式不统一 +- 与高并发网络路径耦合过深 + +### 3.6 代码风格处于“可运行优先”状态 + +当前工程具备完整主链路,但仍保留一些较早期风格: + +- 相对路径 include 混杂 +- `std::bind` 使用较多 +- 原始指针与 `unique_ptr/shared_ptr` 混用 +- 一些对象名较旧,例如 `listenning_`、`eventmanager_` +- `README` 与实际源码已有一定偏差 + +## 4. 建议的重构路线 + +下面这组改动按投入收益比排序,适合逐步推进。 + +### 4.1 第一步:建立统一配置入口 + +目标: + +- 不再让业务层直接 `#ifdef USE_XXX` +- 把编译期开关先收敛到一个地方 + +建议做法: + +- 新增类似 `KBuildConfig.h` 的配置头 +- 暴露 `constexpr bool` 常量,例如: + - `kUseEpollLT` + - `kUseRingBuffer` + - `kEnableConnectionRecycle` + +收益: + +- 业务代码改为普通 C++ 分支或策略选择 +- 搜索宏时只会剩下配置入口和底层实现 + +### 4.2 第二步:把 `EventLoop` 的并发策略抽成单独类 + +目标: + +- 把 `pendingFunctors_` 从 `EventLoop` 本体中拆出来 + +建议抽象: + +```cpp +template +class PendingFunctorQueue { +public: + void push(Functor&&); + template + void consumeAll(Consumer&&); +}; +``` + +可选后端: + +- `LockFreePendingFunctorQueue` +- `LockedPendingFunctorQueue` +- `LockedPendingFunctorQueue` + +收益: + +- `KEventLoop.cpp` 的宏分支显著减少 +- 新增策略时不需要碰业务流程 + +### 4.3 第三步:把连接复用拆成 lifecycle + recycler + +目标: + +- `TcpServer` 只管“拿连接 / 配连接 / 放回连接池” +- `TcpConnection` 只管“销毁时要不要走 recycle 生命周期” + +建议拆分: + +- `TcpConnectionRecycler` +- `TcpConnectionLifecycle` +- `RecycledConnectionPool` + +收益: + +- `USE_RECYCLE` 不再散落在 `KTcpServer` 和 `KTcpConnection` +- 生命周期边界更清晰 + +### 4.4 第四步:把 LT/ET 行为抽成 IO 策略层 + +目标: + +- 业务层不再直接知道自己是 LT 还是 ET + +建议抽象: + +- `acceptOneConnectionPerEvent()` +- `read(buffer, fd, savedErrno)` +- `write(buffer, fd, savedErrno)` +- `writeDirect(fd, payload)` + +收益: + +- `KAcceptor.cpp` +- `KTcpConnection.cpp` +- `KChannel.h` + +这几处都会变干净,LT/ET 差异下沉到底层 helper。 + +### 4.5 第五步:把 buffer 选择收成统一选择层 + +目标: + +- 上层只依赖一个统一 buffer 入口 +- ringbuffer 与线性 buffer 的差异只留在实现层 + +建议改法: + +1. 提供统一选择头,例如 `KSelectedBuffer.h` +2. 给两种 buffer 补齐统一能力接口,例如: + - `readableView()` + - `readableStringUntil()` + - `isReadableContiguous()` + - `retrieveLineAndCRLF()` + +收益: + +- `KHttpContext.cpp` 能用统一解析流程 +- `KTcpConnection::send(Buffer*)` 能少一段分支 + +### 4.6 第六步:替换 stdout 日志 + +目标: + +- 日志不再直接阻塞业务线程 + +建议改法: + +- 引入异步文件 logger +- 默认关闭,按编译开关或环境变量启用 +- 保留统一日志宏接口 + +收益: + +- 性能路径更稳 +- 头文件依赖更轻 +- 运维使用体验更好 + +## 5. 落地顺序建议 + +推荐顺序: + +1. 配置中心 +2. pending functor 队列策略 +3. recycle 生命周期策略 +4. LT/ET IO helper +5. SelectedBuffer + Buffer 公共接口 +6. 异步日志 + +原因很简单: + +- 前两步主要是“收宏”和“降耦合” +- 中间两步处理最容易出错的连接和 IO 行为 +- 最后处理日志和 buffer 统一接口时,改动面虽然大,但结构已经稳定 + +## 6. 测试与回归建议 + +每次做结构性重构时,至少覆盖下面几组构建: + +```bash +make -j4 +make USE_EPOLL_LT=1 -j4 +make USE_RINGBUFFER=1 -j4 +make USE_EPOLL_LT=1 USE_RINGBUFFER=1 -j4 +make USE_RECYCLE=1 -j4 +make USE_SPINLOCK=1 -j4 +make USE_LOCKFREEQUEUE=1 -j4 +``` + +还应至少手工验证: + +- `runHttpServer` 能启动 +- `/hello`、`/good`、`/file` 路径行为正常 +- 长连接多次请求不串包 +- 连接关闭后不会出现 double close / use-after-free + +## 7. 结论 + +当前代码的优点是主干非常清晰: + +- `EventLoop + Poller + Channel` 是标准 Reactor 核心 +- `TcpServer + TcpConnection` 划分自然 +- `HttpContext + HttpRequest + HttpResponse` 形成了完整的应用层链路 + +当前代码的主要问题不在“功能缺失”,而在“策略实现已经渗透到业务对象中”。 +因此后续最有价值的工作,不是继续堆功能,而是把这些策略层抽出来,让: + +- 业务对象更专注 +- 宏更集中 +- 测试维度更清晰 +- 以后继续优化性能时不需要反复碰主链路 diff --git a/webserver/docs/wiki-code-architecture.md b/webserver/docs/wiki-code-architecture.md new file mode 100644 index 0000000..7e25c98 --- /dev/null +++ b/webserver/docs/wiki-code-architecture.md @@ -0,0 +1,335 @@ +# WebServer Wiki: Code Architecture Walkthrough + +## 1. 这份 Wiki 怎么用 + +这不是“再讲一遍架构说明”,而是一份源码导读地图。 +适合的使用场景是: + +- 你第一次接手这个项目 +- 你想知道应该从哪个文件开始看 +- 你想带着问题去读代码,而不是从头到尾硬啃 + +最推荐的使用方式: + +1. 打开一个源码文件 +2. 对照本文“阅读路线”与“问题清单” +3. 一边看,一边在 IDE 里跳转 + +## 2. 第一张图:整个项目的阅读地图 + +```text +main: runHttpServer.cpp + -> HttpServer + -> TcpServer + -> Acceptor + -> TcpConnection + -> EventLoopThreadPool + -> HttpContext + -> HttpRequest + -> HttpResponse + +TcpConnection + -> Buffer + -> Channel + -> EventLoop + -> EventManager +``` + +如果你只能记住一件事,请记住: + +> `TcpConnection` 是单连接 I/O 核心,`EventLoop` 是线程内调度核心,`HttpContext` 是请求解析核心。 + +## 3. 新手阅读路线 + +### 第 1 站:先看程序怎么启动 + +文件: + +- `webserver/runHttpServer.cpp` + +你要回答的问题: + +- 主线程先创建了什么? +- `HttpServer` 是怎么被挂到 `EventLoop` 上的? +- 为什么最后是 `loop.loop()` 而不是 `server.loop()`? + +看完以后你应该知道: + +- 这是一个以 `EventLoop` 为中心驱动的系统 +- `HttpServer` 只是构建在 `TcpServer` 之上的应用层对象 + +### 第 2 站:看 HTTP 层怎么接入 TCP 层 + +文件: + +- `webserver/http/KHttpServer.h` +- `webserver/http/KHttpServer.cpp` + +重点方法: + +- `HttpServer::HttpServer()` +- `HttpServer::onConnection()` +- `HttpServer::onMessage()` +- `HttpServer::onRequest()` + +你要回答的问题: + +- HTTP 层是如何把自己的逻辑挂到 TCP 层上的? +- 为什么 `HttpServer` 自己不直接收 fd,而是通过 `TcpConnection`? +- `HttpContext` 是在哪里创建、存放和重置的? + +### 第 3 站:看 TCP 层总控 + +文件: + +- `webserver/tcp/KTcpServer.h` +- `webserver/tcp/KTcpServer.cpp` + +重点方法: + +- `TcpServer::start()` +- `TcpServer::newConnection()` +- `TcpServer::removeConnection()` +- `TcpServer::removeConnectionInLoop()` + +你要回答的问题: + +- 新连接到来后为什么不是当前线程直接处理? +- `connections_` 这张 map 存的是什么? +- `TcpConnection` 为什么是 `shared_ptr`? +- `USE_RECYCLE` 为什么会让 `TcpServer` 变复杂? + +### 第 4 站:看单连接 I/O 核心 + +文件: + +- `webserver/tcp/KTcpConnection.h` +- `webserver/tcp/KTcpConnection.cpp` + +重点方法: + +- `connectEstablished()` +- `handleRead()` +- `sendInLoop()` +- `handleWrite()` +- `handleClose()` +- `connectDestroyed()` +- `hpSendFile()` + +你要回答的问题: + +- 一个连接对象从建立到销毁经历了哪些状态? +- 为什么写数据时先尝试直接写,而不是永远先进 output buffer? +- 为什么关闭连接要区分 `shutdown()` 和 `handleClose()`? + +### 第 5 站:看事件循环内核 + +文件: + +- `webserver/loop/KEventLoop.h` +- `webserver/loop/KEventLoop.cpp` + +重点方法: + +- `loop()` +- `runInLoop()` +- `queueInLoop()` +- `doPendingFunctors()` + +你要回答的问题: + +- `EventLoop` 为什么要保存 `threadId_`? +- 跨线程调用为什么不能直接操作 `Channel`? +- `pendingFunctors_` 的作用是什么? + +### 第 6 站:看 epoll 分发骨架 + +文件: + +- `webserver/poller/KEventManager.h` +- `webserver/poller/KEventManager.cpp` +- `webserver/poller/KChannel.h` +- `webserver/poller/KChannel.cpp` + +你要回答的问题: + +- `EventManager` 和 `Channel` 分别负责什么? +- 为什么 `Channel` 不拥有 fd? +- `revents_` 和 `events_` 分别表示什么? + +### 第 7 站:最后再看 Buffer 和 HTTP 解析 + +文件: + +- `webserver/tcp/KBuffer.*` +- `webserver/tcp/KRingBuffer.*` +- `webserver/http/KHttpContext.*` +- `webserver/http/KHttpRequest.h` +- `webserver/http/KHttpResponse.*` + +你要回答的问题: + +- 两种 Buffer 的区别是什么? +- 为什么 ringbuffer 模式下 HTTP 解析更复杂? +- `HttpContext` 为什么用状态机? + +## 4. 如果你是带着“一个请求怎么跑完”这个问题来看的 + +最推荐按下面这个顺序追: + +```text +runHttpServer.cpp + -> HttpServer::start + -> TcpServer::start + -> Acceptor::listen + -> Acceptor::handleRead + -> TcpServer::newConnection + -> TcpConnection::connectEstablished + -> TcpConnection::handleRead + -> HttpServer::onMessage + -> HttpContext::parseRequest + -> HttpServer::onRequest + -> HttpResponse::appendToBuffer + -> TcpConnection::send/sendInLoop + -> TcpConnection::handleWrite +``` + +如果是 `/file` 路径,再加: + +```text +HttpServer::onRequest + -> open("./index.html") + -> HttpResponse::appendToBuffer + -> TcpConnection::sendAllOneTimeInLoop + -> TcpConnection::hpSendFile +``` + +## 5. 如果你是带着“跨线程是怎么协调的”这个问题来看的 + +要看这几处: + +- `KEventLoop::queueInLoop` +- `KAsyncWaker::wakeup` +- `KAsyncWaker::handleRead` +- `KEventLoop::doPendingFunctors` +- `KEventLoopThread::threadFunc` +- `KEventLoopThreadPool::getNextLoop` + +理解方式: + +1. `EventLoop` 只能在所属线程内安全操作。 +2. 其他线程想让它干活,必须先把任务投进 `pendingFunctors_`。 +3. 然后用 `eventfd` 把 loop 唤醒。 +4. loop 醒来后执行 `doPendingFunctors()`。 + +这就是“线程安全 + 线程亲和性”同时成立的关键。 + +## 6. 如果你是带着“为什么会有这么多宏开关”这个问题来看的 + +先看这些宏: + +- `USE_EPOLL_LT` +- `USE_RINGBUFFER` +- `USE_LOCKFREEQUEUE` +- `USE_SPINLOCK` +- `USE_RECYCLE` +- `USE_STD_COUT` + +再观察这些文件: + +- `webserver/tcp/KTcpConnection.cpp` +- `webserver/tcp/KAcceptor.cpp` +- `webserver/http/KHttpContext.cpp` +- `webserver/loop/KEventLoop.cpp` +- `webserver/tcp/KTcpServer.h` + +你会发现当前工程的一个鲜明特点: + +> 这个项目既是一个 WebServer,也是一个“多种并发/缓冲/IO 策略的实验场”。 + +这也是它很有学习价值的原因,但同时也是后续维护成本升高的来源。 + +## 7. 重点类速查表 + +| 类 | 所在文件 | 一句话职责 | 最应该先看的方法 | +| --- | --- | --- | --- | +| `EventLoop` | `loop/KEventLoop.*` | 线程内事件循环与回调调度核心 | `loop()`、`queueInLoop()` | +| `AsyncWaker` | `loop/KAsyncWaker.*` | 用 `eventfd` 唤醒阻塞中的 loop | `wakeup()`、`handleRead()` | +| `EventManager` | `poller/KEventManager.*` | epoll 封装层 | `poll()`、`updateChannel()` | +| `Channel` | `poller/KChannel.*` | fd 与回调之间的桥 | `handleEvent()` | +| `Acceptor` | `tcp/KAcceptor.*` | 监听 socket 与 accept 分发 | `listen()`、`handleRead()` | +| `TcpServer` | `tcp/KTcpServer.*` | 管理连接对象与 I/O 线程分发 | `newConnection()` | +| `TcpConnection` | `tcp/KTcpConnection.*` | 单连接读写与状态机核心 | `handleRead()`、`sendInLoop()` | +| `HttpServer` | `http/KHttpServer.*` | 在 TCP 层上挂接 HTTP 语义 | `onMessage()`、`onRequest()` | +| `HttpContext` | `http/KHttpContext.*` | HTTP 请求解析状态机 | `parseRequest()` | +| `Buffer` | `tcp/KBuffer.*` / `tcp/KRingBuffer.*` | 收发缓冲区 | `readFd*()`、`writeFd*()` | + +## 8. 常见阅读误区 + +### 误区 1:以为 `HttpServer` 是主角 + +实际上 HTTP 层只是应用层外壳。 +真正驱动请求收发的是 `TcpConnection + EventLoop + Channel`。 + +### 误区 2:以为 `EventManager` 拥有 `Channel` + +不是。 +`EventManager` 只是保存 `fd -> Channel*` 映射,生命周期管理仍然在上层对象。 + +### 误区 3:以为 `ThreadPool` 就是服务器线程池 + +`webserver/thread/KThreadPool.*` 更像实验/辅助实现。 +真正服务网络 I/O 的线程池是 `EventLoopThreadPool`。 + +### 误区 4:以为 `send()` 一定马上发出去 + +不一定。 +如果 socket 当前不可写,数据会先进 `outputBuffer_`,等下次可写事件再继续发送。 + +## 9. 调试时最值得打断点的地方 + +如果你要用 IDE 单步,推荐这些断点: + +- `Acceptor::handleRead` +- `TcpServer::newConnection` +- `TcpConnection::connectEstablished` +- `TcpConnection::handleRead` +- `HttpServer::onMessage` +- `HttpContext::parseRequest` +- `HttpServer::onRequest` +- `TcpConnection::sendInLoop` +- `TcpConnection::handleWrite` +- `TcpConnection::handleClose` + +如果你只打一个断点,建议先打在 `TcpConnection::handleRead()`。 + +## 10. 推荐的源码搜索命令 + +你可以直接在仓库根目录里用这些命令: + +```bash +rg -n "newConnection|handleRead|handleWrite|connectDestroyed" webserver +rg -n "queueInLoop|runInLoop|doPendingFunctors" webserver +rg -n "parseRequest|appendToBuffer|hpSendFile" webserver +rg -n "USE_EPOLL_LT|USE_RINGBUFFER|USE_RECYCLE" webserver +``` + +这几组搜索足够把核心控制流串起来。 + +## 11. 如果你要继续改这个项目,先做什么 + +最推荐的第一步不是加功能,而是先做结构整理: + +1. 把配置宏收敛成统一配置入口。 +2. 把并发策略、回收策略、IO 模式、buffer 选择抽成策略层。 +3. 把 stdout 日志换成统一日志系统。 + +原因是当前最主要的复杂度已经不在“功能少”,而在“策略差异已经散到主链路里”。 + +## 12. 读完这份 Wiki 后,下一步看什么 + +- 想系统理解架构:看 [architecture-guide.md](./architecture-guide.md) +- 想继续做重构规划:看 [change-notes.md](./change-notes.md) + +如果你是维护者,可以把这三份文档一起看; +如果你是第一次接手项目的人,只看这份 Wiki 再配合 IDE 跳转,通常就已经能进入状态了。