feat(h2c): 添加明文 HTTP/2 升级支持

- server.c: 检测 PRI 魔术字直接连接 + Upgrade: h2c 协商
- server.c: 101 Switching Protocols 响应,正确会话生命周期管理
- http2.h/c: 新增 http2_session_upgrade() 注册 stream 1
- http2.c: 修复 nghttp2_session_upgrade2 head_request 参数误用
- tests: 新增 4 项 h2c 集成测试(prior knowledge、Upgrade、404)
- plan: 标记 h2c 完成,更新测试计数 52 项
This commit is contained in:
xfy911 2026-06-04 17:56:44 +08:00
parent 8a80ec9d96
commit a93e9f32f5
5 changed files with 210 additions and 11 deletions

View File

@ -35,7 +35,7 @@
- [x] **Brotli 压缩** — 比 gzip 更高压缩率 ✅ 2026-06-04
- [x] **HTTPS / TLS** — OpenSSL Memory BIO 集成,支持命令行与配置文件启用 ✅ 2026-06-04
- [x] **HTTP/2** — nghttp2 完整实现TLS ALPN 协商 + 静态文件服务 + 缓存)✅ 2026-06-04
- [ ] h2c 升级支持 — 明文 HTTP/2 升级PRI 方法 + 魔术字)
- [x] **h2c 升级支持** — 明文 HTTP/2PRI 魔术字直接连接 + Upgrade: h2c 协商)✅ 2026-06-04
- [ ] Windows 兼容性
### Phase 4 — 生态
@ -46,7 +46,7 @@
## 当前状态
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
- **49 项集成测试全部通过**(新增 10 项 HTTP/2 测试ALPN 协商、GET、HEAD、404、缓存、brotli 压缩 ×3、图片不压缩
- **52 项集成测试全部通过**(新增 4 项 h2c 测试prior knowledge、Upgrade 协商、404 ×2
- **35 个单元测试全部通过**Unity 框架)
- 压测数据wrk -t4 -c100 -d10s → 16,179 RPS平均延迟 59.86μs
- POST 支持JSON 和 form-urlencoded 回显multipart 文件上传Content-Length 解析8MB 上限
@ -66,15 +66,25 @@
## 待办池
1. **[中] h2c 升级支持** — 明文 HTTP/2 升级PRI 方法 + 魔术字server.c 读取前几个字节检测
2. **[中] Windows 兼容性** — 平台抽象层platform.h / platform.csendfile 替代方案io_uring 替代方案
3. **[中] Doxygen 中文注释** — 所有模块的公共 API 需要完整文档http2.c 已部分完成)
4. **[低] 检查远程更新内容** — 上游有 new commits需 review 9041d8d
1. **[中] Windows 兼容性** — 平台抽象层platform.h / platform.csendfile 替代方案io_uring 替代方案
2. **[中] Doxygen 中文注释** — 所有模块的公共 API 需要完整文档http2.c 已部分完成)
3. **[低] 检查远程更新内容** — 上游有 new commits需 review 9041d8d
4. **[低] HTTP/2 目录浏览** — 目前返回 404可考虑支持目录列表
## 最近行动记录
- **2026-06-04: 本轮行动 — HTTP/2 静态文件服务 + ALPN 修复**
- **2026-06-04: 本轮行动 — h2c 升级支持(明文 HTTP/2**
- server.c: 添加 h2c 检测逻辑PRI 魔术字 + Upgrade: h2c 协商)
- server.c: `is_h2c_upgrade_request()` 检查 Upgrade 和 Connection 头
- server.c: `send_h2c_upgrade_response()` 发送 101 Switching Protocols
- server.c: `client_handler` 循环中检测 h2c 前言和 Upgrade 请求,正确销毁会话(避免双重 free
- http2.h: 新增 `http2_session_upgrade()` 声明
- http2.c: 实现 `http2_session_upgrade()`,通过 `nghttp2_session_upgrade2` 注册 stream 1
- http2.c: 修复 `nghttp2_session_upgrade2` 第四个参数误用(`head_request` 而非 `stream_id`
- tests/integration_test.sh: 新增 4 项 h2c 集成测试prior knowledge、Upgrade 协商、404 ×2
- 编译通过,零警告
- **52 项集成测试全部通过35 个单元测试全部通过**
- 推送到 main
- http2.c: 实现完整的 HTTP/2 静态文件服务GET/HEAD、路径安全、ETag、304 缓存)
- http2.c: 修复 :path 伪头长度解析 bug4→5和路径空终止问题
- http2.c: 修复 send_callback 返回值 bugsend_all 返回 0 而非已发送字节数,导致 nghttp2 重试死循环)

