镜像自 github.com/Chieko3020/HttpFramework
  • C++ 86.2%
  • Shell 6.5%
  • HTML 4.9%
  • CMake 2.4%
Find a file
Chieko3020 c95c894e15 fix(bench): /stats 的内存池统计改读服务自持池(此前恒为 0)
- HttpServer 暴露 memoryPool();App 暴露 server(),用于读取服务自持的运行时资源
- bench_server 的 /stats 由 GlobalMemoryPool::getInstance() 改为 app.server()->memoryPool():
  两者是两个不同对象,旧实现压测再久 alloc_calls 也恒为 0;未开池时那次调用还会白创建一个
  5000 块的池(默认 12KB/块),让内存基准口径失真
- 新增 tests/test_http_mempool_stats.cpp 锁死三条契约:未启用为 nullptr / 启用后指向自持池 /
  计数随请求增长(防止再改回读全局单例);ctest 14/14
- README 内存池节改为给出分配频率实测(原「该计数恒为 0」提示删除)
2026-09-21 04:20:20 +08:00
cmake chore: 清理 main 上的 WSS 残留,并把基准脚本改造为单分支 2026-09-20 09:01:16 +08:00
examples fix(bench): /stats 的内存池统计改读服务自持池(此前恒为 0) 2026-09-21 04:20:20 +08:00
include fix(bench): /stats 的内存池统计改读服务自持池(此前恒为 0) 2026-09-21 04:20:20 +08:00
results fix(bench): /stats 的内存池统计改读服务自持池(此前恒为 0) 2026-09-21 04:20:20 +08:00
scripts chore: 清理 main 上的 WSS 残留,并把基准脚本改造为单分支 2026-09-20 09:01:16 +08:00
src chore(sync): 从 feature/WebSocket 同步非 WSS 改动到 main(不含 WSS 模块) 2026-09-20 08:26:08 +08:00
templates chore(sync): 从 feature/WebSocket 同步非 WSS 改动到 main(不含 WSS 模块) 2026-09-20 08:26:08 +08:00
tests fix(bench): /stats 的内存池统计改读服务自持池(此前恒为 0) 2026-09-21 04:20:20 +08:00
.gitignore chore: 清理 main 上的 WSS 残留,并把基准脚本改造为单分支 2026-09-20 09:01:16 +08:00
CMakeLists.txt chore(sync): 从 feature/WebSocket 同步非 WSS 改动到 main(不含 WSS 模块) 2026-09-20 08:26:08 +08:00
init.sql chore(sync): 从 feature/WebSocket 同步非 WSS 改动到 main(不含 WSS 模块) 2026-09-20 08:26:08 +08:00
README.md fix(bench): /stats 的内存池统计改读服务自持池(此前恒为 0) 2026-09-21 04:20:20 +08:00

HttpFramework

项目描述

HttpFramework 是一个 C++17 的 HTTP 服务框架。 它覆盖从 socket 到业务处理的完整一层:多 Reactor 网络层、HTTP/1.1 解析与响应构造、 路由匹配、链式中间件、会话管理,以及基于线程池的异步处理模型。网络层由本项目实现, 不引入第三方网络库;接入方式是一个 CMake 目标,可以被 find_package(HttpFramework) 引入。

规模约 6,700 行(src/ + include/);测试为 13 个 ctest 目标、115 个用例。

快速开始

依赖

# 必需
sudo apt install build-essential cmake libboost-all-dev

# 可选:数据库支持(未安装时相关功能与测试自动跳过)
sudo apt install libmysqlcppconn-dev

# 可选:性能基准
sudo apt install wrk

构建

# 基础构建
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j

两种使用方式

推荐使用 http::App 类,将 Router、HttpServer、SessionManager、DbConnectionPool 封装在一起,提供链式 API,大幅减少代码编写。

方式一:编写代码

最简服务器

#include "HttpFramework.h"
int main() {
    http::App app;
    app.get("/", [](auto& req, auto& res) {
        res.setHtml("<h1>Hello, HttpFramework!</h1>");
    });
    app.start(8080);
}

完整功能服务器

#include "HttpFramework.h"
#include "utils/TemplateLoader.h"

int main() {
    http::App app;

    app.enableLogging()
       .enableSession()
       .enableMemoryPool();

    app.get("/", [](auto& req, auto& res) {
        res.setHtml(utils::TemplateLoader::loadTemplate("index.html"));
    });

    app.get("/api/hello", [](auto& req, auto& res) {
        res.setJson(R"({"message": "Hello from HttpFramework!"})");
    });

    app.get("/users/:id", [](auto& req, auto& res) {
        res.setJson(R"({"user_id": ")" + req.getParam("id") + R"("})");
    });

    app.get("/session", [&app](auto& req, auto& res) {
        auto s = app.sessionManager()->getSession(req.getUserData("session"));
        int count = s && s->has("visits") ? std::stoi(s->get("visits")) + 1 : 1;
        if (s) s->set("visits", std::to_string(count));
        res.setJson(R"({"visits": )" + std::to_string(count) + "}");
    });

    app.notFound([](auto& req, auto& res) {
        res.setStatus(http::HttpStatus::NOT_FOUND);
        res.setJson(R"({"error": "Not Found"})");
    });

    app.start(8080, 4);
}

自定义中间件

// 全局中间件 — 请求日志
app.use([](const http::HttpRequest& req, http::HttpResponse& res, std::function<void()> next) {
    std::cout << req.getMethodString() << " " << req.getPath() << std::endl;
    next();
});

