feat(http2): HTTP/2 代理流式转发

- 新增 proxy_data_source_read_callback:实时从后端读取数据流式转发
- http2_serve_proxy 重构:解析响应头后立即发送,body 通过 DATA 帧实时转发
- 解除 64KB 响应缓冲区限制,支持任意大小响应体
- 流结构新增代理字段(backend_fd/tls_conn/eof/close_backend/backend)
- 已缓存 body(recv_headers 预读部分)优先发送,再续读后端
- 连接池归还/关闭自动处理
This commit is contained in:
xfy911 2026-06-10 00:22:46 +08:00
parent 2280b318a8
commit e6b4a11d16
3 changed files with 161 additions and 31 deletions

View File

@ -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` 回调函数
- 优先返回已缓存的 bodyrecv_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`:配置初始化清零

165
http2.c
View File

@ -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 EOFNGHTTP2_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;
}
/* 先返回已缓存的 bodyrecv_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;
/* 缓存已读取的 bodyrecv_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_nvnghttp2 不会修改这些内存,但要求非 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);
}
/**

View File

@ -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;
/**