31
http2.c
View File

@ -234,6 +234,37 @@ void http2_session_set_context(http2_session_t *h2, const char *root_dir, bool g
h2->brotli_enabled = brotli_enabled;
}
int http2_session_upgrade(http2_session_t *h2, const http_request_t *req) {
if (!h2 || !h2->session || !req) return -1;
/* 创建流数据stream_id=1 */
http2_stream_data_t *stream = calloc(1, sizeof(http2_stream_data_t));
if (!stream) return -1;
stream->stream_id = 1;
stream->file_fd = -1;
stream->request = *req;
stream->request.body = NULL; /* 升级请求通常无 body避免双重释放 */
stream->request_complete = true;
/* 注册升级流到 nghttp2 */
int is_head = (req->method == HTTP_HEAD) ? 1 : 0;
if (nghttp2_session_upgrade2(h2->session, NULL, 0, is_head, stream) != 0) {
free(stream);
return -1;
}
/* 关联到会话 */
stream->next = h2->streams;
h2->streams = stream;
nghttp2_session_set_stream_user_data(h2->session, 1, stream);
/* 提交静态文件响应 */
http2_serve_static(h2, stream);
return 0;
}
/* ===================== 数据收发 ===================== */
int http2_recv(http2_session_t *h2, const uint8_t *buf, size_t len) {

13
http2.h
View File

@ -148,7 +148,18 @@ bool http2_want_write(http2_session_t *h2);
int http2_on_connection_accepted(int fd, bool tls_mode);
/**
* http2_session_set_context - HTTP/2
* http2_session_upgrade - h2c
*
* stream_id=1
* SETTINGS
*
* @param h2 HTTP/2
* @param req HTTP/1.1
* @return 0 -1
*/
int http2_session_upgrade(http2_session_t *h2, const http_request_t *req);
/**
*
* @param h2
* @param root_dir

105
server.c
View File

@ -563,13 +563,58 @@ static void handle_http2(connection_t *conn) {
http2_session_destroy(h2);
}
/* HTTP/2 h2c 连接前言24字节 */
static const char H2C_PREFACE[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
#define H2C_PREFACE_LEN 24
/**
* is_h2c_upgrade_request - HTTP/1.1 Upgrade: h2c
*
* @param req HTTP
* @return true h2c
*/
static bool is_h2c_upgrade_request(const http_request_t *req) {
if (strcmp(req->version, "HTTP/1.1") != 0) return false;
bool has_upgrade = false;
bool has_connection_upgrade = false;
for (int i = 0; i < req->num_headers; i++) {
if (strcasecmp(req->headers[i].name, "upgrade") == 0) {
if (strcasestr(req->headers[i].value, "h2c") != NULL) {
has_upgrade = true;
}
} else if (strcasecmp(req->headers[i].name, "connection") == 0) {
if (strcasestr(req->headers[i].value, "upgrade") != NULL) {
has_connection_upgrade = true;
}
}
}
return has_upgrade && has_connection_upgrade;
}
/**
* send_h2c_upgrade_response - 101 Switching Protocols
*
* @param conn
* @return true
*/
static bool send_h2c_upgrade_response(connection_t *conn) {
const char *resp =
"HTTP/1.1 101 Switching Protocols\r\n"
"Connection: Upgrade\r\n"
"Upgrade: h2c\r\n"
"\r\n";
return send_all(conn->fd, resp, strlen(resp)) == 0;
}
static void client_handler(void *arg) {
connection_t *conn = (connection_t *)arg;
if (!conn) return;
conn->coro = coco_self();
/* 检查是否是 HTTP/2 连接 */
/* 检查是否是 HTTP/2 连接TLS ALPN 协商) */
if (http2_session_is_http2(conn->fd)) {
handle_http2(conn);
conn_cancel_timer(conn);
@ -600,6 +645,64 @@ static void client_handler(void *arg) {
/* 有数据读取,重置定时器 */
conn_reset_timer(conn, timeout_ms);
/* 检查 h2c 直接连接前言 */
if (conn->buf_len >= H2C_PREFACE_LEN &&
memcmp(conn->buf, H2C_PREFACE, H2C_PREFACE_LEN) == 0) {
log_debug("fd=%d 检测到 h2c 直接连接前言", conn->fd);
http2_session_t *h2 = http2_session_create(conn->fd, false);
if (h2) {
http2_session_set_context(h2, conn->root_dir,
conn->gzip_enabled,
conn->brotli_enabled);
if (http2_send_pending(h2) == 0) {
if (http2_recv(h2, (const uint8_t *)conn->buf, conn->buf_len) == 0) {
conn->buf_len = 0;
handle_http2(conn);
break; /* handle_http2 内部已销毁会话 */
}
}
http2_session_destroy(h2);
}
break;
}
/* 检查 HTTP/1.1 Upgrade: h2c 请求 */
http_request_t req;
int parsed = http_parse_request(conn->buf, conn->buf_len, &req);
if (parsed > 0 && is_h2c_upgrade_request(&req)) {
/* 消费已解析的 HTTP/1.1 请求数据 */
if ((size_t)parsed < conn->buf_len) {
memmove(conn->buf, conn->buf + parsed, conn->buf_len - (size_t)parsed);
}
conn->buf_len -= (size_t)parsed;
if (send_h2c_upgrade_response(conn)) {
http2_session_t *h2 = http2_session_create(conn->fd, false);
if (h2) {
http2_session_set_context(h2, conn->root_dir,
conn->gzip_enabled,
conn->brotli_enabled);
if (http2_send_pending(h2) == 0) {
if (http2_session_upgrade(h2, &req) == 0) {
if (http2_send_pending(h2) == 0) {
/* 消费缓冲区中的剩余数据 */
if (conn->buf_len > 0) {
http2_recv(h2, (const uint8_t *)conn->buf, conn->buf_len);
conn->buf_len = 0;
}
handle_http2(conn);
break; /* handle_http2 内部已销毁会话 */
}
}
http2_session_destroy(h2);
}
}
}
http_request_free(&req);
break;
}
http_request_free(&req);
/* 尝试处理请求 */
bool keep = handle_request(conn, conn->root_dir);
if (!keep) {

View File

@ -572,13 +572,57 @@ fi
assert_http2_brotli "https://$HOST/index.html" "HTTP/2 HTML brotli"
assert_http2_brotli "https://$HOST/style.css" "HTTP/2 CSS brotli"
assert_http2_brotli "https://$HOST/app.js" "HTTP/2 JS brotli"
assert_http2_not_compressed "https://$HOST/image.png" "HTTP/2 图片不压缩"
# 停止 HTTP/2 服务器,恢复 HTTP 服务器
kill_server
sleep 1
start_server
# h2c 测试(明文 HTTP/2
echo ""
echo "=== h2c 测试 ==="
# h2c prior knowledge 直接连接
h2c_pk_status=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_code}" "http://$HOST/")
h2c_pk_version=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_version}" "http://$HOST/")
if [[ "$h2c_pk_status" == "200" && "$h2c_pk_version" == "2" ]]; then
echo " ✓ h2c prior knowledge — HTTP 200, 协议 HTTP/2"
pass
else
echo " ✗ h2c prior knowledge — 期望 200+HTTP/2, 实际 $h2c_pk_status+HTTP/$h2c_pk_version"
fail
fi
# h2c prior knowledge 404
h2c_pk_404=$(curl --http2-prior-knowledge -s -o /dev/null -w "%{http_code}" "http://$HOST/nonexist.html")
if [[ "$h2c_pk_404" == "404" ]]; then
echo " ✓ h2c prior knowledge 404 — HTTP 404"
pass
else
echo " ✗ h2c prior knowledge 404 — 期望 404, 实际 $h2c_pk_404"
fail
fi
# h2c Upgrade 协商
h2c_up_status=$(curl --http2 -s -o /dev/null -w "%{http_code}" "http://$HOST/")
h2c_up_version=$(curl --http2 -s -o /dev/null -w "%{http_version}" "http://$HOST/")
if [[ "$h2c_up_status" == "200" && "$h2c_up_version" == "2" ]]; then
echo " ✓ h2c Upgrade 协商 — HTTP 200, 协议 HTTP/2"
pass
else
echo " ✗ h2c Upgrade 协商 — 期望 200+HTTP/2, 实际 $h2c_up_status+HTTP/$h2c_up_version"
fail
fi
# h2c Upgrade 404
h2c_up_404=$(curl --http2 -s -o /dev/null -w "%{http_code}" "http://$HOST/nonexist.html")
if [[ "$h2c_up_404" == "404" ]]; then
echo " ✓ h2c Upgrade 404 — HTTP 404"
pass
else
echo " ✗ h2c Upgrade 404 — 期望 404, 实际 $h2c_up_404"
fail
fi
echo ""
echo "=== 结果汇总 ==="
echo "通过: $PASS"