// 路径特定中间件 — API 认证
app.use("/api", [](const http::HttpRequest& req, http::HttpResponse& res, std::function<void()> next) {
    if (req.getHeader("authorization").empty()) {
        res.setStatus(http::HttpStatus::UNAUTHORIZED);
        res.setJson(R"({"error": "Unauthorized"})");
        return;
    }
    next();
});

// 自定义响应头
app.use([](const http::HttpRequest& req, http::HttpResponse& res, std::function<void()> next) {
    res.setHeader("X-Powered-By", "HttpFramework");
    next();
});

数据库集成

http::App app;

// 启用数据库(失败时自动降级,服务器正常启动)
app.enableDatabase("localhost", "root", "password", "mydb");

app.get("/db/users", [&app](auto& req, auto& res) {
    auto pool = app.dbPool();
    if (!pool) {
        res.setJson(R"({"error": "Database not available"})");
        return;
    }
    auto conn = pool->getConnection();
    if (!conn) {
        res.setStatus(http::HttpStatus::INTERNAL_SERVER_ERROR);
        res.setJson(R"({"error": "Failed to get connection"})");
        return;
    }
    try {
        auto result = conn->executeQuery("SELECT * FROM users");
        // ... 处理结果 ...
        pool->returnConnection(conn);
    } catch (const std::exception& e) {
        pool->returnConnection(conn);
        res.setStatus(http::HttpStatus::INTERNAL_SERVER_ERROR);
        res.setJson(R"({"error": ")" + std::string(e.what()) + R"("})");
    }
});

App 配置一览

方法 默认值 说明
enableLogging() — 请求日志中间件
enableSession(expire, cleanup) 1800s, 300s 会话管理 + 后台自动清理
enableMemoryPool() — 12KB 固定块内存池
enableDatabase(host, user, pass, db, port, max) port=3306, max=5 MySQL 连接池(可选)
start(port, threads) threads=4 启动服务器并阻塞等待信号

需要完全控制时,通过 app.router()、app.sessionManager()、app.dbPool()、app.stats() 直接操作底层对象。

方式二:底层 API

直接使用 HttpServer、Router、SessionManager、DbConnectionPool 等底层类,自由组装。参考 examples/full_demo.cpp,展示了手动管理信号处理、全局资源生命周期、端口检测等用法。

示例程序暴露的端点

full_demo 在 8080 端口启动后,可直接访问以下端点验证各模块:

端点 说明
/ 首页
/api/status 服务状态
/api/stats 框架内部统计
/api/time 服务端时间
/api/echo 请求回显(POST)
/session/info 当前会话信息
/session/stats 会话统计
/session/set 写入会话数据(POST)
/db/test 数据库连接测试
/db/users 用户列表(GET 查询 / POST 新增)
/db/add-user 添加用户表单
# 压力测试
wrk -t4 -c100 -d30s http://localhost:8080/

# 接口检查
curl http://localhost:8080/api/status

数据库相关端点需要 MySQL;未配置时 enableDatabase 会自动降级,服务器仍能正常启动。

集成到现有项目

HttpFramework 安装后可作为 CMake 包被外部项目引用。

# 外部项目的 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyWebApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)

find_package(HttpFramework REQUIRED)

add_executable(my_server main.cpp)
target_link_libraries(my_server PRIVATE HttpFramework::http_framework)
// main.cpp
#include "HttpFramework.h"
int main() {
    http::App app;
    app.get("/", [](auto&, auto& res) { res.setHtml("<h1>Hello</h1>"); });
    app.start(8080);
}

编译时指定安装前缀:

cmake -S . -B build -DCMAKE_PREFIX_PATH=/usr/local
cmake --build build

构建选项

构建选项的完整列表见后文"构建选项与配置";最常用的一项是关掉示例编译:

# 仅构建库,不编译示例
cmake -S . -B build -DBUILD_EXAMPLES=OFF
cmake --build build

功能特性

HTTP 与路由

  • HTTP/1.1 解析与响应构造:请求行/头部/正文字节级解析,含 400(畸形请求)、413(超限)等边界; HEAD 抑制响应体但保留 Content-Length;支持静态文件与模板渲染
  • 三类路由匹配:静态路径(哈希)、:param 动态段(分段匹配)、* 通配符(正则回退); 按 method 建索引,路径先按段切分再匹配,避免逐条正则
  • 链式中间件:洋葱模型 + 路径前缀过滤;鉴权、日志、限流等横切逻辑挂在这里
  • 会话管理:内存与文件两种存储,Cookie 生命周期(含滑动续期)、过期清理

并发模型

  • 多 Reactor:1 个主 Reactor 只做 accept,按 round-robin 把连接分给 N 个子 Reactor; 每连接注册 EPOLLIN | EPOLLET | EPOLLONESHOT
  • 独立线程池:业务逻辑不在 Reactor 线程里跑,交给共享线程池;响应回写经 eventfd 唤醒 子 Reactor 完成(工作线程不直接操作别人的 epoll)
  • HTTP 长连接:支持 keep-alive 与空闲超时回收;管线化请求按序处理
  • 固定块内存池(可选):请求缓冲从预分配块里取;响应侧不使用固定块池, 由 I/O 线程独占的 std::string 承载。块大小与块数可配

能力边界

  • 只实现 HTTP/1.1(无 HTTP/2、HTTP/3);不含反向代理、负载均衡、限流
  • 单进程单实例,无集群与主备;会话默认在内存,多实例间不共享
  • 数据库层是可选封装(连接池 + 查询接口),不是 ORM
  • 内存池对大响应不友好:请求侧超块回 413,响应侧超块回退 std::string
  • 仅 Linux(epoll + eventfd)

模块组成

