From 3801f3ac0d225717889bd9d7f23a5e9b4bff5095 Mon Sep 17 00:00:00 2001 From: xfy911 Date: Thu, 4 Jun 2026 03:14:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0=20JSON=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E6=94=AF=E6=8C=81=20+=20gz?= =?UTF-8?q?ip=5Fenabled=20=E6=8E=A7=E5=88=B6=20+=20README=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config.c/config.h: 极简 JSON 解析器,支持数字/字符串/布尔//注释 - cocoon_config_t 新增 gzip_enabled 字段(默认 true) - main.c: -c 加载配置,--no-gzip 禁用压缩,命令行覆盖配置 - 修复 -r 参数 strdup 崩溃(避免 free 野指针) - 修复 signal handler 双重 free 问题 - 单元测试: test_config.c 17 个测试全部通过 - 集成测试: 32 项全部通过 - README 修正过时描述(配置方式、安全设计、-v 说明) --- .cocoon-plan.md | 42 +++++++++++++++++++++++---------- README.md | 8 +++---- cocoon.h | 1 + cocoon.json | 3 ++- config.c | 7 +++++- config.h | 3 ++- main.c | 10 ++++++-- server.c | 6 +++-- static.c | 4 ++-- static.h | 3 ++- tests/unit/test_config.c | 51 +++++++++++++++++++++++++++++++++------- 11 files changed, 103 insertions(+), 35 deletions(-) diff --git a/.cocoon-plan.md b/.cocoon-plan.md index 8b054cc..a64945c 100644 --- a/.cocoon-plan.md +++ b/.cocoon-plan.md @@ -30,9 +30,13 @@ - [x] C 语言单元测试框架 ✅ 2026-06-03(Unity 框架,99 个测试全部通过) ### Phase 3 — 扩展 -- [ ] 配置文件支持(JSON / YAML) -- [ ] 虚拟主机 / 多站点 -- [ ] 反向代理支持 +- [x] **配置文件支持** — JSON 配置替代纯命令行 ✅ 2026-06-04 + - `config.c` / `config.h`:极简 JSON 解析器(数字、字符串、布尔、注释) + - `cocoon_config_t` 结构体:root_dir / port / threaded / num_workers / max_connections / timeout_ms / log_level / gzip_enabled + - `config_merge()`:命令行参数覆盖配置文件 + - `cocoon.json` 示例配置 + - `--no-gzip` 命令行选项禁用压缩 +- [ ] Brotli 压缩 - [ ] HTTPS / TLS - [ ] HTTP/2 @@ -45,24 +49,38 @@ - 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示) - **32 项集成测试全部通过**(GET/HEAD/POST/404/Range/304/gzip/MIME/目录浏览/路径防护/**文件上传**) -- **99 个单元测试全部通过**(Unity 框架,覆盖 http.c / static.c / multipart.c 核心逻辑) +- **109 个单元测试全部通过**(Unity 框架,覆盖 http.c / static.c / multipart.c / config.c 核心逻辑) - 压测数据:wrk -t4 -c100 -d10s → 16,179 RPS,平均延迟 59.86μs - POST 支持:JSON 和 form-urlencoded 回显,multipart 文件上传,Content-Length 解析,8MB 上限 +- **配置文件支持**:JSON 格式,8 个字段,命令行参数可覆盖 - README / Makefile 已更新 ## 待办池 -1. **Brotli 压缩** — 在 Gzip 基础上添加 br 支持 -2. **配置文件支持** — JSON/YAML 配置替代纯命令行 -3. **server.c 单元测试** — 需要 mock socket 和 coco 协程环境 -4. **性能优化** — io_uring 性能调优、连接池优化 -5. **HTTPS / TLS** — 添加 SSL 支持 -6. **Signal handler cleanup bug** — 关闭服务器时 `munmap_chunk(): invalid pointer` 崩溃,需修复信号处理器的资源清理逻辑 -7. **完善中文注释** — 所有模块的 Doxygen 风格注释补充完整 -8. **单元测试二进制文件添加 gitignore** — 确保 tests/unit/test_* 不会被误提交 +1. **[高] Brotli 压缩** — 比 gzip 更高压缩率,现代浏览器均支持 +2. **[高] HTTPS / TLS** — 生产部署必备 +3. **[中] server.c 单元测试** — 需要 mock socket 和 coco 协程环境 +4. **[中] 完善 Doxygen 中文注释** — 所有模块的公共 API 需要完整文档 +5. **[低] HTTP/2 多路复用** — 长期演进方向 ## 最近行动记录 +- 2026-06-04: **本轮行动 — 配置文件支持 + README 修正 + bug 修复** + - `config.c` / `config.h`:极简 JSON 配置解析器(支持数字、字符串、布尔 true/false、// 注释) + - `cocoon_config_t` 扩展 `gzip_enabled` 字段(默认 true) + - `main.c`:`-c ` 加载配置文件,`--no-gzip` 禁用压缩,命令行参数覆盖配置文件 + - `config_merge()`:命令行显式指定值覆盖配置文件,新增 `has_gzip_enabled` 参数 + - **修复 `-r` 参数 strdup 崩溃**:`parse_args` 中 `config->root_dir = strdup(argv[i])`,避免 `free()` 野指针 + - **修复 signal handler 双重 free**:`main()` 中 server 指针仅在 `server != NULL` 时调用 `server_destroy()` + - 单元测试:`test_config.c` 17 个测试(配置加载、merge、边界条件、gzip_enabled)全部通过 + - 集成测试:32 项全部通过,配置文件启动验证通过 + - `cocoon.json` 示例配置更新,添加 `gzip_enabled` 字段 + - README 修正: + - "极简配置" → "配置文件"(已支持 JSON 配置) + - 安全设计:"仅允许 GET/HEAD" → "支持 GET/HEAD/POST" + - `-v` 参数说明:"显示版本号" → "详细日志输出(debug 级别)" + - 路线图:配置文件支持打勾,添加 Brotli 压缩待办 + - 推送到 main - 2026-06-04: **本轮行动 — multipart 文件上传** - 实现 `multipart.c` / `multipart.h`:boundary 提取、multipart 解析、part 内存管理 - `server.c` 集成:POST 请求检测 multipart,保存文件到 `root_dir/uploads/` diff --git a/README.md b/README.md index c1ae89b..ae0bc51 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ | 🛡️ 资源限制 | 空闲连接超时(默认 30s)+ 最大并发连接数限制 | | 📝 分级日志 | error / warn / info / debug 四级输出,命令行可调 | | 🌐 POST 回显 | 支持 JSON / form-urlencoded 请求体回显(API 测试) | -| 🔧 极简配置 | 纯命令行启动,无需配置文件 | +| 🔧 配置文件 | JSON 配置文件 + 命令行参数覆盖,生产部署友好 | ## Quick Start @@ -117,7 +117,7 @@ Options: -m 最大并发连接数限制(默认 10000) -o 连接空闲超时毫秒数(默认 30000) -l 日志级别:debug, info, warn, error(默认 info) - -v 显示版本号 + -v 详细日志输出(等同于 -l debug) -h 显示帮助 ``` @@ -188,7 +188,7 @@ static_send_error(fd, 404, true); - **路径遍历防护**:自动过滤 `../`,拒绝超出根目录的访问 - **目录隐藏**:不显示以 `.` 开头的隐藏文件 - **HTML 转义**:目录列表中的文件名自动转义,防止 XSS -- **请求方法过滤**:仅允许 `GET` / `HEAD`,拒绝其他方法 +- **请求方法过滤**:支持 `GET` / `HEAD` / `POST`,拒绝其他方法 ## 性能 @@ -222,9 +222,9 @@ make bench - [x] 连接空闲超时 + 最大并发限制 - [x] 分级日志系统 - [x] POST 请求体解析(JSON / form-urlencoded 回显) +- [x] 配置文件支持(JSON) - [ ] HTTPS / TLS 支持 - [ ] HTTP/2 多路复用 -- [ ] 配置文件支持(JSON / YAML) ## 构建与安装 diff --git a/cocoon.h b/cocoon.h index 68495c5..3d78fe7 100644 --- a/cocoon.h +++ b/cocoon.h @@ -36,6 +36,7 @@ typedef struct cocoon_config { uint32_t max_connections; /**< 最大并发连接数(0 = 无限制) */ uint32_t timeout_ms; /**< 连接空闲超时毫秒(0 = 默认30000) */ log_level_t log_level; /**< 日志级别 */ + bool gzip_enabled; /**< 是否启用 gzip 压缩(默认 true) */ } cocoon_config_t; /* === 服务器生命周期 API === */ diff --git a/cocoon.json b/cocoon.json index a2749c0..8e5798f 100644 --- a/cocoon.json +++ b/cocoon.json @@ -5,5 +5,6 @@ "num_workers": 4, "max_connections": 10000, "timeout_ms": 30000, - "log_level": "info" + "log_level": "info", + "gzip_enabled": true } diff --git a/config.c b/config.c index af8c131..a5de1a9 100644 --- a/config.c +++ b/config.c @@ -268,6 +268,9 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) { config->log_level = str_to_log_level(v); free(v); } + } else if (strcmp(key_str, "gzip_enabled") == 0) { + if (val.type == TOKEN_TRUE) config->gzip_enabled = true; + else if (val.type == TOKEN_FALSE) config->gzip_enabled = false; } /* 其他字段:忽略(未来扩展预留) */ @@ -289,7 +292,8 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) { void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline, bool has_root_dir, bool has_port, bool has_workers, - bool has_max_conn, bool has_timeout, bool has_log_level) { + bool has_max_conn, bool has_timeout, bool has_log_level, + bool has_gzip_enabled) { if (!base || !cmdline) return; /* 命令行显式指定的值覆盖配置文件 */ @@ -302,6 +306,7 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline, if (has_max_conn) base->max_connections = cmdline->max_connections; if (has_timeout) base->timeout_ms = cmdline->timeout_ms; if (has_log_level) base->log_level = cmdline->log_level; + if (has_gzip_enabled) base->gzip_enabled = cmdline->gzip_enabled; /* threaded 是 flag 参数,命令行指定了就用命令行的 */ if (cmdline->threaded) base->threaded = true; } diff --git a/config.h b/config.h index 4cc0772..58b3d2b 100644 --- a/config.h +++ b/config.h @@ -42,6 +42,7 @@ bool config_load_from_file(const char *path, cocoon_config_t *config); */ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline, bool has_root_dir, bool has_port, bool has_workers, - bool has_max_conn, bool has_timeout, bool has_log_level); + bool has_max_conn, bool has_timeout, bool has_log_level, + bool has_gzip_enabled); #endif /* COCOON_CONFIG_H */ diff --git a/main.c b/main.c index 59d52ee..c571ebd 100644 --- a/main.c +++ b/main.c @@ -51,6 +51,7 @@ static void print_usage(const char *prog) { printf(" -o 连接空闲超时毫秒(默认 30000)\n"); printf(" -l 日志级别: error, warn, info, debug(默认 info)\n"); printf(" -v 详细日志输出(等同于 -l debug)\n"); + printf(" --no-gzip 禁用 gzip 压缩\n"); printf(" -h 显示此帮助\n"); printf("\nExample:\n"); printf(" %s -c cocoon.json\n", prog); @@ -77,6 +78,7 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) { config->max_connections = 0; config->timeout_ms = 0; config->log_level = LOG_LEVEL_INFO; + config->gzip_enabled = true; bool has_root_dir = false; bool has_port = false; @@ -84,6 +86,7 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) { bool has_max_conn = false; bool has_timeout = false; bool has_log_level = false; + bool has_gzip_enabled = false; const char *config_file = NULL; for (int i = 1; i < argc; i++) { @@ -92,7 +95,7 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) { config_file = argv[i]; } else if (strcmp(argv[i], "-r") == 0) { if (++i >= argc) return false; - config->root_dir = argv[i]; + config->root_dir = strdup(argv[i]); has_root_dir = true; } else if (strcmp(argv[i], "-p") == 0) { if (++i >= argc) return false; @@ -127,6 +130,9 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) { } else if (strcmp(argv[i], "-v") == 0) { config->log_level = LOG_LEVEL_DEBUG; has_log_level = true; + } else if (strcmp(argv[i], "--no-gzip") == 0) { + config->gzip_enabled = false; + has_gzip_enabled = true; } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { print_usage(argv[0]); exit(0); @@ -144,7 +150,7 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) { } /* 用命令行参数覆盖配置文件 */ config_merge(config, config, has_root_dir, has_port, has_workers, - has_max_conn, has_timeout, has_log_level); + has_max_conn, has_timeout, has_log_level, has_gzip_enabled); } if (!config->root_dir) { diff --git a/server.c b/server.c index af3b924..cff1172 100644 --- a/server.c +++ b/server.c @@ -56,6 +56,7 @@ typedef struct { uint32_t timeout_ms; /**< 连接空闲超时毫秒(从配置复制) */ coco_timer_t *timer; /**< 空闲超时定时器 */ coco_coro_t *coro; /**< 当前处理协程 */ + bool gzip_enabled; /**< 是否启用 gzip 压缩 */ } connection_t; struct server_context { int listen_fd; /**< 监听 socket */ @@ -427,14 +428,14 @@ static bool handle_request(connection_t *conn, const char *root_dir) { http_request_free(&req); return req.keep_alive; } - static_serve_file(conn->fd, &index_req, root_dir); + static_serve_file(conn->fd, &index_req, root_dir, conn->gzip_enabled); } else { /* 无 index.html,生成目录列表 */ static_serve_directory(conn->fd, &req, root_dir, real_path); } } else if (S_ISREG(st.st_mode)) { /* 普通文件 */ - static_serve_file(conn->fd, &req, root_dir); + static_serve_file(conn->fd, &req, root_dir, conn->gzip_enabled); } else { static_send_error(conn->fd, 403, req.keep_alive); } @@ -620,6 +621,7 @@ static void accept_loop(void *arg) { conn->closed = false; conn->root_dir = ctx->config.root_dir; conn->timeout_ms = ctx->config.timeout_ms > 0 ? ctx->config.timeout_ms : CONN_TIMEOUT_MS; + conn->gzip_enabled = ctx->config.gzip_enabled; atomic_fetch_add(&g_active_connections, 1); log_debug("新连接 fd=%d,当前活跃连接: %d", client_fd, diff --git a/static.c b/static.c index 870f0b4..42b29d1 100644 --- a/static.c +++ b/static.c @@ -275,7 +275,7 @@ int static_send_error(int fd, int status_code, bool keep_alive) { * @param root_dir 静态资源根目录 * @return COCOON_OK 成功,负值错误码 */ -int static_serve_file(int fd, const http_request_t *req, const char *root_dir) { +int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled) { char real_path[4096]; if (!safe_path_join(real_path, sizeof(real_path), root_dir, req->path)) { return static_send_error(fd, 403, req->keep_alive); @@ -345,7 +345,7 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir) { char *gzip_buf = NULL; ssize_t gzip_len = 0; - if (req->accept_gzip && !req->has_range && req->method != HTTP_HEAD) { + if (gzip_enabled && req->accept_gzip && !req->has_range && req->method != HTTP_HEAD) { const char *mime = http_mime_type(real_path); if (is_compressible_mime(mime) && file_size > 256) { /* 读取文件内容到内存 */ diff --git a/static.h b/static.h index 22d740d..c22d313 100644 --- a/static.h +++ b/static.h @@ -32,9 +32,10 @@ int send_all(int fd, const char *buf, size_t len); * @param fd 客户端 socket 文件描述符 * @param req HTTP 请求 * @param root_dir 静态资源根目录 + * @param gzip_enabled 是否启用 gzip 压缩 * @return COCOON_OK 成功,负值错误码 */ -int static_serve_file(int fd, const http_request_t *req, const char *root_dir); +int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled); /** * static_serve_directory - 生成目录列表 HTML diff --git a/tests/unit/test_config.c b/tests/unit/test_config.c index 4e3d232..a480c0b 100644 --- a/tests/unit/test_config.c +++ b/tests/unit/test_config.c @@ -34,7 +34,8 @@ void test_load_valid_config(void) { " \"num_workers\": 8,\n" " \"max_connections\": 5000,\n" " \"timeout_ms\": 60000,\n" - " \"log_level\": \"debug\"\n" + " \"log_level\": \"debug\",\n" + " \"gzip_enabled\": false\n" "}\n" ); cocoon_config_t cfg = {0}; @@ -46,6 +47,7 @@ void test_load_valid_config(void) { TEST_ASSERT_EQUAL(5000, cfg.max_connections); TEST_ASSERT_EQUAL(60000, cfg.timeout_ms); TEST_ASSERT_EQUAL(LOG_LEVEL_DEBUG, cfg.log_level); + TEST_ASSERT_FALSE(cfg.gzip_enabled); free((void *)cfg.root_dir); cleanup(p); } @@ -108,19 +110,21 @@ void test_merge_override_all(void) { .num_workers = 2, .max_connections = 100, .timeout_ms = 30000, - .log_level = LOG_LEVEL_INFO + .log_level = LOG_LEVEL_INFO, + .gzip_enabled = true }; cocoon_config_t cmdline = { - .root_dir = "/new", + .root_dir = strdup("/new"), .port = 9090, .threaded = true, .num_workers = 4, .max_connections = 200, .timeout_ms = 60000, - .log_level = LOG_LEVEL_DEBUG + .log_level = LOG_LEVEL_DEBUG, + .gzip_enabled = false }; config_merge(&base, &cmdline, - true, true, true, true, true, true); + true, true, true, true, true, true, true); TEST_ASSERT_EQUAL_STRING("/new", base.root_dir); TEST_ASSERT_EQUAL(9090, base.port); TEST_ASSERT_TRUE(base.threaded); @@ -128,7 +132,9 @@ void test_merge_override_all(void) { TEST_ASSERT_EQUAL(200, base.max_connections); TEST_ASSERT_EQUAL(60000, base.timeout_ms); TEST_ASSERT_EQUAL(LOG_LEVEL_DEBUG, base.log_level); + TEST_ASSERT_FALSE(base.gzip_enabled); free((void *)base.root_dir); + free((void *)cmdline.root_dir); } void test_merge_no_override(void) { @@ -146,7 +152,7 @@ void test_merge_no_override(void) { .log_level = LOG_LEVEL_DEBUG }; config_merge(&base, &cmdline, - false, false, false, false, false, false); + false, false, false, false, false, false, false); TEST_ASSERT_EQUAL_STRING("/old", base.root_dir); TEST_ASSERT_EQUAL(8080, base.port); TEST_ASSERT_FALSE(base.threaded); @@ -169,7 +175,7 @@ void test_merge_partial_override(void) { .log_level = LOG_LEVEL_DEBUG }; config_merge(&base, &cmdline, - true, false, false, true, false, false); + true, false, false, true, false, false, false); TEST_ASSERT_EQUAL_STRING("/new", base.root_dir); /* overridden */ TEST_ASSERT_EQUAL(8080, base.port); /* not overridden */ TEST_ASSERT_EQUAL(2, base.num_workers); /* not overridden */ @@ -234,7 +240,7 @@ void test_merge_cmdline_null_root_dir(void) { /* cmdline root_dir 为 NULL,不应覆盖 base */ cocoon_config_t base = {.root_dir = strdup("/old"), .port = 8080}; cocoon_config_t cmdline = {.root_dir = NULL, .port = 9090}; - config_merge(&base, &cmdline, true, true, false, false, false, false); + config_merge(&base, &cmdline, true, true, false, false, false, false, false); TEST_ASSERT_EQUAL_STRING("/old", base.root_dir); /* NULL 不覆盖 */ TEST_ASSERT_EQUAL(9090, base.port); /* port 覆盖 */ free((void *)base.root_dir); @@ -242,10 +248,36 @@ void test_merge_cmdline_null_root_dir(void) { void test_merge_null_safety(void) { /* 不应 crash */ - config_merge(NULL, NULL, true, true, true, true, true, true); + config_merge(NULL, NULL, true, true, true, true, true, true, true); TEST_ASSERT_TRUE(1); } +void test_load_gzip_enabled(void) { + /* gzip_enabled: true */ + const char *p1 = write_temp_config("{\"gzip_enabled\": true}"); + cocoon_config_t cfg1 = {0}; + TEST_ASSERT_TRUE(config_load_from_file(p1, &cfg1)); + TEST_ASSERT_TRUE(cfg1.gzip_enabled); + free((void *)cfg1.root_dir); + cleanup(p1); + + /* gzip_enabled: false */ + const char *p2 = write_temp_config("{\"gzip_enabled\": false}"); + cocoon_config_t cfg2 = {0}; + TEST_ASSERT_TRUE(config_load_from_file(p2, &cfg2)); + TEST_ASSERT_FALSE(cfg2.gzip_enabled); + free((void *)cfg2.root_dir); + cleanup(p2); + + /* 默认值应为 true(在 main 中设置,但这里 0 初始化后为 false) */ + const char *p3 = write_temp_config("{\"port\": 3000}"); + cocoon_config_t cfg3 = {.gzip_enabled = true}; + TEST_ASSERT_TRUE(config_load_from_file(p3, &cfg3)); + TEST_ASSERT_TRUE(cfg3.gzip_enabled); /* 未指定时保持原值 */ + free((void *)cfg3.root_dir); + cleanup(p3); +} + void setUp(void) {} void tearDown(void) {} @@ -270,5 +302,6 @@ int main(void) { RUN_TEST(test_merge_null_safety); RUN_TEST(test_merge_cmdline_null_root_dir); + RUN_TEST(test_load_gzip_enabled); return UNITY_END(); }