src/http2/ 只有两个公开类,分成两层:
| 层 | 类 | 干什么 | 碰不碰 socket |
|---|---|---|---|
| 协议层 | uvcpp_h2_session |
收发帧、维护流表、判协议违例、算流控。只跟内存说话 | 不碰 |
| 传输层 | uvcpp_h2_connection |
把 session 榨出来的字节写进 uvcpp_tcp_client,把读到的字节喂回去;一次只允许一个异步写在飞 |
碰(经由 net/) |
这一层不会自己接线到 socket。 建会话、判 ALPN、装读回调、销毁,全都要调用方自己做。
web/ 的两条线(uvcpp_http_server / uvcpp_http_client)各有一份接好的实现可以照抄,
本页在 §5 与 §6 给了它们的调用序列。
- 打开方式:
-DUVCPP_ENABLE_NGHTTP2=ON。默认 OFF(CMakeLists.txt:236-236), 而且缺 OpenSSL 或缺 web 模块时会被强制置 OFF 并打 warning (CMakeLists.txt:595-595、CMakeLists.txt:604-604)—— 因为uvcpp_h2_stream里直接放uvcpp_http_request/uvcpp_http_response(src/http2/uvcpp_h2_session.h:70)。 - 包含方式:
<http2/uvcpp_h2_session.h>、<http2/uvcpp_h2_connection.h>、<http2/uvcpp_h2_common.h>。私有的<http2/uvcpp_h2_nghttp2.h>不安装 (CMakeLists.txt:3029-3029)。
本页讲怎么用。它与 HTTP/2 支持现状 分工:那篇讲 实现进度、写死的折衷、已知缺口,本页不重复那些,需要时直接链过去。
签名、默认值、行为都对着当前源码核过,非显然的结论后面都跟了
文件:行号。
// doc-snippet: fragment — 对着头文件抄的接口清单,不是完整翻译单元
explicit uvcpp_h2_session(bool server_side); // src/http2/uvcpp_h2_session.h:201
int init(const callbacks& cbs,
size_t max_header_list_size = 64u * 1024u,
uint32_t max_concurrent_streams = H2_DEFAULT_MAX_CONCURRENT_STREAMS,
uint32_t initial_window_size = 0); // src/http2/uvcpp_h2_session.h:236-236
int recv(const char* data, size_t len); // src/http2/uvcpp_h2_session.h:252-252
int drain(std::string& out, size_t max_bytes = 0); // src/http2/uvcpp_h2_session.h:269-269服务端还是客户端由构造参数一次性决定,构造后改不了。 也没有 is_server() 之类的读法
(src/http2/uvcpp_h2_session.h:201 是唯一一处)—— 外部要问侧只能自己记。
server_side 影响三件事:伪头白名单(src/http2/uvcpp_h2_session.cpp:360-360)、
常规头分流(:503-503)、body 收尾分派(:584-584)。
一条流不是 uvcpp_h2_connection,是 uvcpp_h2_stream。
uvcpp_h2_connection 代表整条连接(一个 h2 会话),会话内部用一个
std::map<int32_t, uvcpp_h2_stream> 按 stream id 管多条流
(src/http2/uvcpp_h2_session.cpp:234-234)。uvcpp_h2_stream 只在会话存活期间有效,
on_close 之后指针即失效(src/http2/uvcpp_h2_session.h:63)。
UVCPP_NGHTTP2_ENABLE 在任何构建里都有定义,只是值不同:ON ⇒ 1,OFF ⇒ 0
(CMakeLists.txt:2307-2307)。三个公开头把全部内容包在 #if UVCPP_NGHTTP2_ENABLE 里
(src/http2/uvcpp_h2_common.h:19、src/http2/uvcpp_h2_session.h:33、src/http2/uvcpp_h2_connection.h:27)。
为 0 时:头还在(源码树里能 include、也编得过),但里面一个类型都不声明 ——
写 uvcpp_h2_session s(true); 会"未定义类型"。安装出来的包里则根本没有这几个头
(CMakeLists.txt:3029-3029 只在开关 ON 时装)。
所以使用者的写法是把自己的那段包起来:
#include <uvcpp/uvcpp_config.h>
#if UVCPP_NGHTTP2_ENABLE
// 这里才能用 uvcpp_h2_session / uvcpp_h2_connection
#endif别自己给使用者 TU 加 nghttp2 的 include 路径、也别自己 include <nghttp2/nghttp2.h>。
这库把"唯一包含 nghttp2"收在一个私有头里(src/http2/uvcpp_h2_nghttp2.h),
其中有一段 MSVC 专用的 #define ssize_t int 补丁(:42)—— 写 SSIZE_T
会静默 ABI 不符。
这一层不碰 socket,所以最小的用法就是"两个会话面对面搬字节"。
tests/functional/h2_session_func.cpp 就是这么测的,uvcpp_h2_connection
做的也正是这两段搬运(§4)。
#include <cstdint>
#include <string>
#include <http2/uvcpp_h2_session.h>
#include <web/uvcpp_http_request.h>
#include <web/uvcpp_http_response.h>
void doc_h2_memory_pair() {
uvcpp::uvcpp_h2_session server(/*server_side=*/true);
uvcpp::uvcpp_h2_session client(/*server_side=*/false);
uvcpp::uvcpp_h2_session::callbacks sc;
sc.on_request = [&server](uvcpp::uvcpp_h2_session&, uvcpp::uvcpp_h2_stream& st, bool) {
// 提交只是**排队**,一个字节都还没出去。
server.submit_response(st.stream_id, uvcpp::uvcpp_http_response::ok("ok", 2));
};
server.init(sc);
uvcpp::uvcpp_h2_session::callbacks cc;
cc.on_response_end = [](uvcpp::uvcpp_h2_session&, uvcpp::uvcpp_h2_stream& st) {
(void)st.response.status_code; // 到这儿才是"响应全收完了"
};
client.init(cc);
// 提交请求(返回新流号,负值是错误码)。
const int32_t sid = client.submit_request(uvcpp::uvcpp_http_request::make_get("/"), "");
if (sid <= 0) return;
(void)sid;
// 搬字节:drain 出来的喂给对端,recv 吃进去;两边各来一轮。
std::string wire;
if (client.drain(wire) != 0 || wire.empty()) return;
if (server.recv(wire.data(), wire.size()) != 0) return; // 这里会同步跑 on_request
wire.clear();
if (server.drain(wire) != 0 || wire.empty()) return;
(void)client.recv(wire.data(), wire.size()); // 这里同步跑 on_response_end
}这一段是真的能跑通的:drain() 榨出来的是一整块待发字节,recv()
会把它吃干净并同步回调进你的 callbacks。
// doc-snippet: fragment — 对着头文件抄的接口清单,不是完整翻译单元
uvcpp_h2_connection(uvcpp_tcp_client* client, bool server_side); // src/http2/uvcpp_h2_connection.h:58
int start(const uvcpp_h2_session::callbacks& h2_cbs,
const callbacks& conn_cbs); // src/http2/uvcpp_h2_connection.h:70
int send_response(int32_t stream_id, const uvcpp_http_response& resp,
bool omit_body = false); // src/http2/uvcpp_h2_connection.h:100
int send_status(int32_t stream_id, int status, const std::string& body); // :104
int send_headers(int32_t stream_id, const uvcpp_http_response& resp); // :109
int send_data(int32_t stream_id, const char* data, size_t len,
bool end_stream, std::function<void(int)> done); // :127-127
int flush(); // :158-158
int begin_goaway(); // :171-171
void shutdown(); // :174-174
void close_now(); // :177-177它不拥有 uvcpp_tcp_client(src/http2/uvcpp_h2_connection.h:34),
但拥有自己的 uvcpp_h2_session —— 会话是在构造函数里 new 出来的
(src/http2/uvcpp_h2_connection.cpp:56),由 shared_ptr 回收。
会话可以脱离 connection 单独用(§3 就是),
反过来不行。
callbacks 只有一个成员:
// doc-snippet: fragment — 结构体定义摘录,不是完整翻译单元
struct callbacks {
std::function<void(uvcpp_h2_connection&)> on_disconnect; // src/http2/uvcpp_h2_connection.h:46
};- socket 必须已经连上。 connection 在
start()里自己装读回调 (src/http2/uvcpp_h2_connection.cpp:115-115用read_start_events), 而没连上的 client 上这个调用返回UV_ENOTCONN—— 而且那个失败是静默的, 没人会再 arm 一次。库内正解是等到 connect 成功回调里才new(src/web/uvcpp_http_client.cpp:312)。 - ALPN 必须已经协商成
h2。 connection 的构造函数不校验这一点 (src/http2/uvcpp_h2_connection.cpp:56只存指针),但头文件的参数说明要求它 (src/http2/uvcpp_h2_connection.h:54)。谁建谁自己判:client->is_tls() && client->tls_alpn_selected() == "h2"(照src/web/uvcpp_http_server.cpp:996-996)。 on_disconnect里必须销毁这个对象。 头文件写得很直白: "回调返回后不要再碰本对象 —— 持有者应当在这里把它销毁" (src/http2/uvcpp_h2_connection.h:46-51)。start()会把on_fatal换成自己的包装(就地shutdown(),src/http2/uvcpp_h2_connection.cpp:96-96)—— 你装的那个仍会被调用,但不是唯一的 收尾动作。
TLS 与 ALPN 全是 uvcpp_tcp_client 内部的事,这一层只读一个字符串
(src/net/uvcpp_tcp_client.h:221-221 / :224 / :236 / :258)。
src/http2/ 里没有一次 #include ssl 头。
on_disconnect 在三种情况下触发:对端关了、读错了、我们自己关完了
(src/http2/uvcpp_h2_connection.cpp:308-308、:493-493)。它排在框架自己的关闭回调之前,
所以那是你最后一次能安全碰这条连接的机会。
能用 uvcpp_h2_connection 上的 send_* 就别用 session().submit_*。
那四个 send_* 是"submit_* + flush()"的合并 —— 忘了 flush() 就是
"响应提交了但一个字节都没发"这种最难查的静默失败(src/http2/uvcpp_h2_connection.h:74)。
库内那份接好的实现(src/web/uvcpp_http_server.cpp:2869-2869 起)的调用序列:
- 分流点判 ALPN,是 h2 就走另一条路(
src/web/uvcpp_http_server.cpp:996-996); new uvcpp_h2_connection(client, /*server_side=*/true)(src/web/uvcpp_http_server.cpp:2869-2869);- 装
uvcpp_h2_session::callbacks(src/web/uvcpp_http_server.cpp:2874-2874)与uvcpp_h2_connection::callbacks(src/web/uvcpp_http_server.cpp:2936-2936); - 挂框架自己的关闭回调 —— 最终
delete在那里(src/web/uvcpp_http_server.cpp:2943-2943); h2->start(h2c, cc)(src/web/uvcpp_http_server.cpp:2943-2943);失败就h2->close_now()(src/web/uvcpp_http_server.cpp:2946-2946);- 业务处理完之后
h2->send_response(stream_id, resp, omit_body)(src/web/uvcpp_http_server.cpp:3027-3027起)。
一个能编的最小响应侧写法:
#include <cstdint>
#include <string>
#include <http2/uvcpp_h2_connection.h>
#include <http2/uvcpp_h2_session.h>
#include <net/uvcpp_tcp_client.h>
#include <web/uvcpp_http_response.h>
// 只演示"装回调 + 回一条响应"的形状;真接线还要照 §4 那四条要求做。
void doc_h2_server_wiring(uvcpp::uvcpp_tcp_client* tcp) {
uvcpp::uvcpp_h2_connection* h2 =
new uvcpp::uvcpp_h2_connection(tcp, /*server_side=*/true);
uvcpp::uvcpp_h2_session::callbacks h2c;
// 回调里一律**重新查表**,不缓存 stream 指针 —— 它过了 on_close 就失效了。
h2c.on_request = [h2](uvcpp::uvcpp_h2_session&, uvcpp::uvcpp_h2_stream& st, bool) {
h2->send_response(st.stream_id, uvcpp::uvcpp_http_response::ok("ok", 2));
};
uvcpp::uvcpp_h2_connection::callbacks cc;
cc.on_disconnect = [](uvcpp::uvcpp_h2_connection& self) {
delete &self; // 契约要求:在这里销毁
};
if (h2->start(h2c, cc) != 0) h2->close_now();
}三段式:send_headers(只发头、不结束流)→ 若干次 send_data(..., end_stream, done) → 最后一块 end_stream = true。三处要点:
- 提交过
end_stream之后这条流不再接受新的submit_data,再发就是协议违例 (src/http2/uvcpp_h2_session.h:333-333); - 每一块的
done必须恰好跑一次,流中途被 RST 或连接断了也会跑,参数是UV_ECANCELED(src/http2/uvcpp_h2_connection.h:153-156); - 拆传输的正确顺序是
take_cancelled_dones(out)→ 销毁对象 → 在自己的上下文 已经拆干净之后逐个跑那些done(src/http2/uvcpp_h2_connection.h:179-191)。 它只取走、不跑,理由写在头里。
低层没有请求侧的合并入口 —— uvcpp_h2_connection 上的四个 send_* 全是响应侧的。
客户端提交完请求必须自己调 flush()。这一点在
tests/functional/web_ssl_h2_server_func.cpp:460 有一段专门的告诫,
库内正解在 src/web/uvcpp_http_client.cpp:1359-1359:
// doc-snippet: fragment — 从库内调用点摘的三句,前后文不在本页
const int32_t sid = conn->session().submit_request(req, body);
if (sid <= 0) return sid; // 负值是错误码,且**什么都没发生**
conn->flush(); // ← 少了这一句,请求就停在队列里submit_request 的三个同步失败码都是有保证的"什么都没发生":
UV_EINVAL(字段非法)、UV_EMSGSIZE(头部块超限)、UV_ENOTCONN
(收到过对端 GOAWAY,或本端流号用尽)—— 没有流被建、没有字节被排队、
没有回调会被叫(src/http2/uvcpp_h2_session.h:411-420)。
#include <cstdint>
#include <string>
#include <http2/uvcpp_h2_connection.h>
#include <http2/uvcpp_h2_session.h>
#include <net/uvcpp_tcp_client.h>
#include <web/uvcpp_http_request.h>
void doc_h2_client_submit(uvcpp::uvcpp_h2_connection* conn) {
uvcpp::uvcpp_http_request req = uvcpp::uvcpp_http_request::make_get("/index.html");
const int32_t sid = conn->session().submit_request(req, std::string());
if (sid <= 0) return; // UV_EINVAL / UV_EMSGSIZE / UV_ENOTCONN 都是同步的"没发生"
(void)sid;
if (conn->flush() != 0) return; // ← 请求侧的合并入口就是这两句
}uvcpp_http_client 那条线是同一套顺序:set_http2_enabled(true)
(src/web/uvcpp_http_client.cpp:1275-1275)→ connect() 里 enable_tls 加 ALPN 名单
(:275 / :291,h2 在前、http/1.1 兜底)→ connect 成功回调里读
tls_alpn_selected()(:310)→ start_h2()(:314 → :1298-1298)→ 装回调(:1300-1300)
→ h2_->start(sc, cc)(:1334-1334)→ send() 分流到 send_h2(:440)。
set_http2_enabled 默认是关的(src/web/uvcpp_http_client.h:474-474、
src/web/uvcpp_http_server.h:357-357)—— 低层的两条线都要显式打开,
只有 webapp/ 框架层是零配置自动协商。
| 回调 | 服务端 | 客户端 | 触发点 |
|---|---|---|---|
on_request(session, stream, end_stream) |
✅ | ✗ | 请求头收全(src/http2/uvcpp_h2_session.cpp:557-557) |
on_request_end(session, stream) |
✅ | ✗ | 请求体收完(:558-558、:585-585) |
on_response(session, stream, end_stream) |
✗ | ✅ | 响应头收全(:560-560) |
on_response_end(session, stream) |
✗ | ✅ | 响应全收完(:562-562、:587-587) |
on_body(session, stream, data, len) |
✅ 请求体 | ✅ 响应体 | 每块 DATA(:637-637) |
on_close(session, stream_id, error_code) |
✅ | ✅ | 流结束(:642-642),回调之后流引用即失效 |
on_fatal(session, nghttp2_error) |
✅ | ✅ | 见 §9 |
两条不看源码一定会写错的地方:
一、无 body 的请求上 on_request 与 on_request_end 是背靠背的。
on_request_end 的触发条件是**「这条流的 END_STREAM 到了」,不是"有请求体才算"
(src/http2/uvcpp_h2_session.cpp:558-558)。HEADERS 自带 END_STREAM 的请求(也就是
绝大多数 GET)也会触发,而且是紧接着 on_request 同步来的 —— 库内自己就依赖
这一点:src/web/uvcpp_http_server.cpp:2896-2897 明说那里不能** erase 流状态,
无 body 的请求这两下是背靠背的,擦掉之后那条请求就没人派发了。
二、客户端交付响应只能放 on_response_end。 带 body 的响应里 on_response 的
end_stream 恒为 false,而 DATA 的 END_STREAM 不经过任何回调 ——
没有 on_response_end 就分不出"响应到头了"和"响应全收完了"
(src/web/uvcpp_http_client.cpp:1312-1312、src/http2/uvcpp_h2_session.h:155-161)。
另外,服务端侧的 uvcpp_h2_stream::response 初值是 HTTP_STATUS_NONE 而不是 200
(src/http2/uvcpp_h2_session.h:71)。这不是疏漏:流在响应头到达之前被 RST
(REFUSED_STREAM 正是如此)时,一个编出来的 200 会一路交付到业务层。
init() 有三个参数,默认值就是全部旋钮(src/http2/uvcpp_h2_session.h:236-236):
| 旋钮 | 默认 | 含义 |
|---|---|---|
max_header_list_size |
64u * 1024u |
我们愿意接收的头部列表上限 |
max_concurrent_streams |
H2_DEFAULT_MAX_CONCURRENT_STREAMS = 100 |
我们宣告出去的最大并发流 |
initial_window_size |
0 |
0 = 这条 SETTINGS 不发 |
max_header_list_size 只是宣告出去的礼貌值 —— 收方向的强制由 h2_header_budget
自己做(src/http2/uvcpp_h2_common.h:121)。头文件把这一点写明了,别以为设了就防住了。
initial_window_size = 0 不是"窗口为 0",是"这一条 SETTINGS 不 push 进去"
(src/http2/uvcpp_h2_session.cpp:839-839)。于是实际用的是 nghttp2 的初值 65535 ——
数值上与 H2_DEFAULT_INITIAL_WINDOW_SIZE 恰好相等,但路径完全不同,
而那个常量全仓零引用(src/http2/uvcpp_h2_common.h:44 是它唯一出现的地方)。
实际发出去的 SETTINGS(src/http2/uvcpp_h2_session.cpp:834-834):
ENABLE_PUSH = 0(我们不收也绝不发 PUSH_PROMISE)、MAX_CONCURRENT_STREAMS、
MAX_HEADER_LIST_SIZE,INITIAL_WINDOW_SIZE 按上面的规则。
其余几个硬上限(都是写死的,没有旋钮):
| 项 | 值 | 位置 |
|---|---|---|
| 单个待发头部块 | 64 KiB(H2_MAX_SEND_HEADER_BLOCK) |
src/http2/uvcpp_h2_common.h:57、设置点 src/http2/uvcpp_h2_session.cpp:815-815、自查 :1106-1106 |
| 单流 body | 64 MiB(H2_DEFAULT_MAX_BODY_BYTES)→ RST |
src/http2/uvcpp_h2_common.h:32、src/http2/uvcpp_h2_session.cpp:618-618 |
| 单流待发队列 | 4 MiB(H2_DEFAULT_MAX_OUT_STREAM_BYTES)→ submit_data 同步返 UV_ENOBUFS |
src/http2/uvcpp_h2_common.h:79、src/http2/uvcpp_h2_session.cpp:1212-1212 |
content-length 荒谬值 |
> 1<<40 判非法 |
src/http2/uvcpp_h2_session.cpp:458-458 |
| 控制帧令牌桶 | burst 64、补充 32 个/秒 | src/http2/uvcpp_h2_session.cpp:75-76、control_frame_ok() :217-217 |
:scheme 与实际传输层一致 |
TLS 只接受 https、明文只接受 http,不符即 RST(两个都收等于给混淆代理开门,RFC 9113 §8.1.1) |
src/http2/uvcpp_h2_session.cpp:384-387 |
没有闲置超时(src/http2/ 里 grep idle|timeout|keepalive 零命中)。
src/web/uvcpp_http_server.h:1370-1373 提到的那个 idle sweep 属于 webapp 层
(默认 60 s),既不是 h1 服务器自带的,也不覆盖 h2 连接。
对端的观测口:peer_max_concurrent_streams()(src/http2/uvcpp_h2_session.h:438-438)、
peer_goaway_received() / peer_goaway_error_code() / peer_goaway_last_stream_id()
(:451-451 / :453-453 / :455-455)、goaway_code()(:473-473)、stream_count()、
peer_window_size()(:539-539,对端在发送方向给这条流留的窗口;流不存在与窗口
恰好用尽都返回 0,想区分先用 find_stream())。
| 口子 | 在哪一层 | 做什么 |
|---|---|---|
pause_stream(id) / resume_stream(id) |
会话层 | 收方向。暂停只影响这一条流:字节照常交付给你的 on_body,但不归还它那一份每流窗口,对端把当前剩余窗口发完就自己停下来 |
resume_stream(id) |
连接层 | 同上,外加 flush() |
set_max_out_stream_bytes(n) |
会话层 | 发方向。单流待发队列的上界,超了 submit_data 当场返 UV_ENOBUFS |
恢复请用连接层那个。 会话层的 resume_stream() 只把 WINDOW_UPDATE 排进 nghttp2
的出站队列;字节要 drain() + 写才出网。从 nghttp2 回调里调没问题(on_read() 在
recv() 返回后必冲一次),但应用最自然的时机 —— 自己的定时器里、下游腾空之后 ——
没有任何人会替你冲一次,表现是"对端永远等不到窗口、那条流挂死",而且不报任何
错。pause_stream() 反过来不 flush(暂停不产生字节)。
暂停的边界:生效点是"下一个进 on_body 的字节",已经解出来的收不回来 ——
所以暂停之后最多还会有"对端剩余窗口"那么多 on_body 到(上界是你宣告的
initial_window_size,不发那条 SETTINGS 时就是 RFC 9113 缺省的 65535)。只管收方向,
不影响你发出的响应、也不影响同一条连接上别的流。幂等;对已关闭的流返 UV_EINVAL。
为什么连接级那一半是必须的:连接窗口是全连接共享的 65535。一条暂停的流如果把
连接级额度也扣住,攒够 65535 就能把同一条连接上别的流一起饿死 —— 那不是背压,
是误伤。所以本层对每个进 on_data_chunk 的字节无条件归还连接级那一份。
框架侧没有调用点。 uvcpp_web_response / uvcpp_http_server 全仓零命中
pause_stream —— h2 上"边收边给"的流式 body 今天仍不可达(§11)。
这三个口子只对直接用低层会话/连接的调用方有意义。
同步返回 int(0 成功)加回调带码,不用异常。 唯一的例外是 submit_request:
它返回新流号(正数)或负的错误码(src/http2/uvcpp_h2_session.h:422-422)。
// doc-snippet: fragment — 引的是库内实现片段,不是本页可复制的用法
if (rv < 0) { // src/http2/uvcpp_h2_session.cpp:863-863
impl_->last_error = static_cast<int>(rv);
if (!impl_->fatal_reported) { impl_->fatal_reported = true;
if (impl_->cbs.on_fatal) impl_->cbs.on_fatal(*this, (int)rv); }
return static_cast<int>(rv);
}理由写在 src/http2/uvcpp_h2_session.h:245-250:nghttp2_session_mem_recv2 有若干返回值
只表示"这条流有问题",但 -905(CONTINUATION 过多)这种是连接级的,而它偏偏不在
返回值上区分 —— 既不发 RST 也不发 GOAWAY。把"看起来像流级"的错误当流级处理,
就会留下一条状态已经错乱的连接继续用。所以负值 ⇒ 会话作废。
致命只通知一次(fatal_reported),而且致命之后不再进 nghttp2:
recv() 直接回上次那个错误(src/http2/uvcpp_h2_session.cpp:852-852)。
on_fatal 有三个触发点(src/http2/uvcpp_h2_session.cpp:869-869、:879-879、:892-892):
nghttp2_session_mem_recv2返回负值;- 输入没吃完(自造一个
-1); - 控制帧超令牌桶 —— 这条是在
mem_recv之外收尾的,返回UV_ECANCELED, 发出去的 GOAWAY 码取goaway_code()(ENHANCE_YOUR_CALM)。
| 事件 | 归类 |
|---|---|
mem_recv 返回负值 / 输入没吃完 / 控制帧洪泛 |
致命:on_fatal 加会话作废 |
| 头部块超预算 | 流级:RST(ENHANCE_YOUR_CALM),连接照用(src/http2/uvcpp_h2_session.cpp:488-488) |
| 单流 body 超 64 MiB | 流级:RST(ENHANCE_YOUR_CALM)(:618-618) |
| 伪头顺序 / 白名单 / 走私 | 流级:RST(PROTOCOL_ERROR) |
| 发方向头部块超上限 | 同步返回 UV_EMSGSIZE,且流状态没被改过 |
可不可重试的判定不在这一层,在 web/ 侧:H2_ERR_REFUSED_STREAM 才算
retryable,NO_ERROR 与 CANCEL 都算正常收尾
(src/web/uvcpp_http_client.cpp:1445-1445)。
客户端提交请求后必须自己 flush()。 见 §6。
连接层没有请求侧的合并入口,那句 flush() 少了就是静默的什么都没发。
uvcpp_h2_connection 必须在 socket 连上之后才建。 它自己装读回调,
而没连上的 client 上 read_start_events 返回 UV_ENOTCONN —— 那个失败没人会再 arm
(src/web/uvcpp_http_client.cpp:312、tests/functional/web_ssl_h2_server_func.cpp:409)。
ALPN 要自己确认。 构造函数不校验 tls_alpn_selected() == "h2",但头文件的参数
说明要求它已经协商好(src/http2/uvcpp_h2_connection.h:54)。谁建谁判。
on_disconnect 里必须销毁 connection。 见 §4。
回调栈里不许 drain()。 那等于在 nghttp2 自己的栈里重入 mem_send,
实测会在"回调里把 RST_STREAM 冲出去"那条路上读一块 nghttp2 刚释放的
nghttp2_stream(src/http2/uvcpp_h2_session.h:281-281)。用 in_nghttp2() 判自己是不是
在回调里;推迟到 recv() 返回之后再冲即可,连接层本来就在返回之后冲一次。
注意 flush() 在回调里调用是安全的 —— 它自己会推迟,排队的字节一个都不会丢
(src/http2/uvcpp_h2_connection.h:158-162)。
recv() 的返回值不能在连接已死之后用来判据。 回调可能把宿主整条析构掉,
那时 this 已经没了。所以调用方进来之前要先本地持一份 shared_ptr
(src/http2/uvcpp_h2_session.h:122-126 的 @code 就是这段)。
收到对端 GOAWAY 之后不要拆这条连接,要另起一条。 GOAWAY 关的是"新流",
不是"连接" —— 已有的流照跑完,新流会被 submit_request 拦成 UV_ENOTCONN
(src/http2/uvcpp_h2_session.h:451-451)。
每一块流式 body 的 done 必须恰好跑一次。 传输层要在断开时整体作废
(cancel_pending_out() + take_cancelled_dones()),少了这一步,
框架的流式响应会等一个永远不来的回调(src/http2/uvcpp_h2_session.h:405-405)。
run_completed() 只在没有写在飞的时候调,而且 done 跑起来之后一个字都不能
写成员 —— done 跑的是用户代码,它可能再补一笔写
(src/http2/uvcpp_h2_connection.h:210-210)。
content-length 校验只在服务端请求方向做。 客户端侧的响应头只挡连接专属头,
不校验 content-length(src/http2/uvcpp_h2_session.cpp:506-506)。
这一节只列用法上会撞到的。实现进度、写死的折衷与完整缺口清单在 HTTP/2 支持现状 的 §2 / §3 / §4,不在这里重复。
- 不自动接线。 建会话、判 ALPN、装读回调、销毁,全要调用方做(§4)。
- 没有闲置超时、没有心跳(§8)。
- 没有请求侧的合并入口:
uvcpp_h2_connection的四个send_*全是响应侧的 (§6)。 - 背压有 API,但框架侧没有调用点(§8):
pause_stream()/resume_stream()/set_max_out_stream_bytes()只对直接用低层 会话/连接的调用方可达。h2 上"边收边给"的流式 body 因此仍不可达 (stream_claim_那条 h1 的路不接 h2)—— 详见 现状 §4.2。H2_DEFAULT_INITIAL_WINDOW_SIZE仍然全仓零引用。 - 不给 h2 加故障注入缝,也没有 h2 的 PUSH。
- 不做
Upgrade: h2c协商、不做协议嗅探。明文 h2 走的是 RFC 9113 §3.4 的 先验知识:头 24 个字节逐字节等于固定前言就按 h2(set_h2c_enabled(true), 默认关)。这条判定是确定性的,与"猜协议"是两件事。 - 明文这条腿上的
:scheme只能是http(TLS 上只能是https):取值跟 实际传输层走,不符即RST_STREAM(PROTOCOL_ERROR)(RFC 9113 §8.1.1,见 §8 那张表)。两边都是硬要求 —— 合规客户端(curl --http2-prior-knowledge、h2load -p h2c)发http,钉死https会把明文 整条腿判死;而"两个都收"会让一条明文连接自称https并被中间那个终结 TLS 的 缓存/代理按 https 的语义处理,那正是 §8.1.1 要拦的。手工搭一条明文会话 时要用uvcpp_h2_session::set_tls(false)说出来(默认是 TLS):库内唯一的 构造者uvcpp_h2_connection一律按client->is_tls()覆盖这个值,所以走 连接层的人不用管它。 - 服务端不推 PUSH_PROMISE,
ENABLE_PUSH恒为 0,而且也没有发 PUSH 的 API。 want_read/want_write不参与致命判定:on_fatal只有两类触发者,这两个公开 成员本层一处都没读(src/http2/uvcpp_h2_session.h:182-194)—— 详见 现状 §2。- C 面(1.4.3 起)只给驱动层那一个句柄:
include/capi/uvcpp_c_http2.h里是uvcpp_c_h2_connection(外加调用方所有的请求 / 响应构造器与回调期的流视图),uvcpp_h2_session不单独出现 —— 会话层的drain()是"吐出字节",照抄成第二个 句柄就等于让 C 侧自己写一遍驱动层;本层里那些"写完成之后再结算"的顺序恰好最容易写错。 同样没有 priority、没有 PUSH、没有逐帧回调。它是什么、不提供什么,见 C ABI 指南 §4。
相关文档:HTTP/2 支持现状、web HTTP 指南、 net 网络层指南、webapp 应用框架开发者指南、 项目 README。
本页用到的头文件:<http2/uvcpp_h2_session.h>、<http2/uvcpp_h2_connection.h>、
<http2/uvcpp_h2_common.h>、<net/uvcpp_tcp_client.h>、
<web/uvcpp_http_request.h>、<web/uvcpp_http_response.h>。