┌─────────────────────────────────────────────────────────┐
│  应用层    │ 路由系统 │ 中间件系统 │ 会话管理 │ 数据库集成 │
├─────────────────────────────────────────────────────────┤
│  HTTP层    │ 请求解析 │ 响应构建 │ 协议处理 │ 状态管理   │
├─────────────────────────────────────────────────────────┤
│  网络层    │ 多Reactor │ epoll ET │ 非阻塞I/O │ 线程池  │
├─────────────────────────────────────────────────────────┤
│  系统层    │ Socket编程 │ 信号处理 │ 资源管理 │ 优雅关闭  │
└─────────────────────────────────────────────────────────┘

网络层

  • 多 Reactor 模式:main Reactor 仅 accept + 轮询分发连接;sub Reactor 各自独立 epoll 处理 read/write;线程池处理业务逻辑
  • epoll ET 模式:边缘触发,减少 epoll_wait 系统调用次数
  • 非阻塞 I/O:避免线程阻塞,提高并发能力
  • 连接管理:连接创建、复用、资源回收

HTTP 层

  • 协议解析:HTTP/1.1 请求行、头部、主体完整解析
  • 响应构建:HTTP 响应生成和格式化
  • 状态管理:HTTP 状态码和错误处理
  • 内容类型:HTML、JSON、文本、文件等多种响应类型

路由层

  • 静态路由:精确路径匹配
  • 动态路由:/users/:id 参数提取
  • 通配符路由:* 通配符支持
  • 正则匹配:基于正则表达式的复杂路径匹配
  • 处理器分发:请求到处理器的映射和分发

中间件层

  • 链式处理:多个中间件串联执行,next() 控制流转
  • 路径过滤:中间件可指定作用路径前缀(如 /api)
  • 上下文传递:请求上下文在中间件间传递
  • 内置中间件:日志、会话管理开箱即用

会话层

  • 会话生命周期:创建、存储、获取、销毁
  • Cookie 处理:会话 Cookie 自动管理
  • 过期清理:后台线程定期清理过期会话,可配置间隔
  • 线程安全:互斥锁保护并发访问
  • 可扩展存储:支持内存存储和数据库持久化

数据库层

  • 连接池:MySQL 连接池管理和优化
  • 健康检查:连接池健康监控和自动恢复
  • 事务支持:数据库事务的完整支持
  • 异常处理:数据库异常的捕获和处理

工具层

  • 内存池:12KB 固定大小内存块,O(1) 分配/释放
  • 线程池:工作线程创建和任务分发
  • 模板引擎:HTML 模板加载和 {{variable}} 变量替换
  • 信号处理:SIGINT/SIGTERM 优雅关闭

架构

线程模型:多 Reactor + 线程池

                      ┌─────────────────────────────────────────┐
   客户端连接 ────────▶│ 主 Reactor 线程                          │
                      │   epoll_wait 只做 accept                 │
                      └────────────────┬────────────────────────┘
                                       │ round-robin 分发
                  ┌────────────────────┼────────────────────┐
                  ▼                    ▼                    ▼
         ┌────────────────┐   ┌────────────────┐   ┌────────────────┐
         │ 子 Reactor 0   │   │ 子 Reactor 1   │   │ 子 Reactor N   │
         │ EPOLLIN|ET     │   │ ...            │   │ ...            │
         │ |ONESHOT       │   │                │   │                │
         └───────┬────────┘   └───────┬────────┘   └───────┬────────┘
                 │  请求收齐后入队      │                    │
                 └───────────┬─────────┴────────────────────┘
                             ▼
                  ┌──────────────────────────┐
                  │ 共享线程池(默认 4 线程) │  业务处理:中间件链 → 路由 → handler
                  └───────────┬──────────────┘
                              │ 写响应:把 PendingResponse 投递到目标连接的 pendingResponses
                              │ 然后 write(eventfd) 唤醒**属于该连接**的子 Reactor
                              ▼
                  子 Reactor 线程:从队列取出 → send → 完整写出后再 EPOLL_CTL_MOD 重新注册

为什么不让工作线程直接 send / 改 epoll:回写动作经 eventfd 唤醒后 收敛回子 Reactor 线程执行;理由见"关键设计与取舍"。

请求生命周期

一个请求从进入到响应写出的完整路径;入口的 accept 与 round-robin 分发见上面的"线程模型", 参与者即线程归属:

sequenceDiagram
    participant C as 客户端
    participant SR as 子 Reactor
    participant TP as 线程池
    participant H as 业务处理
    C->>SR: TCP 数据到达(EPOLLIN|EPOLLONESHOT)
    SR->>SR: recv 进连接缓冲,解析请求;半包则等下一次事件
    SR->>TP: enqueueDetached(请求任务)
    Note over SR: ONESHOT 已消费,暂不再监听该连接
    TP->>H: 中间件链 → 路由匹配 → handler
    H->>H: 生成响应(序列化)
    H->>SR: pendingResponses.push_back({connId, data, keepAlive}) + write(eventfd)
    SR->>C: send 响应
    SR->>SR: 完整写出后释放响应区、重置上下文,EPOLL_CTL_MOD 重新武装 EPOLLIN|ONESHOT
    Note over SR,C: keep-alive 则继续等待下一个请求;空闲超时回收连接

连接生命周期

  • 归属校验:每个连接用 ConnId = (generation << 32) | fd 标识。fd 会被内核复用, 只按 fd 记录会让"旧连接的回调"误伤刚建立的新连接(这正是早期一个真实缺陷的根因)
  • 半关闭处理:对端 FIN 后先把缓冲读尽再判 EOF;只发 FIN 不一定产生新边沿(ET 下尤其要注意), 所以每个 epoll 节拍会主动检查各连接的空闲与 read 状态
  • 优雅停机:stop() 先排空在途业务任务,再释放成员,避免共享线程池上的任务访问已析构对象

