UVHTTP 设计哲学
这份文档阐述 UVHTTP 的核心设计哲学——它是什么、不是什么、以及为什么这样设计。 所有决策和代码评审都应以此为准绳。
一、项目定位:一个组件,不是一个框架
UVHTTP 是一个 C 语言 HTTP/1.1 + WebSocket 服务器库(library),不是一个应用服务器(如 nginx、Apache),也不是一个框架(如 Express、Flask)。
这个定位意味着什么:
| 项目是… | 项目不是… |
|---|---|
| 一个可嵌入的 C 库 | 一个独立运行的守护进程 |
| 一个协议处理组件 | 一个业务逻辑容器 |
| 你应用的一部分 | 你应用的全部 |
推论: UVHTTP 不提供认证、授权、数据库、模板引擎、会话管理、配置热加载等功能。这些是业务层的事。UVHTTP 只做一件事——正确地处理 HTTP 和 WebSocket 协议——并把它做好。
二、分发模型:源码分发,用户编译
UVHTTP 不提供二进制分发(不发布 .deb、.rpm、预编译静态库)。用户通过以下方式使用:
git clone --recurse-submodules https://github.com/adam-ikari/uvhttp.git
cd uvhttp && mkdir build && cd build
cmake .. && make这决定了什么
1. 版本控制 = API 版本控制,不是 ABI 版本控制
因为用户总是从源码编译并与自己的应用静态链接,ABI 兼容性不是承诺。我们的版本号(SemVer)承诺的是源码级 API 兼容性:
MAJOR.minor.patch
│ │ └── 不破坏 API 的修复(bug 修复、测试、文档)
│ └──────── 不破坏 API 的新功能(新函数、新模块)
└─────────────── 破坏 API 的变更(函数签名变更、删除、行为不兼容)- PATCH 升级:完全向后兼容,无需修改用户代码
- MINOR 升级:向后兼容,新功能可选,现有代码不变
- MAJOR 升级:API 不兼容,用户需要修改代码
2. 编译时裁剪是核心体验
用户通过 CMake 选项选择他们需要的功能:
cmake .. -DBUILD_WITH_WEBSOCKET=OFF -DBUILD_WITH_COMPRESSION=OFF未使用的功能不会编译,不增加代码体积。这是嵌入式优先的具体体现。
3. 依赖管理是用户的责任
UVHTTP 使用 git submodule 管理依赖(libuv、llhttp、mbedtls、xxhash、miniz)。用户只需 --recurse-submodules 一次性获取所有依赖。这与"零外部依赖"的哲学一致——没有系统包管理器的依赖。
三、嵌入式优先
UVHTTP 的优化目标是嵌入式设备和长时间运行的服务,不是峰值吞吐量。
优先级排序
内存安全 > 内存稳定性 > 二进制体积 > 启动时间 > 峰值吞吐量具体指标
| 指标 | 目标 | 衡量方式 |
|---|---|---|
| 内存泄漏 | 0(ASan 证明) | ASan CI 门禁,每夜运行 |
| 长时间运行 RSS | 不增长 | scripts/performance/long_run_memory.sh |
| 静态库体积 | ~257 KB | Release build, stripped |
| 32-bit 支持 | 必须 | CI 门禁 (-m32) |
| 冷启动时间 | ~310 ms | 进程启动到首字节 |
| 编译时裁剪 | 按需付费 | CMake 特性开关 |
低资源使用优先
UVHTTP 的"嵌入式优先"不仅指内存,还包括所有形式的资源。在资源受限设备上,CPU、内存、带宽、电池每一项都是宝贵的:
| 资源 | UVHTTP 的态度 | 具体体现 |
|---|---|---|
| 内存 (RAM) | 最小化静态占用 + 零泄漏 | 编译时裁剪、零全局变量、缓存行对齐 |
| CPU | 零成本抽象、无中间层 | 直接调用 libuv、编译期宏消除 |
| 带宽 | 零拷贝路径 | 原生 sendfile、避免不必要的缓冲区拷贝 |
| 存储 | 小静态库 | ~257 KB stripped 静态库 |
| 启动 | 快速冷启动 | ~310 ms 首字节 |
核心信念:在低资源场景下,每一个字节、每一个 CPU 周期都是可以衡量的成本。
UVHTTP 的选择都遵循这一信念:
- 编译时裁剪——未用到的功能不编译,不占 ROM/RAM
- 零全局变量——状态都在实例内,支持多实例也支持销毁释放
- 零拷贝——sendfile 让大文件不经用户态缓冲直接到 socket
- 统一分配器钩子——嵌入式用户可注入自己的内存池,掌控每一次分配
- 32-bit 支持——指针减半,结构体随之缩小
与之相对:如果一个"优化"让代码更复杂、体积更大、依赖更多,即使它提升了一点峰值吞吐量,也违背了低资源优先的原则——除非它有实测数据证明整体资源占用下降。
这意味着什么
- 峰值吞吐量不是卖点(nginx 和 h2o 在这方面更强)
- 每个连接泄漏 1 KB 是一个 bug——在嵌入式设备上,这会在 10 小时内耗尽内存
- 32-bit 支持不是可选项——许多嵌入式设备仍是 32-bit
- 编译时裁剪——用户只链接他们需要的功能,不浪费一个字节
四、内存安全即基础设施
内存安全不是 UVHTTP 的一个"特性"——它是基础设施,和编译一样不可或缺。
三层保障
| 层级 | 机制 | 频率 |
|---|---|---|
| 编译期 | ASan + UBSan CI 门禁 | 每次 PR |
| 每夜 | 完整 ASan + UBSan 跑全部测试 | 每夜 CI |
| 运行时 | 长运行 RSS 稳定性测试 | 每夜 CI |
原则
- ASan/UBSan 零发现是合并 PR 的前提条件,不是可选目标
no_sanitize("address")不被允许——禁用 sanitizer 是最后的手段,且必须附带解释和 issue 跟踪- VALGRIND 辅助但不是替代——ASan 捕获的 bug 比 valgrind 更多,速度更快
- 内存泄漏 = 功能 bug,与逻辑错误同级
五、测试即基础设施
UVHTTP 的测试代码量(52K 行)是生产代码(21K 行)的 2.5 倍。这不是过度工程——这是基础设施。
测试哲学
| 生产代码 | 测试代码 |
|---|---|
| 21K 行 C | 52K 行 C++(gtest) |
| 47 个文件 | 128 个文件 |
| 实现功能 | 证明功能正确 |
原则
- 测试不是附属品——测试代码与生产代码同级
- 测试门禁——所有测试必须通过才能合并 PR
- 测试分离——生产代码不含测试代码(无
#ifdef TEST) - 覆盖不是目标,正确性才是——但仍追求高覆盖率作为副作用
- 新功能必须附带测试——没有测试的功能不存在
六、零成本抽象
零成本抽象(Zero-Cost Abstraction)是 UVHTTP 的核心工程哲学:你为抽象付出的成本为零——抽象只是编译期的组织手段,不产生任何运行时开销。这一概念借鉴自 C++ 的 zero-overhead principle,但 UVHTTP 用 C 的方式实现:通过预处理宏与静态内联,让抽象在生成代码之前就被完全消除。
为什么这是核心哲学
UVHTTP 面向嵌入式低资源场景(见第三节),一个为"写起来方便"而引入、却让每个请求多付出几次分支、几次间接调用或一层栈帧的抽象层,会直接侵蚀吞吐量、增大指令缓存压力、并扩大二进制体积。因此:
抽象是允许的,但抽象的开销必须为零——否则它不是抽象,是负债。
三种零成本抽象手段
| 手段 | 说明 | 例子 |
|---|---|---|
| 编译期宏 | 特性开关在预处理阶段完全消除未使用代码 | UVHTTP_FEATURE_* 系列开关、UVHTTP_LIKELY/UNLIKELY 分支预测 |
| 静态内联函数 | static inline 在优化构建中内联展开,无调用开销 | uvhttp_hash、uvhttp_method_to_string 快速路径 |
| 直接调用,无中间层 | 直接调 libuv / llhttp / mbedtls,不包一层 wrapper 再转调 | uv_tcp_*、llhttp_execute 直接使用 |
验证方式
零成本抽象不能靠"声称",要靠证据:
- 反汇编检查:热路径不包含抽象层残留的函数调用
- 基准对比:启用/禁用某个抽象层,吞吐量无差异(见
docs/PERFORMANCE_TARGETS.md) - 代码审查:性能关键路径上的抽象必须能被编译器完全消除
与低资源优先的关系
零成本抽象是实现"低资源使用优先"(第三节)的手段,低资源使用优先是零成本抽象的目的,二者互为表里:
- 零成本抽象 → CPU 与指令缓存效率
- 低资源优先 → 内存、体积、启动时间的综合资源观
七、代码设计原则
零全局变量
所有状态通过 libuv 的 data 指针(loop->data、handle->data)传递。这使 UVHTTP 支持:
- 多实例(同一进程多个服务器实例)
- 单元测试(无全局状态污染)
- 可测试性(所有状态可注入)
统一错误系统
所有可能失败的函数返回 uvhttp_error_t——一个携带错误码、描述和恢复提示的统一错误类型。没有 -1、NULL 或 errno 作为错误信号。
缓存行对齐
热路径结构体(uvhttp_request_t、uvhttp_response_t、uvhttp_connection_t)按缓存行(64 字节)显式对齐和填充。这不是微优化——这是对多核场景下缓存伪共享的防御。
编译期特性标记
所有可选功能通过编译期宏控制(UVHTTP_FEATURE_COMPRESSION、UVHTTP_FEATURE_WEBSOCKET 等)。未开启的功能在预处理器阶段被完全移除,零运行时开销。
八、C 标准:C99 的取舍
UVHTTP 选择 C99(-std=c99)作为最低标准,理由如下:
选的:
- 可移植性(GCC 4.8+、Clang 3.3+ 均支持)
- 嵌入式编译器支持(不少嵌入式工具链对 C11 支持不完整)
- 最小惊异原则(C99 的行为是已知的)
不选的:
- C11/C17 的可选特性(匿名结构体、泛型表达式)——增加编译器要求,收益有限
- C23 的新特性——多数嵌入式编译器尚未支持
但实际构建使用 C11 标准(CMAKE_C_STANDARD 11)以利用 C11 的编译器特性,同时确保代码风格兼容 C99 编译器。
九、项目边界:明确不做的事
UVHTTP 明确不做以下几件事。如果你的场景需要这些能力,UVHTTP 可能不适合你:
| 领域 | 原因 |
|---|---|
| HTTP/2 | HTTP/2 的多路复用与 libuv 的事件模型不兼容;需要应用层可见的多路复用,而不是连接的透明复用 |
| HTTP/3 | 基于 QUIC,需要完全不同的传输层,超出"轻量级 C 库"的范围 |
| Windows 原生支持 | libuv 在 Windows 上工作,但 UVHTTP 的零拷贝路径(sendfile)和对 epoll 的深度依赖使 Windows 支持需要大量工作 |
| 异步 DNS | libuv 有内置的 DNS 解析,但 UVHTTP 作为服务器库不承担客户端角色 |
| 自动重连/健康检查 | 这是业务层的事,不是协议层的事 |
| 配置热加载 | 嵌入式场景通常不需要;需要热加载的可以在应用层实现 |
| 插件系统 | 动态加载与嵌入式安全目标冲突;用户通过编译时链接扩展功能 |
上述边界不是永久性的。如果社区贡献使某项成为可能且不违背核心哲学,可以重新评估。
十、嵌入验证:真实场景是唯一有效的测试
UVHTTP 的代码质量通过三层门禁保障(编译、测试、CI),但这三个层面都是在库内部验证的。qwrt 嵌入 uvhttp 时暴露了 6 类问题——这些问题在 uvhttp 自己的测试套件中全部通过,但在真实嵌入场景中集体失败。
教训:测试通过 ≠ 嵌入可用。
原则
- 嵌入验证即测试——任何新功能、新 API、新模块,在发布前必须至少有一个真实嵌入者验证通过
- 无嵌入验证的代码视为未验证——即使内部测试全部通过
- 嵌入验证清单是基础设施——每次重要发布前,用嵌入清单验证所有已知的嵌入维度
qwrt 嵌入暴露的问题类别
| 类别 | 问题 | 根因 |
|---|---|---|
| 枚举对齐 | llhttp 与 uvhttp 的方法枚举不对齐,强转损坏 POST/PUT/DELETE | 枚举值耦合未文档化,无 compile-time 断言 |
| 标准兼容 | gzip 输出 zlib 包装(0x78)而非标准 gzip(0x1f8b) | 测试验证了解压而非标准解码器验证 |
| 路径规范 | index 文件路径无前导斜杠 → 403 | 路径验证与路径生成逻辑不匹配 |
| 协议合规 | HTTP 头匹配大小写敏感,拒绝合法大小写变体 | 测试客户端使用规范大小写,掩盖了问题 |
| 处理管线 | 路由未命中时无兜底 handler,嵌入者无法拦截 | 设计时未考虑嵌入者需要最后一道防线 |
| 静态全局 | gzip 缓存使用模块级静态全局变量 | 违反了"零全局变量"原则,CI 未检测 |
这些类别应作为每次嵌入验证的检查维度。
对哲学文档的补充
这份文档的前八章定义了 UVHTTP 的设计哲学(做什么、不做什么、为什么)。本章定义了 UVHTTP 的验证哲学(如何证明做对了)。
UVHTTP 的验证栈:
┌─────────────────────────────────────┐
│ 第三层:嵌入验证(真实场景) │ ← 本章定义
├─────────────────────────────────────┤
│ 第二层:构建组合验证(多配置) │ ← 已补齐(build-matrix)
├─────────────────────────────────────┤
│ 第一层:测试 + ASan/UBSan(基础) │ ← 已完善
└─────────────────────────────────────┘第一层已完善;第二层已由 ci-pr.yml 的 build-matrix job 补齐(验证全开/最简/ROUTER_CACHE/静态文件/无压缩等组合);第三层通过嵌入验证清单落实。本章是第一层到第三层的桥梁。
十一、版本历史与变更
| 版本 | 日期 | 变更 |
|---|---|---|
| 1.0 | 2026-08-20 | 初始文档 |
| 1.1 | 2026-08-20 | 补充"低资源使用优先"、修复第九节格式 |
| 1.2 | 2026-08-20 | 新增顶层章节"零成本抽象" |
| 1.3 | 2026-08-20 | 更新验证栈:第二层构建组合验证已由 ci-pr.yml build-matrix 补齐 |