libuvcpp 是一个基于 libuv 的现代 C++ 封装库,提供简洁的面向对象接口来使用 libuv 的异步 I/O 功能。
v1.8.1 之后的补丁版,把 uvscope 0.3 / 0.4 依赖的一批网络与协议能力一次落齐:
域名解析的 IPv6 回退、TLS 证书链可读、SOCKS5 会话统计、HTTP 客户端的流式与总
deadline、WebSocket 逐帧,以及 h2 / h3 / QUIC 的调试面。
- 域名解析改
AF_UNSPEC,按getaddrinfo顺序逐个尝试(异步与同步),失败返回 最后一次的真实错误;UDP 客户端也接受域名,并按 socket 地址族挑候选。 - 公开对端证书链、
X509_V_ERR校验码与 SAN:peer_cert_chain_der()/peer_verify_result()/peer_verify_error()。 - 阻塞
*_wait的timeout_ms是整次请求的总 deadline;客户端解码br,门槛可配。
set_session_end_callback():每条会话恰好一次,带双方向字节数、UDP 计数、 起止时间与结束原因。socks5_proxy_connect_failed()转发到 ws 客户端,区分「连代理失败」与 「认证 / REP 码失败」。
on_frame()/send_frame():逐帧观察与发送,RSV1 / FIN / 分片边界可见。send_stream():HTTP/1.1 流式响应,on_body返回 false 停读 socket,支持resume()/cancel_stream()。
- h2:
set_h2_frame_callback()(HEADERS / DATA / RST_STREAM / GOAWAY / WINDOW_UPDATE)。 - h3:GOAWAY 平滑退场(
shutdown_notice()/shutdown())与 trailers。 - QUIC:DATAGRAM(RFC 9221)与连接迁移的路径事件(
on_path_validation/on_path_changed)。
兼容性:公开 API 只增不改;唯一有意的语义变化是阻塞 *_wait 的 timeout_ms
从「每次 I/O」变成「整次请求的总 deadline」。
明确不支持:0-RTT、extended CONNECT / WebSocket over h3、QPACK 连接级可观测 字段、主动迁移入口。
v1.8.0 之后的补丁版,修的是真实项目(uvscope / uvp)在 1.8.0 上踩到的问题。最要紧的
一条是异步 HTTPS 客户端对任何外部 TLS 服务端都不可用:仓内用例的 TLS 服务端是本库
自己的 uvcpp_tcp_server,同源对端把三个交付缺陷一起抵消了,公开的 1.8.0 一接上真实
虚拟主机就只剩超时、ECONNRESET 或错误证书。
- 服务端把响应与
close_notify放进同一段 TCP 数据时,客户端先前会丢掉整条正文 (只看到 EOF /ECONNRESET)。现在先交付明文、再收尾。 - "已连接"回调不再早于握手完成 —— 真实主机的证书链常分几段到达,先前会让紧接着的
write()拿到UV_ENOTCONN。 - 以连接关闭定界的响应(HTTP/1.0、
Connection: close且没有长度)会正常收尾,不再被 交付成ECONNRESET。 - 两条客户端路径都发送 SNI(新增
uvcpp_ssl::set_server_name());按 RFC 6066 §3, IP 字面量不发 SNI。 - 回归用例的对端是裸 OpenSSL + 手写 POSIX socket(不是本库的服务端),并且故意把
握手的第一个飞行拆成两段 TCP 写、把正文与
close_notify放进同一次 socket 写。
send_wait*的timeout_ms在 Linux / macOS 上真的生效(此前那段setsockopt整块 在#ifdef _WIN32里);connect_wait()的 TLS 握手同样有上限。- 读错误与超时不再被当成 EOF:半条报文返回
UV_EOF,超时返回UV_ETIMEDOUT。 - 失败时
resp.status_code是HTTP_STATUS_NONE,不再是默认的200 OK。判据仍应是 返回的错误码。
cmake --install 出来的前缀现在是一份可用的 SDK:自编译依赖的共享库跟着装、共享库带
$ORIGIN(macOS @loader_path)RPATH、自编译 libuv 的 include/libuv 通过导出的
target 与 uvcpp.pc 传给消费者。find_package(uvcpp CONFIG) 与 pkg-config 两条路都
在本机用消费者工程验过。
- 公开 API 只增不改,已有代码不需要改。
- 同步
*_wait有两处有意的行为变化:timeout_ms在 POSIX 上真的生效;失败时status_code是 0(HTTP_STATUS_NONE)。别再把status_code == 200当成功判据。 cmake --install之前要 build 默认目标:FetchContent 的 libuv 会在自己的 install 规则里找没编出来的静态库(libuv.a)。
v1.7.4 之后的功能版。三个新模块都遵守同一条边界:要么零外部依赖,要么可以完全
缺席 —— 内置 Redis 不借 hiredis,MongoDB 只 dlopen 使用者自己装的那份客户端库,
SOCKS5 只用 libuv。开发档 1.7.5–1.7.6 折进这一版。
| 模块 | 公开头 | 依赖形态 |
|---|---|---|
| Redis(RESP2/RESP3) | <redis/uvcpp_redis.h> |
无第三方库,默认编进库 |
| MongoDB | <db/uvcpp_mongo.h> |
只要头;运行时加载 libmongoc / libbson |
| SOCKS5(客户端 + 服务端) | <net/uvcpp_socks5_*.h> |
只用 libuv |
Redis 客户端覆盖 AUTH、SELECT、HELLO 3、pipeline、pub-sub 与 rediss://;
解析器是私有的增量实现,公开面只到 uvcpp_redis_value。Cluster / Sentinel /
连接池这一版不做。
MongoDB 是 JSON 进、JSON 出:insert_one / find / update_one / delete_one /
command / ping,同步与异步各一份。发布产物里没有 libmongoc.so / .dll:
UVCPP_DB_RUNTIME_LOAD=ON 下运行时打开机器上那一份,缺库时 open() 返回
NO_DRIVER、错误串列出试过的库名,进程和别的后端一切照常。
SOCKS5 把 RFC 1928 的三种命令都做了(CONNECT / BIND / UDP ASSOCIATE),no-auth 与
RFC 1929 用户名密码、IPv4 / IPv6 / 域名三种地址类型都认。uvcpp_tcp_client、
uvcpp_http_client、uvcpp_ws_client、uvcpp_web_ws_client 各有一组透明
CONNECT 代理接口;打开代理后目标主机不在本地做 DNS(与 curl --socks5-hostname
同义),TLS 的顺序固定为「先 SOCKS 协商、后握手」,SNI 与证书校验用的仍是目标
主机名。HTTP/3 与代理互斥:两边谁先谁后都返回 UV_ENOTSUP,绝不静默直连。
新增的 驱动随包策略 把三件事分开写:仓里有没有后端代码、产物里带不带客户端库、缺库时是什么现象。 MySQL / PostgreSQL 编进库但不链也不带它们的客户端库(运行时加载);SQL Server / Oracle / 达梦等只保留 ODBC 接入面,驱动由使用者自己装。MongoDB 同样只保留运行时 引用 —— 即使它的客户端库本身是 Apache-2.0,也不随包分发。
- 新选项
UVCPP_ENABLE_REDIS(默认 ON)、UVCPP_ENABLE_DB_MONGODB(默认 ON); 新宏UVCPP_REDIS_ENABLE、UVCPP_DB_MONGODB_ENABLE进配置契约门禁。 - 发布腿带上 MongoDB 头,缺头当场红,不静默发半个包;
ldd黑名单与未定义符号 断言覆盖mongoc/bson,包里的数据库后端仍是sqlite,mysql,postgres,odbc。 - CI 的 Linux 腿起
mongod与redis-server跑真实用例;缺服务器时用例退 3, 那条腿设*_TEST_REQUIRE=1把「跳过」变成失败。 - Windows 安装程序换成项目自己的 logo;自签名证书只告警、不挡发布。
- 公开 API 只增不改,已有代码不需要改。
- 默认构建会多编一个 Redis 模块;不需要就
-DUVCPP_ENABLE_REDIS=OFF。 - MongoDB 头仍然只在编进后端时需要;产物运行时不要求机器上有它。
- SOCKS5 服务端的默认配置是开放代理(no-auth、不限目标)。生产上要用
authenticator/allow_target收紧,并绑回环地址;见 SOCKS5 指南。
v1.7.0 之后的补丁版:六条腿的 zip 用法与 1.7.0 完全相同,多出来的是可以直接
安装的发行版包;库里则合进了一轮性能与数据库的改动。开发档 1.7.1–1.7.3
折进这一版。
| 平台 | 装什么 |
|---|---|
| Debian / Ubuntu | apt install libuvcpp-dev |
| Fedora / RHEL 系 | dnf install libuvcpp-devel |
| Windows | 双击 libuvcpp-1.7.4-msvc-<arch>-setup.exe |
两条 Linux 通道走的是自建仓库(apt 在 https://antruly.github.io/libuvcpp/apt,
dnf 在 /rpm),仓库元数据签名,客户端按 packaging/README.md 里那几行配置一次即可。
v1.7.4 起这两条通道已上线。 安装包也是签过的,但用的是自签名证书 —— 签名与
哈希完整,证书却不是公共 CA 签发的,Windows 首次运行仍会提示未知发布者。签名密钥
万一缺席,CI 会跳过仓库发布并打出 warning,包本身照常作为 Release 资产发布,可以手工
dpkg -i / rpm -i 安装。
.deb / .rpm / 安装程序链系统的 libuv.so.1 / libz.so.1:公开头里有
<uv.h>,uvcpp_loop 包的是 uv_loop_t,包里若再静态带一份 libuv,使用者照系统头
编译就是同进程两份实例 —— 真实的堆破坏风险。所以安装包适合"系统包管理器里已经有
libuv"的机器,装完可以 find_package(uvcpp CONFIG) 或 pkg-config uvcpp。
六个 zip 仍是自包含的(除 libc 与 C++ 运行时外无依赖),不要把它们装进 /usr/lib。
两者的选择标准是"这台机器愿不愿意用系统 libuv",不是"哪个更新"。
安装包里的数据库后端是四个:uvcpp_db_drivers() 返回 sqlite,mysql,postgres,odbc。
下面都是本机量具的读数(不是平台那套装置),逐条账与判据在
doc/benchmark.md:
| 测点 | 改前 | 1.7.4 |
|---|---|---|
| h1 动态热路,callgrind 指令/请求(三处白拷) | 22 836.1 | 20 317.6(−11.03%) |
| h1 动态热路,callgrind 指令/请求(一趟扫表等) | 19 153.0 | 17 952.0(−6.27%) |
静态腿取锁次数/请求(LD_PRELOAD 计数) |
2.03 | 0.02~0.03 |
| WS 回声分配次数/消息 | 11.26 | 6.26 |
/json/50?m=6 callgrind 指令数 |
4 550 109 494 | 3 796 143 977(−16.57%) |
| HTTP/3 每请求分配字节 | 72 374.8 | 7 507.5(−89.6%) |
HTTP/3 sendmsg 次数/请求 |
1.03 | 0.05 |
async-db 分配次数/应答 |
204.78 | 102.77 |
| PostgreSQL 异步查询 CPU/请求 | 65.69 µs | 42.56 µs(−35%) |
| brotli 每请求缺页 | 72.06 | 0.00 |
静态腿那一条值得单独说:平台给每一档的 cpuset 是同一个,而走静态处理器的档全封在
2231 核、同轮 4.2 µs),
修法是每循环一份前台副本;空临界区对照证明贵在争用本身,不在临界区里的活。json-tls-4096 在更低的每请求成本下用掉 63 核 —— 缺的是并行度。
量到的是热路上一把跨循环的全局锁(每次请求取 2.03 次、持有 3.7
ODBC 对着的是接口而不是某一种库:连什么、哪种方言,全由连接串里的 Driver= /
DSN= 决定。进它的直接原因是 SQL Server —— 微软没给可链接的 C 客户端库,官方
msodbcsql18 本身就是 ODBC 驱动。它与 MySQL / PG 走同一条 UVCPP_DB_RUNTIME_LOAD
(1.7.2 起):链上的是驱动管理器,具体驱动仍由管理器按需加载,所以没装驱动管理器的
机器照样能加载这个库,只是真去连时拿到 NO_DRIVER。各家驱动随不随包的完整口径见
doc/db-driver-policy.md。
PostgreSQL 的异步查询不再把阻塞查询丢进 libuv 线程池,改走驱动自带的非阻塞协议
(PQsendQueryParams + PQflush,uv_poll 盯 PQsocket())。回调的形状、线程、
返回码都不变。新契约:别带着在飞的查询 loop.stop() —— I/O 由那条循环驱动,
停了查询就永远落不了地,池子析构会一直等。这一条写进了 uvcpp_db_pool.h 的类注释,
两个跑法在
doc/db-guide.md。
- C ABI 没有改动,
UVCPP_C_ABI_VERSION仍是1。 - C++ 侧有两处要重编:
uvcpp_json_write(value, uvcpp_json_writer&)这条旧签名删除,换成uvcpp_json_write(value, std::string&)(yyjson 后端,写的是 yyjson 自己的 序列化器)。同一版新增构造侧uvcpp_json_builder/uvcpp_json_node,键与 字符串值一律拷进 builder 的池,传临时对象、栈缓冲、含\0的键都安全。uvcpp_http_request的公开布局多一个signed char keep_alive(三态,-1= 没算过)。用旧头配新.so会有布局不一致,重编即可。
- 包里模块集合同样变大了一处:ODBC(
UVCPP_DB_ODBC_ENABLE由 0 变 1)。
这一版有两条主线:HTTP/1.1 / HTTP/2 / HTTP/3 / WebSocket 的吞吐与常驻内存(v1.6.4
之后的开发档 1.6.5–1.6.61 里陆续落地的,57 档),以及
把「预编译包是全功能的」这句话补完。
下面这一组是 HttpArena 平台自己那套装置上的
读数(它的机器、它的生成器),同一个条目、同一种测法,对照的是上一版发布包 v1.6.1:
| 档位 | v1.6.1 |
1.7.0 | 倍数 |
|---|---|---|---|
baseline-h3-64(req/s) |
58 | 1 167 034 | ×20 121 |
echo-ws-pipeline-512 / -4096 / -16384(req/s) |
10.85 M / 12.63 M / 10.63 M | 30.03 M / 28.94 M / 25.18 M | ×2.77 / ×2.29 / ×2.37 |
async-32000(req/s) |
725 202 | 1 513 512 | ×2.09 |
pipelined-4096(req/s) |
3 612 234 | 6 784 375 | ×1.88 |
json-tls-4096(req/s) |
370 872 | 672 436 | ×1.81 |
json-comp-4096(req/s) |
194 410 | 325 476 | ×1.67 |
baseline-4096(req/s) |
1 141 979 | 1 639 189 | ×1.44 |
static-h2-1024(常驻内存) |
3.7 GiB | 793 MiB | ÷4.8(吞吐同时 +54%) |
那一轮 42 档的 5xx 全是 0。全套读数、逐档对照、以及"哪几档一格没涨"
(latency-*、8gbit、async-db 这些被外部因子钉住的档)都写在
doc/benchmark.md 的 §4.5,
含每个数的出处(平台把原始读数提交回条目分支,可复核)与逐条归因 ——
其中 h1/h2 那一组高吞吐档的提升只归到"库侧、1.6.23–1.6.51",没有逐提交归因。
此前"预编译包是全功能的"这句话有一处差集:release.yml 十条配置里
一条都不传 UVCPP_ENABLE_YYJSON 与 UVCPP_ENABLE_BROTLI,而 db 模块的两个远程
后端是显式 =OFF。所以从包里能拿到的能力,比从源码默认构建拿到的少三样。1.7.0 起
差集为空:yyjson、brotli、MySQL、PostgreSQL 都在包里,六条腿一致。
| 怎么进包 | 为什么是这种方式 | |
|---|---|---|
yyjson |
-DUVCPP_ENABLE_YYJSON=ON,走 FetchContent 取源码,以目标文件形式并进同一个 uvcpp 库 |
单头 + 单 .c,没有共享库可链,也没有包要多一个 |
brotli |
-DUVCPP_ENABLE_BROTLI=ON 加 -DUVCPP_BROTLI_FROM_SOURCE=ON |
发现顺序是"先系统后源码"。发布腿不能听天由命:镜像哪天装了 libbrotli-dev,配置期就会静默改走共享库,.so / .dll 上多出三条 DT_NEEDED 而不会有任何一条断言变红(ldd 黑名单里原本也没有 brotli —— 已经补上) |
MySQL / PostgreSQL |
-DUVCPP_ENABLE_DB_MYSQL=ON -DUVCPP_ENABLE_DB_PGSQL=ON 加 -DUVCPP_DB_RUNTIME_LOAD=ON |
见下一节 |
DT_NEEDED(Windows 上是 PE 导入表)是加载期解析的,不是调用期。所以链上
libmysqlclient / libpq 的后果不是"用到才报错",而是没装这两种客户端库的机器连
libuvcpp.so 都加载不起来 —— 哪怕那个程序只跑 HTTP、一行 db 代码都不碰。1.6.x 的
六个包正是靠把这两个后端关掉来躲开这件事。
UVCPP_DB_RUNTIME_LOAD=ON 换了一条路:两个后端照编进库(mysql.h / libpq-fe.h 在
配置期仍然要,MYSQL_BIND 的布局必须从真的头里读),但客户端库在运行时才
dlopen / LoadLibrary,符号从一张函数指针表上取。于是:
- 没装客户端库的机器上,除"真去连 MySQL/PG"之外一切照常;
- 真去连时拿到的是
uvcpp_db_status::NO_DRIVER加一句写清了试过哪些库名的话, 而不是一句裸的dlopen失败;UVCPP_DB_MYSQL_LIBRARY/UVCPP_DB_PGSQL_LIBRARY可以指一个明确路径。
这是一处行为变更,写在这里而不是悄悄改:此前"配置期没找到客户端库 ⇒ 那个后端不
存在(宏是 0)",此后是"后端存在,有没有库要运行时才知道"。uvcpp_db_drivers() 在
1.7.0 的包里打印 sqlite,mysql,postgres。从源码构建的人不受影响 —— 新开关默认
OFF,今天的行为逐字节不变。
--exclude-libs,ALL(ELF 上的共享目标)。 本仓在非 Windows 上UVCPP_API是空的, 链进去的静态库符号全部导出。dlopen("libpq.so.5")之后,它那份按系统 OpenSSL 编的SSL_*调用会先撞上我们导出的那份,同进程两份 libcrypto、一手建一手放 —— 堆损坏。 这个链接选项只藏来自静态归档的符号,我们自己的 API(含uvcpp_c_*的 C ABI) 直接由目标文件构成,照样导出。判据是nm -D里SSL_new/EVP_*/Brotli*归零, 而check_capi_symbols.py与.pc可链接性两条门禁仍绿。- Windows 上的客户端头。 两条 MSVC 腿的镜像上没有 MySQL/PG 的开发头(
windows-11-arm上连 PostgreSQL 都没有),所以四条 Windows 腿共用一份钉死版本的头:从 MSYS2 的.pkg.tar.zst(mirror.msys2.org上的不可变 URL)取出来放进C:/client-headers/, 由.github/scripts/win-client-headers.sh做。Linux 两条腿则用apt-get download libmysqlclient-dev libpq-dev+dpkg -x摆一份头出来 —— 不装包: jammy 上这两个都Depends: libssl-dev,而发布腿有一条"不装 libssl-dev"的不变量 (22.04 那份是 3.0.2,没有 QUIC API,装上会让find_package静默产出一个坏包)。 - Linux 两条腿不再传
-DUVCPP_STATIC_RUNTIME=ON。 这个开关在 UNIX 上把-static-libgcc -static-libstdc++同时追加到共享目标与可执行文件的链接行 (CMakeLists.txt里那一段),于是libuvcpp.so与每一个链接它的 C++ 可执行文件 各自带一份 libstdc++ ⇒ 同进程两份。同一份模板实例(std::once_flag/std::__future_base那一族)是 COMDAT、两边都进导出表,动态解析按"可执行文件优先" ⇒ 一侧的__once_proxy会跳进另一侧的_FUN。2026-10-14 那轮预演 9 条用例当场崩 (5 条 SEGV、4 条 SIGABRT,x64 与 arm64 逐条相同),全是"跨线程 + future/promise" 那一类。这不是用例特有的毛病:把 .so 摆成"静态 libstdc++"、只让用例去链系统那份libstdc++.so.6—— 也就是任何一个普通使用者的形状 —— 同一条用例照样段错误; 把开关去掉之后这 9 条与整棵 136 条用例零失败(其中 2 条 db 用例因没起数据库而跳过)。 所以"预编译包给别人直接用"与"静态 libstdc++ 的 .so"是互斥的,选前者。代价是 Linux 的ldd多出libstdc++.so.6与libgcc_s.so.1—— 装了apt的 Linux 上本来就有(libstdc++6是apt自己的依赖)。Windows 两条 MinGW 腿照旧开着:PE 没有全局符号作用域, 不存在互相顶掉的问题,而 MinGW 用户机器上未必有libstdc++-6.dll。
六条腿的模块断言读的是生成的头(<tree>/include/uvcpp/uvcpp_config.h),不是
CMakeCache.txt —— 后者在静默降级时照旧写着 =ON。新增的四枚是
UVCPP_YYJSON_ENABLE、UVCPP_BROTLI_ENABLE、UVCPP_DB_MYSQL_ENABLE、
UVCPP_DB_PGSQL_ENABLE。同一批还加了两条反向断言:Linux 两条腿在 nm -D -u 里
数未定义的 mysql_ / PQ 符号(应为 0),ldd 黑名单补上
libbrotlienc/dec/common 与 mysqlclient / pq。
- C++ API 与 C ABI 都没有破坏性改动,C ABI 版本仍是
1。 - 包里的模块集合变大了(
UVCPP_YYJSON_ENABLE/UVCPP_BROTLI_ENABLE由 0 变 1,uvcpp_db_drivers()由sqlite变sqlite,mysql,postgres), 而 yyjson / brotli / db 这三个后端带来的DT_NEEDED一条没多 —— 这正是上面 那几处构建侧改动的目的。(Linux 上总数多了两条,来自最后那一条改动,与模块无关。) - 自包含的含义要按新的写法理解:Linux 上
ldd libuvcpp.so除 libc 与 C++ 运行时 (libstdc++.so.6/libgcc_s.so.1,见上)之外仍无其他;MinGW 的 DLL 依赖表照样 只有 Windows 自带模块。运行期的例外有两处:真要连 MySQL / PostgreSQL 时那台 机器上得有对应的客户端库;C++ 运行时那份则是基础库,装了apt的发行版本来就有。
v1.6.3 之后的补丁版,一条改动:多循环(set_loops(n > 1))下服务端析构不再与工作循环抢同一张表。
uvcpp_http_server 在 set_loops(n > 1) 下给每条循环各留一张连接上下文表。析构时它逐张清掉,
而工作循环线程此刻还在跑 —— 它们的退出路径要关掉自己名下的连接,那条路上会经由关闭回调
回到 remove_ctx,写的正是同一张表。core 里两条栈刚好对在一起:
| 线程 | 栈 |
|---|---|
| 跑析构的那条 | ~uvcpp_http_server → delete tcp_server_ → ~uvcpp_loop_worker → std::thread::join() |
| 工作循环那条 | uv_run 收尾 → 关闭回调 → uvcpp_http_server::remove_ctx → std::map::erase → SIGSEGV |
remove_ctx 手里那个迭代器指向的结点,刚被析构体那句 clear() 释放掉。
析构体的注释写着「析构发生在所有循环线程都已经停了之后(调用方的责任,见
uvcpp_tcp_server 的停机次序)」,但这条责任今天没人能履行:停 worker 的 stop_workers()
是私有的,stop() 只关监听句柄、不动已建立的连接,close_all_clients() 是异步投递。而唯一
会停 worker 的地方,是那个析构体自己的最后一行 —— 次序正好反了。
判据不是"多跑几遍看看":把 tests/functional/test_web_multiloop_func 用 taskset -c 0-3
钉在 4 个核上(与 CI runner 同形)、8 路进程并发各跑 250 次 —— 改前 2000 次崩 18 次
(rc=139);改后 2000 次 0 崩,另一次 8 路 × 500 = 4000 次也是 0 崩。单进程
--repeat until-fail:30 压不出来,这正是它此前被当成 flake、靠空提交重跑糊过去的原因。
两处,都在停机次序上:
src/net/uvcpp_tcp_server.h:stop_workers()仍然私有,加一条friend class uvcpp_http_server;—— 与仓里uvcpp_web_stream那条「私有 + friend:公开它 等于允许任意调用方在循环还转着的时候把工作线程拆掉」的既有约定同形。src/web/uvcpp_http_server.cpp:析构体第一句tcp_server_->stop_workers();,之后才是那几张 表的收尾。
单循环(set_loops(1))时工作线程列表本来就是空的 ⇒ 这一句是空操作,单循环行为逐字节不变。
回归:本机 build-capi 109/109、build-capi-all(全功能,含 QUIC / HTTP3 / NGHTTP2 / WEBAPP /
CAPI)124/124 全过。
v1.6.2 之后的补丁版,一条改动:cmake --install 现在也装从源码取的 zlib 那两个头。
影响面说清楚:从发布包用库的人不受影响(包里本来就带着那两个头),受影响的是自己从 源码建库、再对着装出来的前缀编译的人 —— 这条路上,公开头里有一处引用是编不过去的。
公开且会被安装的 web/uvcpp_ws_parser.h 在 #if UVCPP_ZLIB_ENABLE 里引 <zlib.h>,而
生成的 uvcpp_config.h 把这个宏默认定义成 1,旁边还有一条 #error 禁止消费者自己把它
定义成 0 —— 也就是说这条依赖关不掉。
库自己编得过,是因为那份 zlib 是 FetchContent 取的源码、被静态链进 libuvcpp.so:头只
存在于构建树里,装的时候没人管它。于是同一件事有了两条路:
| 用法 | 头从哪来 | 结果 |
|---|---|---|
| 解压发布包 | 包里手工补的 include/zlib.h、include/zconf.h |
编得过 |
从源码建库 + cmake --install |
install 规则里没有这两条 | 编不过 |
第二行是这次的缺陷。它是从 HttpArena 上那份从源码
建库的条目里冒出来的:最后一条 RUN g++ … main.cc 死在
include/web/uvcpp_ws_parser.h:31:10: fatal error: zlib.h: No such file or directory。
顺带一提,这条分叉此前没有任何一道门禁看得见:本仓 CI 里唯一一处 make install* 是
OpenSSL 的 install_sw,没有任何一格跑 cmake --install。发布包那条路是
tests/tools/package_release.py 手工组装 stage 的,它补这两个头时注释里写着「install 规则
里没有这一条」—— 两份清单各写各的,谁也没跟谁比过。
install 规则补上这两个头,且只在取源码那条支路装(系统 zlib 的平台上,消费者手里本来 就有这份头):
if(UVCPP_ENABLE_ZLIB AND UVCPP_ZLIB_FROM_SOURCE)
install(FILES "${zlib_SOURCE_DIR}/zlib.h" "${zlib_BINARY_DIR}/zconf.h"
DESTINATION include)
endif()开关是普通变量,这一点是刻意的。最顺手的写法是 if(DEFINED zlib_SOURCE_DIR),但它不
成立:zlib_SOURCE_DIR 是 FetchContent 写进缓存的 zlib_SOURCE_DIR:STATIC,只要某次
配置走过取源支路就一直在,后来翻回系统 zlib 支路也照样 "defined" —— 会装出一份陈旧的
zlib.h,源码目录被清掉时还会让整个 install 中断。这条假阳性是实测出来的,不是推的。
两个头取自两个目录也不能省:zlib.h 在源码目录,zconf.h 由 zlib 自己的 CMake 在配置期
configure_file 生成到构建目录。
三种状态实测过:取源支路 cmake_install.cmake 里 1 条;同一个构建目录翻回系统 zlib 支路
0 条(即上面那个缓存陷阱的形状);再翻回来又是 1 条。
不是平台读数,是"在一个没有系统 zlib 的头树里,只用这个安装前缀能不能编过公开头":
-nostdinc -nostdinc++ 加一个顶替 /usr/include 的符号链接农场,编一个 #include <web/…>
的最小翻译单元 —— 修完 rc=0,且 g++ -M 指明 zlib.h / zconf.h 来自安装前缀;
把这两个头从前缀里抽掉,就复现出上面那条 CI 报错,逐字同一处、同一文件同一行。
这次改动不含任何性能面。 不要拿它去许诺任何分数变化。
v1.6.1 之后的补丁版,一条改动:HTTP/1.1 的 Host 与空 Transfer-Encoding 一律 400。
同样来自 Http11Probe 上那次真实探测 ——
那是本库在 RFC 硬要求上唯一一处实打实的不合规。
RFC 9112 §3.2 三个分句并列:缺 Host 字段行、Host 出现多于一行、Host 字段值非法。
本库此前三条一条都不拦 —— 缺 Host 的 HTTP/1.1 请求照常路由、照常 200。
它是这份规范里本库此前唯一不拦的一条 MUST-400,这一点值得说清楚,免得读成"§3.2 是唯一
一处 MUST-400":同规范里 §5.1 的"字段名与冒号之间有空白"、§6.1 的"Transfer-Encoding
不以 chunked 收尾"、§6.3 的"多个取值互不相同的 Content-Length"也都写着 MUST-400 ——
那三条是 llhttp 拦的,本库一直就回 400,逐条实测过(X-Test : v、TE: chunked, identity
- CL、两个不等的 CL 各回一个 400)。真正缺的只有 §3.2 这一条。
判据就是这三条本身,不是探针的期望值。**分得清"规范要的"和"探针猜的"**是这次改动的
重点,所以修完之后探针上还剩三条 FAIL,一条没动:
RFC9112-5.1-OBS-FOLD(§5.2,探针标 Must)。 那条规范自己给了两个分支: 「回 400,或者把每个折行换成一个或多个 SP 之后再解释」。本库走的是第二个 —— 实测/echo回的X-Test是value continued,折行换成了一个 SP。两个分支都被许可, 探针只认第一个。COMP-ASTERISK-WITH-GET(§3.2.4,探针标 Must)。GET *的 200 不出自解析器, 而是出在入口自己注册的/*path兜底路由上。§3.2.4 那句 "only used for a server-wide OPTIONS request" 约束的是生成(客户端 MUST NOT 发),服务端收到之后 该怎么答规范没说死。COMP-HTTP12-VERSION(rfcLevel: May,不计分)。GET / HTTP/1.2回 400 而不是 200/505。RFC 9110 §2.5 那句"收到自己实现了的主版本、但次要版本更高时,SHOULD 按自己 遵从的最高次要版本处理"是 SHOULD 不是 MUST,而且这个拒绝来自 llhttp 本身,要改得先动 那个 vendored 解析器的请求行。
头两条是探针的期望与本库的合法选择之间的差,第三条是 SHOULD 且不计分 —— 都不属于 "不合规",所以都没动。这条界线本身就是这次改动最想留下的东西。
同一次探测里翻出来的,不属于探针的哪一条:Transfer-Encoding = #transfer-coding、
transfer-coding = token、token = 1*tchar —— 整行为空一个 tchar 都没有,压根不匹配。
而 llhttp 在值整行为空时不把 TE 记成"存在",它那条"TE 与 CL 同时出现就报错"的保护因此
不成立:TE:(空)+ Content-Length 被当成"没有 TE",退回 CL 框架照常应答并保持
keep-alive。前端若按 §6.3 处置(TE 存在就压过 CL)就是标准的请求走私。
行为那半边容易写,位置这半边才是这次真正要守的东西:uvcpp_web_app 装了
set_stream_claim 钩子,而 on_headers_complete 里请求一旦被认领就直接返回。判据只要
排在钩子之后,webapp 这条路就整条绕过 —— 而 webapp 才是绝大多数用法,低层路由反而是少数。
所以两条用例是分工的,不是重复:一条不注册任何路由走低层(合法请求落 404,正好当"非 400"
的对照),一条走 webapp 走认领路径。
次序做了变异验证:给判据加上"只在没有认领钩子时生效",等价于把它挪到钩子之后 —— 低层那条照旧绿,webapp 那条五条必须 400 的全部变红(实际 200),随后还原。
这次改动不含任何性能面。 它是纯语义修复,判据是 RFC 条文与那两条用例,不是平台读数; 不要拿它去许诺任何分数变化。
v1.6.0 之后的补丁版,四条改动、一个形状:一道被当成"保护"的闸门,量错了对象。
三条修复一条性能,全部来自 HttpArena 基准平台上
一次真实跑分暴露的问题 —— 那里的读数与本机的量具互相印证。
HTTP/2 的控制帧预算(32 帧/秒、突发 64)里混进了 WINDOW_UPDATE。那张单子上其余的帧
满足"收到一帧 = 要分配一帧",而 WINDOW_UPDATE 每半个窗口才来一帧、本身比它换来的
DATA 便宜得多;更要命的是它的正常速率与我们已经发出去的字节数成正比,于是任何
"帧/秒"的上限都变成了吞吐上限 —— 32 帧/秒 × 32 KiB = 1 MiB/s,一条连接只要跑得比
这快就会被自己人 RST_STREAM。
判据是这个指纹:默认 64 KiB 窗口的客户端,一条连接稳定在 7 个 200 KiB 响应(≈1.4 MiB)
后开始报 UV_ECANCELED,且与并发流数无关 —— 那是字节预算穿了帧率外衣的样子。
移出之后 64/64 全通。
max_pipelined_requests(默认 8)守的是 HTTP/1.1 同一读缓冲里排队的请求,却对 h2 流
一视同仁。h2 的并发上限是协议内建的 SETTINGS_MAX_CONCURRENT_STREAMS(本库声明 100),
对端自己会守规矩;再套一层 8 只会把合规的流打成 503,而拒绝时还顺手设了
connection: close —— 在 h2 上那是非法头(RFC 9113 §8.2.2),一条流不该决定连接级的事。
文档里早就写着 h2 换成协议内建的限额,是代码没跟上。
这条是四条里唯一在平台读数上肉眼可见的:static-h2 那次 2xx 238,313 / 5xx 556,357
(七成请求被扔,且 reconnects 为 0 —— 连接一直活着,是流被拒不是连接被关)。
服务端从不补发 MAX_STREAMS。ngtcp2.h 写着它不自动补,而本库注册了 stream_open
回调、使那个例外不成立,所以得自己发。补上之前一条连接终生只服务 100 个请求,
baseline-h3 在平台上只跑出 61 rps。
level 6 / windowBits 15 的内核要约 256 KB,而这块内存在请求之间不携带任何信息 ——
deflateReset() 之后产出的字节与刚 init 完的逐字节相同。改成每线程一份、复用前 reset,
产物逐字节不变,json-comp +2.4%。
这一条为什么只有 2.4%,写在这里免得后人误判:单独量 compress() 本身,单线程紧密
循环里两边都是 ~30 µs/次 —— glibc 的 mmap 阈值会自适应,一旦观察到 mmap 块的释放,
阈值就抬上去,紧密循环里那次 init/end 几乎不要钱。差值只在服务端那种混着大量其他
分配的真实节奏里才露出来。
两条改动在本机对 h2load 形状的基准上不产生差异,别拿它们去许诺平台读数的变化:
h2load 把流控窗口抬到 2^30-1 且从不发 WINDOW_UPDATE,那个桶够不着;压缩内核那条只有
2.4%。它们的判据是各自的主题节,不是平台读数。
1.5.1 → 1.5.3 这条开发线收进这一版:新增数据库模块(含它在 C ABI 里的第八片)、
QUIC 传输上四处性能与正确性改动、以及发布包的两轮扩容。
一个连接一个 uvcpp_db_client,同一套接口盖住 SQLite / MySQL / PostgreSQL 三个
后端,结果行按表取(列名或列号)。默认关,而且是本库唯一需要第三方客户端库
的模块(libsqlite3 / libmysqlclient / libpq)。三个后端一个都没找到时整个模块被强制
关掉并打一条说明怎么修的 warning,而不是留一个"能配置、链不上"的组合。
- 占位符是方言差异,不换算。 写
$1是 PostgreSQL 的语法,写?是另外两家的; 本库原样透传给你选的那个后端,不做?→$n的改写。这一条实测过六种组合。 - 事务里不重试。 失败后的重试会把语句挪出那个事务本身,所以门面不替你重试。
DECIMAL有代价:它既到不了浮点也到不了整型,代价写在doc/db-guide.md里。- 异步门面
uvcpp_db_async(回调 +future)不写uv_queue_work—— 要跑在 工作线程池上的样板它替你收掉了。 - 连接池建在同一条连接类型上,把并发从 1 抬到 N;借出的 client 不许
free、池子 被门面绑着时也不许free,这两条都被变异测试钉住(M22–M26)。
uvcpp_c_db.h 是第八片,81 个入口(连接 / 同步查询与事务 / 参数 / 结果集与值 /
连接池 / 异步门面),到此合计 402 个函数。C#、Rust、Python 的 ctypes 今天就能
用数据库模块。
UVCPP_C_ABI_VERSION仍是1。 加一片不 +1 的判据这次比前几批更直接:git diff --stat在src/capi/上只有一行(伞头多一段#if UVCPP_DB_ENABLE的 include),既有的七份头与七份.cpp一个字节都没动。- C 的异步接口不收循环参数 —— 这是它与 C++ 那侧唯一一处形状差别。 公开头里造不出
一个合法的
uvcpp_loop*,而"借调用方自己的uv_loop_t*"会让~uvcpp_loop()去uv_loop_close()并释放一块别人的内存。给一个没人填得合法的参数比不给更糟,所以完成 回调一律在门面自带的那条懒起循环线程上跑;要把结果搬回自己的循环,就在回调里投一次uvcpp_c_net.h那族*_post()。 - C# 绑定仍覆盖前七片(321 of 402)—— db 那 81 条是它自己 README 里如实列出的缺口, 不是漏写。
六条发布腿的预编译包在这几版里逐步补齐可选功能:
| 版本 | 多了什么 | 断言清单 |
|---|---|---|
1.5.2 |
WSDL / SOAP | 十个宏 → 十一个 |
1.5.3 |
数据库(只带 SQLite 一个后端) | 十一个 → 十三个 |
1.7.0 |
yyjson、brotli,以及数据库的 MySQL / PostgreSQL 两个后端(运行时加载,不进链接行) | 十三个 → 十七个 |
WSDL 那次值得单独说,因为它的失败方式很安静:包里照样装着 include/wsdl/*.h
(那份清单由打包脚本的模块表决定,与开关无关),但生成头里 UVCPP_WSDL_ENABLE 是 0,
于是那些头的全部内容落在 #if 外面 —— 头在、功能不在。
db 那次的两个 OFF(MySQL / PostgreSQL)是显式关的,不是"找不到":发布 runner 上
装着 libpq-dev,find_package 会静默成功,于是共享库悄悄多一条 libpq.so.5 的
DT_NEEDED —— 而"装到别的机器上跑不起来"正是发布腿那条 ldd 断言要拦的形状。
PRIVATE 链接挡不住这件事:PRIVATE 关的是头文件与编译定义,共享库上它照样写
DT_NEEDED。SQLite 那份也不链系统的:走配置期下的一份哈希钉死的 amalgamation,
编成静态且强制 PIC 链进去(Ubuntu 24.04 上系统的 libsqlite3.a 不是 PIC)。
1.7.0 那一轮的形状与前两轮不同,细节写在前面「v1.7.0 重点」里:前两轮是"包里多了模块",
这一轮还多一层 —— 那两个远程后端进了包却不进链接行(UVCPP_DB_RUNTIME_LOAD)。
理由是 DT_NEEDED 在加载期解析:链上 libpq / libmysqlclient 就等于"没装它的机器连
加载这份库都做不到",哪怕一行 db 代码都不跑。另外,1.7.0 之前的差集方向是反的:
UVCPP_ENABLE_YYJSON 与 UVCPP_ENABLE_BROTLI 默认 OFF 而发布腿也没传,所以"包里功能
齐全"这句话当时是"发布包比源码默认构建少两个模块"。
每轮的判据都读生成的头(<tree>/include/uvcpp/uvcpp_config.h)而不是
CMakeCache.txt —— 模块被强制关闭时走的是普通 set(),缓存里照旧写着 ON 而编译器
看到 0,只 grep 缓存的断言会放行一个缺功能的包。
- 大载荷下交付出去的字节是错的(
1.5.1) —— 这是本版最重要的一处修复。write_stream()把std::vector<uint8_t>里的一个指针交给 ngtcp2,而那块缓冲会 增长(把地址搬走),丢已确认前缀时又会把尾巴memmove上来(地址没变、字节变了); ngtcp2 要求在字节被确认前它们"原样留着"。症状是 2 MiB 回显 20 次里崩 3 次 (0xC0000005),而崩只是它最响的那种症状:把 HEAD 那份存储换回来重编、跑同一个 量具,6 次运行全部在 15 轮内报DATA MISMATCH—— 旧形状每次吐出的字节都是错的, 那个基线在 2 MiB 上根本不存在。正式修法是分块发送队列:每块出生时预留、此后只写 到这里为止、只在整块被确认时才丢,两条约束按构造成立。 - 数据报上限钉在 1200,把 ngtcp2 自带的 PMTUD 整条关死(
1.5.1)。1200 是"任何 路径都保证能过"的下界,看着最保守,实际让探测表{1406, 1342, 1232, 1444}四档全部越界、PMTUD 当场判完成并被关掉,此后每个数据报都被钉死在 1200 字节 —— 2 MiB 单向要 1748 个数据报。改成 1500(典型以太网 MTU)后,交错跑五轮取最小值, 接收侧on_read(DATA)次数对照组 1743~1766、改完 1392~1446,两组一次都没有 重叠(墙钟在那个分辨率上分不出结论,这一行才是结构证据),全尺寸扫描每一档都 变快(+4.2% ~ +23.8%)。两条static_assert守着这个缺陷的两个入口,都拿对照臂 验过(改回 1200 ⇒ 构建失败)。 - ACK 阈值从 ngtcp2 默认的 2 提到 16(
1.5.1)。低 RTT 链路上延迟 ACK 的窗口塌到 微秒级,阈值成了唯一还在起作用的闸门,接收端几乎每收两个包就回一个 ACK。实测 2 MiB 单向总数据报 2445 → 1783(−27%)、中位耗时 19.25 → 13.81 ms。成本说清楚: ACK 变稀 ⇒ 对端检测丢包、推进 cwnd 的反馈变慢,上界是延迟 ACK 计时器(25 ms);本机 回环无丢包,量到的全是收益,别把这个数读成"公网也 +38%"。 - Windows 发送侧批量交数据报(
1.5.2)。UVCPP_ENABLE_UDP_GSO把一次聚合出来的 一批等长数据报用一次WSASendTo交给协议栈,而不是每个数据报一次(libuv 在 Windows 上是一个数据报一次WSASendTo)。分段尺寸取的是本次聚合写回填的值、不是常量,所以 线上跑的还是原来那些数据报。Windows only —— Linux 上游已经把每批折成一次sendmmsg(),这个开关在那里整段编掉。收方向故意没做(半开会让 libuv 把粘在一起 的数据报当成一次读交给应用)。
- C++ API 没有破坏性改动;C ABI 版本仍是
1,本版新增了 db 那一片 (81 个uvcpp_c_db_*),没有删除或改名任何既有符号。 - 发布包的模块集合变了:换上
1.6.0的包之后,UVCPP_WSDL_ENABLE与UVCPP_DB_ENABLE由0变1。从源码构建的人不受影响 —— 两个开关的默认值仍是OFF。 - 包里
uvcpp_db_drivers()只打印sqlite;要 MySQL / PostgreSQL 就从源码编 (默认三个后端都开)。
逐条的「这一版没做什么」在
doc/db-guide.md、
doc/quic-guide.md、
doc/http3-guide.md。
1.4.1 → 1.4.4 这条开发线(HTTP/2、QUIC、HTTP/3 的 C 面,共 321 个函数)连同上
面那几件 1.5.0 自己的改动,一起收进这一版。
这一版最大的一处用户可见变化在这里。此前的发布包不带 QUIC / HTTP/3(那两个
开关默认 OFF,发布腿也不传);1.5.0 起六条腿(linux-x64 / linux-arm64 / mingw-x64 /
mingw-arm64 / msvc-x64 / msvc-arm64)的发布档与调试档都带上 QUIC + HTTP/3 + C ABI。
判据不是"我们传了 -D",而是读生成的头:每条腿各加一步断言,读
<tree>/include/uvcpp/uvcpp_config.h 里那十个宏(UVCPP_NET_ENABLE、
UVCPP_WEB_ENABLE、UVCPP_WEBAPP_ENABLE、UVCPP_ENABLE_MEMORY_POOL、
UVCPP_ZLIB_ENABLE、UVCPP_OPENSSL_ENABLE、UVCPP_NGHTTP2_ENABLE、
UVCPP_CAPI_ENABLE、UVCPP_QUIC_ENABLE、UVCPP_HTTP3_ENABLE)。为什么不能 grep
CMakeCache.txt:QUIC / HTTP3 缺依赖时走的是 message(WARNING) + 普通变量
set(... OFF),于是缓存里照旧写着 =ON 而编译器看到的是 0 —— 只 grep 缓存的断言会
放行一个缺功能的包,而"发出去的包缺功能"正是它要拦的那件事。生成的头是编译器真正
读到的那个数。(名字映射也不是按名字猜的:第一版把 UVCPP_BUILD_EXPAND 写成
UVCPP_WSDL_ENABLE,拿真生成头实测时当场红了一条。)
QUIC 的前提是一份带 QUIC API 的 OpenSSL ≥ 3.2,而六条腿的来路各不相同(逐条记在
release.yml 文件头那张表里,改任何一条之前先看它):
| 腿 | 这份 OpenSSL 从哪来 |
|---|---|
| linux-x64 / linux-arm64 | 不装 Ubuntu 22.04 的 libssl-dev(3.0.2,没有 QUIC API —— 装上只是给 find_package 添一份不合用的候选),job 里自建 OpenSSL 3.5.0(no-shared no-tests -fPIC;-fPIC 不是保险是承重的,本机量过同一份源码两种命令行生成的 CFLAGS)。编完当场 nm --defined-only libssl.a 查 SSL_set_quic_tls_cbs 有定义 |
| mingw-x64 / mingw-arm64 | MSYS2 包的 openssl(今天 3.6.x,≥ 3.2) |
| msvc-x64 | runner 脚本给的那份(.github/scripts/win-openssl-deps.sh);不够 3.2 就会被预检静默关掉 QUIC,从而被断言判红 |
| msvc-arm64 | 镜像上没有 arm64 的,这条腿自己拿 VC-WIN64-ARM no-asm 编 3.5.8;另外把"这份 OpenSSL 确实带 QUIC API"从配置期探测结果(缓存变量 UVCPP_OPENSSL_HAS_QUIC_CBS)里读回来,好把"OpenSSL 不对"与"开关没传"两类红分开报 |
依赖仍然全静态:libuv / llhttp / zlib / OpenSSL / nghttp2 / ngtcp2 / nghttp3。
Linux 上 ldd libuvcpp.so 只剩 libc 与 ld-linux(那条自包含断言 1.5.0 起把
ngtcp2 / nghttp3 也写进名单 —— 它们今天被本仓以 ENABLE_SHARED_LIB=OFF 压成静态、
不可能出现在 ldd 里,写进去是为了让"哪天有人把上游那个开关改成 ON"变成一条当场
看得见的红)。
321 条 DllImport(UvcppNative.cs 183 + UvcppNative.Protocols.cs 138,同一个
partial 类的两半)+ 一个能跑的 QUIC 回显例子(dotnet run 默认做一次回环:服务端
收、回显、客户端收到同样的字节,逐字节比,收尾断言活句柄数回到 0)。判据是"一条不
多、一条不少":与 tests/tools/capi_symbols.lock 里 321 个 uvcpp_c_* 符号双向
对账,两个方向的差集都为空 —— 一段纯文本脚本就能核,不需要库、不需要编译。用法、
动态库名字那一栏(Windows 上 MinGW 包与 MSVC 包不一样)与"别手写声明"的理由都在
bindings/csharp/README.md。
例子的价值不只是一份示例:它第一次跑就撞出库里的一个必现 SIGSEGV(带着活连接
uvcpp_c_quic_server_free()),见下面「修复」。
- QUIC 端点带着活连接释放会 SIGSEGV(1.5.0)。
uvcpp_c_quic_server_free()边遍历server->conns边detach_conn(),而后者的"反登记"那一半会从这张表里erase——unordered_map::erase把当前迭代器弄失效,++it踩在已回收的桶上;表里只有一条 连接也照样崩。修复是先把句柄抄进一个局部数组,再统一 detach。回归用例test_free_with_live_conn()(自签 TLS + 一次真握手,不 close 也不 stop 直接 free 两端 + 两个 TLS 上下文,再断言登记表回到进场时的数)与变异 M21 一起进 —— 反空转是 量出来的:把修复拿掉重跑,这条用例 SIGSEGV。 uvcpp_c_quic.h补上一处会让人写错两遍的文档(1.5.0):QUIC 的on_read收尾是 两条回调 —— 一个"带数据和 FIN 的 STREAM 帧"先报一条DATA(那条DATA的fin也是非 0),紧接着再报一条PEER_CLOSED。所以流的结束判据是PEER_CLOSED,不是fin。例子第一版拿fin当结束判据,于是回显了两次,第二次写 方向已经关了(ngtcp2 给 -219),异常从 native 调进来的回调里逃出去 → abort(本机实测 退出码 134)。
- C++ API 没有破坏性改动;C ABI 版本仍是
1(uvcpp_c_abi_version()返回 1), 这一版没有新增、删除或改名任何uvcpp_c_*符号 —— 符号面仍是 321 条。 - 但发布包的模块集合变了:拿 1.5.0 的包接上去之后,
UVCPP_QUIC_ENABLE/UVCPP_HTTP3_ENABLE/UVCPP_CAPI_ENABLE由0变1,链接期会多出这三批符号与 头。从源码构建的人不受影响 —— 三个开关的默认值仍是OFF,full那条 CI 格也 没有替谁改默认。 - 拿新包但不想用这几片的人什么都不用做:多出来的头是惰性的,模块宏由包里的
include/uvcpp/uvcpp_config.h给出。
1.3.1 → 1.3.33 这 33 个开发档全部收进这一版。这一版有破坏性改动:日志模块
动了一处 vtable、一处对象布局和四个重载集(逐条见本节末「换二进制之前」)。
| 新增 | 说明 | 档 |
|---|---|---|
| SOAP 1.1/1.2 + WSDL 1.1 | 新模块 src/wsdl/(UVCPP_ENABLE_WSDL,默认 OFF):WSDL 文档模型与发布层、SOAP 信封解析 / Fault / 序列化、operation 派发层(派发键、九种拒绝、响应包装)。依赖 pugixml,随包静态链接 |
1.3.7-dev、1.3.8-dev |
| 应用层 JSON | uvcpp_json 构造器与字段表反射(UVCPP_JSON_REFLECT),不用再手写 to_string |
1.3.3-dev、1.3.4-dev |
| 日志模块完善 | 见下节 | 1.4.0 |
| 多循环的 Linux 分流 | set_loops(n > 1) 在 Linux/BSD 上改为内核分流(n 条循环各自 UV_TCP_REUSEPORT 绑同一端口),Windows 仍是转手;is_fanout() 可问是哪一种 |
1.3.5-dev |
Date 响应头 |
按秒缓存,不再每响应 strftime |
1.3.1-dev |
| 进程内设 libuv 线程池 | uvcpp_set_threadpool_size(),上限来源收口成一个常量 |
1.3.2-dev |
| 静态分片下发的名额闸门 | 按块借还工作池名额,拿不到名额时不再回 503 | 1.3.6-dev |
-
看得到位置了:新增
UVCPP_LOGF(level, category, fmt, ...),注入__FILE__/__LINE__/ 函数名。访问日志原先全表唯一没有位置的一条,现在带(uvcpp_web_middleware.cpp:108)。 -
<<能打的东西变多了:enum class(底层整数)、log_level/log_category(名字)、nullptr(输出null—— 此前<< nullptr是编译错误,const char*与const void*两个候选打平)。 -
浮点不再丢精度:改最短往返表示,
0.1仍是0.1,而 16 位有效数字不会被%g截成 6 位。float与double分开处理。 -
两个新模块标签
SOAP/WSDL,并给src/wsdl/补上 9 处日志点 (uvcpp_soap_service.cpp8 处:1DEBUG/ 3ERR/ 4WARN,uvcpp_wsdl_serve.cpp1 处WARN);log_category::JSON此前全仓 0 处使用,现在序列化失败会记一条 —— 那个失败此前与"空 JSON 文档"完全不可区分(resp.json(j)会发出 200 + 零字节正文)。 -
flush():uvcpp_logger::flush()/uvcpp_log_sink::flush()。stdout 重定向到 文件时是块缓冲的,崩溃或被_exit()会丢掉最后一段日志。 -
少几把锁、少几次分配:等级改
relaxed原子读 ⇒ 被过滤掉的调用点不再抢全局 互斥量,也不再分配、不再拼装;logger::write()把 sink 调用移出锁(锁内只快照 sink 指针)⇒ 每条真的打出去的记录少持一次锁,顺带解掉「sink 里再打日志会自死锁」; 控制台 sink 的min_level_同样改原子 ⇒ 每条的should_log()免锁;uvcpp_logf不再永远格式化两遍(先试 512 B 栈缓冲,只有真截断才走堆); 删掉buf_.reserve(128)⇒ 每条日志不再先来一次堆分配;record.message两处改移动。量程要说准:控制台 sink 自己那把锁与那次
fwrite仍在(消掉它要动异步设计), 所以别把这笔读成"日志开销归零"。
-
一个从来没有链过的公开类型 ——
uvcpp_console_log_options自1.2.0起就缺UVCPP_API,于是共享库版(UVCPP_BUILD_SHARED=ON)下它的构造函数不在导出表里: 指南 §9 教的uvcpp_console_log_options opt;只要真的写进使用者的 TU 就是LNK2019。它能躲过所有门禁有两个原因 —— 片段门禁只编不链 (tests/tools/check_doc_snippets.py传给编译器的是-c),而库自己的 TU 带UVCPP_EXPORTS,只有吃 import lib 的另一个 TU 才会撞上;仓内此前也确实没有 一处构造过它(要自定义选项的人都走sink.options())。现已补上UVCPP_API, 并有了第一条真的链接它的用例。 -
启动钩子抛异常时,属主建的句柄没有任何回收点(外部贡献者 @sercebr 在 #32 报的)。
uvcpp_loop_worker的必填钩子set_on_start()抛异常时,那条分支既没跑set_on_exit()、也没把循环 泵干,于是属主在钩子里建的那些句柄一直挂着。触发条件是内存分配失败 (std::bad_alloc),所以它不是常见路径,但一旦踩到就是每次启动漏一块。 修法是那一支补上set_on_exit(),并把「摘句柄 → 泵 → 关循环」抽成正常路径与 失败路径共用的一段,免得两处各自演化。这一条报的是悬垂写(use-after-free)。实测下来后果是泄漏而不是悬垂:
uvcpp_loop::loop_close()只在uv_loop_close()返回 0 时才置关闭位,钩子建的 句柄还开着就返回UV_EBUSY,于是析构走的是"故意漏掉这一整块、换掉一个悬垂" 那条分支 —— 那块内存是泄漏但有效的。两道互相独立的防线都指向同一结论。 三臂实测(真·修前 / 只补钩子不泵 / 修好)确认补钩子与泵两步都不可或缺: 少任何一步,泄漏都还在。
本版不发布吞吐数字。日志这一笔改的是"少抢几次锁、少几次分配"这类结构性开销,
用本机这个回环靶场去量,前后差落在噪声里(该靶场上服务端 CPU 绝大部分花在内核,
库自身占比很小),拿它当结论等于把噪声当信号。要自己量:-DUVCPP_BUILD_BENCH=ON
后跑 uvcpp_bench_server,它带 --access-log 开关可以整开整关;同机多轮取最小值
再比,绝对值不跨机可比。
功能与回归侧有实测:
- 回归:
build-log(VS 2022 x64 Release,WEBAPP + OPENSSL + WSDL + BENCH 全开) 113/113 通过;build-h2(同生成器,WSDL=OFF)109/109 通过。两棵都是全量 重编,error C计数 0。 - 变异:日志改动配的 10 条变异体全部被现有用例抓住,没有一条是靠超时兜底 蒙混过去的。其中"等级该不该被配置覆盖"是一对反向用例 —— 单跑任何一条, "永远覆盖"或"永远不覆盖"都能骗过去,两条一起才把那个开关钉住。
相对上一次发布(v1.3.0),这一版动过的东西分两类。
一、日志模块的 ABI 变化 —— 旧二进制必须重编
| 变化 | 为什么 |
|---|---|
uvcpp_log_sink 新增虚函数 flush() |
vtable 布局变了。给了默认空实现,所以源码兼容;但任何继承它的既有二进制 sink 必须重编 |
log_category 在哨兵前插入 SOAP / WSDL |
CATEGORY_COUNT 由 15 变 17,uvcpp_logger::category_levels_ 变大 ⇒ uvcpp_logger 的对象布局变了。硬编码过 15、或缓存过某个 category 数值的代码会静默错位 |
新增 UVCPP_API 符号 |
uvcpp_logf_at(...)、uvcpp_logger::flush(),以及补导出的 uvcpp_console_log_options()(见上节「修复」)。前两个是新增功能,第三个是修复 —— 三条都是导出面增加,不强制旧二进制重编 |
uvcpp_log_stream 的重载集变了 |
新增模板 operator<< 与 nullptr_t 重载 ⇒ 无作用域枚举实参的绑定从整型提升变成精确匹配。输出值不变,但重载解析变了 |
二、一条行为变更(不改代码也能观察到)
uvcpp_logger::set_level() 在 app.start() 之前调用的,此前会被
init_process_once() 无条件顶回 cfg_.min_log_level(默认 INFO)且不给任何提示。
现在只有显式调用过 app.set_log_level() 才会套用配置值:
- 调过
app.set_log_level(X)⇒X生效,后调用的那个赢; - 没调过 ⇒ 你在
start()之前设的全局等级原样保留。
本仓的靶场正踩在这个坑里:bench/bench_server --log-level WARN 的等级被顶成 INFO
(TRACE/DEBUG 则被压成 INFO),而自报那行印的是请求值,读数整个是假的。
修好之后自报值改印生效值。
1.2.1 → 1.2.25 这 25 个开发档全部收进这一版。这一版有破坏性改动:3 个公开符号
被删除,另有 7 处布局或导出符号变过 —— 必须重编,不能只换二进制(逐条见本节末
「换二进制之前」)。这也是 1.2.x 这条线的收口。
| 新增 | 说明 |
|---|---|
| 多循环横向扩展 | uvcpp_tcp_server::set_loops(n)(1.2.21)与 uvcpp_web_app::set_loops(n)(1.2.23):一个接受者 + n−1 条专用线程的工作循环,配套两个新公开头(net/uvcpp_loop_worker.h、net/uvcpp_socket_handoff.h)与 socket 转手机制。新连接落哪条循环分两种形状(运行时探测,is_fanout() 可问):Linux 侧自 1.3.5-dev 起 n 条循环各自绑同一端口、内核分流(0 号自己也承载连接),Windows 上没有 SO_REUSEPORT,走的是「单接受者 + 无锁转手」,默认开着(只有 set_loops(1) 例外);那条路上有一笔量过的吞吐代价,口径与数字见 doc/multiloop-design.md。 |
| HTTP/2 流级背压 | 收方向的 pause_stream() / resume_stream()、发方向单流待发队列的上界(默认 4 MiB)、以及 peer_window_size()(1.2.25)。这是协议层机件,仓内没有应用层调用方 —— 框架侧不驱动它,所以 h2 上「边收边给」的流式请求体仍不可达,见 doc/http2-status.md。 |
| TLS 主机名校验 | uvcpp_ssl::set_verify_hostname()(1.2.24)—— 见下面「修复」的第一条。 |
1.2.x 修掉的问题里,三类值得单列:
- 安全 ——
tls_verify_mode::PEER_STRICT此前与PEER完全等价:两个值映射到 同一个SSL_VERIFY_PEER位,头注释自己也如实写着"主机名那一半从未实现"。于是它 挡不住「证书链可信、但签发给别的域名」的对端 —— 而那正是中间人最省事的一种做法。 现在客户端把connect()收到的那个名字钉给证书(数字 IP 字面量走X509_VERIFY_PARAM_set1_ip_asc(),其余走set1_host()),名字对不上的对端 建立不起来(1.2.24)。服务端仍与PEER等价:本库的服务端不发 SNI、也不要求 客户端证书,没有可校验的名字。 - 内存 —— 内存池四条释放路径的缺陷(SUPER 静默泄漏、三处 use-after-free、
两块内存两套释放器混用);
max_total_memory从假限额改成按实占字节记账; 池的元数据不再走全局operator new(在那之前,接上池会卡死)。 - 公开契约 —— 三个从来没有编过、也从来没有链过的公开 API 修好并各自配上用例
(
DEFINE_FUNC_REQ_CPP宏里一处重复定义、两处写错的名字);uvcpp_handle的拷贝 构造、拷贝赋值与clone()从「静默泄漏 / 破坏活句柄」改成编译期拒绝。
1.2.x 期间有三笔性能改动,各自带读数。三组来自不同装置与不同轮次,不能互相加减,
也不能与 doc/benchmark.md 那页的 75 k 单循环口径混引:
- 队列里攒着的响应合成一次写 —— 段/请求从 3.43 降到 2.0,同装置配对跑 RPS +46%(对照 38 839 RPS / 服务端 23.57 µs 每请求,其中约 74% 花在内核);
- 每请求分配次数 16.00 → 13.00(2 848.2 B → 1 984.2 B);
- 读缓冲不再清零 —— 此前每请求白清 128 KiB;交错 A/B 读数 QPS 101 251 → 115 085。
相对上一次发布(v1.2.0),这一版动过的东西分两类。
一、导出符号被删除 —— 旧代码会编不过(删的理由见
doc/lowlevel-guide.md §10;删的两个 clone() 全仓零调用点,
而拷贝句柄本来就是泄漏或破坏活句柄)
| 删掉的符号 | 档 |
|---|---|
uvcpp_handle 的拷贝构造与拷贝赋值 |
1.2.5 |
uvcpp_handle::clone() |
1.2.5 |
uvcpp_req::clone()(uvcpp_req 的拷贝本身保留) |
1.2.5 |
还有一处只是导出符号变了、布局没变:uvcpp_web_context 的构造函数多了一个前置的
凭证参数(1.2.20)。它不构成实际破坏 —— 那个构造函数在此之前是私有的,类外既没有
也不可能有调用点 —— 写在这里只是让这份清单完整。
二、布局变了 —— 旧头文件配新库会读到错的偏移
| 类 | 变的是什么 | 档 |
|---|---|---|
uvcpp_h2_stream |
新增 paused / paused_owed 两个成员,sizeof 变了 |
1.2.25 |
uvcpp_ssl_context |
新增私有成员 verify_mode_ |
1.2.24 |
uvcpp_memory_pool |
私有成员换过(destroying_ → held_bytes_) |
1.2.x |
uvcpp_http_server、uvcpp_ws_server、uvcpp_ws_sessions、uvcpp_tcp_server、uvcpp_web_app |
私有布局重排(每循环一格容器、状态位改原子量),多循环那一批 | 1.2.21–1.2.23 |
uvcpp_web_file_transfer、uvcpp_web_response |
各新增私有成员(静态分片下发那一支的块级名额闸门:gate_ / gate_held_ / gate_wakeup_ / gate_waits_ 与 file_gate_),sizeof 变了 |
1.3.6-dev |
三、还有一条编得过、单循环下也对,只有多循环才会读到错的数据
uvcpp_web_app::connections() 与 connection_count() 同名同签名,语义变了:
connections() 现在返回本循环那一份登记表,connection_count() 变成所有循环求和,
逐循环要用 connection_count_at(int)。单循环(n == 1)下行为与旧版等价,
只有 set_loops(n > 1) 时才不同 —— 旧代码会编过,然后读到错的那一份。
另有两处行为变化,不改代码也能观察到:uvcpp_h2_session 的 on_request_end 现在
只要 end_stream 为真就触发(裸 GET 也来,且紧跟 on_request),原先只在有 body
时触发;submit_data() 新增返回码 UV_ENOBUFS(待发队列超上界)。
1.1.1 → 1.1.35 这 35 个开发档全部收进这一版。无破坏性改动,但有两处 ABI
变化(见本节末)。
| 新增 | 说明 |
|---|---|
| HTTP/2 | nghttp2 集成、ALPN、h2 会话与连接层。uvcpp_web_app 零配置自动协商(ALPN 挑 h2 / h1.1);低层的 uvcpp_http_client / uvcpp_http_server 仍默认 HTTP/1.1,要 h2 得显式 set_http2_enabled()。明文上不做 h2c:没有 ALPN 就没有协商,自动"升级"只能是猜。 |
| 发布包里的调试档 | 每个 zip 同时有发布档与调试档(uvcppd.dll / libuvcppd.dll / libuvcppd.so),MSVC 那份另带 uvcppd.pdb。它是用来调试的,不是用来发布的 —— 前提见下面「调试档」一节。 |
| 性能 | 静态响应的压缩变体缓存(命中时不再重复 deflate);write() 先试 uv_try_write(),装得下的部分不进待写缓冲;响应体零拷贝、与响应头合成两个 write block(nbufs = 2)一次发出。 |
doc/benchmark.md 是在用本库构建出的一个真实 webapp(静态文件 +
动态路由 + 上传下载 + WebSocket + TLS)上跑出来的读数,不是估算:
- 每条空闲连接 4.62 KiB(4 734 B)—— 八档最小二乘、R² = 0.9999869、除原点外每点 残差 ≤ 0.51%,外推 100 万连接 ≈ 4.42 GiB;
- 单事件循环 75 k RPS(流水线 10–50 深可到 88 k);静态 44 k RPS / 107 MB/s;
- 10 分钟长跑 3 840 万请求 / 0 错误,RSS 漂移 0.2%。
那一页还把这个数与 Hical 的自报数据做了对照:单位口径
是查实的(Hical 标 KB/GB,但它那两个数只在二进读法下自洽 —— 17.44 × 1024 × 10⁶ B = 16.63 GiB 正好对上它自己写的 ~16.6 GB —— 两边本来就同口径,不必折算),剩下的口径
问题(内核 TCP 缓冲计不计入、平台不同)逐条写明,没有单选对本库有利的读法;归因未
闭合的那组(TLS 稳态代价)同样说明了为什么不放进去。读数测于 1.1.33,该页 §1 逐个
commit 说明为什么它对 1.2.0 仍然成立。
四类问题一并收掉。逐条的一档号在 CHANGELOG.md 里 —— 那边是唯一的清单,这边不抄第二份(两份手写的清单正是本仓已经栽过的形状)。
- 协议正确性 —— 错误路径伪造状态码、中途断流送出假的
200、206被压缩 (Content-Range与Content-Encoding本就互斥)、HEAD与GET响应头不一致、Accept-Encoding里显式的q=0被*覆盖、跨读边界的请求被吞、请求头/URL 超长在接收时就按431/414拒掉;WebSocket 侧服务端强制客户端掩码并校验 文本帧 UTF-8,客户端也真的掩码了。 - 内存 —— 大块分配重新计入
span->in_use(此前每块漏掉一整个 span)、uvcpp_write第二个写槽被顶替时泄漏、内存池在用块数不跟分配走、压缩变体表的 字节账与表内容脱节。 - 生命周期 —— h2 拆解的两处 use-after-free 与一处泄漏、TLS 握手留下孤儿定时器、
从自己的回调里销毁
tcp_client累积包装对象、对端断开时 HTTP 客户端的关闭 观察者永不触发。 - 打包与消费方契约 —— 模块使能宏随包发布,宏集合与 dll 不一致从"静默读错偏移"
改成编译期硬失败;22 个公开头补 UTF-8 BOM,消费方不传
/utf-8不再级联报错; Linux 包的libuvcpp.so从bin/挪到lib/;六个打包 job 漏传UVCPP_ENABLE_NGHTTP2导致 h2 包缺模块。 - 发布门槛(本版新加) —— 包里的两档库各自自报家门(Windows 看导入表与
RSDS,MinGW/Linux 看调试节里的本库源文件名),staging 把两档装反时拒绝出包, 而不是发一份文件名全对、内容错了的包。
两处 ABI 变化:uvcpp_buf(1.1.28)与 uvcpp_http_server(1.1.34)的布局
变了。要重编,不能只换二进制 —— 旧头文件配新库会读到错的偏移。
(1.2.x 期间追加的 ABI 变化不记在这里,它们属于下一版 —— 见上面
v1.3.0 的「换二进制之前」。)
v1.1.0 在 v1.0.0 的 libuv 封装之上,新增了网络层、HTTP/1.1 与 WebSocket、TLS、 以及一套 Web 应用框架,并首次提供预编译的动态库。全部为向后兼容的增量。
uvcpp_tcp_server/uvcpp_tcp_client— TCP 服务端与客户端uvcpp_udp_server/uvcpp_udp_client— UDP 服务端与客户端uvcpp_net_read— 统一的读回调
uvcpp_http_server/uvcpp_http_client— HTTP 服务端与客户端(含流水线)uvcpp_http_parser/uvcpp_http_request/uvcpp_http_response— 基于 llhttp 的解析与报文构造uvcpp_http_compress— gzip / deflate 压缩(zlib)uvcpp_static_server— 静态文件服务uvcpp_ws_server/uvcpp_ws_client/uvcpp_ws_parser/uvcpp_ws_frame/uvcpp_ws_sessions— WebSocket(RFC 6455)
uvcpp_ssl_context/uvcpp_ssl— TLS 上下文与连接(OpenSSL)
uvcpp_web_app+uvcpp_web_router+uvcpp_web_handler— 应用与路由uvcpp_web_middleware— 中间件uvcpp_web_static/uvcpp_web_mime/uvcpp_web_file— 静态资源与文件下发uvcpp_web_stream— 流式响应uvcpp_web_multipart/uvcpp_web_upload— multipart 上传uvcpp_web_json— JSON(nlohmann/json)uvcpp_web_ws/uvcpp_web_ws_client— 接入 webapp 的 WebSocketuvcpp_log/uvcpp_log_console— 日志
低层(uvcpp_loop / uvcpp_tcp / uvcpp_pipe / uvcpp_udp / uvcpp_timer /
uvcpp_signal / uvcpp_process / uvcpp_fs* 这些句柄,与 uvcpp_write /
uvcpp_connect / uvcpp_fs / uvcpp_work / uvcpp_getaddrinfo 这些请求)之外,
本库另有 net / web / webapp / ssl / db 五个高层模块,与 expand / wsdl / capi 三个。
逐个类的清单在 README.md 的功能特性一节 —— 那边是唯一的
清单,此处不抄第二份。
C++11 或更高、CMake 3.20+、libuv 1.0.0+,支持 Windows / Linux / macOS。可选依赖:
OpenSSL(TLS;QUIC 与 HTTP/3 还要 ≥ 3.2)、zlib(压缩)、brotli(响应体的第二种
Content-Encoding)、llhttp 与 nlohmann/json(HTTP / Web 框架,可由 FetchContent
自动获取)、yyjson(可选的第二个 JSON 解析后端)、pugixml(WSDL/SOAP)、ngtcp2 与
nghttp3(QUIC / HTTP/3)、libsqlite3 / libmysqlclient / libpq(数据库,至少一个 ——
预编译包三个后端都在,其中 MySQL/PG 的客户端库是运行时才加载的)。
各平台依赖与全部开关在 doc/build-guide.md。
从 clone 到跑绿的那条路,以及可以照抄的完整例子(每一个都被 CI 逐条编译过)在
README.md 的快速入门;examples/ 下另有可运行示例。
按主题汇总的清单在 CHANGELOG.md —— 那一段是唯一的清单,
这边不抄一份(两份手写的清单正是本仓已经栽过的形状)。逐版本的发布叙事就是
上面那些 ## vX.Y.Z 重点 小节,而每个已发布 tag 的清单条目在 CHANGELOG.md 末尾的
「逐个版本」里。
开发版线是怎么折叠的:1.1.1 → 1.1.35 收进 v1.2.0,1.2.1 → 1.2.25 收进
v1.3.0,1.3.1 → 1.3.33 收进 v1.4.0,1.4.1 → 1.4.4 收进 v1.5.0,
1.5.1 → 1.5.3 收进 v1.6.0。逐条的「这一版没做什么」在
doc/db-guide.md、doc/quic-guide.md 与
doc/http3-guide.md。
这一分界线以下是归档段落。它们记的是跨版本仍然成立的东西,不是某一版的新增,
所以 release_notes.py 按 tag 切出的 Release 正文里没有这几节 —— 要看它们,
来这份文件;要按版本看,翻上面的 ## vX.Y.Z 重点。
1.5.0 起每一档的发布档与调试档都是全功能的:QUIC + HTTP/3 + C ABI
(UVCPP_QUIC_ENABLE / UVCPP_HTTP3_ENABLE / UVCPP_CAPI_ENABLE 都是 1,此前的发布包
这三项是 0 —— 逐条的来路与判据见上面「v1.5.0 重点」)。数据库模块自 1.5.3 起也进了
六条腿(UVCPP_DB_ENABLE 从 0 变 1),1.7.0 起 yyjson、brotli 与 db 的两个远程
后端也都进包 —— 「全功能」这句话到 1.7.0 才真的没有差集(在那之前 release.yml
既不传 UVCPP_ENABLE_YYJSON 也不传 UVCPP_ENABLE_BROTLI,MySQL/PG 更是显式 =OFF)。
包里的 db 模块 1.7.0 起三个后端都在。 六条腿各传这几个 -D:
-DUVCPP_ENABLE_DB=ON -DUVCPP_DB_SQLITE_FROM_SOURCE=ON \
-DUVCPP_ENABLE_DB_MYSQL=ON -DUVCPP_ENABLE_DB_PGSQL=ON \
-DUVCPP_DB_RUNTIME_LOAD=ON
1.6.x 及以前后两行是 -DUVCPP_ENABLE_DB_MYSQL=OFF -DUVCPP_ENABLE_DB_PGSQL=OFF,
而且是显式关的、不是"找不到":发布腿的 runner 上装着 libpq-dev,
find_package(PostgreSQL QUIET) 会静默成功,于是 libuvcpp.so 悄悄多一条
libpq.so.5 的 DT_NEEDED —— 而"装到别的机器上跑不起来"正是发布腿那条 ldd 断言要拦的
形状(名单里 sqlite3 / mysqlclient / pq 三枚都在)。1.7.0 换了一条路而不是继续关着
它:UVCPP_DB_RUNTIME_LOAD=ON 让这两个后端编进库里、但客户端库在运行时才
dlopen/LoadLibrary。理由是 DT_NEEDED(Windows 上是导入表)加载期解析 ——
链上就等于"没装过这两种客户端库的机器连加载这份包都做不到",哪怕它只跑 HTTP。
开了之后那种机器一切照常,只有真去连 MySQL/PG 时才会拿到
uvcpp_db_status::NO_DRIVER 加一句写清了试过哪些库名的话。所以 uvcpp_db_drivers()
现在打印 sqlite,mysql,postgres,而"这台机器上能不能用 MySQL"是运行期的事。
SQLite 那一份仍不链系统的:UVCPP_DB_SQLITE_FROM_SOURCE=ON 让配置期下一份
钉死哈希的 amalgamation、编成静态且带 PIC 的 uvcpp_sqlite3 链进去 —— 链系统的
libsqlite3.so 会多一条 DT_NEEDED,而 Ubuntu 24.04 上系统的 libsqlite3.a 不是 PIC。
两条理由的实测数据在 doc/db-guide.md。
这就是"自包含"这句话在 1.7.0 的准确写法:Linux 上 ldd libuvcpp.so 除 libc 与 C++
运行时(libstdc++.so.6 / libgcc_s.so.1,见下)之外仍然什么都不多(brotli 走
UVCPP_BROTLI_FROM_SOURCE=ON 静态链,MySQL/PG 根本不在链接行上);MinGW 的 DLL 依赖表
照样只有 Windows 自带的那几个模块。运行期的例外有两处:其一,真要连
MySQL / PostgreSQL,那台机器上得有对应的客户端库(libmysqlclient / libmariadb /
libpq,Windows 上是 libmysql.dll / libmariadb.dll / libpq.dll),或者用
UVCPP_DB_MYSQL_LIBRARY / UVCPP_DB_PGSQL_LIBRARY 指个路径;其二,C++ 运行时那一份
是基础库 —— libstdc++6 本身就是 apt 的依赖,任何能装软件包的 Linux 都有。
那两条 Linux 腿不再传 -DUVCPP_STATIC_RUNTIME,而这一条是被实测逼出来的:
这个开关在 UNIX 上会把 -static-libgcc -static-libstdc++ 同时塞进共享目标与可执行文件
的链接行,于是 libuvcpp.so 与每一个链接它的 C++ 可执行文件各带一份 libstdc++,同进程
两份;std::once_flag / std::__future_base 那族模板实例是 COMDAT、两边都进导出表,
动态解析按"可执行文件优先" ⇒ 一侧的 __once_proxy 会跳进另一侧的 _FUN。2026-10-14
那轮发布预演里 9 条用例当场崩(5 条 SEGV、4 条 SIGABRT,x64 与 arm64 逐条相同),全是
"跨线程 + future/promise"那一类;改用例救不了它:把 .so 摆成静态 libstdc++、只让
用例去链系统那份 libstdc++.so.6(正是任何一个普通使用者的形状),同一条用例照样
段错误,而把开关去掉之后 9 条与整棵 136 条用例零失败(其中 2 条 db 用例因没起数据库而
跳过)。Windows 两条 MinGW 腿照旧开着:
PE 没有全局符号作用域,不会互相顶掉,而 MinGW 用户机器上未必有 libstdc++-6.dll。
本版本提供 6 个平台(x64 与 arm64 × MinGW-w64 / MSVC / GCC)的预编译动态库,
每档都含发布版与调试版两份(v1.1.0 起就是 6 份,此前这里只列了 3 个 x64 ——
是这段写漏了,不是少发了包):
| 平台 | 工具链 | 发布档 | 调试档 |
|---|---|---|---|
| Windows x64 | MinGW-w64 (GCC) | libuvcpp.dll + libuvcpp.dll.a |
libuvcppd.dll + libuvcppd.dll.a |
| Windows arm64 | MinGW-w64 (GCC) | 同上 | 同上 |
| Windows x64 | MSVC (VS2022) | uvcpp.dll + uvcpp.lib |
uvcppd.dll + uvcppd.lib + uvcppd.pdb |
| Windows arm64 | MSVC (VS2022) | 同上 | 同上 |
| Linux x64 | GCC | libuvcpp.so |
libuvcppd.so |
| Linux arm64 | GCC | 同上 | 同上 |
每个 zip 内含 bin/(动态库)、lib/(导入库)、include/(公开头,含 libuv、
nlohmann/json、zlib 的头)、lib/pkgconfig/(uvcpp.pc 与 uvcpp-debug.pc)、
bindings/csharp/(C# 绑定与示例)与文档。
| 文件 | 说明 |
|---|---|
bin/libuvcpp.dll |
动态库。libuv / llhttp / zlib / brotli / OpenSSL / nghttp2 / ngtcp2 / nghttp3 / SQLite 以及 MinGW 运行时均已静态链接进去。MySQL / PostgreSQL 不在这张依赖表里:它们的客户端库是 UVCPP_DB_RUNTIME_LOAD=ON 下运行时才打开的,没装也只影响"真去连这两家" |
lib/libuvcpp.dll.a |
导入库(供 MinGW/GCC 链接,-luvcpp) |
bin/libuvcppd.dll |
调试档动态库(MSVC 那份叫 uvcppd.dll)。用法与前提见下面「调试档」一节 |
lib/libuvcppd.dll.a |
调试档导入库(-luvcppd);MSVC 那份是 uvcppd.lib |
bin/uvcppd.pdb |
仅 MSVC:调试档的符号文件,与 uvcppd.dll 同目录 |
include/ |
公开头文件,含 expand/(内存池)与 db/(数据库 — 公开头里不出现 sqlite3.h,三个后端都是 pimpl,所以用包里的 db 模块不需要装任何客户端库的头) |
include/uvcpp/uvcpp_config.h |
生成的模块使能宏。每个公开头自己包含它,使用者不必再传任何 -D(见下) |
bindings/csharp/ |
C# 绑定:UvcppNative.cs + UvcppNative.Protocols.cs(321 条 DllImport)与 examples/QuicEcho/(一个能跑的回环例子)。放在包里是为了让 C# 侧"这两份声明 + 这份动态库"就能开工,不必再回仓里捞 |
命名遵循各工具链的惯例:MinGW/GCC 产出
libuvcpp.dll,MSVC 产出uvcpp.dll。
MinGW 版 libuvcpp.dll 的依赖只有 Windows 自带系统库(KERNEL32 / WS2_32 / CRYPT32 /
ADVAPI32 / USER32 / SHELL32 / IPHLPAPI / USERENV / ole32 / dbghelp / msvcrt),
不需要安装 MSYS2 或 MinGW 运行时,任意 64 位 Windows 直接可用。
MSVC 版 uvcpp.dll 用 /MD 构建,因此 bin/ 里一并带了
msvcp140.dll / vcruntime140.dll / vcruntime140_1.dll,
不需要预装 VC++ 可再发行组件。若构建机上只有动态版 OpenSSL,bin/ 里还会多出
libcrypto-<主版本>-<架构>.dll 与 libssl-<主版本>-<架构>.dll —— 两个占位都随构建机上
那份 OpenSSL 变,所以不同平台包里的名字不同:已发布的包里 x64 是 libcrypto-4-x64.dll /
libssl-4-x64.dll,arm64 是 libcrypto-3-arm64.dll / libssl-3-arm64.dll。
(本机构建用的是静态版,因此本地这份包没有这两份。)
打包脚本不靠手写的依赖清单,而是读产物自己的导入表:凡是不是 Windows 自带的
模块,包里必须有,找不到就拒绝出包(tests/tools/package_release.py)。早先那张手写
清单是猜的 —— MSYS2 同时装了 libssl.a 和 libssl.dll.a,find_library 默认挑
.dll.a,产物会凭空多一个 libssl-<主版本>-x64.dll 而清单里没有。
它也不靠文件名认那两份库:文件名是给人看的,staging 把调试档拷成发布档的名字(或
反过来)时,所有基于名字的判据都会全绿而包是错的。所以两档各自要自报家门,
而且两个方向都判:Windows 上比对导入表里有没有 /MDd 那几份调试版 CRT、以及
PE 里有没有 RSDS 指向自己的 .pdb;MinGW / Linux 上比对调试节里有没有本库自己的
源文件名(发布档按 -O3 -DNDEBUG 编,一个都不该有 —— 而"有没有 .debug_* 节"
不是判据:静态链进去的依赖本身就带着 355 KB 调试节)。
.pdb 还会与 .dll 对源:比 PE 里 CodeView 记录的 GUID+age 与 PDB 里 stream 1
的 GUID+age,对不上就拒绝出包 —— 一个配错符号的 .pdb 比没有 .pdb 更坏,它会让
调试器停在一份不相干的源码上。
⚠️ 两个 Windows 版互为替代、不可混用:由 MinGW-w64 编译的动态库不能被 MSVC 链接,反之亦然(C++ ABI 不同)。用哪套工具链就用哪个 zip。
每个 zip 里除了发布档,还有一份调试档:文件名多一个 d(libuvcppd.dll /
uvcppd.dll / libuvcppd.so),与发布档同放在 bin/(Linux 是 lib/),导入库同在
lib/,.pc 是 uvcpp-debug(-luvcppd)。MSVC 那份还带 bin/uvcppd.pdb。
它是用来调试的,不是用来发布的,四条前提:
- MSVC 的调试档用
/MDd链,所以包里没有、也不会带msvcp140d.dll/vcruntime140d.dll/ucrtbased.dll—— 微软的调试版运行库不可再分发, 只在 VS 安装树里。要跑调试档,机器上得装了 Visual Studio,或者把那几个目录挂到PATH上(…\VC\Redist\MSVC\<版本>\debug_nonredist\x64\Microsoft.VC143.DebugCRT\与…\Windows Kits\10\bin\<版本>\x64\ucrt\)。不挂的话每个程序都以0xc0000135(找不到 DLL)退出,看起来像库坏了,实际是缺那三份不可再分发的运行库。 打包脚本刻意不发它们,并且按成品再扫一遍确认没混进去。 - 两档不能混链:调试档只能链进
/MDd(MSVC)或带-g的程序,发布档只能链进/MD的程序。混着链是未定义行为,不是"能跑但慢一点"。 - 不要拿调试档做性能测量(
-O0,且带各种调试期检查)。 - MinGW / Linux 没有独立的符号文件:调试信息(DWARF)内嵌在动态库里,
包里没有、也不会有
.pdb之类的东西。发布档不带-g,要调试就用调试档。
使用者不需要传任何 -D。 模块使能宏由包里的
include/uvcpp/uvcpp_config.h 给出,每个公开头都会在自己第一个模块守卫之前
包含它,所以只要 -I 指对,宏就自动与这个 dll 一致:
export PKG_CONFIG_PATH=/path/to/libuvcpp-1.5.0-mingw-x64/lib/pkgconfig
g++ -std=c++11 $(pkg-config --cflags uvcpp) your_app.cpp $(pkg-config --libs uvcpp) -o your_app.exe
⚠️ --cflags与--libs要分开写、--libs要放在源文件之后。 合成一条$(pkg-config --cflags --libs uvcpp) your_app.cpp会把-luvcpp排到源文件前面,ld不会回头再看一遍归档文件 ⇒ 28 个undefined reference(库本身没问题,纯粹是命令行顺序)。
不用 pkg-config 时,等价的命令行是:
g++ -std=c++11 -I include your_app.cpp -L lib -luvcpp -o your_app.exe要链调试档就把包名与库名都换成带 d 的那个(-luvcppd);头文件是同一份,
一个字节都不差:
g++ -std=c++11 -g -I include your_app.cpp -L lib -luvcppd -o your_app.exe(上面两条命令都在包的根目录下执行;-L lib 是库所在处。跑的时候
bin/libuvcpp.dll 要在 PATH 上,或直接拷到 exe 旁边;Linux 那份 .so 就在
lib/ 里,用 LD_LIBRARY_PATH=$(pkg-config --variable=libdir uvcpp) 或 rpath 指过去。)
Linux 包里的
libuvcpp.so从 1.1.28 起放在lib/。在那之前它放在bin/, 而.pc里写的是-L${libdir} -luvcpp、lib/是空的 —— 也就是说照本文档在 Linux 上根本链不上(ld: cannot find -luvcpp)。Windows 两套不受影响, 仍是bin/*.dll配lib/里的导入库。
MSVC 消费者不需要额外传
/utf-8。仓内头文件里含中文注释的那批本来靠add_compile_options(/utf-8)兜着,而那个开关是目录作用域的 —— 既不进导出集, 也到不了预编译包的消费者。按系统代码页(936)读这些头时,中文注释的末字节会吞掉 换行、把*/吃掉,注释不闭合,报错却落在<algorithm>里(C4819是唯一的线索)。 因此 1.1.27 起,打包脚本给所有含非 ASCII 的头补了 UTF-8 BOM,MSVC 会据此自动 按 UTF-8 读,什么开关都不用加。包里的头因此与源码树里的不逐字节相同(多一个 BOM), gcc/clang 前导 BOM 一样接受,各平台包保持同一份字节。
⚠️ 不要自己定义这些宏。 如果你显式传了一个与包不一致的值 (-DUVCPP_OPENSSL_ENABLE=0拿到一个开着 OpenSSL 编的包),uvcpp_config.h会直接#error停编译,并指出包里那个值。 这是刻意的:宏不一致时公开头里的成员布局会与 dll 不同, 内联访问器按错误偏移读成员,拿到的是一个天文数字, 没有编译错误、没有链接错误、没有运行时告警 —— 比编不过危险得多。 需要别的宏集就重新构建 uvcpp,不要从这一侧改。1.1.26 及更早的包需要你手工传一串
-D,且那份清单是写死的 (与包实际怎么编无关)。升级到本版时把那些-D删掉即可; 留着它们只有在与包冲突时才会报错,值相同时无害。
⚠️ 换 dll 而不重编消费者会崩 —— 本条自 1.1.28 起生效,且有两次实例。 拿新的uvcpp.dll配旧的 exe / obj,会在启动或首次用到那个类时崩:0xC0000409(栈缓冲溢出)或0xC0000374(堆损坏),在 ctest 里表现为 0.01 秒、一行输出都没有。这个形状最容易被当成环境问题查很久。
- 1.1.28:
uvcpp_buf加了共享视图(多一个shared_ptr成员)与移动 构造/移动赋值。- 1.1.34:
uvcpp_http_server去掉了一个私有成员(压缩变体表的字节 计数器 —— 改成从表里算),sizeof因此变了。两次都是加/删私有成员:消费者源码一行都不用改,但必须重新编译。 一句话:换了 dll 就必须重编,别只换二进制。
1.1.28 同版还有两条行为变化(不崩,但要知道):
uvcpp_buf从前没有移动构造(本类有用户声明的拷贝构造与析构,编译器 因此不会隐式生成),uvcpp_buf b = std::move(a);一直是静默的深拷贝、a原封不动;现在a会被搬空。对"move 之后接着用a"的调用点, 这是行为变化(虽然那几乎肯定是写错了)。uvcpp_http_server::send_response的返回类型从void变成size_t(实际发出去的字节数)。返回类型不进名字修饰,不会造成链接错误。
头文件的入口是包根 include/uvcpp.h(聚合头,含 loop / handle / req)。
net / web / webapp / ssl 的类不在聚合头里,按模块显式 include,例如
#include "handle/uvcpp_tcp.h"、#include "web/uvcpp_http_server.h"。
早先 MinGW-w64 打开 UVCPP_BUILD_EXPAND=ON 会让 ctest 80 项里 7 项崩溃
(0xc0000374 堆损坏或 SegFault),而同一份源码用 MSVC 构建全绿。
根因已定位并修复,两个平台现在都是 80/80。
根因不属于内存池的数据结构,而在于线程缓存由谁销毁:
- MinGW-w64 下 DLL 里的
thread_local走的是 emutls(libgcc 在堆上按线程 分配的数组),带非平凡析构的thread_local对象则经__cxa_thread_atexit登记析构。线程退出时 emutls 先把那个数组还给了堆,登记的回调才被调用 —— 于是析构函数 walk 的是已释放、已被复用的内存:读出来的span是野指针, 紧接着在central_cache::push里对它做 CAS。写进只读页就是访问违例,写进 可写页就是堆损坏;崩在哪个地址全看那块内存被谁捡走了。 - MSVC 没有 emutls,所以一直是对的。这解释了全部既有现象:只在 MinGW 复现、 只在开池时复现、崩溃点在"线程退出"、改成共享运行时会更糟(libgcc 的释放 时机变了)。
修法是不再依赖 thread_local 对象自己的析构函数:线程缓存改由 OS 的线程
存储槽管理(Windows FlsAlloc / POSIX pthread_key_create),槽的析构回调
把缓存指针当参数收进来,回调自身一个字节的 TLS 都不读,因此不存在
"读一个已经被释放的 TLS 槽"这回事。
因此本版发布的两个 Windows 动态库都带内存池(UVCPP_BUILD_EXPAND=ON,
产物里 UVCPP_ENABLE_MEMORY_POOL=1,由 include/uvcpp/uvcpp_config.h 带出来)。
但 CMake 的默认值仍然是 OFF —— 从源码构建要用池,得显式传
-DUVCPP_BUILD_EXPAND=ON。
这里原来给的理由是「默认开的话,使用者的 TU 忘了定义 UVCPP_ENABLE_MEMORY_POOL,
宏求值为 0 ⇒ 使用者侧走 std::malloc、dll 侧走池,库会拿池去 free 一个 malloc
的指针,静默堆损坏」。这条与现在的实际行为对不上:uvcpp_config.h 生成之后
它经 uvcpp_alloc.h 等公开头带进来,规则是「外部定义过且与本包不同 ⇒ #error;
没定义 ⇒ 取本包的值」。所以"忘了定义"拿到的是包的值,两边一致;定义成别的
值是编译期硬失败。默认关因此只是"从源码构建时默认拿到哪套分配器"的选择,不再是
安全守卫 —— 用预编译包则什么都不用传。
cmake --install在 Windows 腿上是坏的:libuv 由FetchContent_MakeAvailable引入,它登记的 install 规则引用了一个从未构建的libuv.dll,且它的规则排在 本项目的规则之前 —— 一失败就整体中止,本项目的头文件与库一个都装不出来 (Linux 上没有这个.dll,同一条 install 实测走得通,2026-10-12 验过)。 本次的 zip 绕过它、照CMakeLists.txt:3099-3579的规则手工组装,与 install 只剩一处刻意的差异:libuv 的头放在include/顶层(本库的公开头写的是#include <uv.h>,放进include/libuv/会找不到)。原先还有第二处 —— zip 里 补了zlib.h/zconf.h,而 install 规则里没有这一条 —— 2026-10-12 起 install 规则自己也装这两个头(CMakeLists.txt:3593-3596),两边落到同一个 位置,这处差异消失。
这条曾经是个真问题,现在只剩一半。 预编译包的消费者从来不受影响
(打包脚本给含非 ASCII 的头补了 BOM),受影响的一直是 find_package(uvcpp)
与直接把 src/ 加进 -I 的那条路。
当时:src/ 下有一批公开头是无 BOM 的 UTF-8。MSVC 按系统代码页(936/GBK)解码,
中文注释的末字节吞掉换行、把 */ 吃掉,注释不闭合,报错却落在 <algorithm> 里;
C4819 是唯一的线索(warning C4819: 该文件包含不能在当前代码页(936)中表示的字符)。
仓内编译看不出来,有两个原因叠加:顶层 CMakeLists.txt 里
if(MSVC) add_compile_options(/utf-8) endif() 是目录作用域的 —— 导出集里
INTERFACE_COMPILE_OPTIONS 是空的,find_package 的消费者拿不到它;而
install(FILES ...) 是把源码树那几个头原样拷出去,BOM 不会凭空多出来。
现在(2026-09-21 实测):src/ 下 99 个 .h 里 97 个带 BOM,剩下 2 个
(expand/uvcpp_memory_pool_span.h、expand/uvcpp_page_allocator.h)是纯 ASCII、
不含非 ASCII ⇒ 没有一个头还会踩这个坑。package_release.py 里那道"含非 ASCII
又没 BOM 就补一个"的下限仍在,但它现在不会命中任何东西。
仍未修的那一半:add_compile_options(/utf-8) 依旧不进导出集,
INTERFACE_COMPILE_OPTIONS 依然是空的。所以你自己写的头若含非 ASCII 又没 BOM,
照样会中招 —— 给自己的目标加 /utf-8(或 /source-charset:utf-8),
或者把源文件存成带 BOM 的 UTF-8(这也是本仓的约定)。
感谢所有为这个项目做出贡献的人!
MIT License