工作原理

请求从进入到写出的整体路径见"架构"一节的"请求生命周期";下面按环节展开实现。

Reactor 事件循环

主 Reactor — 仅 accept + round-robin 分发连接给子 Reactor:

void HttpServer::mainReactorLoop() {
    const int MAX_EVENTS = 128;
    struct epoll_event events[MAX_EVENTS];              // 局部数组,不在对象上保存

    while (running_.load()) {
        int nfds = epoll_wait(mainEpollFd_, events, MAX_EVENTS, 1000);
        if (nfds < 0) {
            if (errno == EINTR) continue;
            break;
        }
        for (int i = 0; i < nfds; ++i) {
            int fd = net::connIdFd(events[i].data.u64);  // data 里存的是 ConnId,不是裸 fd
            if (fd == listenFd_ && (events[i].events & EPOLLIN))
                handleAccept();                 // accept 轮询分发到 sub reactor
        }
    }
}

子 Reactor — 多个线程各带独立 epoll,处理分配的连接的 I/O:

void HttpServer::subReactorLoop(int index) {
    const int MAX_EVENTS = 1024;
    struct epoll_event events[MAX_EVENTS];
    auto& sr = subReactors_[index];

    while (running_.load()) {
        int nfds = epoll_wait(sr->epollFd, events, MAX_EVENTS, 1000);
        if (nfds < 0) {
            if (errno == EINTR) continue;
            break;
        }
        for (int i = 0; i < nfds; ++i) {
            net::ConnId connId = events[i].data.u64;
            int fd = net::connIdFd(connId);
            uint32_t ev = events[i].events;

            if (fd == sr->wakeFd)  { handleWake(index);  continue; }  // eventfd:批量发送响应
            if (fd == sr->timerFd) { handleTimer(index); continue; }  // timerfd:空闲连接清理

            // 代际校验:fd 号被回收再分配后,内核可能仍投递一次属于旧连接的事件。
            // 不符时直接丢弃,绝不按"未知连接"去关闭它。
            if (!ownsConnection(fd, index, connId)) continue;

            if (ev & (EPOLLERR | EPOLLHUP)) {
                // 不能"看到 HUP 就关":对端 shutdown(SHUT_WR) 后仍在等响应,
                // 直接关闭会丢掉已到达的请求与未发出的响应,所以先把可读数据读尽。
                if (ev & EPOLLIN) handleRead(fd, index, connId);
                HttpContext* ctx = lookupContext(index, fd);
                if (ctx && (ctx->inFlightCount() > 0 || ctx->hasCurrentResponse()))
                    continue;                     // 还有在途请求或待发响应,交给写路径收尾
                closeConnection(fd, index);
            } else {
                if (ev & EPOLLIN)  handleRead(fd, index, connId);
                if (ev & EPOLLOUT) handleWrite(fd, index);
            }
        }
    }
}

工作线程池

子 Reactor 读取到完整 HTTP 请求后,提交到线程池异步处理,子 Reactor 线程立即返回继续监听 I/O:

void HttpServer::handleRead(int clientFd, int subReactorIndex, net::ConnId connId) {
    HttpContext* ctx = lookupContext(subReactorIndex, clientFd);
    bool peerClosed = false;
    const size_t totalRead = readAllData(clientFd, ctx, &peerClosed);  // 循环 recv 直到 EAGAIN
    // totalRead == 0 时用 peerClosed 区分"对端已关闭"与"暂无数据";
    // 413(非池缓冲超限)/ 503(池耗尽)等判定略
    dispatchBufferedRequests(clientFd, subReactorIndex, connId);   // 解析出完整请求后派发
}

void HttpServer::enqueueRequestTask(int subReactorIndex, int clientFd, net::ConnId connId,
                                    std::shared_ptr<HttpRequest> request,
                                    std::shared_ptr<HttpResponse> response) {
    markInFlight(subReactorIndex, connId, /*enter=*/true);
    auto task = std::make_shared<HttpRequestTask>(
        clientFd, request, response,
        [this, subReactorIndex, connId](int fd, std::shared_ptr<HttpRequest> req,
                                        std::shared_ptr<HttpResponse> res) {
            processHttpRequest(subReactorIndex, connId, req, res);  // 回写按 ConnId 归属
        });
    // 请求任务不关心返回值,走无 packaged_task / future 的轻量提交路径
    threadPool_->enqueueDetached([this, subReactorIndex, connId, task]() {
        try { task->execute(); } catch (...) {}
        markInFlight(subReactorIndex, connId, /*enter=*/false);
        finishInFlightTask();
    });
}

边缘触发(ET)读处理

ET 模式下必须循环读取直到 EAGAIN,否则可能丢失数据。读尽的动作封在 readAllData 里; 读到 EOF 只置位 peerClosed、不在这里关闭连接——对端 shutdown(SHUT_WR) 之后仍要把响应发完, 关闭时机由调用方决定:

size_t HttpServer::readAllData(int fd, HttpContext* ctx, bool* peerClosed) {
    char buffer[4096];
    size_t readBytes = 0;
    if (peerClosed) *peerClosed = false;

    while (true) {
        ssize_t n = ::recv(fd, buffer, sizeof(buffer), 0);
        if (n > 0) {
            ctx->appendData(buffer, static_cast<size_t>(n));    // 直接读进连接请求缓冲
            readBytes += static_cast<size_t>(n);
        } else if (n == 0) {
            if (peerClosed) *peerClosed = true;                 // 对端关闭写方向
            break;
        } else {
            if (errno == EAGAIN || errno == EWOULDBLOCK) break; // 数据读完
            if (errno == EINTR) continue;
            if (peerClosed) *peerClosed = true;                 // 致命错误
            break;
        }
    }
    return readBytes;
}

