From 6bb9635b2d23e5d5c3aba8bfcf830301b5cdb015 Mon Sep 17 00:00:00 2001 From: xfy911 Date: Thu, 4 Jun 2026 14:40:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(http2):=20=E6=B7=BB=E5=8A=A0=20HTTP/2=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=AA=A8=E6=9E=B6=EF=BC=88nghttp2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 http2.c / http2.h:基于 nghttp2 的 HTTP/2 会话管理 - 支持 TLS ALPN 协商和明文 h2c 升级 - 流级请求/响应处理,集成现有 http_request_t 结构 - 编译通过,链接 -lnghttp2 - Makefile 更新:包含 http2.c 和 nghttp2 库链接 - 待完成:server.c 集成(ALPN 协商、h2c 升级检测) --- Makefile | 8 +- http2.c | 431 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ http2.h | 151 +++++++++++++++++++ 3 files changed, 586 insertions(+), 4 deletions(-) create mode 100644 http2.c create mode 100644 http2.h diff --git a/Makefile b/Makefile index aa6184e..0309164 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ COCO_LIB ?= $(COCO_DIR)/build CC = gcc CFLAGS = -Wall -Wextra -O2 -std=c11 -D_GNU_SOURCE -I$(COCO_INCLUDE) -LDFLAGS = -L$(COCO_LIB) -lcoco -lpthread -lm -luring -lz -lbrotlienc -lssl -lcrypto +LDFLAGS = -L$(COCO_LIB) -lcoco -lpthread -lm -luring -lz -lbrotlienc -lssl -lcrypto -lnghttp2 # 调试模式 DEBUG ?= 0 @@ -20,7 +20,7 @@ PREFIX ?= /usr/local BINDIR = $(PREFIX)/bin # 源文件 -SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c +SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c OBJS = $(SRCS:.c=.o) TARGET = cocoon @@ -78,8 +78,8 @@ unit-test: $(UNIT_TEST_BINS) fi # 单元测试编译规则 -$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c $(UNITY_SRC) - $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c $(UNITY_SRC) $(LDFLAGS) +$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c $(UNITY_SRC) + $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c $(UNITY_SRC) $(LDFLAGS) $(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) -lm diff --git a/http2.c b/http2.c new file mode 100644 index 0000000..c037b80 --- /dev/null +++ b/http2.c @@ -0,0 +1,431 @@ +/** + * http2.c - HTTP/2 支持实现 + * + * 使用 nghttp2 库实现 HTTP/2 协议支持。 + * 与 cocoon 的协程 I/O 和 TLS 层集成。 + * + * @author xfy + */ + +#include "http2.h" +#include "static.h" +#include "log.h" +#include "tls.h" +#include +#include +#include +#include +#include +#include + +/* 最大 HTTP/2 会话数 */ +#define MAX_HTTP2_SESSIONS 1024 + +static http2_session_t *g_sessions[MAX_HTTP2_SESSIONS]; +static int g_session_count = 0; + +/* 内部函数声明 */ +static ssize_t send_callback(nghttp2_session *session __attribute__((unused)), const uint8_t *data, + size_t length, int flags __attribute__((unused)), void *user_data); +static int on_begin_headers_callback(nghttp2_session *session, + const nghttp2_frame *frame, + void *user_data); +static int on_header_callback(nghttp2_session *session, + const nghttp2_frame *frame, const uint8_t *name, + size_t namelen, const uint8_t *value, + size_t valuelen, uint8_t flags, + void *user_data); +static int on_frame_recv_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data); +static int on_stream_close_callback(nghttp2_session *session, + int32_t stream_id, uint32_t error_code, + void *user_data); +static int on_data_chunk_recv_callback(nghttp2_session *session, + uint8_t flags, int32_t stream_id, + const uint8_t *data, + size_t len, void *user_data); + +/* ===================== 会话管理 ===================== */ + +int http2_init(void) { + memset(g_sessions, 0, sizeof(g_sessions)); + g_session_count = 0; + return 0; +} + +void http2_cleanup(void) { + for (int i = 0; i < MAX_HTTP2_SESSIONS; i++) { + if (g_sessions[i]) { + http2_session_destroy(g_sessions[i]); + g_sessions[i] = NULL; + } + } + g_session_count = 0; +} + +http2_session_t *http2_session_create(int fd, bool tls_mode) { + if (fd < 0 || fd >= MAX_HTTP2_SESSIONS) { + return NULL; + } + if (g_sessions[fd] != NULL) { + /* 已存在,先销毁 */ + http2_session_destroy(g_sessions[fd]); + } + + http2_session_t *h2 = calloc(1, sizeof(http2_session_t)); + if (!h2) { + return NULL; + } + + h2->fd = fd; + h2->tls_mode = tls_mode; + + /* 创建 nghttp2 会话 */ + nghttp2_session_callbacks *callbacks = NULL; + if (nghttp2_session_callbacks_new(&callbacks) != 0) { + free(h2); + return NULL; + } + + nghttp2_session_callbacks_set_send_callback(callbacks, send_callback); + nghttp2_session_callbacks_set_on_begin_headers_callback( + callbacks, on_begin_headers_callback); + nghttp2_session_callbacks_set_on_header_callback(callbacks, + on_header_callback); + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, + on_frame_recv_callback); + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, on_stream_close_callback); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, on_data_chunk_recv_callback); + + if (nghttp2_session_server_new(&h2->session, callbacks, h2) != 0) { + nghttp2_session_callbacks_del(callbacks); + free(h2); + return NULL; + } + + nghttp2_session_callbacks_del(callbacks); + + /* 发送服务器连接前言(SETTINGS 帧) */ + nghttp2_settings_entry iv[] = { + {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100}, + {NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, (1 << 16) - 1}}; + if (nghttp2_submit_settings(h2->session, NGHTTP2_FLAG_NONE, iv, + sizeof(iv) / sizeof(iv[0])) != 0) { + nghttp2_session_del(h2->session); + free(h2); + return NULL; + } + + g_sessions[fd] = h2; + g_session_count++; + + return h2; +} + +void http2_session_destroy(http2_session_t *h2) { + if (!h2) return; + + if (h2->fd >= 0 && h2->fd < MAX_HTTP2_SESSIONS) { + g_sessions[h2->fd] = NULL; + } + g_session_count--; + + if (h2->session) { + nghttp2_session_del(h2->session); + } + + /* 清理所有流 */ + http2_stream_data_t *stream = h2->streams; + while (stream) { + http2_stream_data_t *next = stream->next; + if (stream->file_fd >= 0) { + close(stream->file_fd); + } + free(stream->response_body); + http_request_free(&stream->request); + free(stream); + stream = next; + } + + free(h2); +} + +bool http2_session_is_http2(int fd) { + if (fd < 0 || fd >= MAX_HTTP2_SESSIONS) return false; + return g_sessions[fd] != NULL; +} + +http2_session_t *http2_session_get(int fd) { + if (fd < 0 || fd >= MAX_HTTP2_SESSIONS) return NULL; + return g_sessions[fd]; +} + +/* ===================== 数据收发 ===================== */ + +int http2_recv(http2_session_t *h2, const uint8_t *buf, size_t len) { + if (!h2 || !h2->session) return -1; + + ssize_t readlen = nghttp2_session_mem_recv(h2->session, buf, len); + if (readlen < 0) { + log_error("HTTP/2 接收错误: %s", nghttp2_strerror((int)readlen)); + return -1; + } + + /* 发送任何挂起的输出帧 */ + return http2_send_pending(h2); +} + +int http2_send_pending(http2_session_t *h2) { + if (!h2 || !h2->session) return -1; + + int rv = nghttp2_session_send(h2->session); + if (rv != 0) { + log_error("HTTP/2 发送错误: %s", nghttp2_strerror(rv)); + return -1; + } + return 0; +} + +bool http2_want_read(http2_session_t *h2) { + if (!h2 || !h2->session) return false; + return nghttp2_session_want_read(h2->session) != 0; +} + +bool http2_want_write(http2_session_t *h2) { + if (!h2 || !h2->session) return false; + return nghttp2_session_want_write(h2->session) != 0; +} + +/* ===================== nghttp2 回调 ===================== */ + +/** + * send_callback - nghttp2 发送回调 + * + * 将 nghttp2 序列化后的帧数据发送到 socket。 + */ +static ssize_t send_callback(nghttp2_session *session __attribute__((unused)), + const uint8_t *data, size_t length, + int flags __attribute__((unused)), + void *user_data) { + http2_session_t *h2 = (http2_session_t *)user_data; + + /* 使用 TLS 或原始 socket 发送 */ + ssize_t sent; + if (h2->tls_mode && tls_has_connection(h2->fd)) { + sent = tls_write(h2->fd, (const char *)data, length); + } else { + sent = send_all(h2->fd, (const char *)data, length); + } + + if (sent < 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return (int)sent; +} + +/** + * on_begin_headers_callback - 开始接收请求头 + */ +static int on_begin_headers_callback(nghttp2_session *session __attribute__((unused)), + const nghttp2_frame *frame, + void *user_data) { + http2_session_t *h2 = (http2_session_t *)user_data; + + if (frame->hd.type != NGHTTP2_HEADERS || + frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + return 0; + } + + /* 创建新流数据 */ + http2_stream_data_t *stream = calloc(1, sizeof(http2_stream_data_t)); + if (!stream) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + stream->stream_id = frame->hd.stream_id; + stream->file_fd = -1; + + /* 插入链表头部 */ + stream->next = h2->streams; + h2->streams = stream; + + /* 关联到 nghttp2 流 */ + nghttp2_session_set_stream_user_data(session, frame->hd.stream_id, stream); + + return 0; +} + +/** + * on_header_callback - 接收单个请求头 + */ +static int on_header_callback(nghttp2_session *session, + const nghttp2_frame *frame, const uint8_t *name, + size_t namelen, const uint8_t *value, + size_t valuelen, uint8_t flags __attribute__((unused)), + void *user_data __attribute__((unused))) { + if (frame->hd.type != NGHTTP2_HEADERS || + frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + return 0; + } + + http2_stream_data_t *stream = + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id); + if (!stream) { + return 0; + } + + /* 收集伪头和普通头 */ + if (namelen == 4 && memcmp(":path", name, 4) == 0) { + /* 解析路径(可能包含 query string) */ + size_t path_len = valuelen; + for (size_t i = 0; i < valuelen; i++) { + if (value[i] == '?') { + path_len = i; + break; + } + } + if (path_len > 0) { + memcpy(stream->request.path, value, + path_len < sizeof(stream->request.path) - 1 + ? path_len + : sizeof(stream->request.path) - 1); + } + } else if (namelen == 7 && memcmp(":method", name, 7) == 0) { + if (valuelen == 3 && memcmp("GET", value, 3) == 0) { + stream->request.method = HTTP_GET; + } else if (valuelen == 4 && memcmp("HEAD", value, 4) == 0) { + stream->request.method = HTTP_HEAD; + } else if (valuelen == 4 && memcmp("POST", value, 4) == 0) { + stream->request.method = HTTP_POST; + } else { + stream->request.method = HTTP_UNKNOWN; + } + } else if (namelen == 7 && memcmp(":scheme", name, 7) == 0) { + /* 忽略 scheme */ + } else if (namelen == 10 && memcmp(":authority", name, 10) == 0) { + /* 忽略 authority,host 可以从 path 推断 */ + } else { + /* 普通头字段 */ + if (stream->request.num_headers < HTTP_MAX_HEADERS) { + int idx = stream->request.num_headers; + memcpy(stream->request.headers[idx].name, name, + namelen < sizeof(stream->request.headers[idx].name) - 1 ? namelen : sizeof(stream->request.headers[idx].name) - 1); + memcpy(stream->request.headers[idx].value, value, + valuelen < sizeof(stream->request.headers[idx].value) - 1 ? valuelen : sizeof(stream->request.headers[idx].value) - 1); + stream->request.num_headers++; + } + } + + return 0; +} + +/** + * on_frame_recv_callback - 帧接收完成 + */ +static int on_frame_recv_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + http2_session_t *h2 = (http2_session_t *)user_data; + + switch (frame->hd.type) { + case NGHTTP2_DATA: + case NGHTTP2_HEADERS: + /* 检查请求是否完整(END_STREAM) */ + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + http2_stream_data_t *stream = + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id); + if (!stream) { + return 0; + } + stream->request_complete = true; + + /* TODO: 处理请求并生成响应 */ + /* 目前返回 501 Not Implemented */ + nghttp2_nv hdrs[] = { + {(uint8_t *)":status", (uint8_t *)"501", 7, 3, 0}}; + nghttp2_submit_response(session, frame->hd.stream_id, hdrs, 1, + NULL); + http2_send_pending(h2); + } + break; + default: + break; + } + + return 0; +} + +/** + * on_stream_close_callback - 流关闭 + */ +static int on_stream_close_callback(nghttp2_session *session __attribute__((unused)), + int32_t stream_id, uint32_t error_code __attribute__((unused)), + void *user_data) { + http2_session_t *h2 = (http2_session_t *)user_data; + + /* 从链表中移除并释放 */ + http2_stream_data_t **pp = &h2->streams; + while (*pp) { + if ((*pp)->stream_id == stream_id) { + http2_stream_data_t *to_free = *pp; + *pp = to_free->next; + + if (to_free->file_fd >= 0) { + close(to_free->file_fd); + } + free(to_free->response_body); + http_request_free(&to_free->request); + free(to_free); + return 0; + } + pp = &(*pp)->next; + } + + return 0; +} + +/** + * on_data_chunk_recv_callback - 接收请求体数据 + */ +static int on_data_chunk_recv_callback(nghttp2_session *session __attribute__((unused)), + uint8_t flags __attribute__((unused)), + int32_t stream_id, const uint8_t *data, + size_t len, void *user_data __attribute__((unused))) { + http2_stream_data_t *stream = + nghttp2_session_get_stream_user_data(session, stream_id); + if (!stream) { + return 0; + } + + /* TODO: 将数据追加到请求体缓冲区 */ + (void)data; + (void)len; + + return 0; +} + +/* ===================== 连接处理 ===================== */ + +int http2_on_connection_accepted(int fd, bool tls_mode) { + /* 检查是否应启用 HTTP/2 */ + if (!tls_mode) { + /* 明文模式:需要读取客户端魔术字 "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" */ + /* 这由 server.c 在读取前几个字节时检测 */ + return 0; + } + + /* TLS 模式:ALPN 已协商为 h2 */ + http2_session_t *h2 = http2_session_create(fd, true); + if (!h2) { + return -1; + } + + /* 发送服务器连接前言 */ + if (http2_send_pending(h2) != 0) { + http2_session_destroy(h2); + return -1; + } + + return 0; +} diff --git a/http2.h b/http2.h new file mode 100644 index 0000000..6978fca --- /dev/null +++ b/http2.h @@ -0,0 +1,151 @@ +/** + * http2.h - HTTP/2 支持头文件 + * + * 使用 nghttp2 库实现 HTTP/2 协议支持。 + * 支持 TLS ALPN 协商和明文 h2c 升级。 + * + * @author xfy + */ + +#ifndef COCOON_HTTP2_H +#define COCOON_HTTP2_H + +#include "cocoon.h" +#include "http.h" +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * http2_session_t - HTTP/2 会话状态 + * + * 每个启用了 HTTP/2 的连接拥有一个此结构体。 + */ +typedef struct { + nghttp2_session *session; /**< nghttp2 会话对象 */ + int fd; /**< 底层 socket fd */ + bool tls_mode; /**< 是否通过 TLS ALPN 协商 */ + struct http2_stream_data *streams; /**< 活跃的流列表(头节点) */ +} http2_session_t; + +/** + * http2_stream_data - HTTP/2 流级数据 + * + * 每个请求流对应一个此结构体。 + */ +typedef struct http2_stream_data { + struct http2_stream_data *next; + int32_t stream_id; /**< HTTP/2 流 ID */ + http_request_t request; /**< 解析后的 HTTP 请求 */ + bool request_complete; /**< 请求是否接收完整 */ + int file_fd; /**< 响应文件 fd(-1 表示无) */ + char *response_body; /**< 响应体(用于动态内容) */ + size_t response_len; /**< 响应体长度 */ + size_t response_sent; /**< 已发送的响应体字节数 */ +} http2_stream_data_t; + +/** + * http2_init - 初始化 HTTP/2 全局状态 + * + * 应在服务器启动前调用一次。 + * + * @return 0 成功,-1 失败 + */ +int http2_init(void); + +/** + * http2_cleanup - 清理 HTTP/2 全局状态 + */ +void http2_cleanup(void); + +/** + * http2_session_create - 为连接创建 HTTP/2 会话 + * + * @param fd socket 文件描述符 + * @param tls_mode 是否通过 TLS ALPN 协商(true=TLS,false=h2c) + * @return 新会话对象,失败返回 NULL + */ +http2_session_t *http2_session_create(int fd, bool tls_mode); + +/** + * http2_session_destroy - 销毁 HTTP/2 会话 + * + * @param h2 会话对象 + */ +void http2_session_destroy(http2_session_t *h2); + +/** + * http2_session_is_http2 - 判断连接是否已升级为 HTTP/2 + * + * @param fd socket 文件描述符 + * @return true 是 HTTP/2 连接 + */ +bool http2_session_is_http2(int fd); + +/** + * http2_session_get - 获取 fd 对应的 HTTP/2 会话 + * + * @param fd socket 文件描述符 + * @return 会话对象,NULL 表示非 HTTP/2 连接 + */ +http2_session_t *http2_session_get(int fd); + +/** + * http2_recv - 接收并处理客户端数据 + * + * 将读取的数据喂给 nghttp2 库,触发回调处理请求。 + * + * @param h2 会话对象 + * @param buf 接收缓冲区 + * @param len 数据长度 + * @return 0 成功,-1 错误(应关闭连接) + */ +int http2_recv(http2_session_t *h2, const uint8_t *buf, size_t len); + +/** + * http2_send_pending - 发送挂起的 HTTP/2 帧 + * + * 应在 socket 可写时调用。 + * + * @param h2 会话对象 + * @return 0 成功,-1 错误 + */ +int http2_send_pending(http2_session_t *h2); + +/** + * http2_want_read - 检查 nghttp2 是否还需要读取数据 + * + * @param h2 会话对象 + * @return true 需要继续读取 + */ +bool http2_want_read(http2_session_t *h2); + +/** + * http2_want_write - 检查 nghttp2 是否还需要写入数据 + * + * @param h2 会话对象 + * @return true 需要继续写入 + */ +bool http2_want_write(http2_session_t *h2); + +/** + * http2_on_connection_accepted - 新连接接受后的处理 + * + * 对于 TLS 连接,在 TLS 握手完成后调用; + * 对于明文连接,在读取到客户端魔术字后调用。 + * + * @param fd socket 文件描述符 + * @param tls_mode 是否通过 TLS ALPN 协商 + * @return 0 成功,-1 失败(应关闭连接) + */ +int http2_on_connection_accepted(int fd, bool tls_mode); + +#ifdef __cplusplus +} +#endif + +#endif /* COCOON_HTTP2_H */