基于 Boost 的静态 HTTP 服务器
本文介绍了如何借助C++Boost库设计并开发一个简单 静态HTTP服务器
项目概览
定位:理解 Asio 异步模型与服务器底层实现,而非业务层开发
| 指标 | 数据 |
|---|---|
| 代码规模 | 3300 行 |
| 测试用例 | 140 个gTest单元测试 |
为什么使用 Boost 作为依赖?
Boost库是一组高质量的、经过同行评审的C++库集合。 这些库旨在扩展C++标准库的功能,提供更多的工具和功能,使开发者能够更高效地编写复杂的程序。
Boost就是一个庞大的C++工具库,其Asio与Beast组件刚好符合开发一个简单的静态HTTP服务器需求。
技术选型总览
| 选型 | 候选方案 | 选择 | 理由 |
|---|---|---|---|
| 语言 | C++ / Go / Nodejs | C++17 | 熟悉,学习底层实现,而非业务抽象 |
| 网络库 | 手写 epoll / libuv / Boost.Asio | Boost.Asio | 成熟异步模型,抽象层级合适,跨平台 |
| HTTP 协议库 | 手写解析 / Boost.Beast | Boost.Beast | parser / serializer 抽象,避免协议细节重复造轮子 |
| 交付方式 | 自研 / nginx 二次开发 | 自研 | 学习效率优先 |
为什么不使用Go语言,Dragon、Nodejs框架等建立在更成熟生态之上的技术栈?
使用Go、Nodejs提供的接口就能够简易开发一个高并发的异步、协程服务器 为什么还要使用C++编写一个呢?
Go、Dragon、Nodejs等语言/框架提供的接口进行了高度的抽象, 抽象层次太高,隐藏了大量的实现细节,让用户只需要专注于业务层次, 该项目的定位是建立对底层实现与业务层应用间的理解,而非业务层面的开发
为什么不使用现成的Nginx为首的服务器软件?
为什么不使用现成的服务?
像Nginx 这一静态服务器标杆那样,从底层进行编写能够减少包装极大程度的压榨性能, 并使用sendfile()、tcp_nopush()、gzip_static()、CPU核心绑定、系统级的文件句柄配置 等技巧, 进行文件零拷贝、减少内核调用与I/O开销、异步非阻塞事件驱动开销等优化, 以达到最极致的rps优化
这些内核级技巧正是 nginx 的性能壁垒:同为 i5-1235U 平台实测(qps/MHz 归一化),本服务器约为 nginx 的 67~73%,差距来源即为上述尚未实现的优化(sendfile 大文件零拷贝、CPU 绑定等列入 TODO 按需推进)
除非阅读源码,相比于自己进行开发,虽然能理解底层的实现与优化,但是学习效率太低
为什么不做libc底层包装?
既然要使用C++从底层开发,为什么不使用操作系统提供的libc库?
对于该静态HTTP服务器,从更原子化的方式进行项目编写其时间成本对个人而言是极为庞大的, 而且大多数的成本集中在底层封装上,虽然能够从操作系统内核层次理解上层服务器设计,
对于服务器而言, UNIX/Linux 提供了一切皆文件 的抽象, 让网络流的收发被简化为文件的接收、读取、处理、发送 操作, 对于网络报文对象而言,则是字符串匹配、字节流解析操作, 在原语层面提供了良好的抽象方式,
Boost提供的包装是更易用的接口封装,减少了底层交互的过多暴露, 只需要理解底层功能核心libc接口的原理,使用上层的简易包装进行实现,能节省开发时间, 与理解接口以及服务器设计这一目标是相符的
为什么不手写HTTP报文解析?
为什么使用 Boost.Beast ?
与原计划于预期目标相悖:学习静态HTTP服务器设计与并发功能实现
Beast 提供的接口抽象
排除上述备选方案后,Boost.Asio/Beast 提供了服务器开发的完整骨架。 Beast 的价值在于把 HTTP/1.1 协议中最容易出错的部分收敛为以下三个抽象:
tcp_stream ------ 套接字与超时的合成
cpp
// /includes/server.hpp
beast::tcp_stream stream_; // TCP 连接流
beast::flat_buffer buffer_; // 平坦的缓存区域
boost::optional<http::request_parser<http::string_body>> parser_; // 请求分析器
beast::tcp_stream 将 TCP 套接字与超时控制合为一体, expires_after() 为每次异步读写设置超时,读超时 / 空闲超时统一为流上的一个调用, 避免了为每个连接自建定时器的繁琐
request_parser ------ 报文边界的处理者
HTTP/1.1 报文的解析繁琐点在于:Content-Length / chunked 编码的 body 边界、 keep-alive 下多次请求的帧切分、请求行与头部的字段校验。 http::request_parser 配合 flat_buffer 异步消费字节流:
- 内部状态机管理解析进度,
body_limit()超限时async_read直接返回 413 - 每次读前
emplace新实例,天然隔离前后请求的解析状态 - 解析完成后经
parser_->get()取出请求报文,交由业务层处理
message_generator ------ 业务与连接的边界
http::message_generator 是 Beast 对"响应"的抽象, 既可以是立即构造的普通响应,也可以是尚未完成的异步响应
业务层接口据此定义为:
cpp
// includes/router.hpp
using Handler = std::function<http::message_generator(const http::request<http::string_body>&)>;
业务模块只需要处理"一个请求对象"并返回"一个响应生成器", 连接生命周期与写回时机均由网络层负责------这是本服务器 request_handler 解耦设计的基础
项目报告
功能模块
| 功能 | 模块 | 说明 |
|---|---|---|
| 静态文件服务 | static_file_service | GET/HEAD,LRU 内容缓存 + 路径解析缓存 |
| 条件请求 | static_file_service / utils | ETag / Last-Modified,304 空 body |
| Range 断点续传 | static_file_service | 206/416,shared_slice_body 零拷贝切片 |
| gzip 内容协商 | static_file_service / utils | 预压缩缓存,Vary 头,Range 不压缩 |
| 动态 API | dynamic_api_service | 端点注册表,405 + Allow,OPTIONS |
| 路由分发 | router | 精确匹配 + 前缀匹配 |
| 限流 / 超时 / body 限制 | server | 503 + Retry-After、expires_after、413 |
| 配置 / 日志 / 缓存 | config / logger / cache | 三级优先级、六级别日志轮转、泛型 LRU |
| 优雅关闭 | graceful_shutdown | 信号捕获 -> 会话排空 -> 强制超时 |
| 路径穿越防护 | utils | 轻量检查 + weakly_canonical 双重校验 |
三层架构
项目压测
平台:i5-1235U(10 核 12 线程)/ Fedora 44(kernel 7.1.6)
目标文件
/app/index.html(9KB)服务端 8 线程、关访问日志,wrk 30s
| 配置 | QPS |
|---|---|
| t6-c100 | ~153k |
| t6-c400 | ~138k |
| t6-c1000 | ~110k |
nginx 对比(qps/MHz 归一化,消除锁频平台跨轮次偏差):当前平台同等条件下约为 nginx 的 67~73% 完整 18 组压测矩阵(t4/t6/t8 * c100~c1000)见项目 /docs/stress-test/
工程化
- CI:GitHub Actions,ubuntu-24.04 + g++,Debug/Release 双构建 + ccache + ctest
- Docker:多阶段构建(编译->运行) + 非 root 用户
- 公网验证:cloudflared 临时隧道暴露 HTTPS URL 实测
- SELinux:容器标签适配(开发平台为 Fedora 44)
本文为项目总览,聚焦技术选型与最终成果;实现细节将在以下文章中展开:
- 框架设计:request_handler 解耦、三层架构与每连接 strand、缓存设计演进
- HTTP 协议细节:304 条件请求、Range 断点续传、gzip 协商、405/OPTIONS 语义
- 性能方法论:压测方案设计、优化链条、最终成绩
- 项目工程化:基于 gtest 的测试体系、路径穿越防护、CI / Docker / cloudflared提供的网络边缘隧道服务进行公网验证