内存池分配

utils::MemoryBlock* utils::HttpMemoryPool::allocate() {
    allocCalls_.fetch_add(1, std::memory_order_relaxed);
    std::lock_guard<std::mutex> lock(mutex_);
    if (freeBlocks_.empty()) {
        // 池耗尽:显式计数后返回 nullptr,调用方据此回 503,而不是静默降级
        allocateFailures_.fetch_add(1, std::memory_order_relaxed);
        return nullptr;
    }
    MemoryBlock* block = freeBlocks_.front();   // 空闲块是 FIFO 队列
    freeBlocks_.pop();
    block->isUsed = true;
    usedBlocks_++;
    return block;
}

会话创建与过期清理

std::shared_ptr<Session> SessionManager::createSession() {
    std::lock_guard<std::mutex> lock(sessionsMutex_);

    std::string sessionId;
    do {
        sessionId = generateSessionId();            // 无锁生成,唯一性在此处校验
    } while (sessions_.find(sessionId) != sessions_.end());   // id 冲突则重取

    auto session = std::make_shared<Session>(sessionId);
    session->setExpirationTime(defaultExpiration_); // 构造时即带上默认过期时间
    sessions_[sessionId] = session;
    return session;
}

void SessionManager::cleanupExpiredSessions() {
    std::lock_guard<std::mutex> lock(sessionsMutex_);
    for (auto it = sessions_.begin(); it != sessions_.end(); ) {
        if (it->second->isExpired()) it = sessions_.erase(it);
        else ++it;
    }
}

核心组件

组件 职责与关键设计
HttpServer epoll ET + EPOLLONESHOT 主循环、accept 分发、连接表(代际校验)、空闲超时扫描、连接数上限
HttpContext 单连接状态:请求缓冲(读游标 + 半包起点)、在途请求计数、待发响应、连接代际
HttpRequest / HttpResponse 解析与构造:URL 解码、头名大小写归一化、HEAD 抑制响应体、序列化用 reserve + 直接拼接
Router 静态哈希 + :param 分段匹配 + * 正则回退;method 索引;运行期注册走"拷贝-换入"快照
Middleware 洋葱模型 + 路径前缀过滤;入口处一次性算好匹配列表,避免每请求整表重算
SessionManager 内存 / 文件两种存储、Cookie 生命周期(滑动续期)、过期清理、文件存储 ID 白名单 + 原子写
MemoryPool 固定块(默认 12 KB × 5000)、分配计数可观测、启动前开关(默认关闭,运行期调用被忽略并告警)
ThreadPool 任务队列 + 状态统计;enqueueDetached 提供无 packaged_task/future 的轻量路径

单连接的"读写状态机"没有单独的类:读入、解析派发与写出分别由 HttpServer::handleRead、HttpServer::dispatchBufferedRequests、HttpServer::writeCurrentResponse 完成,连接状态全部放在 HttpContext 上。

HttpServer 关键默认值

参数 默认值 设置入口
最大并发连接数 10000(0 = 不限制) setMaxConnections()
空闲连接超时 60 秒 setIdleTimeout()
空闲检查周期 5 秒 setIdleCheckIntervalSec()
单请求体上限 64 MB setMaxRequestBodyBytes()
非池模式单连接请求缓冲上限 1 MB setMaxRequestBytes()
请求头上限 16 KB setMaxRequestHeaderBytes()

空闲检查周期与内存池的块容量/块数在 start() 时落地(timerfd 与池实例此时创建), 运行期修改无效。

关键设计与取舍

回写为什么走 eventfd。 子 Reactor 的 epoll 实例被它自己的线程独占,工作线程直接 EPOLL_CTL_MOD 等于两个线程并发操作同一个 epoll;再加上连接注册的是 EPOLLONESHOT, 跨线程改状态相当于把 oneshot 的监听状态重置,会打乱"一次事件一次处理"的语义, 可能造成重复投递或丢失事件。用 eventfd 把回写收敛回 Reactor 线程, 代价是每次响应多一次跨线程唤醒(实测这个开销远小于并发操作 epoll 的风险)。

为什么做固定块内存池,以及为什么默认关闭。 动机是"高频分配"——早期是短连接形态, 每请求新建/销毁缓冲。但长连接改造之后,500 并发 30 秒里池的 allocate/deallocate 只增加 1002 次(每千请求约 1 次),复用度已经由连接本身提供;在 glibc 的 per-thread tcache 面前,这把带锁的池不再有优势。三轮交替复测的方向分别是 −3.5% / +6.1% / +1.1%, 只能说"无可观测收益",所以默认关闭(另外启用它会在 start() 阶段一次性构造 5000 个块, 按 sizeof(MemoryBlock) = 12312 字节计约 61.6 MB 堆分配;数据区在写入前不被触碰, 实测常驻增量约 20 MB,即"20 MB"是 RSS 口径,不是堆分配量)。

响应序列化。 早期实现逐个字段拼接、反复扩容;现在先 reserve 再直接拼接, 并把 Date 头按秒缓存,避免每个响应都做一次时间格式化。

路径参数怎么塞回请求对象。 路由匹配出的 :param 需要交给 handler。当前实现由 RouterHandler 用 const_cast 把参数写回 HttpRequest——它不在"未定义行为"的意义上有问题 (对象本身不是 const),但确实是个不漂亮的接口,记在待办里。

项目难点

