%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # 思路梳理 ##### ❓ 项目学习路径 - (1)编译运行该项目 √ - (2)从 `main.cpp` 开始,明确程序**主体执行流程**。 - (3)浏览主体代码,梳理记录其**实现思路、主要逻辑**。 - (4)**个人复现**: - 明确各模块需要提供的功能点。 - 明确如何实现某一功能? - **参考开源代码**。 - **同步翻书**,明确不懂的函数调用,返回值,功能; - 如何进行测试、debug? - (1)单元测试:每完成一个模块,进行功能测试。 - (2)集成测试:例如 HttpServer 项目中,用 postman 构造请求,测试整体。 - 实践: - 先实现**主体框架 demo**,跑通流程,再一步步扩充,完善各个模块。 ##### ❓ 这份文档应该怎么样去组织? 1. 总结: - 罗列具体实现了哪些功能点; - 项目的架构设计,由哪些模块构成 2. 各个模块的实现说明 - 类介绍; - 实现逻辑/思路; - 注意事项 ## 已明确的点 HTTP/1.1 服务器。 - 明确实现方案: - **Reactor 模式** + **LT 水平触发**。 - **==reactors + thread pool==**(即 muduo 库的做法,**one-loop per thread + 线程池**) - 阻塞 I/O、非阻塞 I/O 下的 read/write() 行为 √ - **如何确保 `read` 读取完毕了缓冲区中的数据**? => 循环读取,直至为空后读到 EOF,返回字节数为 0. --- ## ❓待明确的点 muduo这个分层下,调整ET/LT触发应该只需要修改**TcpServer的handleRead/handleWrite()**? 以及 **Buffer 的 readfd**? 这两个是作为回调函数注册到TcpConnection类关联的conn_fd的channel中的。 ## TODO 功能细节: - 关闭日志 & 关闭定时器功能; ✔️ - mmap 映射完成后直接 `close(fd)` ✔️ - 文件描述符耗尽的处理; - 指定文件描述符上限 ✔️ `ulimit -n` 修改, - 限制并发连接上限 ✔️ 增加参数限制 - 优雅地关闭 TCP 连接 ✔️ - 关闭 Nagle 算法 ✔️ - 关于响应报文 ✔️ - 长连接下,错误代码必须返回content-lenght:0 --- - 短连接下,Acceptor 变为瓶颈的问题 - => 可考虑多个线程用作 Acceptor,开启 `SO_REUSEPORT` 允许多线程共享 `listen_fd` 而并发调用 `accept()`,从而**缓解主线程负载**。 - FIXME: - Any 类不能传值类型,只能以 `unique_ptr` 或者 `shared_ptr` 形式赋予,否则会触发段错误,暂未未明确原因。 - 例如 `loop->setContext(make_unique<TimingWheel>(...))`。 - TcpConnection的MessageCallback可以不需要buffer参数,内部直接通过conn->getOutputBuffer获取即可 - 修改TcpConnection中的state为原子操作, 用CAS - 将 epoller & eventLoop 中对 **`Channel*` 原生指针**的使用进行替换; - 使得 channel 析构时,**自身不需要再去判断是否仍位于 loop_->epoller 中**。 - 改用 muduo 的http层,检查效率是否提升 - 实现 Httpserver 业务逻辑     - 处理 POST 请求;需要维护登录注册,弄数据库连接池 ## 后期改进方向 - 用同步 API 模拟 Proactor 模式? - 如何利用第三方库: - 能否**替换/升级为 libevent、spdlog** 这样的库?**是否能灵活地替换接入这些库**?**是否有可参照实现的地方**? - libevent 库的功能是什么?能如何使用? - spdlog 库能如何使用? --- ## 时间线总结 - 2月26-27 共两天:实现事件循环模块: EventLoop、Channe、Epoller 雏形 - 2月28 共一天:实现定时器模块:TimerManage、Timer、Timestamp  - 3月2-5 共四天:     - 实现 Logger模块、复习CMake&整理笔记、为项目配置CMake、修正channel类 - 3月6-9 共四天:     - 实现 Tcp & Http 模块 TcpConn & TcpSever、HttpConn & HttpServer  - 3月10~11 共两天     - 编写 build.sh & 修复bug,初步压测  - 3月  - 3月19日     - 引入 TimingWheel,替换为每个连接直接建立 one-shot Timer的做法     - 完成压测 - 3月20&21日 完成项目README,github发布 %% # 项目总结 ## (1)已完成功能 - (1)参照 muduo 库实现底层 **网络库** 部分 - 包括:Reactor 模块、异步日志模块、通用定时器、TCP 缓冲区、TCP Server部分,同时去除 boost 依赖。 - (2)补充实现 **Tiny HTTP/1.1 服务器**: - 可解析 GET/HEAD 请求,响应指定资源路径下的静态资源文件; - 可响应 200、400、403、404、500、501 状态码。 - 支持长、短连接; 支持剔除超时的空闲连接 <br> > [!info] 可指定的运行参数 > > - 服务器相关: > - IP 地址 & 端口号; > - Web 资源根目录 > - I/O 线程数量 > - HTTP 超时时间(s) > - 允许的最大并发连接数量 > - 日志相关 > - 是否输出日志 > - 日志级别 > - 日志文件名(为空时输出到 stdout,非空时开启 AsyncLogger 异步写入到日志文件) > - 日志目录 > - 日志文件滚动大小(即单份日志文件的字节上限,写满后自动切换新文件) > - 日志刷新间隔(从日志缓冲区 flush 到日志文件的间隔;除定期刷新外,缓冲区写满后也会触发刷新) <br> ## (2)总体框架 ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-7DB1C537DDFBDF16D76EA600FEC60A2B.png]] ## (3)技术要点 - **Reactor 并发模型** - **主-从Reactor**,即 "**one loop per thread**" 方案 - **I/O 多路复用**: 非阻塞 IO + epoll(水平触发 LT) - **事件驱动下的回调机制** - 统一事件源:通用定时器基于单个 timerfd 实现,IO 线程间通过 eventfd 异步通知 - 回调机制:连接建立/断开、消息到达、写完成、定时器触发等均以回调方式进行, 解耦HTTP业务逻辑与底层事件. - **基于红黑树的通用定时器** + **利用 "时间轮" 方式断开超时的空闲连接** - 通用定时器(微秒级计时)使用 `std::set` 管理,支持 O(log⁡n) 插入 & 查找 & 移除既有定时器; - 基于通用定时器,采用时间轮的方式剔除超时的 HTTP 空闲连接(秒级计时) - (避免为每个连接启用一个独立的通用 Timer,开销略大) - **异步日志模块**:基于多缓冲区的高效异步日志 - 支持开启/关闭日志输出,可指定输出到日志文件或 stdout - 支持不同日志级别:TRACE、INFO、DEBUG、WARN、ERROR、FATAL - 支持日志滚动:按天滚动 & 单份文件写满指定 rollsize 后滚动,即换新日志文件 - 支持指定日志文件名、日志目录、单份日志文件大小上限(rollsize) - **独立的 TCP 接收/发送缓冲区**:epoll 水平触发下监听 EPOLLOUT 异步发送,保证数据交付 - **基于主-从状态机实现的 HttpParser**:针对不完整的 HTTP 报文,可通过多次解析保证有效结果; <br> ## (4)模块划分 - **Http 模块** - HttpServer 类 - HttpConnection 类 - HttpParser 类 - HttpMessage 基类 以及 HttpRequest & HttpResponse 子类 - **Tcp 模块** - TcpServer 类、Acceptor 类 - TcpConnection 类、Buffer 类 - **Reactor 模块** - EventLoop 类、EventLoopThread、EventLoopThreadPool 类 - Epoller 类 - Channel 类 - **异步日志模块** - 日志前端: - Logger 类、LogStream 类 - 日志后端: - AsyncLogger 类 - LogFile 类、AppendFile 类 - FixedBuffer 类 - **定时器模块** - TimingWheel 类 - TimeManager 类 - Timer 类 - Timestamp 类 - **辅助部分** - 线程封装:Thread 类、CurrentThread 命名空间 - Any 类 - countDownLatch 类 - InetAddress 类 <br><br> # Reactor 模块(事件循环下的 I/O 多路复用) Reactor 模块根据 muduo 中 "**one Loop per thread**" 的方式实现,思想如下: ![[66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明#^yf6phu]] 主要涉及三个类: - EventLoop 类 - Epoller 类 - Channel 类 ## 设计说明 - 每个 EventLoop 对象绑定于一个 **I/O 线程**,即作为 "**Reactor**" 运行事件循环: - 其持有一个 **Epoller 对象**,管理绑定于该线程的 fd。 - 其持有一个 **TimerManager 对象**,管理该线程上的**定时器**。 - 每个 Epoller 对象对应一个 epoll 实例,负责实现 **==IO 多路复用==**。 - 其封装了在 epoll 实例上**注册/更新/移除监听事件**的操作,以及 `epoll_wait()` 函数。 - 持有一组 Channel 对象,维护着 `fd -> Channel` 的映射。 - 每个 Channel 对象**服务于一个文件描述符 fd**,完成以下工作: - **事件注册**:向 Epoller 为该 fd 注册、更改、移除监听事件。 - **事件分发& 处理**:为该 fd 绑定 **==事件回调函数==**,根据 epoll 上触发的事件类型,执行相应回调 - 回调函数: readCallBack、writeCallBack、closeCallback、errorCallback; ### 事件循环过程 整个事件循环过程如下: 每个 fd 会被分配&**绑定**给一个特定 EventLoop,此后**该 fd 上的所有 I/O 事件均只在该 EventLoop ==所属 I/O 线程==下完成**,不会跨线程执行。 (fd 对应的 Channel 对象内持有指向其 owner EventLoop 的指针) 每个 I/O 线程上运行着 `EventLoop::loop()` 事件循环,其中会调用 `Epoller::poll()` 进行 **I/O 多路复用**,后者**返回存在触发事件的 fd 对应的活跃Channel 列表**,随后在 `loop()` 中调用各个 `Channel::handleEvent()` 完成 **事件分发 & 事件处理**。 在每一轮监听事件处理完成后,EventLoop 还会执行其任务队列中的 "**PendingFunctors**",如下图所示: (例如其他线程插入到该线程的任务,比如移除该 I/O 线程上的某个定时器或是 Channel 等) > [!note] `EventLoop::loop()` 事件循环 > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-FB90D031716D38BE221CD3D69018BEE3.png|734]] > <br> ## 实现说明 ### (1)关于 Eventloop 类 Eventloop 提供了系列函数,用于在其所持有的 Epoller 和 TimerManager 中添加 Channel 和 Timer。这些函数都会: - `updateChannelInEpoller()` 、`removeChannelFromEpoller`,这些函数只允许在 EventLoop 所属 IO 线程调用; - `runAt`、`runAfter`、`runEvery`、`removeTimer` ,这些函数**允许跨线程调用**,会进一步调用下列的 `runInLoop()` 来执行。 EventLoop 类提供了下列函数,可供**跨线程**调用,允许其他线程将任务**提交到 `EventLoop` 绑定的 IO 线程** 中执行。 - `runInLoop()`:供插入一个任务 Functor。 - 若调用该函数的线程即为 EventLoop 绑定线程,则直接执行; - 否则,调用 `addToQueueInLoop()` 加入其任务队列,视作 `PendingFunctor` 执行。 - `addToQueueInLoop()`:直接向任务队列插入 Functor,若是被跨线程插入,则**通过 ==`eventfd` 异步唤醒==该 EventLoop**。 ##### 关于 EventLoopThread 类 每个类对象代表一个**运行 EventLoop 的 I/O 线程**,其封装了 IO 线程的线程函数: - `startLoop()`:运行 `threadFunc()` 线程函数,**而后阻塞等待条件变量唤醒**。阻塞目的是待新线程中 EventLoop 构建完成后获取其指针。 - **`threadFunc()`** :**线程函数**。 - 在**新线程**中将创建一个 EventLoop 对象(线程唯一),执行**初始化回调** 后唤醒上述 `startLoop()` 阻塞,而后**调用 `loop()` 运行事件循环**。 即 `startLoop()` 供主线程调用,调用之后**将创建==新的 I/O线程== 执行 `threadFunc()`**,在其中**运行事件循环**。 ##### 关于 EventLoopThreadPool 类 IO 线程池的封装。该类对象由 **TcpServer 持有**,其持有: - 指向**主线程 EventLoop 对象**(mainReactor)的指针; - 一组 **EventLoopThreads 对象**(IO 线程,作为 subReactor) 在 TcpServer 中(主线程下),会调用 `EventLoopThreadPool::start()` 开启各个 **IO 线程**。 <br> ### (2)关于 Channel 类 每个 Channel 类对象 **服务于一个 fd**,但不控制 fd 生命周期,即 **析构时不会调用 `close(fd)`**。 Channel 持有**指向其所属 EventLoop**的指针,**只允许被添加到其 ==所属 EventLoop== 关联的 Epoller 中**。 其内部通过调用 **EventLoop 的接口**,即 `Channel->ownerEventloop->Epoller->epoll` 这条路径,**实现在 epoll 实例上的注册/更改/移除**。 Channel 提供了注册 "**==事件回调函数==**" 的接口,当 **epoll 监听事件**发生时会在 `Channel::handleEvent()` 中进行**事件分发**,调用相应的下列回调: | | epoll 事件 | | ---------------- | --------------------------------- | | `readCallback_` | EPOLLIN \| EPOLLPRI \| EPOLLRDHUP | | `writeCallback_` | EPOLLOUT | | `closeCallback_` | EPOLLHUP (& ! EPOLLIN) | | `errorCallback_` | EPOLLERR | Channel 的下列成员函数封装了在 epoll 上**注册/修改/移除监听事件**的过程: - `enableReading()` / `disableReading()`: 注册/移除 EPOLLIN | EPOLLPRI | EPOLLRDHUP 监听 - `enableWriting()` / `disableWriting()`: 注册/移除 EPOLLOUT 监听 - `disableAll()`:设置不监听任何事件 & **从 epoll 实例中移除对 fd 监听**; - `removeFromEpoller()`:**从 Epoller 的 `channels_` 列表中移除该 Channel 对象** & **从 epoll 实例中移除对 fd 监听**; 上述的前三项会进一步调用 `Channel::updateInEpoller()` 函数,根据 channel 的 **==当前 state==**(见下表) 以及 **设置的监听事件** 进行处理: - 若 `state==NOT_EXIST`,则添加到 `channels_` 中; - 若设置了监听事件: - 若当前状态为 `LISTENING`,则执行 `EPOLL_CTL_MOD` 操作; - 若当前状态为 `NOT_EXIST` 或 `DETACHED`,则执行 `EPOLL_CTL_ADD` 操作,更新状态为 `LISTENING`; - 若未设置监听事件: - 若当前状态为 `LISTENING`,则执行 `EPOLL_CTL_DEL` 操作,更新状态为 `DETACHED`。 - 否则,设置状态为 `DETACHED`。 Channel 的 `state_` 成员标记了**该 channel 在 Epoller 中的三种状态**: | 枚举值 | 是否记录于Epoller 的 `channels_` 列表中 | 是否注册于 epoll 实例中正在监听 | | ----------- | ------------------------------ | ------------------- | | `NOT_EXIST` | ❌ | ❌ | | `LISTENING` | ✔️ | ✔️ | | `DETACHED` | ✔️ | ❌ | > [!note] 由于**不同 fd 对相同 epoll 事件的处理方式**不同,所**需监听的事件也可能不同**,故**为每个 fd 关联 Channel 类**,在该类中: > > - **指定该 fd 上监听的事件**; > - **注册该 fd 上不同事件的==回调函数==**,由 Channel 进行 **==事件分发==**。 > <br> #### (2.1)fd 的生命周期 EventLoop、Epoller、TimerManager、Acceptor、TcpConnection 类中,均**持有一个 fd 及其关联 Channel 实例**。 - 例如,分别持有 `eventfd`、`epoll_fd`、`timerfd`、`listen_fd`、`conn_fd`。 - 这些类负责**控制 fd 的生命周期**,**==析构时调用 `close(fd)`==**。 <br> #### (2.2)Channel 的生命周期 EventLoop、TimerManager、Acceptor、TcpConnection 类中,均**持有一个 fd 及其关联 Channel 实例**。 这些类是 **Channel 对象的 ==owner==**,负责 Channel 的生命周期: - owner 析构时,Channel 对象随之析构。 - owner 析构函数中,需 **手动调用 `Channel::disableAll()` 与 `Channel::removeFromEpoller()`**, **将其 channel 从 Epoller 的 `channel_` 记录列表中移除 & 从 epoll 监听中移除**。 > [!faq] ❓哪些地方持有对 Channel 对象的引用? > > - Channel 对象的 owner 对象; > - Epoller 对象中,记录有 **`fd->channel` 的映射**。 > - epoll 实例中,一个 fd 对应的 `epoll_event` 结构中的 **`.data.ptr` 指向其绑定的 Channel 实例**。 <br> #### (2.3)`Channel::tie()` 防止错误析构 Channel 提供了一个 `tie(const std::shared_ptr<void>& obj)` 函数, 其作用是**延长由 `shared_ptr` 管理的对象的生命周期**,主要针对 **Channel 对象本身**或其 **owner 对象**, **防止其在 Channel 调用 `handleEvent()` 期间析构**(例如注册到 Channel 的回调本身是移除该 Channel 对象时)。 Channel 持有通过 `tie()` 注册的对象的 `std::weak_ptr<void>`。 在调用 `Channel::handleEvent()` 时,**若目标对象仍存在,则通过 `weak_ptr` 获取一个 `shared_ptr` 指向该对象,保证其存活**。 > [!example] 使用示例 > > 例如,**在 `TcpConnection::channel_` 上注册的回调** 其操作本身可能就是 "**断开 TCP 连接**",因此会从 `TcpServer::connections_` 列表中移除该 TcpConnection 类。 > > 但**在 `Channel::handle_event()` 过程中,必须保证 Channel 及其 owner 存活**,因此需要**调用 `Channel::tie(TcpConnection)` 来确保==延长 Channel 所属 TcpConnection 对象==的生命周期**。这也是 TcpServer 以 `shared_ptr<TcpConnection>` 形式来管理 TcpConnection 的原因之一。 > [!note] 参见 muduo 书本 [^1] > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-A74255511F8DF313520117F023CF45EE.png|522]] <br> ### (3)关于 Epoller 类 Epoller 封装 epoll 接口,**面向 Channel 对象进行管理**: - 记录有 **`fd->channel` 的映射关系**; - 以 `Channel` 为参数进行 epoll **事件注册**、**修改**、**移除**。 <br> ## 备注:区分几种 Reactor 实现方案 > 参见 muduo 书 161 页 [^2] - **reactor** + thread pool 方案: - **单个 reactor**:**主线程负责 I/O(包括 accept, read, write)** - **线程池**中工作线程负责**计算**。 - "**==one loop per thread==**" 方案(reactors in threads,即只有 "**==Reactor 池==**",muduo 的默认实现): - main Rector 负责 accept 连接,**将一个 TCP 连接随机分配给某个 subReactor**(采用 round-robin 方式来选择 subReactor) - **每个 subReactor 对应一个线程,建立一个 epoll Loop**; - 每个 subReactor 负责其所关联连接的 **==I/O + 计算==**,即**既负责该连接的 I/O,也负责该连接的请求背后涉及的计算**。 - "==**one loop per thread + 线程池**==" 方案 (即 **reactors** + thread pool): - main Reactor 负责 accept 连接,**将一个 TCP 连接随机分配给某个 subReactor**(采用 round-robin 方式来选择 subReactor) - **每个 subReactor 对应一个线程,分别建立一个 epoll Loop**; - 每个 subReactor 负责其所关联连接的 **==I/O==** - **==线程池==** 中工作线程供各个 subReactor 调用,**只负责==计算==**。 > [!NOTE] 不同 reactor 方案说明 > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-7D4B661AE1EFEAD16BCC24A2BC24C5E4.png|606]] > [!NOTE] "**one loop per thread**" 方案(即 reactors in threads,muduo 的默认实现) > > - 主线程只负责 accept; > - 其余 IO 线程同时负责 **IO +计算**。 > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-D6AE0C3F90DCF94FD00B7106B5D2CC82.png|621]] > ^yf6phu > [!NOTE] "**one loop per thread + 线程池**" 方案(即 reactors + thread pool 的方案) > > - 主线程(mainReactor)只负责 **accept**; > - 其余 IO 线程(即 subReactor)只负责 **IO**。 > - 线程池中 "**工作线程**" 负责**计算**。 > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-A97C3AA7C9DFD47484357B7A0FF3EA8F.png|617]] > --- <br> <br> # TCP 模块 TCP 模块涉及以下几个类: - Acceptor 类 - TcpConnection 类 - Buffer 类 - TcpServer 类 此部分包含了 **TCP 连接建立/断开、消息收发** 的管理。 具体来说,TcpServer 持有一个 **Acceptor 对象**,运行在**主线程**,其监听 `listen_fd`,主线程 EventLoop 中等待新连接到来。 每当一个新连接到来时,触发 `Acceptor::handleAccpet()` ,通过 `accept()` 获取 `conn_fd`,并传递给 **`TcpServer::newConection()` 回调**。 该回调中会将 `conn_fd` 以 "Round-Robin" 的方式 **分配给某一个 I/O 线程(即某个 EventLoop)**,并为其 **创建一个 TcpConnection 对象**。 该过程背后会进行以下操作: - 为该 `conn_fd` 关联一个 channel 对象; - 向 channe 中设置**监听事件**、注册**回调函数**; - 通过其 ower EventLoop,将 channel 注册到**该 EventLoop 所持有的 Epoller** 中: - **添加 `fd->channel` 的映射** - **向 epoll 上注册监听事件**(获取 channel 中指定事件,而后注册到 epoll 实例) 此后该 `conn_fd` 上所有 I/O 事件都**只由其绑定的 I/O 线程负责处理**。 ## 实现说明 ### (1)关于 Acceptor 类 Acceptor 类用于**接收新 TCP 连接并返回 `conn_fd`**,其会**作为 TcpServer 的成员**,供后者使用。 Acceptor 完成以下工作: - **持有 `listen_fd`** & 关联的 **Channel 实例**。 - **封装 socket 相关的基本操作**:创建 socket、绑定地址&端口、开启监听、调用 `accept` 获取连接并返回 `conn_fd`; - **提供注册 ==NewConnectionCallback 用户回调== 的接口**。 - 其**成员函数 `Acceptor::handleAccpet()`** 会注册为 `listed_fd` 关联 channel 的**事件回调函数**,处理 `accpet()` 事件。 - 该函数中在 `accept()` 成功返回 `conn_fd` 后,会调用 "**==NewConnectionCallback 用户回调==**"。 Acceptor 构造时就会接收地址&端口,**创建 socket 并完成 bind**,之后只需调用 `Acceptor::listen()` 开启监听即可。 > [!faq] ❓ 如何处理 "**文件描述符耗尽**" 的问题? [^3] > > 1. **通过参数限制 Server 的最大连接数 < 最大文件描述符数**; > 2. 预留一个**空描述符**,当**这些空描述符被使用时,先允许建立连接,再==主动关闭连接==**) > <br> ### (2)关于 TcpConnection 类 TcpConnection 类封装了**对一个==已成功建立的 TCP 连接==的管理**: - 以 `accpet()` 返回的 `conn_fd` 为参数进行构造,析构时 `close(fd)`。 - 持有 `conn_fd` 关联的 Channel 对象,由 Channel 处理**事件分发 & 事件回调**。 - 持有两个 Buffer 类对象,分别作为用户空间层面的 "**TCP 发送/接收缓冲区**"。 TcpConnection 的工作方式: 当 fd 上可读事件触发时,TcpConnection 负责完成**内核缓冲区=>TCP 接收缓冲区**的数据拷贝,然后将 TCP 接收缓冲区的指针传递给 **MessageCallback 用户回调**。 同时,其成员函数 `TcpConnection::send()` 供用户调用,发送 TCP 数据。 TcpConnection 类的细节: - 提供了一系列设置 "**==回调函数==**" 的接口: - **供 ==用户== 使用的回调**: - `ConnectionCallback`:**连接==建立 & 销毁==时** 的用户回调; - `MessageCallback`:**收到 TCP 消息时**的用户回调; - `WriteCompleteCallback`:调用 `TcpConnection::send()` 的消息全部发送完成时的回调; - **供 ==TcpServer== 使用的回调**: - `CloseCallback`:用于**通知 TcpServer 移除其持有的指向该对象的 TcpConnectionPtr**。 - 定义了一系列**成员函数**,均会**注册到 conn_fd 关联 ==channel== 中**作为对应的 **==事件回调函数==**(即 epoll 中对 conn_fd 监听事件的响应函数) - `handleRead()`: - (1)读 fd :即内核缓冲区 -> TCP 接收缓冲区; - (2)根据 `n = read(fd)` 返回值,会**分别调用其余几个回调**: - (2.1)`n > 0` 时,执行 **==MessageCallback 用户回调==**; - (2.2)`n == 0`时,调用 `handleClose()`; - (2.3)`n < 0` 时,调用 `handleError()`; - `handleWrite()`:写 fd,即 TCP 发送缓冲区 -> 内核缓冲区; - `handleClose()`:调用用户回调、以及 TcpServer 的回调。 - `handleError()`:日志记录 socket 上的错误值或者 errno 值 - 封装了自身**关于 ==连接建立/销毁== 时的处理操作**,**供 ==TcpServer== 调用**: - `TcpConnection::connectionEstablished()`:设置 `channel` 监听、调用 `channel_.tie()` 进行绑定、执行**用户回调** `ConnectionCallback`。 - `TcpConnection::connectionDestroyed()`:移除 `channel` 监听,执行**用户回调** `ConnectionCallback`。 - 提供了 `send()` 函数供用户调用,发送数据。实现逻辑如下: - 若**当前 TCP 发送缓冲区中无剩余数据**(OutputBuffer 为空且 fd 上未监听 EPOLLOUT),则**直接 `write(fd, msg, )`**; - 若 **当前 TCP 发送缓冲区还有剩余数据** or **单次 `write()` 未能全部发送完**,则将剩余数据**存入到 TCP 发送缓冲区**,而后开启**监听 `EPLLLOUT` 事件**,交由 `handleWrite()` 负责将缓冲区中剩余数据写到 `fd`。(防止数据乱序) - 具有一个**自定义的 `Any` 类型**的成员 `context_`,用于绑定该 TcpConnection 关联的上下文,例如 **HttpConnection 对象**。 > [!NOTE] 上述 "用户回调" 是指向 TcpServer 注册的回调。由于 TcpServer 又由 HttpServer 持有,因此实际上即是 HttpServer 里指定的成员函数。 ![[66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明#^31657s]] <br> ##### (2.1)TcpConnection 生命周期 TcpConnection 由 TcpServer 以 `shared_ptr<>` 的形式持有,当其 `connectionDestroyed()` 被调用后析构。 该函数是作为 "**PendingFuntors**" 追加到 EventLoop 任务队列中的,所以会在 EventLoop 的 `doPendingFunctor()` 执行,执行完成后析构 TcpConnection。 > [!example] TCP 连接断开的处理时序图[^4] > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-9FF92F994BFA2D995AB575F9F58246FC.png|596]] > > [!note] `shared_ptr<TcpConnection>` 引用计数跟踪 > > ![[Excalidraw/Excalidraw-Solution/TinyWebServer_qinguoyi 项目说明_3.excalidraw.svg|880]] > %% [[Excalidraw/Excalidraw-Solution/TinyWebServer_qinguoyi 项目说明_3.excalidraw.md|🖋 Edit in Excalidraw]] %% <br><br> ### (3)关于 Buffer 类 Buffer 类用作 **TcpConnection 的发送/接收缓冲区** [^5](日志缓冲区与此无关,后者使用的是 `log_tream.h` 下的 FixedBuffer 类)。 Buffer 内部实现 **采用 `vector<char>`**,对外表现为连续内存,且方便自动扩容。为防止内存地址变化后 **迭代器失效**: - (1)利用 `int` 类型下标来标记边界,而不是用 `char*` 指针; - (2)提供 `beginPtr()` 函数来获取 `buffer_` **起始地址**。 > [!NOTE] TcpConnection 中**保证对 Buffer 的操作==只会在其所属 EventLoop 所在线程==中进行**,因此无需保证线程安全。 ##### (3.1)关于 "内核缓冲区 => TCP 接收缓冲区" 的拷贝 `Buffer::readFd()` 的实现是**利用内部 buffer + ==临时栈空==间(64KB)** 共同构成 **接收缓冲区**, 只调用一次 `readv(fd)` 进行读取。由于采用 LT 水平触发,因此**若未能读取完,EPOLLIN 将仍然有效**。 目的在于: 1. **避免每个连接初始** Buffer 过大而浪费过多空闲内存(内部 `buffer_` 初始为 1KB) 2. **避免反复调用 `read` 的系统开销**(缓冲区够大,故通常一次读取即足够) ##### (3.2)关于 "TCP 发送缓冲区 => 内核缓冲区" 的拷贝 这部分由 `TcpConnection::send(msg)` 负责,参见上文。 > [!NOTE] 边缘触发下,每当 TCP 发送缓冲区有数据待发送时 **开启 EPOLLOUT 监听**,**待清空发送缓冲区后再关闭 EPOLLOUT 监听**。 --- %% #### 关于扩展为环形缓冲区 muduo 当前的 Buffer 实现是: - 读:对外提供 `peek()` & `retrieve()`,**由外部从起始地址读取**后,**外部手动调用 `retrieve()` 来告知已读取字节数**,移动 Buffer 内的 `read_pos_`。 - 写:对外提供 `append()`,接收**外部指针**,其**内部调用 `std::copy()` 将指定地址的数据拷贝至 `buffer_`**。 如果要实现环形缓冲区,意味着接口形式要变: - 读:不能由外部读取,而必须**由 Buffer 类==从其内部的环形缓冲区 `ring_buffer_` 读取完整数据==后传递给外部**。 - 然而这就涉及到 **TCP 粘包**的问题。**Buffer 并不知道数据边界**,上层应用也只有解析 Buffer 中数据后才知道。 - 因此,意味着**TCP 之上的==应用层必须自己提供缓冲区==**(例如 HttpParser 提供缓冲区),**将其缓冲区可写入地址&可写入字节数传入给 Buffer,交由 Buffer 完成写入**。 - 写:仍然同上。 ##### 实现参考 - markparticle 中采用了 **muduo 的 I/O 缓冲区设计**,**独立为一个 Buffer 类**。 - qinguoyi 中是在 **每一个 `HttpConn` 实例** 内以 "**字符数组**" 作为用户层面的 I/O 缓冲区 - `m_read_buf` 和 `m_write_buf`; - 🚨 这两个缓冲区是**不可复用**的,`m_read_idx` 与 `m_write_idx` 指针只会后移,而没有重置。 - ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-7240CA9FD7A2E090ADE0A29301C6390E.png|337]] - linyacool 中直接采用了 `std::string` 作为缓冲区,**有点太简陋了**,很耦合,直接把 string 放到了 `HttpData` 类里面,指针的定位管理全都放在了一起。 #### 备注 缓冲区有哪些实现方案? - 普通缓冲区 - 环形缓冲区 - brpc 中的实现:**非连续内存块** & **零拷贝缓冲** ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-CDB9286A45375B2E0097605A2613EE62.png|846]] %% <br><br> ### (4)关于 TcpServer 类 TcpServer 类用于**实现对 TcpConnection 的管理**,其为 HttpServer 的成员。 TcpServer 作用如下: - **持有一个 Acceptor 实例**,用于创建 socket、监听 TCP 连接、获取 `accpet()` 返回的 `conn_fd`。 - **持有 EventLoopThreadPool 实例**,用以开启**多个 I/O 线程**。 - **管理 TcpConnection 集合** - 存储 `name->TcpConnection` 映射,**每个连接具有一个字符串形式的名称 name**; - **提供了设置下列==用户回调函数==的接口** => **会注册为 ==TcpConnection 的回调==** - `ConnectionCallback`:**连接成功建立后**的用户回调; - `MessageCallback`:**收到 TCP 消息时**的用户回调; - 成员函数 **`TcpServer::newConnection()`** 会注册作为 **==Acceptor 的回调==**。 - 该函数接收参数 **`conn_fd`** 与 **对端地址**,**为 `conn_fd` 创建一个关联的 ==TcpConnection== 类实例**,并向后者**注册上述用户回调**。 - 成员函数 **`TcpServer::removeConnection()`** 会注册作为 **==TcpConnection 的 CloseCallback 回调==**。 - (1)将 TcpConnetion 从 `TcpServer::connections_` 列表中移除; - (2)将 `TcpConnection::connectionDestroyed()` **加入到 EventLoop 的任务队列**,**作为 PendingFunctor 而执行**。 > [!FAQ] ❓<font color="#c00000"> TcpConnection 的 `connectionDestroyed()` 必须作为 PendingFunctor 执行的原因</font> > > 原因在于,防止 `Channel::handleEvent()` 中调用 `connectionDestroed()` 后 TcpConnection 析构,导致其持有的该 Channel 本身也被析构。 > > ![[Excalidraw/Excalidraw-Solution/TinyWebServer_qinguoyi 项目说明_2.excalidraw.svg|874]] > > %% [[Excalidraw/Excalidraw-Solution/TinyWebServer_qinguoyi 项目说明_2.excalidraw|Edit in Excalidraw]] %% <br><br><br> # HTTP 模块 HTTP 模块负责 TCP 层之上,应用层 HTTP 协议的处理,包括以下类: - HttpServer 类:整个程序最外层的 OOP 封装 - HttpConnection 类:每个对象代表一个**独立 HTTP 连接**,其生命周期与 TcpConnection 对象绑定。 - HttpParser 类:负责**读取 TCP 接收缓冲区**,**解析得到完整 HTTP 报文**。 - HttpMessage 基类, HttpRequest 及 HttpResonse 派生类,用以 **存储 HTTP 消息中的各字段**。 ## 实现说明 ### (1)HttpServer 类 HttpServer 类是整个程序里最外层的封装,其: - 持有 Config 对象,记录着配置信息,包括 IP 地址&端口号,IO 线程数,资源文件根目录,日志文件名,日志级别,日志目录等。 - 持有 **TcpServer 对象**,由其进行 TCP 连接相关的**所有管理**。 - 持有 AsyncLogger 对象,负责将日志缓冲区中数据**异步写入**到日志文件。 > [!NOTE] 程序入口 > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-E0348A6EF576D10A5BD3FB9C87473A8C.png|254]] <br> ##### (1.1)关于 "HttpConnection" 与 "TcpConnection" 对象的绑定 HttpServer 类并没有维护 "**HttpConnection <=> TcpConnection**" 对象之间的映射,这样也避免了线程安全问题,减少不必要的开销。 > [!example] 例如用 map 记录的话,"新建连接" 均由**主线程 Acceptor** 处理必然安全,但 "断开连接" 是在各个 IO 线程下完成的,意味着 **每次移除连接都需要加锁操作**)。 每个 HttpConnection 与 TcpConnection 对象的绑定**直接由二者本身记录**,在连接建立时完成: - `TcpConnection::context_` 会被赋予 `sharded_ptr<HttpConnection>`; - `HttpConnection` 则持有 `weak_ptr<TcpConnection>`。 即 **HttpConnection 的生命周期与 TcpConnection 对象绑定**。 > [!NOTE] `HttpServer::onMessage()` 回调中通过自定义的 `any_cast` 获取 TcpConnection 关联的 HttpConnection 对象 > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-D95AA49E1AC6C812B9E1DB76EA7A1D93.png|643]] > > <br> ### (2)HttpConnection 类 HttpConnection 对象代表一个独立的 HTTP 连接,封装了以下内容: - 持有一个 **HttpParser**(有状态的) - 持有一个 `weak_ptr<Timer>`,即该连接关联的**定时器**,用于实现该连接上的**超时移除**。 - 持有一个 `weak_ptr<TcpConnection>`,指向其**所属的 TCP 连接**。 定义了下列成员函数: - `handleMessage()`:**消息回调**,TCP 数据到来时**解析 HTTP 请求报文**。 - `handleTimer()`:**定时器回调**,实现超时断开连接。 - `handleRequest()`:**业务层逻辑**,在 `handleMessage()` 成功解析得到一条完整 HTTP 报文后调用。 其余函数均与业务层逻辑相关,包括根据 URL 检查请求的资源文件是否存在&可读,mmap 映射,构造 & 回发响应报文等。 <br> ### (3)HttpParser HttpParser 负责从 **TCP 接收缓冲区**中解析得到**单条完整的 HTTP 报文**。 由于可能存在 "**拆包**" 问题,即 TCP 接收缓存冲区中为 **不完整的 HTTP 报文**,因此,HttpParser 采用 **==状态机==** 实现,旨在**维护已解析的==状态信息==**,在单次解析未完成时,再次解析时可根据既有状态继续。 其接口 `HttpParser::parse(const char* data, size_t len, size_t& parsed_size)` 可被反复调用,会根据解析情况返回不同状态: - `ParseReulst::SUCCESS`:成功解析到一条完整 HTTP 报文; - `ParseReulst::AGAIN`:尚未解析得到完整报文,需要继续解析 => 将等待次 `EPOLLIN` 事件到来后,再次调用`parse()`,继续上次的解析。 - `ParseReulst::ERROR`:解析过程出现错误 每次调用 `parse` 后,**TCP 接收缓冲区中已被解析的 `parsed_size` 个字节将被清除**,而不完整的字段将仍保留在 TCP 接收缓冲区中(例如不构成一个完整的头部键值对),待下一次解析。 在具体实现上,采用 "**主-从" 状态机**,主状态机即 `parse()` 函数,包含四种状态: - `ParsePhase::REQUEST_LINE`:当前正在解析首行 => 调用 `parseRequestLine()` - `ParsePhase::HEADER`:当前正在解析头部 => 调用 `parseHeader()` - `ParsePhase::BODY`:当前正在解析 body 部分 => 调用 `parseBody()` - `ParsePhase::DONE`:已解析得到完整 HTTP 报文 其中: - `parseRequestLine()` 与 `parseHeader()` 则是两个 **从状态机**,会维护解析状态并进行**状态跳转**。 - `parseBody()` 则仅需根据 `Content-Length` 指示的长度进行解析,读取字节数未满时返回 `ParseReulst::AGAIN` 结果。 解析所得的各字段信息直接保存在 HttpParser 对象中,其提供一系列 `get` 函数以供 HttpConnection 对象获取。 <br> #### 备注:关于 HttpParser 的实现思考 ##### (1)❓Parser 本身是否存储数据副本 **Parser 必须为解析所得数据保存副本**,而 **不能只是保存指向缓冲区 pos 的指针**,因为外部缓冲区可能会变化,导致迭代器失效。 Parser **不能对外部缓冲区做任何假设**: 1. **不能假定 "整个 HTTP 报文" 在外部缓冲区中==占据连续空间==** => 因为外部缓冲区可能是 "**环形缓冲区**",或是**由非连续内存块构成**; 2. **不能假定 pos 不会失效** => 因为外部缓冲区**扩容时可能会移动过数据**; 因此,其 Parser 只依赖于**其最小的输入接口** `parse(const char* data, size_t len, size_t& parsed_size)`,要求**保证传入的地址 `data` 具有长度为 `len` 的可读数据**。其参数 `parsed_size` 返回 "**==已消费的字节数==**"(成功解析)。 ##### (2)HttpParser 的两种实现思路 - (1)**基于状态机**:Parser 中记录**当前解析状态、当前指针位置(指向缓冲区中待解析的头部)**,在一次解析时若发现缓冲区中数据不完整,则返回 `AGAIN` 结果,下次调用时基于既有状态继续解析。 - (2)**无状态的实现**:不存储状态,每次调用时都重头开始解析,解析不完整时就返回 `AGAIN` 状态,保留数据在 TCP 接收缓冲区中,下次**再重头开始**。 ##### (3)状态机方案的实现 采用 "**状态机**" 的范式进行编程,也有两种思路: - (1)**"主-从" 状态机方式**: - 说明: - 从状态机**每次读取一行,定位到 `\r\n` 并将其置为`\0\0`**; - 主状态机**每次只对一行数据进行处理**; - 特点:会对报文扫描两遍,效率略低,但**结构和状态转移过程更清晰**。 - (2)**单一状态机方式**: - 说明:**逐个字节扫描**,单个状态机**涵盖整个 HTTP 报文的解析过程**。 - 缺点:状态多,状态转移复杂; - 示例:node.js 中的 http_parser 实现 %% #### 参考方案 ###### qinguoyi 采用 "**主-从状态机**" 的方式 ⭐ 挺巧妙的: - `parse_line()` 每次先按 `\r\n` **解析得到一行**,置为 `\0\0`,返回指向这一行开头的 `char*` 指针; - 针对 "**分段解析**" 的处理:体现在 "**从状态机**" 的 `parse_line()` 保证了**仅当每次缓冲区中存在完整的一行时,才会交由主状态机去提取**。 - 如果没有读取到完整的一行(没有`\r\n`),就会**标记状态为 `LINE_OPEN`**,**表示读取不完整**,则**下次会继续读取**。 - 读取 Body 部分时, `parse_line()` 会始终返回 `LINE_OPEN` ,继续进入循环,而 `parse_content` 中检查已读取的字节数,等于 `Content-length` 时返回。 ###### mark 通过 `search` 定位到 `\r\n` 的位置后,截取这一行得到 `std::string` ,再对其进行解析。 - 每个解析函数中,用的是**正则表达式匹配** ❌ 正则效率太低了。 - 针对 "**分段解析**" 处理: 同 qinguoyi ###### linyacool **采用 "主-多从" 状态机**。此外,**直接使用 `std::string` 作为 I/O 缓冲区**。 每次解析完一段后 **调用 `string.substr()`**,**去除掉已解析的部分**,效率略低。 - 主:**`handleRead()` 中,与 qinguo 一样,划分了处理请求行、请求头、BODY 的状态。 - 从: - 请求行:直接用 `.find()` 查找 `\r`,`GET`、`POST`、`HEAD`,检查查找结果,再用 `substr` 去提取,无状态机; - **多次地反复调用 `find()`,效率不会很低吗?** - 请求头:应用了**从状态机**,仅针对 "**请求头**" 进行提取; - 消息体:略; - **考虑了==分段读取==的情况**,为请求行,请求头的读取都指定了 `AGAIN` 、`SUCCESS`、`FAILURE` 状态。 - 请求行:**保证读取到完整的请求行时,才开始解析请求行**; - 请求头:状态机实现读取; ###### workflow - workflow 中解析时内部用了一个缓冲区,其将 TCP 接收缓冲区中的 **n 字节数据全部追加到 `parser->msgbuf`**,并且令 `parser->msgsize+=n`;解析完成后计算 `total = headoffset + transfer_length`,如果大于则更新返回字节数 `n = msgsize - total; ` 。 - 记录解析状态: - 首行 & 头部部分:`HPS_START_LINE`,`HPS_HEADER_NAME`, `HPS_HEADER_VALUE`, `HPS_COMPLETE` 等; - 解析首行: - **扫描一遍直至找到 `\r\n`,如果不构成完整的请求行,则返回 INCOMPLETE。确保请求行完整后,再通过 `strchr` 分别找到两个空格 ` ` 的位置,将这些位置置为 `\0`,分割**,得到 `p1`, `p2`, `p3` 分别指向 method, uri, version,再判断是否有效。 - 解析完一整行后,**移动 `header_offset`**,转移状态为 `HPS_HEADER_NAME`; ##### HttpParser 开源项目参考 - [http-parser/http\_parser.c at main · nodejs/http-parser · GitHub](https://github.com/nodejs/http-parser/blob/main/http_parser.c#L641) (16k stars) - 基于状态机的实现,**单个状态机**,**==逐字节读取==** 并进行状态转换。 - **数千行代码,太复杂了,上百个状态。。。** - [ h2o/picohttpparser: tiny HTTP parser written in C](https://github.com/h2o/picohttpparser?tab=readme-ov-file) (1.9k stars) ⭐⭐ - **无状态的**(未采用状态机),基于对 "**字符流**" 的直接匹配和解析,不分配动态内存而**只是通过 "设置指针" 来指出各个字段的位置**。 - 针对 **HTTP 报文不完整** 的解决方案: - `phr_parse_request()` 函数调用**必须放在循环中**,当返回值为 `-2` 时,**表示数据不完整**,需要等待下一波数据到来后再次调用。由于 **==未保存状态==**,因此每次调用该函数都会 "**==重新从头开始解析==**",不会存储前一次的解析进度。 - 尽管不存储状态,每次都重头解析,但是**解析效率比 `http-parser` 快上好几倍**。 - [GitHub - nekipelov/httpparser: HTTP request, response and urls parser](https://github.com/nekipelov/httpparser) (118 stars) - **基于状态机的实现思路** - ⚠️ 没有对提取出来的 method **字段进行合法性检查,只是做了拆分**。 %% <br><br> ### (4)HttpMessage 类 HttpMessage 类记录了 HTTP 报文相关的所有内容,共 HttpConnection 类的成员函数中调用,其提供了: - 一系列 `add`/ `set` 函数供**设置各个报文字段**。 - `encode()` 函数用以 **编码报文后写入到指定地址**。 由于 HTTP 请求、响应报文中实际上**只有第一行不同**, 因此抽象为一个 **HttpMessage 纯虚基类**,其为报文中首行提供三个占位符,再**派生两个 HttpRequest 与 HttpResponse 类**, 其中 `fillStarLine()` 定义为**纯虚函数**,由两个派生类实现,负责填入第一行信息: - 请求行:方法,url,版本号; - 响应行:版本号,状态码,状态短语 在该项目中,**只需要用到 HttpResponse 类**,而 HttpRequest 类则仅用于单元测试中构造报文时使用。 %% #### 关于数据发送 `HttpMessage::encode()` 会将报文直接写入到一个**临时的栈内存缓冲区**中,然后再调用 `TcpConnection::send()` 发送。 由于 `send()` 本身会首先判断能否直接调用 `write(fd)` 发送,不行时才拷贝到 TCP 发送缓冲区,因此可能存在两种过程: 1. `HttpMessage::encode()` 写入到临时 Buffer => `write(fd)` 2. `HttpMessage::encode()` 写入到临时 Buffer => TCP 发送缓冲区 => `write(fd)` #TODO :可以自己再为 TcpConnection 封装一个 `send()` & `sendInLoop`,接收 `struct env` 啥为参数,**调用 `writev()` 写**,写不完则将剩余数据拷贝到 **TCP 发送缓冲区**。即 `TcpConnection::send(msg.encode())`。 不应当越过 TcpConnection::send 而直接操作 Tcp 的 Outputbuffer!! %% %% #### 参考方案 ###### qinguoyi 中的实现 `add_response()` 函数直接**将 response 内容写入到 TCP OutputBuffer**。 ###### mark 中的实现 `Resonse` 对象的各个 `add` 函数是**直接将数据追加到 TCP OutputBuffer 中**,**自身不保存任何数据**。 同时 body 部分则直接通过 mmap 映射得到指针 `fileptr`,将 OutbufBuffer 与 `fileptr` 两项作为一个 struct iovec 数组,用 `writev` 写。 ###### workflow 中的实现 workflow 中不设有 Conn 的 OutputBuffer。 `Response` 对象持有一个 parser 实例,`add` 等函数**在其自身保存副本**,最后**通过 `encode()` 返回 `struct iovec` 数组,而 Conn 据此直接写入到 `fd`**。 %% <br><br><br> # 异步日志模块 异步日志模块可分为**日志前端、后端**两个部分[^6]: - **日志前端**: - Logger 类:供程序中**打印日志**时使用,规定了**单条日志格式信息**。 - LogStream 类:提供了一系列 **`<<` 操作符重载** 以及 **行缓冲区**(约 4KB,缓冲 **`<<` 链式调用** 时输入的全部内容) - **日志后端**: - AsynLogger 类:提供**日志缓冲区** & 负责定期将日志缓冲区中内容**写入到日志文件**,采用 "**==双缓冲区==**" 实现方案。 - LogFile 类 & AppendFile 类: 对**日志文件**的封装, > [!NOTE] 异步日志模块的前、后端两部分是 **==彼此独立==** 的 > > 前端 Logger 类通过 `<<` 接收日志信息并做 "行缓冲",而后将 **行缓冲区地址** 传递给 "**回调函数**",由回调函数实际决定输出位置。 > > - 默认回调函数是 **输出到 stdout**。 > - 当注册了 **AsynLogger 提供的回调函数**时,才会写入到 **AsynLogger 的日志缓冲区**。 > > 后端 AsynLogger 会单独开一个 "**==日志线程==**",负责 **==异步==** 地将其日志缓冲区内容**写入到日志文件**。 > > [!info] 在该项目中,当**配置的 "日志文件名" 为空**时,就是默认输出到 stdout,不启用 AsyncLogger。 <br> ## (1)日志前端的实现说明 `Logger.h` 中定义了一系列 **宏**,供打印日志时使用,如下所示: ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-10E0BAA71E89B27C0C7C1FAF1331B1C3.png|442]] 打印一条日志的方式形如 `LOG_INFO << str;` ,该语句**创建一个 Logger 类对象** 并返回其内部持有的 **LogStream 对象**,后者提供 `<<` 操作符重载,供接收日志内容。 Logger 对象在 "**==析构==**" 时调用 "**日志回调函数**",完成日志输出: ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-E9767BD3A0D47F309DF64FFBCE5E231E.png|403]] 整个过程具体如下: 1. Logger 对象构造时,确定该条**日志级别,文件名,行号**信息,同时将 `str` **写入 LogStream 对象内部的行缓冲区**。 3. Logger 对象 **析构时** 调用**全局变量 `g_output` 以及 `g_flush`** 指定 **==回调函数==**,将**LogStream 缓冲区** 地址传给回调函数,**由==回调函数==来确定 "日志信息" 输出到哪**。 --- ### (1.1)LogStream 类 LogStream 类提供了**对 `<<` 运算符的重载**,支持**链式调用**,内部持有一个**小缓冲区**(约 4KB),**缓冲 `<<` 接收的数据**。 ### (1.2)Logger 类 Logger 类作为可供用户使用的 "**Logger 前端**"。 ##### 嵌套 SourceFile 类 该类用于**去掉文件名的路径前缀**,只保留 basename。 每条日志内容中会包含 "**打印日志**" 操作**所在的源文件及代码行**(`__FILE__` 与 `__LINE__`),该类即用于**去掉 `__FILE__`的路径前缀**。 实现上有两个构造函数,其中一个是**模版构造函数**,旨在实现**编译时获取==完整文件名长度==**,**节省一次 `strlen()` 调用开销**。 例如 `SouceFile file(__FILE__)` 编译时可被展开为 `SourceFile<15> file("src/logger.cpp")`,**编译时即确定了字符数组长度**。 > [!NOTE] `__FILE__` 的值作为字符串字面量,保存在 `.rodata` 段。 > > 因此 SourceFile 类中**无需申请字符数组**来存储文件名,只需要用一个 `const char*` 指针标记位置即可。 > <br> ## (2)日志后端的实现说明 ### (2.1)FixedBuffer 类 表示**固定大小的缓冲区**,本质是对 "**字符数组**" 的封装: - 提供了 `append()`、`bzero()`、`reset()` 等相关操作可供**写入、清零、重置**。 - 维护当前可写入位置、已占用的缓冲区大小、剩余缓冲区容量等信息。 ### (2.2)AsyncLogger 类 AsyncLogger 类作为 **Logger 后端**,负责**提供日志缓冲区 & 将缓冲区中数据写入日志文件**。 - 提供**可供写入日志信息的==缓冲区==** (初始时两块空闲的 **4MB 缓冲区**,不够用时自动申请新内存) - `current_buffer_`、`next_buffer_` 分别指向**两块空闲缓冲区**,后一块备用。 - `buffers_` 存储 **已填满而等待被写入文件的缓冲区块**。 - 提供了 `append()` 接口(**==带锁==,线程安全**),**可供==前端 Logger 类==调用,写入日志**。 - 当 `current_buffer_` 写满时,加入 `buffers_` 中。 - 若 `next_buffer_` 可用,则将其作为 `current_buffer_`,否则**直接 `make_unique<Buffer>` 申请一块新内存给 `current_buffer_`**。 - 同时,会通过条件变量的 `.notify_one()` **唤醒 `threadFunc()` 工作线程,将缓冲区日志写入文件**。 - **独立线程** 挂着 `threadFunc()`,会**定期将 `buffers_` 中的数据写入==日志文件==**。 - 局部持有一个 **LogFile 类**,通过调用其 `append()` 写入文件。 - 仅在该线程写,故无需带锁保证线程安全。 - **==双缓冲区策略==**: - 内部会创建两个**新的空闲缓冲块 `new_buffer_1` 与 `new_buffer_2`** 以及一组**空的 `buffers_to_write_`**。 - 上述三者用于**在==临界区内==与当前 `current_buffer_`、`next_buffer_`、`buffers_`(前端已写满的缓冲区)进行 ==swap==**。 - 基于**条件变量**进行同步: - `buffers_` 为空时,**阻塞指定的 `flush_interval_` 时间**,期间**若 `current_buffer_` 被写满**时可被唤醒; - 唤醒后,**仅在临界区内执行上述交换过程**,临界区很小。 > [!note] 双缓冲区方案示意 > > ![[Excalidraw/Excalidraw-Solution/TinyWebServer_qinguoyi 项目说明_1.excalidraw.svg|769]] > > %% [[Excalidraw/Excalidraw-Solution/TinyWebServer_qinguoyi 项目说明_1.excalidraw.md|🖋 Edit in Excalidraw]] %% <br> ### (2.3)LogFile 类 & AppendFile 类 AppendFile 类封装了 **C API 下的文件 I/O 操作**: - 根据传入的文件名,以 **==append 模式==** 打开一个文件并持有 `FILE*` 指针,**内部持有一个 64KB 的缓冲区**用作**文件 I/O 缓冲**。 - 提供 `append()` 与 `flush()` 接口,**可供向文件追加写入数据** & **刷入磁盘**。 - 提供 `writtenBytes()` 接口,记录了**已向文件中写入的字节数**。 LogFile 类**持有指向 AppendFile 类的指针**,基于后者进一步封装了对 "**日志文件**" 的相关操作: - 提供 `append()` 与 `flush()` 接口**供 ==AyncLogging 类==调用**,用以 **向==日志文件==追加写入数据** & **刷入磁盘**,内部进一步调用 AppendFile 的接口。 - 提供 `rollFile()` 接口,用以**创建新一份日志文件**。其只在两种情况触发: - (1)当前日志文件**写入量超过 `roll_size_` 阈值**时。 - (2)**写入操作执行 `check_every_n` 次**后,检查**日志文件就日期而言已需要换新时**(例如每天一份日志文件) - 记录&维护着日志文件相关信息,例如: - **日志文件名**; - **日志文件目录**; - **滚动阈值 `roll_size_`**:当前日志已写入字节数超过阈值时,创建新一份日志; - **时间戳**:最后滚动时间戳、最后刷新时间戳等; - **检查间隔** `check_every_n`:每执行多少次写入后,检查**是否需要换新日志 or 调用 flush**。 - **刷新间隔** `flush_interval_`:执行 `flush` 刷入磁盘的间隔; <br><br><br> # 定时器模块 定时器模块共涉及四个类: - Timerstamp 类:对 `std::chrono` 的封装,旨在方便使用。 - Timer 类:定时器对象,记录有**回调函数**、**到期时间**、**定时间隔**。(间隔非 0 时代表为周期性定时器) - TimerManager 类:定时器容器,直接 **==基于 `std::set` 实现==**。 - TimingWheel 类:简单时间轮,基于一个间隔为 1s 的通用 Timer 实现 **剔除超时的 HTTP 长连接**。 <br> ##### ❓ 关于定时器的实现选型 定时器容器的实现方式包括升序链表、小根堆、红黑树等。 对于该 HttpServer 项目而言,定时器的用处在于**为每个 TCP 连接设置超时事件**,**超时后踢掉空闲连接**,涉及场景如下: 1. **创建定时器**:每个建立连接时创建,注册 `HttpConnection::handleTimer()` 回调,其内部会调用 `TcpConnection::forceClose()`,实现 **超时关闭连接**。 2. **重置定时器**:每当 TCP 连接收到新消息(EPOLLIN 触发)时,重置**到期时间**。 3. **移除定时器**:当前**连接已断开**时移除,包括: - 客户端主动断开——即触发 EPOLLIN 且read() == 0` 或触发 EPOLLHUP 时。 - 服务器主动断开——即 `HttpConnection::forceClose()` 在业务逻辑中被调用时。 由于涉及到 **查找 & 移除指定 Timer**,链表和小根堆都无法高效实现这一点($O(n)$ 复杂度), 因此同 muduo 库,**==直接基于 `std::set` 实现==** [^7](内部实现为红黑树,$O(\log n)$ 效率) , 以 `pair<Timestamp, shared_ptr<Timer>` 作为 key 存储,**按到期时间戳排序,再根据指针地址区分不同定时器**。 <br> ## TimerManager 类实现说明 TimerManager 实现特点如下: 1. 使用 **==`std::set`==** 管理定时器 Timer,以 `<pair<Timerstamp, shared_ptr<Timer>` 作为 key 存储。 2. 基于 **==单个 timerfd==** 实现 **定时触发**,其超时设置为 **当前所有 Timer 中的最小到期时间戳**。 - 每次**执行完定时回调后** 再次更新 timerfd。 在 "one loop per thread" 思想下, **每个 EventLoop 下持有一个 TimerManager**,即**每个 I/O 线程下进行==独立的定时管理==**。 - TimerManager **持有 timerfd 及其关联的 Channel**,在其所属 EventLoop 关联的 Epoller 中注册**监听该 timerfd**; - timerfd 对应 Channel 上绑定的 **定时回调事件**为 `TimerManager::handleExpiredTimer()`,即**处理所有到期定时器**。 - 定时器中的回调任务,仍然是在 `Event`handleEvent 处执行 TimerManager 提供 `addTimer()` 与 `removeTimer()` 两个接口,但**仅供其 owner EventLoop 调用**。 由其 owner EvenLoop 封装后再**对外提供**下列接口,**保证定时器的添加/移除操作都只在该 EventLoop 所属 IO 线程下执行**: - `runAt()`:创建一个 **到期时间为绝对时间** 的定时器 - `runAfter()`:创建一个**到期时间为相对时间**(相较于 now)的定时器 - `runEvery()`:创建一个 **周期性定时器**。 - `removeTimer()`:移除**指定定时器**。 前三个函数会返回 `weak_ptr<Timer>` 作为 **TimerId**,供 HttpConnection 持有。 当需要**移除指定计时器**时,则传参该 TimerId 调用 `removeTimer()` 函数。 <br> > [!NOTE] TimerManager 中本身没有用 "**锁**" 保护,是**非线程安全**的 > > 在实现上,对定时器的增加/移除操作,都**保证会放到 TimerManager 的 owner EventLoop 所在 IO 线程执行**。 > > `TimerManager::addTimer()` 实际会调用 `addTimerInLoop()` 函数,若当前线程非其所属 EventLoop 的线程,则会**封装为 `Functor` 后添加到其所属 EventLoop 的 PendinFunctor 队列中**,实现上述保证。 > > 例如, EventloopA 可调用 EventloopB 的 `runAt()` 添加定时器,则此时 EventLoopB's TimerManager 会将其 `addTimerInLoop` 成员函数作为 Functor,调用`loop_->runInLoop()` 加入到 EventLoopB's PendingFunctorQueue 中。 > > 因此,**无需额外的互斥机制**。 > [!faq] ❓ Timer 如何实现 "自注销" ? > > "自注销" 是指 **Timer 的定时任务本身是执行 `TimerManager::removeTimer()` 来移除该 Timer自身**。 > 即调用栈为 `EventLoop::loop() => Channel::handleEvent() => TimerManager::handleExpiredTimer() => TimerManager::removeTimer()`。 > > 实现方法是利用一个**状态变量 `calling_expired_timers_`**,标识**当前是否正在处理定时到期事件**。 > 若是,则将 `TimerManager::removeTimer()` 的操作是将 **当前 Timer 插入到 `canceling_timers` 列表** ,此后**不再重新加入到 TimerManager 内部容器中**。 > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-B9981EABB077F36B42852AF3363FCB39.png|643]] > %% #### 定时器的作用是什么? - 定期执行任务 - **清理过期会话或缓存** - **定时日志记录**:**统计、记录服务器的性能指标** - 超时控制 - **连接超时**:未在规定时间内建立链接,则终止操作; - **读写超时**:未在规定时间内完成读写,则关闭连接; - **请求超时处理**:客户端请求在指定时间内无响应,则返回错误; - 定时重试机制 - 操作失败时(网络中断、远程服务不可用等情况),**在指定时间后重新尝试连接**; - 心跳机制 - 在 WebSocket 或 TCP 长连接中,**定时发送心跳包以确保连接的稳定性**。 #### 定时器与定时器容器 "**定时器**" 是对 "**定时事件**" 的封装,而 "**定时器容器**" 是容纳所有定时器、实现对定时时间统一管理的 "**容器数据结构**"。 %% <br><br><br> # 辅助部分 位于项目路径 `/src/base` 下,主要**对既有 API 的 OOP 封装**,简化使用 or 提供辅助功能。 ### (1)线程信息封装 > [!NOTE] 封装的目的在于两点: > > 1. 方便 EventLoopThread 类、AsyncLogger 类开启新线程。 > 2. logger 记录的日志信息中会包含**线程 id、线程名**。为避免频繁调用`gettid()` 的开销,故**缓存线程 id**。 > ##### (1.1)Thread 类 Thread 类基于 **C++ Thread API** 进行封装,旨在记录**线程 id、线程名**信息。 其接收一个 `std::function<void()>` 作为**线程运行函数**,再对外提供 `.start()`、`.join()` 接口。 调用 `.start()` 时,内部会创建 **`std::thread` 实例**,**运行注册的线程函数**,在新线程运行后记录线程 id。 ##### (1.2)CurrentThread 命名空间 该命名空间下定义了: - 几个**全局的 `thread_local` 变量**,记录**线程 id 值、线程 id 字符串、线程名称、线程名长度**。 - **获取/缓存==当前线程 id== 的函数**; <br> ### (2)countDownLatch 类 countDownLatch 是基于 **条件变量** 封装的一个**计数器**,其作用类似于线程同步方式里的 "**屏障**" barrier,但只具有"**==一次性阻塞==**"的效果。 其会从给定初始值开始: * 线程调用 `countDown()`时会将**计数值减一**, 当**计数值减为 0 时, 唤醒所有被 `wait()`阻塞的线程**,继续执行; * 线程调用 `wait()` 时会阻塞等待,直至计数值≤0; 该项目中有两个地方使用了该类,**初始值均取为 1**: - Thread 类中:`start()` 函数会调用 `wait()` 而阻塞,直至 `threadFunc` 成功开启新线程并**获取线程 ID** 后调用 `countDown()` 唤醒。 - AsyncLogger 类中:`start()` 函数会调用 `wait()` 而阻塞,确保 **日志线程**开启后调用 `countDown()` 唤醒。 <br> ### (3)Any 类 > [!note] 自定义的 ==Any 类== 旨在实现 "**存储任意类型对象**",类同于 `boost::Any` 以及 C++17 引入的 `std::any`。 > > Any 类在实现上涉及 C++ 模版和多态的应用,通过一个**基类的指针**来实际指向**派生的模版类**,实现**类型擦除**。 > > 该项目中,TcpConnection 对其**所需绑定的 "应用层" 信息**是未知的,因此令其持有 Any 类型的 `context_` 成员,实现解耦。 > ^31657s ### (4)InetAddress 类 对 `struct sockaddr_in` 与 `struct sockaddr_in6` 的封装,提供 "sockaddr 结构体" 与 "IP 字符串及端口号" 之间的转换。 <br> ### (5)Config 类 对 HttpServer 程序 **运行配置参数**的封装,通过 `getopt_long` **解析命令行参数**,传递给 HttpServer 类。 %% # 线程池 # 数据库连接池 # 备注说明 ## 有界阻塞队列 项目里使用了一个 "**循环数组**" 来作为有界阻塞队列,用的是 "**动态数组**",存储的元素类型是 **`<T*>` 指针**。 这个有界阻塞队列 API 很有问题,**虽然 `size`,`empty`,`full` 这些接口都有加锁**,但是**并不能避免==条件竞争==**。 **不能根据这些接口返回的瞬时状态去==决定后续逻辑==**。 --- `BlockingQueue<T>` 是多线程编程的利器,用途包括: - 用作 "**任务队列**"(线程池中),模版参数 `T` 为 "**函数对象**"。 - 用作 "**数据队列**"(生产者-消费者模型中),模版参数 `T` 为 "**数据**"; - 例如, "**消息队列**"(日志库中) ## 回调函数 **回调函数**(callback function) 是指在程序中将 **"一个函数的引用或地址"** 传递给另一个接收函数,由后者**在执行时的某一时间点或条件下触发调用该函数**的机制。 #### 应用场景 常用于异步编程、事件驱动编程等场景: - **异步操作**: - 异步 I/O,等待**数据传输完成后**通过回调通知用户。 - **事件驱动** - GUI 程序中,用户点击按钮时,**调用预先注册的回调函数(响应函数)**。 - **定时器**: - 在设定时间到达后,通过**回调函数**执行特定任务。 # 压测 `webbench` 工具、`benchmark` 工具。 #### 影响因素 ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-205B5A3F49B1B1D65E5C67D74C59204B.png|545]] ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-24907C91A53AE39DEC7EE6A4926667A2.png|543]] --- 修改后的如下: ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-E8D9B6124D743D9C557C09707B1AD617.png|455]] | | QPS | | ------------------------------- | ----------- | | 常规运行模式(保留日志输出 + 定时器 + 磁盘 I/O) | 约 7800 | | 关闭日志输出 | 约 8400 | | 关闭定时器 | 约 9600 | | 移除磁盘 I/O 访问(**解析完报文后直接返回内存消息**) | **约 12000** | - 日志线程: - 关闭日志打印(保留日志线程)  => 提升至 **QPS 8400**  - 关闭日志线程 => 无进一步的明显提升 - 定时器: - => 移除定时器  => **提升至 QPS 10200!!!达成破万 QPS** - => 多测几次,平均 9600 的样子 - 补充设置选择,**超时时间为 0 时不启用计时器**。 - => 改用升序链表实现; - 磁盘 I/O 影响 - => 解析完报文后直接返回内存消息  => 提升 QPS 至 12000!! - HttpParser 解析效率 - => 替换用 muduo 的 http 层 - 底层 TcpServer 实现 - => 改用 muduo 库 --- 阿里云服务器下,Release 与 Debug 版本对比: ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-5D44AB89EDC63BB823ED6F811F2341A2.png|1055]] --- #### 带网络带宽 ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-B0CB7D308B391C0F310365312A7DA3AE.png|536]] #### 编译 根据 webbench 主页说明,`sudo make && sudo make install` 进行编译。 编译时报错如下,但**检查 `sudo apt install libtirpc-dev` 已安装**。 原因在于较新系统上,tirpc 头文件是位于 `/usr/include/tirpc/`, 因此头文件 `<rpc/types.h>` 的引入路径应改为 `<tirpc/rpc/types.h>`。 可**通过 `make CFLAGS=-I/usr/include/tirpc` 直接指定路径**,而不必修改源码,随后再次编译正常,如下图所示: ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-8340C83D4186D0912D820274E0573220.png|489]] #### 关于监听端口 ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-7BB1FB35FD072A9AB5A5143CAB99C2C3.png|821]] %% %% # Bug 记录 ###### (1)错误地在 HttpServer 中使用 `std::map<TcpConnectionPtr, HttpConnectionPtr>` 来管理 TCP 连接与 HTTP 连接间的映射关系 - 说明:**未加锁**,**非线程安全**,存在条件竞争。 - 结果: **`map` 被多线程修改后错误释放内存,触发段错误**。 - 修复: - 引入 **Any 类**作为 `TcpConnection::context_` 成员,令其**指向关联的 `HttpConnection` 实例**。 - 在 `HttpServer::onMessage()` 回调中,**通过 `any_cast<HttpConnection>(tcp_conn->getMutableContext())` 来获取绑定 HttpConnection 实例**。 > [!example] 同一个回调函数,连接建立只由 **主线程的 Acceptor** 完成,是安全的。然而 **==连接断开==是由各个 I/O 线程处理的,故存在条件竞争**。 > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-45562905C1F37C13554632FE60B9A612.png|675]] > ###### (2)错误地将 `bind(&HttpConnection::onTimer, this)` 作为定时器的回调函数。 - 说明:HttpConnection 的生命周期与 TcpConnection 绑定,故在**定时器触发时可能已析构**。 - 结果:`this` 指向**无效内存**,导致在其中通过 `tcp_conn_wkptr.lock()` **获取到一个非空但指向错误地址的 `share_ptr<TcpConnection>` 指针**,在由其进一步调用 `getOwnerLoop()->removeTimer()` 时,`getOwnerLoop()` 返回的是无效地址,进而**调用函数时触发段错误**。 - 备注:最初**误以为 bug 出在 `removeTimer()` 上**,实际不是。 - 修复: - 方案一:改用 HttpConnection 的 "**静态成员函数**" 作为定时器回调,**绑定 `weak_ptr<TcpConnection>` 作为参数**。 ✔️采用 - 方案二:改由 HttpServer 管理定时器并**提供其成员函数**作为回调,HttpServer 生命期为整个程序运行周期,故可绑定 `this`。 > [!example] 如下图,`EventLoop` 的 `this` 值为非法内存地址 `0x1`,且回调函数 `forceClose` 内的 `tcp_conn_wkptr` 与 `conn_sptr` 都 > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-A82E963FD057B514FFC4D5E481BE30CD.png|458]] > > 表面上看,是 **`tcp_conn_wkptr` 本身无效**且返回了一个**非空但指向无效内存的 shared_ptr**。**实际上是==绑定给函数的 `this` 指针非法==**。 > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-AC96F03F11E1F6E2D7562A4C655AE959.png|821]] > > 尝试修改,但又引入了新错误,导致下面的错误(3)。 > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-53A041CD3394CA17FE5871855A962073.png|947]] > ###### (3)错误地在 `HttpConnection` 的**构造函数**中调用 `shared_from_this()` - 说明:起因是希望得到 HttpConnection 的 share_ptr 指针,进而取 weak_ptr 来**绑定给定时器回调函数**。 - `shared_from_this()` 不能在构造函数中使用,因为**此时 `this` 尚未由 `shard_ptr` 管理**。(先 new 构造对象,然后才给到 shared_ptr 包裹) - 结果:导致了很莫名其妙的错误报告,**即没有任何前兆地直接跳转至 `~EventLoop` 析构**。最后定位到是在 `shared_from_this()` 语句触发的。 - 修复:同上一点。 > [!example] 单看崩溃时的函数栈,无法直接定位问题。 > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-41BE28F90CEC6BC41A447702B78545C3.png|814]] > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-8A0F4067CEBB32C26AB8CC4A7BE5CA4C.png|815]] > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-EB99BEBB78985AE6E12F1A1411954FBF.png|548]] > > ![[_attachment/66-项目文档/MyHttpServer 项目/MyHttpServer 项目说明.assets/IMG-MyHttpServer 项目说明-C10E7E6DC221B210C56498845C868280.png|443]] > > %% <br><br><br> # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later <br><br> # ♾️参考资料 ### 开源项目 - TinyWebServer: - [GitHub - qinguoyi/TinyWebServer: :fire: Linux下C++轻量级WebServer服务器](https://github.com/qinguoyi/TinyWebServer) (16.5k stars) - [WebServer](https://github.com/markparticle/WebServer):TinyWebServer 的 C++11 实现版本,更简洁、更优雅(3.5k stars) - [ linyacool/WebServer: A C++ High Performance Web Server](https://github.com/linyacool/WebServer) :C++11 编写的 Web 服务器 (7.7k stars) - [GitHub - importcpp/WebServer: C++高性能网络服务器](https://github.com/importcpp/WebServer?tab=readme-ov-file) 74 stars %% 参考项目: - **TinyWebServer_==qinguoyi==**:基于 C++ 基本语法和 POSXI API 实现; - **同步模拟 Proactor** 方案: - **主线程 Main Loop 负责 I/O**(accept + read/write) - 线程池中工作线程负责**请求处理** - **Reactor 方案**() - **主线程 Main Loop 仅负责 ==accept==** - 工作线程同时负责 **I/O + 计算** - 🚨 注:在其实现中,当**工作线程处理 I/O** 时,会**以轮询方式阻塞主线程**!! - Webserver_**markparticle**:基于 C++11&14 实现,**qinguoyi 方案的更简洁、更优雅实现**。 - **同 qinguoyi**; - Webserver_**agedcat** - **同 markparticle** --- - WebServer_**==linyacool==**:基于 C++11 实现,参照了 **muduo 库**。 - **one loop per thread** 方案 - Webserver_**importcpp**: - one loop per thread 方案,**同 linyacool**,参照了 **muduo 库**。 - Webserver_**hanAndHan** - one loop per thread 方案,**同 linyacool**,参照了 **muduo 库**。 这三个都没放资源文件。 --- - linyacool 与 importcpp,只做了 GET,**静态资源的访问**。 - 硬编码的。 - **给了文件后缀的映射** √ 可参考的是 qinguoyi、markparticle,给了**文件后缀映射,文件处理**。 **** %% <br> ### 参考书籍 - 《Linux 多线程服务端编程:使用 muduo C++ 网络库》 - 《Linux高性能服务器编程》——游双 - 《UNIX 环境高级编程》 - 《UNIX 网络编程卷 1:套接字联网 API》 # Footnotes [^1]: 《Linux 多线程服务端编程:使用 muduo C++ 网络库》(P274) [^2]: 《Linux 多线程服务端编程:使用 muduo C++ 网络库》(P161、169~174) [^3]: 《Linux 多线程服务端编程:使用 muduo C++ 网络库》(P238,第 7.7.2 节) [^4]: 《Linux 多线程服务端编程:使用 muduo C++ 网络库》(P308) [^5]: 《Linux 多线程服务端编程:使用 muduo C++ 网络库》(P204 第 7.4 节, P313) [^6]: 《Linux 多线程服务端编程:使用 muduo C++ 网络库》(P108、P478~P479) [^7]: 《Linux 多线程服务端编程:使用 muduo C++ 网络库》(P291)