src/web/ 的 HTTP 半边有四个类:
| 类 | 干什么 | 头文件 |
|---|---|---|
uvcpp_http_server |
建 TCP 服务、每条连接一个解析器、路由、发响应 | <web/uvcpp_http_server.h> |
uvcpp_http_client |
发请求、收响应,异步/同步双模式 | <web/uvcpp_http_client.h> |
uvcpp_http_parser |
llhttp 的包装:把字节变成 uvcpp_http_request / uvcpp_http_response |
<web/uvcpp_http_parser.h> |
uvcpp_static_server |
静态文件托管(懒加载 + mtime 校验 + 线程池读盘) | <web/uvcpp_static_server.h> |
外加三个被前后两者共用的值类型:uvcpp_http_request、uvcpp_http_response
(同一对类在两个方向上用,见 §5)、以及
<web/uvcpp_http_common.h> 里的枚举与工具函数。
这一层是自足的:它完全不依赖 webapp/。 src/web/ 里没有任何
#include <webapp/...>。反过来 webapp/ 大量包含 web/(src/webapp/uvcpp_web_app.h:137-138
拿的就是 web/uvcpp_http_server.h 与 web/uvcpp_ws_server.h)。
本页讲
web/的 HTTP 半边。WebSocket 在 web WS 指南, 应用框架在 webapp 应用框架开发者指南,低层 TCP 在 net 网络层指南,TLS 在 TLS 指南, HTTP/2 在 HTTP/2 低层指南。签名、默认值、行为都对着当前源码核过,非显然的结论后面都跟了
文件:行号。
uvcpp_http_server 不是从零写的 socket 服务器 —— 它建在 uvcpp_tcp_server
之上(src/web/uvcpp_http_server.cpp:263-265:构造函数第一件事就是
new uvcpp_tcp_server()),每条连接配一个独立的 uvcpp_http_parser 上下文
(src/web/uvcpp_http_server.h:1752-1752 的 contexts_ —— 装的是每连接状态,
单循环时它是一张 std::map<uvcpp_tcp_client*, conn_ctx>;多循环下它按循环分成
若干张,取表见 doc/multiloop-design.md §4.1)。
三条贯穿全篇的结论:
一、连接的生死归 uvcpp_tcp_server,不归 http 层。 新连接在业务回调之前
就被登记并装好收尾(src/net/uvcpp_tcp_server.h:53-58),关闭时框架摘除并 delete
(src/net/uvcpp_tcp_server.cpp:129-131)。http 层那张 contexts_ 表不拥有
uvcpp_tcp_client。所以地址会被复用 —— 判"还是不是原来那条连接"要用
connection_generation()(§6),不能比指针。
二、没有统一的 uvcpp_web_next。 那是 webapp/ 层的东西
(<webapp/uvcpp_web_handler.h>)。web 层的异步续跑只有一条路:resp.deferred = true。
三、web/ 有 h2,但入口有三条。 服务端 set_http2_enabled(true)
(src/web/uvcpp_http_server.h:357-357)加 TLS+ALPN,服务端 set_h2c_enabled(true)
(明文先验知识,RFC 9113 §3.4),客户端 set_http2_enabled(true)
(src/web/uvcpp_http_client.h:474-474)。
前两条都默认关;只有 webapp/ 框架层默认开、零配置自动协商。
不做 Upgrade: h2c 协商(RFC 9113 已删除该机制),也不做嗅探 —— 明文这条腿
只认那 24 个字节。
整个 HTTP 半边包在 #if UVCPP_WEB_ENABLE 里(src/web/uvcpp_http_server.h:19)。
三个块是可选的,各自有自己的宏(压缩那块有两个来源,见下表):
| 功能 | 宏 | 关掉之后 |
|---|---|---|
| 响应压缩(gzip / deflate) | UVCPP_ZLIB_ENABLE |
set_compression_enabled() 那一组消失 |
响应压缩(br) |
UVCPP_BROTLI_ENABLE |
少了 br 这一种编码,其余照旧(§10) |
TLS / https:// |
UVCPP_OPENSSL_ENABLE |
没有 set_ssl_context()、没有 send_wait_ssl() |
| HTTP/2 | UVCPP_NGHTTP2_ENABLE |
set_http2_enabled() 那一组消失 |
两行"响应压缩"是同一个块的两个来源:压缩器的总开关是
UVCPP_HTTP_COMPRESS_ENABLE,它由那两个宏或出来(CMakeLists.txt:1819-1819)——
任一为 1 就有这个块,两个都为 0 才整块消失。三个"块"的开关都默认 OFF,brotli
那条也是(CMakeLists.txt:222-236 那一组 option(),四个块开关默认全是 OFF)。编译进 UVCPP_SRC_FILES 的是
src/web/*.cpp 全体(CMakeLists.txt:1457-1457),但每个 .cpp 里的功能块同样带宏。
uvcpp_http_server 自己不碰 TLS —— 它没有任何 SSL API。要上 https 必须
get_tcp_server()->set_ssl_context(...),而且必须在 listen() 之前
(tests/functional/web_ssl_h2_client_func.cpp:185)。握手超时归
uvcpp_tcp_server(默认 10000 ms,src/net/uvcpp_tcp_server.h:792)。
这就是 src/web/uvcpp_http_server.h:163-172 那段官方示例(我把它补成了完整 TU):
#include <web/uvcpp_http_server.h>
#include <web/uvcpp_http_response.h>
#include <web/uvcpp_http_request.h>
#include <net/uvcpp_tcp_client.h>
int main() {
uvcpp::uvcpp_http_server server;
server.get("/hello", [](uvcpp::uvcpp_http_request& req,
uvcpp::uvcpp_http_response& resp,
uvcpp::uvcpp_tcp_client* client) {
(void)req;
(void)client;
resp = uvcpp::uvcpp_http_response::ok("world", 5);
});
if (server.bind("0.0.0.0", 8080) != 0) return 1;
if (server.listen() != 0) return 1;
server.run(); // 默认 UV_RUN_DEFAULT
return 0;
}bind / bindIpv4 / bindIpv6 / listen 四个都在这里转调 uvcpp_tcp_server,
返回 libuv 错误码(src/web/uvcpp_http_server.cpp:622-651)。listen 的默认 backlog 是
128(src/web/uvcpp_http_server.h:218-218)。四个里只有 listen 多做一件事:转调之前
先把每循环的上下文表按 loop_count() 定型 —— 多循环时工作循环的线程是在
set_loops() 里放行的,等它接手连接再取表就晚了(doc/multiloop-design.md §4)。
uvcpp_http_server 的公开 API 里没有 close()。 唯一能"关服"的是 stop()
(:385),而它 = tcp_server_->stop():只关监听句柄,不动已建立的连接
(src/net/uvcpp_tcp_server.h:490-503)。已经在处理的请求照跑。
#include <net/uvcpp_tcp_server.h>
#include <web/uvcpp_http_server.h>
void doc_graceful_shutdown(uvcpp::uvcpp_http_server& server) {
server.stop(); // 1) 不再收新连接
server.get_tcp_server()->close_all_clients(); // 2) 送走剩下的
}想一步到位就两个都调。连接级的 close_connection() 是 private
(src/web/uvcpp_http_server.h:1341-1341)—— 单个连接只能从 uvcpp_tcp_client 那侧关。
注册 API 全部返回 void,没有失败通道(src/web/uvcpp_http_server.h:463-496):
#include <net/uvcpp_tcp_client.h>
#include <web/uvcpp_http_server.h>
#include <web/uvcpp_http_request.h>
#include <web/uvcpp_http_response.h>
void doc_routes(uvcpp::uvcpp_http_server& server) {
server.get("/api/users", [](uvcpp::uvcpp_http_request& req,
uvcpp::uvcpp_http_response& resp,
uvcpp::uvcpp_tcp_client* client) {
(void)req; (void)client;
resp = uvcpp::uvcpp_http_response::ok("[]", 2, "application/json");
});
server.post("/api/users", nullptr); // 同上,签名完全一样;存的是函数对象
server.put("/api/users", nullptr);
server.del("/api/users", nullptr);
server.options("/api/users", nullptr);
server.patch("/api/users", nullptr);
server.head("/api/users", nullptr);
// 兜底:所有没命中的请求都到这儿。单槽,后装覆盖前装。
server.on_request([](uvcpp::uvcpp_http_request& req,
uvcpp::uvcpp_http_response& resp,
uvcpp::uvcpp_tcp_client* client) {
(void)req; (void)client;
resp = uvcpp::uvcpp_http_response::not_found();
});
}匹配规则(src/web/uvcpp_http_server.cpp:964-977):剥掉 query 再逐字节比路径,
线性扫描,首次命中即返回。 于是:
- 没有参数、没有通配、没有正则、没有中间件(
src/webapp/uvcpp_web_router.h:9-14也是这么描述它的)。 - 重复注册同一路径时先注册的赢,后面的永远不可达 —— 而 API 返回
void, 没有任何提示。 - 都没命中 →
default_handler_;default_handler_为空 → 直接 404 (src/web/uvcpp_http_server.cpp:1499-1504)。 - 分不出 404 和 405:
get("/x")已注册时来一个POST /x,走的还是兜底 或 404,不会回 405。
路由表就是个 std::vector(src/web/uvcpp_http_server.cpp:939-962),
listen() 之后仍可以改;但它没有锁。
web 层不提供任何 query / cookie / form / URL 解码工具,req.url 就是原始请求目标
(含 ?query)。 路由匹配时会把 query 剥掉再去比(src/web/uvcpp_http_server.cpp:966-969),
但解析 query 是 handler 自己的事 —— 这一层没有这个函数。要用就在 webapp/ 的
web_parse_query()(src/webapp/uvcpp_web_util.h:179)或者自己写一个。
除精确路由外还有两条:
post_stream(path, http_stream_handler)(:250):只支持 POST,命中后 body 不再被服务器缓冲,直接以HEADERS/BODY/END三个事件交给你。set_stream_claim(hook)(:263):post_stream没中时问它。它返回一个http_stream_handler(认领)或空(不认领)。webapp/正是靠它把自己的路由器 接进来的(src/webapp/uvcpp_web_app.cpp:1970)。
stream_handler 拿到的 req 是 ctx.stream_request,头部视图只在当次调用内有效,
要留就自己拷(src/web/uvcpp_http_server.h:119-121)。
同一对类在两个方向上用,靠"你拿它干什么"区分,类本身不区分:
| 类 | 服务端 | 客户端 |
|---|---|---|
uvcpp_http_request |
解析结果(handler 的入参) | 待发送报文(to_string() 序列化) |
uvcpp_http_response |
待发送报文(handler 填) | 解析结果(回调的入参) |
两侧都有 static from_parser(const uvcpp_http_parser&, const uvcpp_buf& body)
(src/web/uvcpp_http_request.h:140-140、src/web/uvcpp_http_response.h:195)。
两边都是公开字段 + 少量方法,不是 getter/setter 墙:
#include <cstdint>
#include <string>
#include <web/uvcpp_http_common.h>
#include <web/uvcpp_http_request.h>
#include <web/uvcpp_http_response.h>
void doc_req_resp_fields(uvcpp::uvcpp_http_request& req,
uvcpp::uvcpp_http_response& resp) {
// 请求:全是公开字段
uvcpp::http_method m = req.method; // 默认 HTTP_GET src/web/uvcpp_http_request.h:40-40
std::string u = req.url; // 默认 "/",含 query src/web/uvcpp_http_request.h:41-41
uvcpp::http_headers h = req.headers; // name 已转小写 src/web/uvcpp_http_request.h:43-43
uvcpp::uvcpp_buf b = req.body; // 公开字段,不是方法 src/web/uvcpp_http_request.h:44-44
(void)m; (void)u; (void)h; (void)b;
// 这三个是**按值返回**的,别写成 req.get_header("host")[0]
const std::string host = req.get_header("host", "localhost");
const std::string ct = req.content_type();
const bool has = req.has_header("range");
(void)host; (void)ct; (void)has;
// 响应:status_code / headers / body 也全是公开字段
resp.status_code = uvcpp::http_status::CREATED;
resp.status_message = "Created";
resp.set_header("content-type", "application/json");
resp.body.append_data("{}", 2); // uvcpp_buf,不是 std::string
}一、status_code 默认就是 200 OK。 src/web/uvcpp_http_response.h:40 的初值是
http_status::OK,不是"未设置"。所以用 err != 0 判失败,别用 status_code
(src/web/uvcpp_http_common.h:246-247 也是这么写的)。
二、resp = uvcpp_http_response::ok(...) 会把 stream_id 冲成 0。
stream_id(:59)由服务端在处理函数之前填好,整对象替换就丢了;
h2 连接上要么自己设回去,要么改用带 stream_id 的 send_response 重载
(src/web/uvcpp_http_response.h:52-58)。h1 上 stream_id 恒为 0,无所谓。
三、uvcpp_http_request 与 uvcpp_http_response 的拷贝构造/赋值是手写的、
逐字段列举的。 加字段时编译器一声不吭 —— stream_id 就漏过一次
(src/web/uvcpp_http_request.cpp:25-36、src/web/uvcpp_http_response.cpp:53-70)。
handler 返回之前没发响应,就再也没机会发了 —— 除非把 resp.deferred 置真。
框架见到它就直接 return(src/web/uvcpp_http_server.cpp:1510-1511),
由你自己在之后(且必须在 loop 线程上)把响应送出去。
"之后"用什么把响应送出去,取决于你在哪条传输上。req.reply 把这件事统一了:
它是这枚请求的「稍后回话」凭据(<web/uvcpp_http_reply.h>),框架在 h1 / h2 / h3
三条腿上都填好;兑现入口是 uvcpp_http_server::send_reply()。处理函数因此不必
知道自己在哪条腿上 —— 这正是它存在的理由。
#include <string>
#include <net/uvcpp_tcp_client.h>
#include <req/uvcpp_work.h>
#include <web/uvcpp_http_request.h>
#include <web/uvcpp_http_response.h>
#include <web/uvcpp_http_server.h>
void doc_deferred(uvcpp::uvcpp_http_server& server) {
server.get("/slow", [&server](uvcpp::uvcpp_http_request& req,
uvcpp::uvcpp_http_response& resp,
uvcpp::uvcpp_tcp_client*) {
// **按值**留一份凭据,别留引用:`req` 那份对象只活到本函数返回。
const uvcpp::uvcpp_http_reply reply = req.reply;
resp.deferred = true; // 声明"这次不由框架发"
uvcpp::uvcpp_work* w = new uvcpp::uvcpp_work();
// 投回**这条请求自己那条循环** —— 异步结果回来时人可能在任何线程上,
// 而 send_reply() 只在 loop 线程上合法。
w->queue_work(reply.loop(),
[](uvcpp::uvcpp_work* self) {
// 工作线程:这里绝不能碰 server / reply / resp
self->get_loop(); // 只为演示;真活放这儿
},
[&server, reply](uvcpp::uvcpp_work* self, int status) {
// loop 线程
if (status == 0 && server.reply_alive(reply)) {
uvcpp::uvcpp_http_response late =
uvcpp::uvcpp_http_response::ok("done", 4);
server.send_reply(reply, late);
}
delete self;
});
});
}凭据不保证"还能回话":它记住的是那一刻那条连接,而对端断开、连接被回收、
指针被新连接复用都可能在排队等结果的期间发生。所以写之前问一次
reply_alive()(代次对得上);send_reply() 自己也查,漏问的后果是"响应被丢掉"
(UV_ENOENT)而不是"发给别人"。valid() 是另一回事 —— 它只说明"这枚凭据是从
一条真请求上取下来的",取下来之后一直是真。三条传输上凭据背后是什么、为什么它
不是 uvcpp_http_request 那种带虚表的类,见 src/web/uvcpp_http_reply.h 的头注释。
h1 / h2 上还有一条逐字等价的老路:手里那个 uvcpp_tcp_client* 自己就是凭据,
代次号靠 connection_generation() 现取。它在 h1/h2 上仍然完全有效(库内
uvcpp_static_server 走过它很多年),但 h3 上不成立 —— 那条腿上第三个形参恒为
nullptr,没有 client 可记。新代码用 §6.1。
connection_generation()(src/web/uvcpp_http_server.h:847-847)是本层最容易被忽略、
又最容易出致命错的 API:返回 0 表示这条连接已经不在服务器的登记表里,此时不该写它。
代次号单调递增、永不复用,所以"连接还活着"时号不变、"地址被新连接复用"时号对不上
(:1711-1721)。is_connected() 就是 generation != 0(:850-850)。
send_response() 会把 resp.body 移走。 两块写(nbufs = 2)时 body 被
std::move,之后 resp.body.size() 是 0。要记"上线了多少 body"就用它的返回值
(src/web/uvcpp_http_server.h:677-690)。webapp 正是这么记访问日志的。
deferred 的标志位在检查之后会被清掉,所以那个 resp 对象不必一直活着 ——
但你要发送的那一份得自己留着。
stop() 与 close_all_clients() 都不唤醒写队列里的 done
(src/web/uvcpp_http_server.cpp:361-363 明说)。
先发头、再分块发体,三段式:begin_stream → write_stream × N → end_stream。
#include <string>
#include <net/uvcpp_tcp_client.h>
#include <web/uvcpp_http_request.h>
#include <web/uvcpp_http_response.h>
#include <web/uvcpp_http_server.h>
void doc_stream(uvcpp::uvcpp_http_server& server) {
server.get("/stream", [&server](uvcpp::uvcpp_http_request& req,
uvcpp::uvcpp_http_response& resp,
uvcpp::uvcpp_tcp_client* client) {
(void)req;
resp.status_code = uvcpp::http_status::OK;
resp.set_header("content-type", "text/plain");
resp.deferred = true; // 流式同样要声明"框架别发"
server.begin_stream(client, resp); // 只入队头部
std::string chunk = "hello ";
// h1 上写的是**已组好 chunked 帧**的字节;h2 上写的是裸 body(见下)
if (server.write_stream(client, chunk, [](int) {}) == 0) {
std::string tail = "world";
server.write_stream(client, tail, [](int) {});
}
server.end_stream(client, /*close_after=*/true);
});
}write_stream 的 done 契约(src/web/uvcpp_http_server.h:732-755):
- 入队后保证恰好一次,包括连接还在队列里就被关掉的场合;
- 但只有
write_stream返回 0 才有这个保证 —— 非 0 时done不会被调, 而且不要两边都调(:470-475); done绝不在函数返回前同步跑(:487)。
同名方法在 h1 与 h2 上含义不同,而且没有编译期区分手段
(src/web/uvcpp_http_server.h:729-776):
| h1 | h2 | |
|---|---|---|
write_stream(client, bytes, ...) |
bytes 是已组好 chunked 帧的字节 |
bytes 是裸 body |
end_stream(client, close_after) |
可能关连接 | 不关连接,只发 END_STREAM |
| 在 h2 连接上调 h1 那两个重载 | write_stream 返回 UV_EINVAL、end_stream 静默返回(src/web/uvcpp_http_server.cpp:1831-1831、src/web/uvcpp_http_server.cpp:1860-1860)/ begin_stream(client, resp) 打一条 stderr 后丢弃(src/web/uvcpp_http_server.cpp:1732-1737) |
— |
is_head 与 accept_encoding 是连接级单槽,h2 上必须按流记。
src/web/uvcpp_http_server.h:1016-1046 把三类后果写得很细;send_h2_response 里
是临时把"这条流的值借到连接级字段"再调压缩的
(src/web/uvcpp_http_server.cpp:3046-3054)。
on_upgrade(handler)(src/web/uvcpp_http_server.h:445)是单槽(src/web/uvcpp_http_server.cpp:925-927)。
它被调时这条连接已经从 http 解析里摘出来了,之后的字节归你;升级时已经读进来但
还没解析的字节用 take_upgrade_leftover(client)(src/web/uvcpp_http_server.h:460-460 / src/web/uvcpp_http_server.cpp:929-929)取走 ——
客户端把 Upgrade 请求与第一帧 WebSocket 数据包在同一个 TCP 段里发过来是合法的,
少了这一步那一帧就永远丢了。
attach() 之类的安装动作会覆盖已装的 on_upgrade,而且不报警
(WebSocket 那半边就是这么装上自己的升级处理器的,详见
web WS 指南)。
#include <string>
#include <handle/uvcpp_loop.h>
#include <net/uvcpp_tcp_client.h>
#include <web/uvcpp_http_client.h>
#include <web/uvcpp_http_request.h>
#include <web/uvcpp_http_response.h>
int doc_client_async() {
uvcpp::uvcpp_http_client client;
int rc = client.connect("127.0.0.1", 8080, [](int status) {
if (status != 0) return;
// 注意:这一段跑在连接完成回调里,禁止阻塞
});
if (rc != 0) return rc;
// 回调签名是 (const uvcpp_http_response&, int err)。
// 第一个参数是**整条**响应,不是流式;第二个是 libuv 错误码。
return client.send(uvcpp::uvcpp_http_request::make_get("/"),
[](const uvcpp::uvcpp_http_response& resp, int err) {
if (err != 0) return; // 失败一律看 err,别信 status_code
(void)resp.status_code;
});
}send / get / post 的回调签名逐字是
std::function<void(const uvcpp_http_response&, int)>(src/web/uvcpp_http_client.h:170-185)。
交付的 resp 是成员 pending_resp_(:449)的 const 引用 —— 存到下次 send()
就被覆写。
异步路径没有超时,connect() 也不设(src/web/uvcpp_http_client.cpp:56-60)。
要超时只能用 *_wait 系列。
构造里那个 close 观察者不能省(src/web/uvcpp_http_client.cpp:51-70):少了它,
"对端在响应收完前断开"时 send() 的回调永远不来。
在响应回调里 delete 客户端是预期的用法。 所有闭包都捕一个 alive_token_
弱引用(src/web/uvcpp_http_client.h:432-443),析构第一件事就 reset
(src/web/uvcpp_http_client.cpp:76)—— 你不需要为它做任何额外的事。
#include <string>
#include <web/uvcpp_http_client.h>
#include <web/uvcpp_http_request.h>
#include <web/uvcpp_http_response.h>
int doc_client_sync() {
uvcpp::uvcpp_http_client client;
int rc = client.connect_wait("127.0.0.1", 8080, 5000);
if (rc != 0) return rc;
uvcpp::uvcpp_http_response resp;
rc = client.send_wait(uvcpp::uvcpp_http_request::make_get("/"), resp, 5000);
if (rc != 0) return rc;
return static_cast<int>(resp.status_code);
}同一个 client 上同步与异步不能交叉,越界一律 UV_ENOTSUP
(src/web/uvcpp_http_client.cpp:271、:431、:574、:598)。
一、keep-alive 有两个方向:请求那头听调用方的,响应那头听对端的。
keep_alive_ 是调用方的偏好(src/web/uvcpp_http_client.h:427-427 初值 true、
:235 声明、src/web/uvcpp_http_client.cpp:841-841 setter)。set_keep_alive(false)
会让请求报文带上 connection: close(src/web/uvcpp_http_client.cpp:516-535);
默认的 true 则不补头,报文里那条 connection: keep-alive 是
uvcpp_http_request::to_string() 补的(src/web/uvcpp_http_request.cpp:156-159)。
响应说的同样作数:on_response_complete() 按 llhttp 算出的
should_keep_alive()(HTTP 版本默认值 + Connection 的逗号列表)判断这条连接
还能不能再用,不能就当场清掉 HTTP_CLIENT_CONNECTED
(src/web/uvcpp_http_client.cpp:710-730)—— 此后第二次 send() 拿到
UV_ENOTCONN,而不是往一条服务端已决定关闭的连接上写。
tests/functional/web_http_client_keepalive_func.cpp 把这三条都钉住了。
二、开了 OpenSSL 的构建里 send_wait() 走的是阻塞 fd,不是事件循环。
src/web/uvcpp_http_client.cpp:587-604 是一个 #if UVCPP_OPENSSL_ENABLE 分支:
有 TLS 就走阻塞路径,没有才走"轮询 loop + 1ms sleep"的异步实现。
后果是同一个程序的 keep-alive 行为会随构建而变(src/web/uvcpp_http_client.cpp:992-995 把这段历史写下来了)。
三、阻塞路径的 timeout_ms 只有 Windows 生效。
setsockopt(SO_RCVTIMEO/SO_SNDTIMEO) 两处都被 #ifdef _WIN32 包着
(src/web/uvcpp_http_client.cpp:1134-1138、:1202-1206),POSIX 上不设,
send_wait() / send_wait_plain() 在 Linux/macOS 上可以无限阻塞。头文件现在把这个
缺口写在参数说明里(src/web/uvcpp_http_client.h:139-141、:155-159),
不再让读者以为有超时兜底。
四、阻塞路径读不到任何字节时返回 0,而 resp 是默认的 200 OK。
read_one_message() 的返回值被丢弃(src/web/uvcpp_http_client.cpp:1226-1235),空头交给
parse_response_head() 之后什么都不改,status_code 保持初值 200。
判据只能是返回值。(异步 h1 路径相反:错误时会把 status_code 设成
HTTP_STATUS_NONE,src/web/uvcpp_http_client.cpp:458-460。)
不跟随重定向。 src/web/uvcpp_http_client.{h,cpp} 里
redirect|30[1238] 零命中 —— 3xx 只是一个状态码交给你。
压缩默认关,且门槛改不了。 set_compression_enabled(true)
(src/web/uvcpp_http_client.h:214-226)之后请求会补
accept-encoding: gzip, deflate、响应收全后自动解压 —— 异步与阻塞两条路径都算
(send() 与 send_wait() / send_wait_plain() / send_wait_ssl())。顺带记一笔
曾经不是这样:1.6.32 之前这两件事只写在异步那条路上,阻塞那两条静默地什么都不做
(请求不补头、响应也不解压,调用方拿到的是贴着 content-encoding: gzip 的压缩字节)。
但门槛硬编码 compress_min_body_ = 1024(h:513),客户端没有
set_compress_min_body_size()。于是 < 1024 字节的 gzip 响应不会被解压,
content-encoding: gzip 也照样留着 —— 门槛比的是压缩后的长度,而调用方拿到的
就是压缩字节加一个说"这是 gzip"的头。
uvcpp_static_server 是独立类,要自己挂(uvcpp_http_server 不会自动启用它):
#include <string>
#include <web/uvcpp_http_server.h>
#include <web/uvcpp_static_server.h>
int doc_static() {
uvcpp::uvcpp_http_server server;
// 三个都是**构造参数**,没有 setter:local_dir、url_root、index_file
uvcpp::uvcpp_static_server files("frontend/dist", "/", "index.html");
server.on_request(files.handler(&server)); // 兜底:没命中 API 的都给它
if (server.bind("0.0.0.0", 8000) != 0) return 1;
return server.listen();
}files 必须活得比 server 长。 handler() 捕获的是 this 裸指针
(src/web/uvcpp_static_server.h:160-164)—— 上面那个写法(files 先于 server 声明、
后于它析构)是对的。
默认值(构造参数 src/web/uvcpp_static_server.h:153-155、成员初值 :484-485):
url_root = "/"、index_file = "index.html"、spa_fallback = true、cache_enabled = true。
后两个有 setter(set_spa_fallback() :177-177、set_cache_enabled() :180-180),
前两个只能走构造函数。另有一组 setter:set_precompressed()(:276-276,默认 false)、
缓存的两个预算 set_cache_max_entries() / set_cache_max_bytes()(:193-208,默认
256 条 / 32 MiB)和 set_revalidate()(:293-293,默认
uvcpp_static_revalidate::POOL)—— 前者见下面讲预压缩孪生的那一节,后者见下面讲校验跑在
哪条线程上的那一节。构造函数还会把 local_dir 去掉尾斜杠、把 url_root 规范成 "/" 或
"/xxx"(src/web/uvcpp_static_server.cpp:321-339)。
它是懒加载的:首次请求某文件才读盘并进内存缓存,此后每次请求 stat 一次, mtime/大小变了就异步重载 —— 所以文件改完下一个请求就返回新内容,不需要重启。 那次 stat 默认在线程池里做(整个库的硬性约定是"事件循环线程上不做 IO"),也可以 搬回循环线程:见下面「校验跑在哪条线程上」。
缓存是有界的(1.6.44 起):条目数 ≤ set_cache_max_entries() 且
字节数 ≤ set_cache_max_bytes(),超了按 LRU(命中即提升)从队尾淘汰;
单份文件本身就超过字节预算时根本不缓存它(否则它一进来就把整张表冲空、下一次仍然
装不下)。0 表示"不限"而不是"预算为零"。这张表被多条循环线程共用,所以
cache_size() / cache_bytes() 都是走锁读的,clear_cache() 三者一起归零。
- 不做 Range / 206、不做 ETag / 304、不做百分号解码
(
src/web/uvcpp_static_server.h:31-53)。 - 多循环(
set_loops(n > 1))下可用(1.6.8 起)。cache_与retired_从前 是"循环线程只有一条"写的(两张表都没有锁)。这条在 h3 上从"没人跑"变成必需: h3 端点默认就扇在 n 条循环上,而 h3 上挂静态文件只有本类可用 (uvcpp_http_server没有uvcpp_web_app那种serve_static())。 - 防目录穿越靠段级归一化(
src/web/uvcpp_static_server.cpp:44-81), 并额外拒掉\、:、NUL 与控制字符(:45-49)。 spa_fallback默认开,意味着找不到文件时也返回 200 的index.html(src/web/uvcpp_static_server.cpp:678-683)—— 接口写错路径时会静默拿到一页 HTML 而不是 404。- 防"地址被复用"用的是凭据里那个代次号(
src/web/uvcpp_static_server.cpp:739-742取到凭据、reply_alive()逐处核),不是连接 id。 - MIME 表是硬编码 switch(
src/web/uvcpp_static_server.cpp:898-931);要可配的用 webapp 层的uvcpp_web_static(src/webapp/uvcpp_web_static.h)。 - 实时压缩对它生效了(1.6.44 起):
1.6.43及以前它不填resp.cache_tag,于是uvcpp_http_server那个按cache_tag做键的响应变体缓存对它不生效 —— 开着compress时它每请求在循环线程上把整份文件 deflate 一遍(20 个文件的语料上整条腿掉到 977 rps)。现在它填了(文件身份:盘上路径 + 大小 + mtime 秒 + 纳秒),同一个装置上 变成压一次、之后查表(66 554 rps)。想连那一次也省掉就用set_precompressed(true)(1.6.18 起,默认关):盘上放了x.js.br/x.js.gz就整份 读它、原样发出去并附content-encoding与vary: accept-encoding。挑选规则与框架层uvcpp_web_static的precompressed是同一份实现(web/uvcpp_http_common.h的http_pick_precompressed_variants()),边界也一样:孪生文件不存在就退回原文件、带Range的请求不取孪生。实测(ext4 语料、8 循环,2026-10-14, 66 554.63 → 89 791.80 rps,+34.9%,同时线上字节 −24.5%),见benchmark.md§4.4。
一次命中的请求,本类要做的唯一 IO 是重新校验那一次 stat。它在哪条线程上做,
由 uvcpp_static_revalidate 选:
| 档 | stat 在哪 | 一次命中的池往返 |
|---|---|---|
POOL(默认) |
线程池 worker | 有(排队 + 池锁 + after-work 回调各一次) |
LOOP_STAT |
循环线程就地(serve() 里同步做完) |
零 |
默认是 POOL,也就是 1.6.25 之前唯一的行为 —— 老调用方一行不改,语义逐字不变。
LOOP_STAT 只把校验那一步搬回来:缓存未命中时的读盘、以及 LOOP_STAT 自己
递归出的目录 index / SPA 回退,都还是同一套逻辑(命中与 404 在调用者的 resp 上回完,
未命中才落到异步读盘那条路并置 resp.deferred)。
为什么它能省:命中路径上 stat 是唯一需要池的地方,而一次池往返(排队 + 池锁 +
uv_work 的两次 futex + after-work 回调)在 8 核饱和、64 连接下是十几微秒量级。
实测(h3 那条腿、平台形状 -c 64 -m 64 -t 8 -D 5s,同一份 .so、同一个二进制、
只差一个环境变量):futex/req 6.28 → 0.66、statx/req 2.24 → 2.24(一个不少)、
总系统调用/req 28.81 → 22.62;us_per_req 中位数 125.733 → 117.361(−6.7%),
p99 170.5 ms → 140.5 ms。读数与口径见 benchmark.md §4.8。
只在本地盘上开。 循环线程上那次
stat是阻塞调用:ext4 上 1.35 µs, FUSE / 网络盘上 46 µs 量级(uvcpp_static_server.h的选项注释里两个数都写着)。 后者会拖住同一条循环上的其它连接 —— 尾延迟会先坏,rps 却看着还行,所以要 同时看这两个数。默认档POOL就是为此留的:一个字段回退。
池规模不是这条腿的杠杆。 上面"一次池往返十几微秒"很容易被读成"那就少开几个线程"。
本机拿同一份 .so、同一份 main.cc(只差 UV_THREADPOOL_SIZE 一个环境变量)做过一次 2×2:
| 校验档 | 池 8 线程 | 池 64 线程 |
|---|---|---|
POOL(默认) |
106.1 / 113.6 / 111.3(中位 111.3) | 109.0 / 109.4 / 109.9(中位 109.4) |
LOOP_STAT |
84.2 / 88.7 / 87.1(中位 87.1) | 87.9 / 95.3 / 89.6(中位 89.6) |
(static-h2 形状、20 个 URI、-c 256 -m 32 -D 5s、ext4 语料,每格 3 次取中位数;
单位与口径见 benchmark-rig.md;不经静态处理器的 baseline2 两档
都在 9.8~10.4 µs,不受影响。)
两个方向都读得出来:池 8 与池 64 之间只差 1.92.5 µs,落在 58 µs 的次间散布里
(8 核上 256 连接本来就把核跑满了,池里排不排队不改变总吞吐);而同一个池规模下换成
LOOP_STAT 省下约 22 µs/req(−20 %)。所以要省的是"每请求一次池往返"这件事本身,
不是池里有几个线程 —— 按档位把 threadpool_threads() 降下来既换不到读数,又会挤掉
/async-db 的槽位。默认 POOL 保持不动。
三个上限的默认值全是 0,也就是全都不限(src/web/uvcpp_http_server.h:1707-1709):
| 旋钮 | 默认 | 越限的答复 |
|---|---|---|
set_max_body_size(n) |
0 = 不限 | 413 + Connection: close |
set_max_header_bytes(n) |
0 = 不限 | 431 + Connection: close |
set_max_url_bytes(n) |
0 = 不限 | 414 + Connection: close |
三者"生效时机"不一致:头/URL 上限在 accept 时被拷进解析器
(src/web/uvcpp_http_server.cpp:1006-1007)⇒ listen 之后改它只对新连接生效;
max_body_size_ 是每块 body 现读成员(src/web/uvcpp_http_server.cpp:1069-1069)⇒ 立刻生效。
三处头文件注释一个字都没提这个区别。
头/URL 上限对"被 claim 的请求"照样生效。 这两道是在 llhttp 的头部回调里边收边判的
(src/web/uvcpp_parser 侧 src/web/uvcpp_http_parser.cpp:430-478),而 claim hook
是在 headers_complete 才被问的(src/web/uvcpp_http_server.cpp:1076-1149)。
所以被认领的请求照样会被 414/431 拒掉并直接关连接,认领者连 HEADERS 事件
都收不到。只有 max_body_size 真正停在认领边界(src/web/uvcpp_http_server.cpp:1059-1075)——
头文件把这条区别写在了 src/web/uvcpp_http_server.h:526-529(「a claimed request is not exempt」)
与 :126-128(max_body_size 豁免)两处。
头/URL 是"边收边判"的:一超就停,代价有界,而不是先收完再判
(src/web/uvcpp_http_server.h:516-521)。
zlib 编进来时压缩默认开(src/web/uvcpp_http_server.h:1814-1814),最小 body 1024
(src/web/uvcpp_http_server.h:1814-1814),排除的 MIME 走 default_excluded_mime_types()
(src/web/uvcpp_http_compress.cpp:215-242)。压缩变体缓存三道限:
单条 ≤ 4 MiB、总量 ≤ 32 MiB、条数 ≤ 1024(src/web/uvcpp_http_server.cpp:1905-1915)。
第五个旋钮:档位。 set_compress_level() / compress_level()
(src/web/uvcpp_http_server.h:612-612),默认 -1(zlib 的
Z_DEFAULT_COMPRESSION,今天等价第 6 档),传进来的值由
http_compress::clamp_compress_level()(src/web/uvcpp_http_compress.h:112-112)
夹进 -1 与 0..9 —— 越界不报错,落到最近的端点上。它和上面那四个不是一类:
那四个改的是要不要压,档位改的是压成哪些字节,压这件事照样发生。所以档位
进变体缓存的键(编码字符 + 档位字符 + cache_tag,
src/web/uvcpp_http_server.cpp:2120-2120;两个定长前缀使键对"编码 + 档位 + tag"
单射),换档必然 miss 必然重压,而旧档位那条留在表里 —— 换回去还能拿回它自己的
字节(test_compress_level() 钉的就是这一发)。它也可以在服务跑着的时候改:成员是
原子的,改完下一个请求就按新档位来。档位只改字节、不改内容,解压回来永远逐字节等于
原文,代价结算在"线上字节数 ↔ CPU"上(本机 zlib 1.3:8 397 B 那份从 6 档换到
1 档是 65.4 → 42.8 µs、1 566 → 1 957 字节),所以默认值一个字节都没动。
第六个旋钮:br(brotli)。 UVCPP_ENABLE_BROTLI(默认 OFF,
CMakeLists.txt:223-223)给压缩器加上第二种编码:br 与 gzip/deflate 走同一套
协商(http_compress::parse_accept_encoding(),Accept-Encoding 里 q 相同时
br > gzip > deflate,* 兜底、br;q=0 排除、指名要一个本构建编不出来的
编码时原样发原文而不是拿另一种编码的字节冒充 ——
src/web/uvcpp_http_compress.cpp:122-183)、同一张排除 MIME 表、
同一个变体缓存。这不是为了快:质量档固定在
http_compress::k_brotli_quality_default(= 5,src/web/uvcpp_http_compress.h:160),
默认档下单份语料比 gzip 6 档小 11 % 左右,而每请求的代价与 gzip 相同 ——
因为压缩变体命中时只是一次 std::map 查找加一次 shared_ptr 交接,编码名只
出现在键里(编码字符 + 档位字符 + cache_tag),两条路走的是同一条
(src/web/uvcpp_http_server.cpp:2119-2122)。反过来说:不填 cache_tag 就
每请求重压一遍,那时 br 比 gzip 更贵(本机 8.4 KB 那份:gzip 51.6 µs、
br 88.6 µs),所以"要不要开"取决于条目有没有把响应体的指纹写进 cache_tag。
br 没有独立的质量旋钮:set_compress_level()
(src/web/uvcpp_http_server.h:612)只管 gzip/deflate 那个量纲(-1 与 0..9),
br 用 0..11,两把尺子不能共用(clamp_brotli_quality() 与
clamp_compress_level() 之所以不合并,就是这条)。.br 孪生文件与这个开关
无关:http_pick_precompressed_variants()(src/web/uvcpp_http_common.h:641)
只看 Accept-Encoding 与盘上有没有那个文件,盘上有 .br 就照发(连
vary: accept-encoding 一起发,src/webapp/uvcpp_web_static.cpp:1810-1824)——
开关管的是"现场编得出编不出 br",不管"能不能转发别人编好的 br"。
排除表里带 / 的条目按前缀匹配(image/ 管住所有位图),不带 / 的按精确
匹配。字体那一组是刻意写成精确匹配的:font/woff 与 font/woff2 里装的本来就是
压缩流(WOFF 是 gzip 过的 sfnt、WOFF2 是 brotli 过的),再 deflate 一遍不但白烧
CPU,产物还比原文件大(本仓语料 22 528 → 22 556、18 432 → 18 460 字节);
而 font/ttf / font/otf / font/collection 是未压缩的轮廓数据,deflate 有实打实
的收益 —— 写成 font/ 前缀会把这两类一起排掉。Caddy 的 encode 跳过表同样是把
这两个类型逐个列出来。
- keep-alive:HTTP/1.1 默认开、1.0 默认关;handler 显式设了
connection头就听 handler 的(src/web/uvcpp_http_server.cpp:1620-1634)。webapp 这一层 还会在发送前把「这一条请求自己」的结论钉成显式头 (src/webapp/uvcpp_web_app.cpp:2697-2700)——HTTP 层那边现问的是连接级的 解析器状态,而 llhttp 的 flags 属于「最近一条解析完的报文」:流水线里末条写Connection: close时,第 1 条响应(异步的那条)序列化时读到的已经是末条的 结论,服务器会在第 1 条之后关连接、丢掉第 2..N 条(1.6.46-dev修的)。 Date每个响应都发,且按秒缓存:四个出报文的口子各补一次 ——send_response(src/web/uvcpp_http_server.cpp:1605-1605)、h1 流式begin_stream(src/web/uvcpp_http_server.cpp:1763-1763)、h2 流式begin_stream(src/web/uvcpp_http_server.cpp:1796-1796)、h2 整包send_h2_response(src/web/uvcpp_http_server.cpp:3038-3038)。值由uvcpp::http_date()造(src/web/uvcpp_http_date.cpp:86-107):IMF-fixdate、 恒 GMT,星期名与月份名是写死的 ASCII 表 —— 它不用strftime的%a/%b,那两个说明符在setlocale(LC_TIME, "")之后会吐本地文字,对端 解析不了。handler 自己设过Date就不覆盖 (src/web/uvcpp_http_date.cpp:114-124)。1xx 那两条报文是裸字节直接写 出去的、没经这个收口,所以不带Date:100 Continue(src/web/uvcpp_http_server.cpp:1168-1168)与 WebSocket 握手 (src/web/uvcpp_ws_server.cpp:202-202)—— RFC 9110 §6.6.1 对 1xx 只是 可发,不发也合规;这是"没经手",不是"写了排除"。Expect: 100-continue支持,且在 headers 回调里就补HTTP/1.1 100 Continue\r\n\r\n裸字节(src/web/uvcpp_http_server.cpp:1154-1169)—— 注释解释了为什么 不能用send_response()(1xx 不该带Content-Length)。HTTP/1.0 上Expect被忽略(src/web/uvcpp_http_server.cpp:1519-1524);未知的Expect回 417(src/web/uvcpp_http_server.cpp:1534-1539)。- 声明的
Content-Length超限 → body 还没到就回 413(src/web/uvcpp_http_server.cpp:1542-1554)。 - 畸形报文走正规响应路径回 400 +
Connection: close(src/web/uvcpp_http_server.cpp:1333-1342), 不是手搓字节。 - 流水线:一次读里可以有多条报文,解析器按消息边界 reset
(
src/web/uvcpp_http_server.cpp:1033-1056、src/web/uvcpp_http_server.cpp:1281-1297);web 层不限条数,webapp 才限。
没有报头条数上限(既无常量也无成员,uvcpp_http_common.h 与
uvcpp_http_parser.h 全文都没有)。web 层没有任何超时 —— 见 §12。
不用异常表达失败。 三条通道:
- 返回 libuv 错误码:
bind/listen/run;客户端的connect/connect_wait/send/send_wait/post_wait。 常见值UV_ENOTCONN、UV_ENOTSUP、UV_ETIMEDOUT、UV_ECONNRESET、UV_EINVAL。 - 回调里的
err参数:客户端(resp, err)、流式写的done(int)。 - 返回值加 stderr:
send_response在连接已不在表里时打一条 stderr 警告 并返回 0(src/web/uvcpp_http_server.cpp:1589-1599);begin_stream同理 (src/web/uvcpp_http_server.cpp:1715-1722)—— 但它是void,你无从判断。
异常只在一处被接住:on_connection / stream_claim / h2 连接钩子三个钩子
里抛出的异常会被吞掉并打 stderr(src/web/uvcpp_http_server.cpp:1019-1030、src/web/uvcpp_http_server.cpp:1124-1135、src/web/uvcpp_http_server.cpp:2854-2863)。
handler(req, resp, client) 本身没有 try/catch(src/web/uvcpp_http_server.cpp:1499-1504),
抛出去会穿过 llhttp 的 C 回调栈。webapp 层自己包了 try/catch 并扇出到错误中间件
(src/webapp/uvcpp_web_app.cpp:3123-3129)。
| 静默点 | 表现 |
|---|---|
set_keep_alive(false) |
完全无效(§8) |
阻塞 send_wait 读空 |
返回 0 且 resp 是默认 200(§8) |
阻塞路径的 timeout_ms(非 Windows) |
被忽略,可以无限阻塞 |
send_response 落空 |
只打 stderr,返回 0 |
begin_stream 落空 |
只打 stderr,void 无从判断 |
write_stream 非 0 |
done 不会被调,要自己结算 |
| 重复注册同一路由 | 后一个永远不可达,无提示 |
在 h2 连接上调 h1 的 write_stream |
打 stderr 后丢弃 |
| 压缩响应 < 1024 B | 不解压,content-encoding 还留着 |
一、服务端没有超时,一个连上不发字节的连接会永久占着。
uvcpp_http_server 与 uvcpp_tcp_server 里没有任何超时机制
(src/web/ 下 grep idle_timeout 只命中 uvcpp_ws_parser::is_idle),
src/web/uvcpp_http_server.cpp:1323-1324、src/web/uvcpp_http_server.cpp:2381-2383 与头文件
src/web/uvcpp_http_server.h:1370-1373 现在都明写这一点。
真实的闲置清扫在 webapp 层:idle_timeout_ms 默认 60000
(src/webapp/uvcpp_web_app.cpp:214),而且为 0 时连扫描句柄都不建
(src/webapp/uvcpp_web_app.cpp:2234-2239)。升级成 WebSocket 的连接被排除在闲置超时之外
(src/webapp/uvcpp_web_app.h:866)。
二、req 与 resp 只在本次调用期间有效。
resp 仍是 on_request_complete 的局部对象(src/web/uvcpp_http_server.cpp:1451-1451);req 自 1.3.26 起是
连接上那个请求对象的引用(src/web/uvcpp_http_server.cpp:1416-1417),handler 返回后它不被析构
—— 下一条请求会在原地把它重填一遍,所以违反「返回之后不得再读它」的症状变成了
静默读到下一条请求(改前是悬垂读)。deferred 那句是唯一的例外通道。
三、别在回调里做重活。 所有回调都在 loop 线程上;重活走 uvcpp_work
(§6)。也不要在回调里用同步 write_wait / read_wait 变体 ——
cb == nullptr 通常正是同步版本。
四、ctx.stream_request 的头部视图是借的。 只在当次调用内有效
(src/web/uvcpp_http_server.h:119-121)。
五、h1 与 h2 的 write_stream / end_stream 同名不同义,且没有编译期区分
(§7)。
六、send_response() 之后不要回头读 resp.body.size() —— 它已经被移走了,
要计数用返回值(§6)。
七、写队列的 done 保证是有条件的(§7):只有 write_stream
返回 0 才有"恰好一次"的保证;返回非 0 时 done 不会被调。
八、连接指针会被复用。 异步路径上先记 connection_generation(),
回来先比对(§6)。
只列用法上会撞到的,不列实现细节。
- 没有
close():关服是stop()+close_all_clients()两步(§3)。 - 没有参数路由、通配、正则、中间件、405 区分(§4)。
- 没有任何 query / cookie / form / URL 解码工具,
req.url就是原始请求目标。 - 没有任何超时(服务端与客户端异步路径都没有), 闲置清扫在 webapp 层(§12)。
- 没有报头条数上限。
- 客户端不跟随重定向,没有请求侧的重试/退避。
- 压缩门槛在客户端改不了,且 < 1024 B 的 gzip 响应不解压(§8)。
set_keep_alive()在客户端是死代码(§8)。- 不做
Upgrade: h2c协商:明文 h2 只有"先验知识"一条腿 (set_h2c_enabled(true),默认关,见 §1)。 - 静态服务不做 Range / ETag / 百分号解码,且
spa_fallback默认开 (§9)。 uvcpp_http_server不自己管 TLS,要在listen()之前装到get_tcp_server()上。- C 面(1.4.3 起)只给精选的那一片:客户端在
include/capi/uvcpp_c_web.h(http_client/http_response),服务端那一片在include/capi/uvcpp_c_webapp.h(C 侧的服务端就是 app,没有单独的uvcpp_c_http_server)。同样没有 TLS 参数、没有重定向跟随。它是什么、不提供 什么,见 C ABI 指南 §4。
相关文档:web WS 指南、net 网络层指南、 TLS 指南、HTTP/2 低层指南、 webapp 应用框架开发者指南、HTTP/2 支持现状、 项目 README。
本页用到的头文件:<web/uvcpp_http_server.h>、<web/uvcpp_http_client.h>、
<web/uvcpp_http_request.h>、<web/uvcpp_http_response.h>、
<web/uvcpp_http_reply.h>、<web/uvcpp_http_common.h>、
<web/uvcpp_static_server.h>、
<net/uvcpp_tcp_client.h>、<net/uvcpp_tcp_server.h>、<req/uvcpp_work.h>。