① 跨线程回写:工作线程不能碰别人的 epoll

难在哪:连接注册的是 EPOLLONESHOT,事件被消费后必须由同一个 Reactor 线程重新注册。 第一版让工作线程直接对子 Reactor 的 epoll 做 EPOLL_CTL_MOD(两个原因见"关键设计与取舍")。

怎么做的:每个子 Reactor 一个 eventfd + 一个 pendingResponses 列表。工作线程只做两件事: 把 {ConnId, 响应字节, keepAlive} 投递进列表、write(eventfd)。响应内容随投递一起交付, 工作线程因此完全不触碰 HttpContext 的字段;send 与重新注册全部收敛回子 Reactor 线程。 顺带:正因为回写带了 ConnId,这里必须做归属校验(见 ③),否则队列里躺着的旧连接 回调会打到已经复用同一 fd 的新连接上。

② 内存池带来的两个问题

问题一:12 KB 单块导致响应被静默截断。 早期实现用一个 12 KB 的池块承载响应, 写入超出单块时 PooledBuffer::write 的返回值没有被检查(该缓冲挂在 HttpContext 上,现已移除), 现象很隐蔽:响应头写着 Content-Length: 20000,客户端实收 12117–12289 字节,keep-alive 下后续请求还会错位。 当时的修法是响应侧超限回退 std::string、请求侧比对实际写入字节数并回 413(带 {"limit":...}); 后续(H5)连这条路径也一并去掉——响应方向改用 std::string,不再从池里取块(见"核心组件"的 HttpContext)。

问题二:它其实没有收益。 长连接改造之后池的调用频率降到每千请求约 1 次,三轮交替复测方向翻转 (调用次数、三轮吞吐与常驻增量见"实测性能"的"内存池:无可观测收益")。 结论是默认关闭——理由是无收益加内存占用,而不是"性能更差"。

③ 连接生命周期与 fd 归属:陈旧回调误杀新连接

难在哪:fd 会被内核复用。只按 fd 记录连接状态时,"上一个连接的回调"可能作用在 刚复用同一 fd 的新连接上。现象是服务端日志里出现两条相同 fd 的关闭记录 (分别来自不同的子 Reactor),而新连接复用几次之后就被单方面断开——客户端看到的是"莫名 RST"。

怎么做的:连接标识改为 ConnId = (generation << 32) | fd,所有回写与关闭都带归属校验, 代际不匹配就丢弃。复测:记录恒为每 Reactor 各一条、连接复用 8–17 次全部正常。

为什么值得讲:这类缺陷在压测里表现为"偶发连接断开",很容易被归因成"客户端或网络问题" 而放过去;把它定位到"fd 复用 + 缺少代际"需要先怀疑自己的状态模型。

工程难点

模块解耦

通过清晰接口抽象隔离模块:Router 只关心路径匹配和处理器分发,不感知 HTTP 解析细节;Middleware 通过 next() 函数控制链式流转,不依赖具体业务逻辑;Session 和 DB 通过 enable*() 按需启用,不强制绑定。

API 设计

底层 API 更加灵活,App 类更加简洁,满足不同场景需求。链式调用减少中间变量,默认参数覆盖常见配置,同时保留 router() / sessionManager() / dbPool() 访问器直接操作底层对象。

项目结构

HttpFramework/
├── CMakeLists.txt                      # 主构建配置
├── cmake/
│   └── HttpFrameworkConfig.cmake.in    # find_package 包配置模板
├── include/
│   ├── HttpFramework.h                 # App 类
│   ├── http/                           # HttpServer / HttpRequest / HttpResponse / HttpContext
│   ├── router/                         # Router / RouterHandler
│   ├── session/                        # Session / SessionManager / SessionStorage
│   ├── middleware/                     # SessionMiddleware
│   └── utils/                          # MemoryPool / ThreadPool / TemplateLoader / db
├── src/                                # 源文件
├── templates/                          # HTML 模板({{variable}} 变量替换)
├── examples/
│   ├── hello_world.cpp                 # 最简示例
│   ├── full_demo.cpp                   # 全功能演示(路由/会话/DB/模板/统计)
│   └── bench_server.cpp                # 性能基准测试服务器
├── tests/                              # 单元测试
├── scripts/                            # 基准测试脚本
├── templates/                          # HTML 模板({{variable}} 变量替换)
└── init.sql                            # 数据库初始化脚本

实测性能

环境:2 vCPU / 1968 MB 的 VPS(压测端与被测服务同机 loopback),服务线程池 4,wrk 4.2.0 两套脚本口径不同、数字不可互比:scripts/bench_http.sh(wrk -t4,并发 100–5000,各 30 s)与 scripts/run_http_bench.sh main results/main(wrk -t2,每项独立启动服务,另出 results/main/REPORT.md) 原始输出:results/http_bench_20260921_0254.txt、results/main/

并发梯度(长连接,wrk -t4)

并发 吞吐 (req/s) p50 p99 socket 错误
100 35,858 2.62 ms 6.61 ms 0
500 36,039 13.44 ms 23.97 ms 0
1000 31,658 30.84 ms 51.57 ms 0
2000 27,608 70.46 ms 132.08 ms 0
5000 29,236 169.60 ms 231.39 ms 0

读法:5,000 并发下保持 2.9 万 req/s 量级、零 socket 错误。p50 从 100 档的 2.62 ms 升到 5000 档的 169.60 ms,量级符合 延迟 ≈ 并发 ÷ 吞吐 的排队关系,所以"低延迟"与"高并发"必须同时标注档位才有意义。

每请求 CPU

