%%
# 纲要
> 主干纲要、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(logn) 插入 & 查找 & 移除既有定时器;
- 基于通用定时器,采用时间轮的方式剔除超时的 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)