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:
parent
8a80ec9d96
commit
a93e9f32f5
@ -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/2(PRI 魔术字直接连接 + 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.c),sendfile 替代方案,io_uring 替代方案
|
||||
3. **[中] Doxygen 中文注释** — 所有模块的公共 API 需要完整文档(http2.c 已部分完成)
|
||||
4. **[低] 检查远程更新内容** — 上游有 new commits,需 review 9041d8d
|
||||
1. **[中] Windows 兼容性** — 平台抽象层(platform.h / platform.c),sendfile 替代方案,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 伪头长度解析 bug(4→5)和路径空终止问题
|
||||
- http2.c: 修复 send_callback 返回值 bug(send_all 返回 0 而非已发送字节数,导致 nghttp2 重试死循环)
|
||||
|
||||
31
http2.c
31
http2.c
@ -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
13
http2.h
@ -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
105
server.c
@ -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) {
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user