口径 数值
bench_http.sh(-t4,c=1000;/proc/<pid>/stat 差分 ÷ 请求数) 37.9 µs(3,667 jiffies / 967,885 请求)
run_http_bench.sh A6 项(-t2,c=500) 29.6 µs(3,616 jiffies / 1,102,625 请求)

路由与中间件(-t4,c=20)

bench_server --middleware 50 / --routes 500,原始输出 results/main/opt_c20_20260921.txt:

场景 吞吐 (req/s) p50
50 层中间件 23,855 0.77 ms
500 条路由、命中末条 32,901 0.58 ms
500 条路由、命中靠前 33,762 0.56 ms

根因是每个请求都把中间件链与路由表整表重算(findMiddlewares 走 O(M) 正则、路由逐条匹配)。 改动是入口处一次性算好中间件列表,路由改为 method 索引 + 静态哈希 + 分段匹配。 "命中靠前与命中末条几乎同速"说明收益来自"找得对不对",而不是"找得快"。

内存池:无可观测收益

交替执行(默认 ↔ --mempool),各 30 秒(-t4,c=1000):

轮次 无池 (req/s) 开池 (req/s)
1 33,677 37,100
2 31,913 35,682
3 34,751 36,644

分项脚本同批(-t2):无池 36,271 → 开池 39,074 req/s。两者差异落在同批测量的波动范围内, 因此只表述为无可观测收益,不给加速比。池在启动时预分配 5000 块,开池后空载 RSS 从 4.3 MB 升到 23.9 MB,默认关闭。

分配频率(curl /stats 的 mempool.alloc_calls 差分;500 并发 × 30 s,原始输出 results/main/mempool_alloc_20260921.txt):

连接模式 请求数 alloc 增量 每千请求
长连接 1,167,554 502 0.43
短连接(Connection: close) 420,792 421,047 1,000.6

长连接下每条连接只取一次请求缓冲,调用频率是千分之零点几;短连接下每请求一次分配, 这才是内存池的设计场景——而实测短连接下开池同样没有正向收益(-t4 与 -t2 两套口径都不给正收益), 因此不值得为它引入一套预分配与生命周期管理。--mempool 下 /stats 读的是服务自持的池 (app.server()->memoryPool()),未开池时该字段为 null。

短连接

三次测量是 10,533 / 12,864 / 13,737 req/s。短连接吞吐受客户端端口与 TIME_WAIT 资源影响、 波动可达 30%+,只作区间参考(约 1.1–1.4 万 req/s),不参与任何结论。

测试与基准

单元与集成测试

# 默认配置
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j
cd build && ctest --output-on-failure          # 14/14
测试文件 覆盖内容 用例数
test_infra_threadpool 线程池:入队/批量/队列/关闭/状态 9
test_infra_mempool 内存池:分配/FIFO/耗尽/RAII/统计 11
test_infra_logger 日志:级别过滤/模块过滤/输出 6
test_http_route 路由:静态/动态/通配符/:param 形态等价性/404/方法 11
test_http_middleware 中间件链:洋葱模型/路径过滤/鉴权 6
test_http_session 会话:CRUD/过期/清理/Cookie 13
test_http_response 响应构建:HTML/JSON/File/Binary/重定向 12
test_http_template 模板:加载/变量替换/fallback 5
test_http_db 数据库连接池:初始化/获取/查询/计数/TCP 探测(依赖缺失时明确跳过) 6
test_edge_input 异常输入:Header 过大/路径穿越/畸形请求 7
test_edge_stress 并发压力:1000 请求/统计/重启/fd 泄露 4
test_http_mempool_stats 内存池统计口径:未启用为 nullptr、启用后指向自持池、计数随请求增长 2
test_http_hardening 加固回归:信号/管线化/框架头/慢速滴灌/畸形请求 20
test_http_keepalive 长连接:复用/管线化/空闲回收 5

"跳过"是独立的第三种结果:需要外部依赖(数据库)的用例在依赖缺失时明确 SKIP 并打印原因, 不计入通过——例如数据库已装但服务未启动时 test_http_db 是"1 通过 + 5 跳过"、退出码 0; 完全未编译数据库支持时它只跑 1 个用例。 (ctest 自 3.16 起可用 SKIP_RETURN_CODE / SKIP_REGULAR_EXPRESSION 表达"跳过",本项目尚未采用。)

测试设计:全部使用真实文件系统 + 真实 loopback socket,不用 mock。 好处是能覆盖真实的失败路径:EADDRINUSE 端口冲突、缺失文件回 404、模板缺失 fallback、 session 过期与清理、中间件鉴权短路 401、shutdown 后 enqueue 抛异常、内存池耗尽返回 nullptr。

性能基准

仓库里有三套互补的脚本,产出都落在 results/:

脚本 用途 产出
scripts/bench_http.sh 标准化单次测量:并发梯度、每请求 CPU、内存池交替、短连接 results/http_bench_<时间戳>.txt(单文件)
scripts/run_http_bench.sh <label> [dir] 分项全指标:A1–A6(吞吐 / JSON / 延迟分布 / 最大并发 / 内存 / CPU 成本)、B1–B3(线程 / 内存池 / 路由)、C1–C2(中间件 / 会话),每项独立启动服务 results/<label>/ 下每指标一个文件(共 14 个)
scripts/generate_report.sh 把分项文件汇总成报告(缺数据的项写"数据缺失",只对已锚定的文件算百分比) results/REPORT.md
# 标准化单次测量(对外引用数字时用它,带装置自检)
bash scripts/bench_http.sh          # 全量:并发梯度 + 每请求 CPU + 内存池交替 + 短连接(约 8 分钟)
bash scripts/bench_http.sh quick    # 快速回归:100/1000 两档 + 内存池单轮(约 2 分钟)

