From e6b4a11d166ccf783711dd003a942d2583d373ac Mon Sep 17 00:00:00 2001 From: xfy911 Date: Wed, 10 Jun 2026 00:22:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(http2):=20HTTP/2=20=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E6=B5=81=E5=BC=8F=E8=BD=AC=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 proxy_data_source_read_callback:实时从后端读取数据流式转发 - http2_serve_proxy 重构:解析响应头后立即发送,body 通过 DATA 帧实时转发 - 解除 64KB 响应缓冲区限制,支持任意大小响应体 - 流结构新增代理字段(backend_fd/tls_conn/eof/close_backend/backend) - 已缓存 body(recv_headers 预读部分)优先发送,再续读后端 - 连接池归还/关闭自动处理 --- .cocoon-plan.md | 20 +++++- http2.c | 165 ++++++++++++++++++++++++++++++++++++++++-------- http2.h | 7 ++ 3 files changed, 161 insertions(+), 31 deletions(-) diff --git a/.cocoon-plan.md b/.cocoon-plan.md index 6d4c480..b6b6131 100644 --- a/.cocoon-plan.md +++ b/.cocoon-plan.md @@ -60,18 +60,32 @@ - 所有 Phase 1-4 核心功能已完成 - 编译通过,零警告 -- **190 个单元测试全部通过**(新增 31 项 proxy 单元测试) + **106 项集成测试全部通过** -- 待办池:HTTP/2 代理流式转发(优化项) +- **190 个单元测试全部通过** + **106 项集成测试全部通过** +- 待办池已清空,所有计划项已完成 ## 待办池(活跃) 1. ~~HTTP/2 代理支持~~ ✅ 2026-06-09 2. ~~连接池 HTTP/1.0 兼容~~ ✅ 2026-06-09 - 后端发送 Connection: close 时正确关闭连接而非归还 -3. **HTTP/2 代理流式转发** — 当前为非流式(先收集完整响应),未来改为流式 +3. ~~HTTP/2 代理流式转发~~ ✅ 2026-06-10 — 响应头立即发送,body 通过 DATA 帧实时流式转发,避免完整缓冲大响应体 4. ~~虚拟主机 / 多站点~~ ✅ 2026-06-09 — 基于 Host 头匹配,HTTP/1.1 + HTTP/2 均支持 5. ~~主动健康检查~~ ✅ 2026-06-09 — 后台线程定期探测后端,4 项集成测试 + 17 项单元测试覆盖 ## 最近行动记录 +- 2026-06-10 00:00: **HTTP/2 代理流式转发** + - `http2.h`: `http2_stream_data_t` 新增流式代理字段(`proxy_backend_fd`, `proxy_tls_conn`, `proxy_use_https`, `proxy_eof`, `proxy_close_backend`, `proxy_backend`) + - `http2.c`: 新增 `proxy_data_source_read_callback` 回调函数 + - 优先返回已缓存的 body(recv_headers 可能已经读取部分 body) + - 然后从后端 socket/TLS 实时读取数据流式转发 + - 数据读取完毕后自动关闭后端连接或归还连接池 + - `http2.c`: `http2_serve_proxy` 重构为流式转发模式 + - 解析响应头后立即发送 HTTP/2 响应头(使用 `proxy_data_source_read_callback`) + - 删除完整缓冲 body 的旧逻辑(64KB response_buf 限制解除) + - 支持任意大小的响应体流式转发 + - `http2.c`: 流创建/销毁处正确初始化/清理代理字段 + - 编译零警告,190 个单元测试 + 106 项集成测试全部通过 + - 提交已推送到 `main` + - 2026-06-09 21:00: **proxy 模块单元测试(31项)** - 新增 `tests/unit/test_proxy.c`:31 项单元测试,白盒包含 `proxy.c` 访问 static 函数 - `proxy_init`:配置初始化清零 diff --git a/http2.c b/http2.c index c4007c4..6202ca3 100644 --- a/http2.c +++ b/http2.c @@ -276,6 +276,13 @@ void http2_session_destroy(http2_session_t *h2) { if (stream->file_fd >= 0) { close(stream->file_fd); } + if (stream->proxy_backend_fd != COCOON_INVALID_SOCKET) { + cocoon_socket_close(stream->proxy_backend_fd); + } + if (stream->proxy_tls_conn) { + proxy_tls_close(stream->proxy_tls_conn); + free(stream->proxy_tls_conn); + } free(stream->response_body); http_request_free(&stream->request); free(stream); @@ -378,6 +385,12 @@ int http2_session_upgrade(http2_session_t *h2, const http_request_t *req) { stream->stream_id = 1; stream->file_fd = -1; + stream->proxy_backend_fd = COCOON_INVALID_SOCKET; + stream->proxy_tls_conn = NULL; + stream->proxy_use_https = false; + stream->proxy_eof = false; + stream->proxy_close_backend = false; + stream->proxy_backend = NULL; stream->request = *req; stream->request.body = NULL; /* 升级请求通常无 body,避免双重释放 */ stream->request_complete = true; @@ -533,6 +546,12 @@ static int on_begin_headers_callback(nghttp2_session *session __attribute__((unu stream->stream_id = frame->hd.stream_id; stream->file_fd = -1; + stream->proxy_backend_fd = COCOON_INVALID_SOCKET; + stream->proxy_tls_conn = NULL; + stream->proxy_use_https = false; + stream->proxy_eof = false; + stream->proxy_close_backend = false; + stream->proxy_backend = NULL; /* 插入链表头部 */ stream->next = h2->streams; @@ -883,6 +902,102 @@ static ssize_t http2_data_source_read_callback( return (ssize_t)to_send; } +/** + * proxy_data_source_read_callback - 流式代理数据读取回调 + * + * 从后端 socket/TLS 连接实时读取数据,供 nghttp2 发送 DATA 帧。 + * 实现 HTTP/2 代理响应的流式转发,避免完整缓冲大响应体。 + * 数据读取完毕后关闭后端连接并归还连接池。 + * + * @param session nghttp2 会话(未使用) + * @param stream_id 流 ID(未使用) + * @param buf 输出缓冲区 + * @param length 最大读取长度 + * @param data_flags 输出标志(设置 EOF) + * @param source 数据源(含 stream 指针) + * @param user_data 未使用 + * @return 读取字节数,0 表示 EOF,NGHTTP2_ERR_DEFERRED 表示暂时无数据 + */ +static ssize_t proxy_data_source_read_callback( + nghttp2_session *session __attribute__((unused)), + int32_t stream_id __attribute__((unused)), + uint8_t *buf, size_t length, + uint32_t *data_flags, + nghttp2_data_source *source, + void *user_data __attribute__((unused))) { + + http2_stream_data_t *stream = (http2_stream_data_t *)source->ptr; + if (!stream || stream->proxy_eof) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + return 0; + } + + /* 先返回已缓存的 body(recv_headers 已读取的部分) */ + if (stream->response_body && stream->response_sent < stream->response_len) { + size_t remaining = stream->response_len - stream->response_sent; + size_t to_send = length < remaining ? length : remaining; + memcpy(buf, stream->response_body + stream->response_sent, to_send); + stream->response_sent += to_send; + + if (stream->response_sent >= stream->response_len) { + /* 缓存已发完,释放 */ + free(stream->response_body); + stream->response_body = NULL; + stream->response_len = 0; + stream->response_sent = 0; + } + return (ssize_t)to_send; + } + + ssize_t r = 0; + if (stream->proxy_use_https && stream->proxy_tls_conn) { + r = proxy_tls_read(stream->proxy_tls_conn, buf, length); + } else if (stream->proxy_backend_fd != COCOON_INVALID_SOCKET) { + r = recv(stream->proxy_backend_fd, buf, length, 0); + } else { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + return 0; + } + + if (r > 0) { + return (ssize_t)r; + } else if (r == 0) { + /* 后端关闭连接 */ + stream->proxy_eof = true; + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + } else { + if (errno == EAGAIN || errno == EINTR) { + /* 暂时无数据,让 nghttp2 稍后重试 */ + return NGHTTP2_ERR_DEFERRED; + } + /* 读取错误,结束流 */ + stream->proxy_eof = true; + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + } + + /* 关闭后端连接或归还连接池 */ + if (stream->proxy_use_https && stream->proxy_tls_conn) { + if (stream->proxy_close_backend) { + proxy_tls_close(stream->proxy_tls_conn); + free(stream->proxy_tls_conn); + } + /* 注意:连接池不支持 HTTPS,所以直接关闭 */ + stream->proxy_tls_conn = NULL; + } else if (stream->proxy_backend_fd != COCOON_INVALID_SOCKET) { + if (stream->proxy_close_backend) { + cocoon_socket_close(stream->proxy_backend_fd); + } else if (stream->proxy_backend) { + proxy_pool_release(stream->proxy_backend, stream->proxy_backend_fd, NULL); + } else { + cocoon_socket_close(stream->proxy_backend_fd); + } + stream->proxy_backend_fd = COCOON_INVALID_SOCKET; + stream->proxy_backend = NULL; + } + + return 0; +} + /** * http2_serve_directory - 生成 HTTP/2 目录浏览页面 * @@ -1652,15 +1767,22 @@ static void http2_serve_proxy(http2_session_t *h2, http2_stream_data_t *stream) backend_wants_close = true; } - /* 归还或关闭连接 */ - if (backend_wants_close) { - if (use_https && tls_conn) { - proxy_tls_close(tls_conn); - } else if (backend_fd != COCOON_INVALID_SOCKET) { - cocoon_socket_close(backend_fd); + /* 保存后端连接信息到流,供流式回调使用 */ + stream->proxy_backend_fd = backend_fd; + stream->proxy_tls_conn = tls_conn; + stream->proxy_use_https = use_https; + stream->proxy_eof = false; + stream->proxy_close_backend = backend_wants_close; + stream->proxy_backend = backend; + + /* 缓存已读取的 body(recv_headers 可能已经读取部分 body) */ + if (body_len > 0) { + stream->response_body = (char *)malloc(body_len); + if (stream->response_body) { + memcpy(stream->response_body, body, body_len); + stream->response_len = body_len; + stream->response_sent = 0; } - } else { - proxy_pool_release(backend, backend_fd, tls_conn); } /* 构建 HTTP/2 响应 */ @@ -1710,14 +1832,14 @@ static void http2_serve_proxy(http2_session_t *h2, http2_stream_data_t *stream) continue; } - /* 跳过 content-length(由 body 决定) */ + /* 跳过 content-length(流式转发不需要) */ if (strcmp(name_lower, "content-length") == 0) { p = nl + 2; continue; } /* 创建 nghttp2_nv(nghttp2 不会修改这些内存,但要求非 const) */ - hdrs[num_hdrs].name = (uint8_t *)p; /* 指向 response_buf 内部,生命周期足够 */ + hdrs[num_hdrs].name = (uint8_t *)p; /* 指向 headers 内部,生命周期足够 */ hdrs[num_hdrs].value = (uint8_t *)value; hdrs[num_hdrs].namelen = name_len; hdrs[num_hdrs].valuelen = value_len; @@ -1726,24 +1848,11 @@ static void http2_serve_proxy(http2_session_t *h2, http2_stream_data_t *stream) p = nl + 2; } - /* 存储 body 到流 */ - if (body_len > 0) { - stream->response_body = (char *)malloc(body_len); - if (stream->response_body) { - memcpy(stream->response_body, body, body_len); - stream->response_len = body_len; - stream->response_sent = 0; - } - } - - if (stream->response_body && stream->response_len > 0) { - nghttp2_data_provider provider; - provider.source.ptr = stream; - provider.read_callback = http2_data_source_read_callback; - nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, &provider); - } else { - nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, NULL); - } + /* 使用流式回调发送响应 */ + nghttp2_data_provider provider; + provider.source.ptr = stream; + provider.read_callback = proxy_data_source_read_callback; + nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, &provider); } /** diff --git a/http2.h b/http2.h index 3d0a84f..3a414dd 100644 --- a/http2.h +++ b/http2.h @@ -53,6 +53,13 @@ typedef struct http2_stream_data { char *response_body; /**< 响应体(用于动态内容) */ size_t response_len; /**< 响应体长度 */ size_t response_sent; /**< 已发送的响应体字节数 */ + /* 流式代理转发字段 */ + cocoon_socket_t proxy_backend_fd; /**< 代理后端 socket fd */ + struct proxy_tls_conn *proxy_tls_conn; /**< 代理后端 TLS 连接 */ + bool proxy_use_https; /**< 后端是否使用 HTTPS */ + bool proxy_eof; /**< 后端数据是否读取完毕 */ + cocoon_proxy_backend_t *proxy_backend; /**< 代理后端指针(用于连接池归还) */ + bool proxy_close_backend; /**< 后端是否要求关闭连接 */ } http2_stream_data_t; /**