# 分项全指标(逐项对照、出报告)
bash scripts/run_http_bench.sh main results/main   # 本机约 10 分钟
bash scripts/generate_report.sh                    # 汇总为 results/REPORT.md

两个测量脚本都会先做装置自检(端口就绪且进程存活),不通过则把整批标记作废。

引用基准数字时必须带上工具与参数:bench_http.sh 用 wrk -t4、并发 100–5000; run_http_bench.sh 用 wrk -t2(避免压测端与被测服务抢 CPU),各档时长 15/30 秒混合。 两者口径不同,同一台机器上的绝对值也不可直接互相比较。

构建选项与配置

选项 默认 说明
CMAKE_BUILD_TYPE 未指定 建议 Release;-O2 恒定生效,Release 另加 -O3 -DNDEBUG(引用性能数据须注明构建类型)
BUILD_EXAMPLES ON 编译 examples/ 下的示例与 bench_server
ENABLE_STRICT_WARNINGS OFF 把部分警告提升为错误(-Werror=return-type / -Werror=unused-result / -Werror=uninitialized / -Werror=delete-non-virtual-dtor)
SANITIZE 空 传 address / thread / undefined 启用对应 sanitizer(需要手工跑,未进 CI)

运行时配置分两层:http::App 负责路由、会话(过期与清理间隔)、数据库与内存池开关;连接数上限、 空闲超时、内存池块大小与块数等在 HttpServer 上设置,且必须在 start() 之前调用(运行期 修改会被忽略并打印警告),App 未透出这些选项。模板目录来自编译期宏 TEMPLATES_DIR,不是运行期配置。

API 参考

下面是公开接口速查;完整签名与默认值以 include/ 下的头文件为准,示例见 examples/。

App

namespace http {
class App {
public:
    using Handler    = router::Router::Handler;
    using Middleware = router::Router::Middleware;

    // 路由注册(链式调用)
    App& get(const std::string& path, Handler h);
    App& post(const std::string& path, Handler h);
    App& put(const std::string& path, Handler h);
    App& del(const std::string& path, Handler h);
    App& patch(const std::string& path, Handler h);
    App& head(const std::string& path, Handler h);
    App& options(const std::string& path, Handler h);

    // 中间件
    App& use(Middleware m);                        // 全局
    App& use(const std::string& path, Middleware m); // 路径特定
    App& notFound(Handler h);

    // 快捷功能
    App& enableLogging();
    App& enableSession(int expirationSeconds = 1800, int cleanupIntervalSeconds = 300);
    App& enableMemoryPool(bool enable = true);
    App& enableDatabase(const std::string& host, const std::string& user,
                        const std::string& password, const std::string& database,
                        int port = 3306, int maxConnections = 5);

    // 生命周期
    void start(int port, int threads = 4);
    void stop();

    // 访问底层对象
    std::shared_ptr<router::Router>           router();
    std::shared_ptr<session::SessionManager>  sessionManager();
    std::shared_ptr<db::DbConnectionPool>     dbPool();
    const http::HttpServer::Statistics&       stats();
};
}

HttpServer

class HttpServer {
public:
    HttpServer(int port, size_t threadPoolSize = hardware_concurrency,
               size_t subReactorCount = 0);     // 0 = hardware_concurrency
    bool start();                                // false = 启动失败
    void stop();
    void setRouter(std::shared_ptr<router::Router> router);
    void enableMemoryPool(bool enable = true,    // 须在 start() 之前调用
                          size_t blockSizeBytes = 0, size_t poolBlocks = 0);  // 0 = 12KB × 5000
    bool isRunning() const;
};

Router

class Router {
public:
    void get(const std::string& path, Handler handler);
    void post(const std::string& path, Handler handler);
    void put(const std::string& path, Handler handler);
    void del(const std::string& path, Handler handler);
    void patch(const std::string& path, Handler handler);
    void head(const std::string& path, Handler handler);
    void options(const std::string& path, Handler handler);
    void use(const std::string& path, Middleware middleware);
    void use(Middleware middleware);
    void setNotFoundHandler(Handler handler);
};

HttpRequest

class HttpRequest {
public:
    HttpMethod getMethod() const;
    std::string getMethodString() const;
    std::string getPath() const;
    std::string getHeader(const std::string& name) const;
    std::string getParam(const std::string& name) const;   // 路由参数 :id
    std::string getQuery(const std::string& name) const;   // 查询参数 ?key=val
    std::string getBody() const;
    bool isKeepAlive() const;
    size_t getContentLength() const;
    void setUserData(const std::string& key, const std::string& value);
    std::string getUserData(const std::string& key) const;
};

HttpResponse

class HttpResponse {
public:
    void setStatus(HttpStatus status);
    void setHeader(const std::string& name, const std::string& value);
    void setHtml(const std::string& html);
    void setJson(const std::string& json);
    void setText(const std::string& text);
    void setFile(const std::string& filePath);
    void setBody(const std::string& body);
    void setContentType(const std::string& contentType);
};

故障排除

编译错误:确保依赖完整 apt install libboost-all-dev libmysqlcppconn-dev,清理重编 rm -rf build && cmake -S . -B build && cmake --build build

数据库连接失败:MySQL 不可用时 enableDatabase() 自动降级,检查 sudo systemctl status mysql,或直接不调用 enableDatabase()

端口被占用:netstat -tlnp | grep :8080 查看占用进程

禁止源码内构建:项目强制 out-of-source build。cmake . 会直接 FATAL_ERROR,防止 make clean 误删源文件。始终使用 cmake -S . -B build