feat(compression): 添加 Brotli 压缩支持,优先于 Gzip
- static.c: 新增 brotli_compress(),使用 libbrotlienc(质量 11,窗口 22) - static_serve_file(): 优先 Brotli,回退 Gzip,二进制文件跳过 - http.c: 解析 Accept-Encoding: br,http_request_t 新增 accept_brotli - cocoon.h: cocoon_config_t 新增 brotli_enabled(默认 true) - config.c/h: 解析 brotli_enabled JSON 字段,config_merge() 新增 has_brotli_enabled - main.c: --no-brotli 命令行选项 - server.c: 连接结构体新增 brotli_enabled - Makefile: 链接 -lbrotlienc - 单元测试: test_static.c 新增 4 个 Brotli 测试;test_config.c 新增 brotli 配置测试 - 集成测试: 新增 5 项 Brotli 测试(br 压缩、优先策略、图片不压缩) - cocoon.json: 添加 brotli_enabled 字段 - README: 更新特性描述、curl 示例、命令行参数、路线图 - 113 个单元测试全部通过,37 项集成测试全部通过
This commit is contained in:
parent
3801f3ac0d
commit
ea5436febb
@ -36,7 +36,7 @@
|
|||||||
- `config_merge()`:命令行参数覆盖配置文件
|
- `config_merge()`:命令行参数覆盖配置文件
|
||||||
- `cocoon.json` 示例配置
|
- `cocoon.json` 示例配置
|
||||||
- `--no-gzip` 命令行选项禁用压缩
|
- `--no-gzip` 命令行选项禁用压缩
|
||||||
- [ ] Brotli 压缩
|
- [x] **Brotli 压缩** — 比 gzip 更高压缩率,现代浏览器均支持 ✅ 2026-06-04
|
||||||
- [ ] HTTPS / TLS
|
- [ ] HTTPS / TLS
|
||||||
- [ ] HTTP/2
|
- [ ] HTTP/2
|
||||||
|
|
||||||
@ -48,8 +48,8 @@
|
|||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
|
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
|
||||||
- **32 项集成测试全部通过**(GET/HEAD/POST/404/Range/304/gzip/MIME/目录浏览/路径防护/**文件上传**)
|
- **37 项集成测试全部通过**(GET/HEAD/POST/404/Range/304/gzip/brotli/MIME/目录浏览/路径防护/文件上传)
|
||||||
- **109 个单元测试全部通过**(Unity 框架,覆盖 http.c / static.c / multipart.c / config.c 核心逻辑)
|
- **113 个单元测试全部通过**(Unity 框架,覆盖 http.c / static.c / multipart.c / config.c 核心逻辑)
|
||||||
- 压测数据:wrk -t4 -c100 -d10s → 16,179 RPS,平均延迟 59.86μs
|
- 压测数据:wrk -t4 -c100 -d10s → 16,179 RPS,平均延迟 59.86μs
|
||||||
- POST 支持:JSON 和 form-urlencoded 回显,multipart 文件上传,Content-Length 解析,8MB 上限
|
- POST 支持:JSON 和 form-urlencoded 回显,multipart 文件上传,Content-Length 解析,8MB 上限
|
||||||
- **配置文件支持**:JSON 格式,8 个字段,命令行参数可覆盖
|
- **配置文件支持**:JSON 格式,8 个字段,命令行参数可覆盖
|
||||||
@ -57,14 +57,29 @@
|
|||||||
|
|
||||||
## 待办池
|
## 待办池
|
||||||
|
|
||||||
1. **[高] Brotli 压缩** — 比 gzip 更高压缩率,现代浏览器均支持
|
1. **[高] HTTPS / TLS** — 生产部署必备
|
||||||
2. **[高] HTTPS / TLS** — 生产部署必备
|
2. **[高] HTTP/2 多路复用** — 长期演进方向
|
||||||
3. **[中] server.c 单元测试** — 需要 mock socket 和 coco 协程环境
|
3. **[中] server.c 单元测试** — 需要 mock socket 和 coco 协程环境
|
||||||
4. **[中] 完善 Doxygen 中文注释** — 所有模块的公共 API 需要完整文档
|
4. **[中] 完善 Doxygen 中文注释** — 所有模块的公共 API 需要完整文档
|
||||||
5. **[低] HTTP/2 多路复用** — 长期演进方向
|
|
||||||
|
|
||||||
## 最近行动记录
|
## 最近行动记录
|
||||||
|
|
||||||
|
- 2026-06-04: **本轮行动 — Brotli 压缩支持**
|
||||||
|
- `static.c`:`brotli_compress()` 函数,使用 `libbrotlienc` 高质量压缩(质量 11,窗口 22)
|
||||||
|
- `static_serve_file()`:优先 Brotli,回退 Gzip,不可压缩/二进制文件跳过
|
||||||
|
- `http.c`:解析 `Accept-Encoding: br`,`http_request_t` 新增 `accept_brotli` 字段
|
||||||
|
- `cocoon.h`:`cocoon_config_t` 新增 `brotli_enabled` 字段(默认 true)
|
||||||
|
- `config.c` / `config.h`:解析 `brotli_enabled` JSON 字段,`config_merge()` 新增 `has_brotli_enabled` 参数
|
||||||
|
- `main.c`:`--no-brotli` 命令行选项,默认启用 Brotli
|
||||||
|
- `server.c`:连接结构体新增 `brotli_enabled`,传递至 `static_serve_file()`
|
||||||
|
- `Makefile`:链接 `-lbrotlienc`,单元测试编译也添加
|
||||||
|
- 单元测试:`test_static.c` 新增 4 个 Brotli 测试(可压缩/不可压缩/小数据/溢出)
|
||||||
|
- 单元测试:`test_config.c` 新增 `brotli_enabled` 解析与 merge 测试,修复所有 `config_merge()` 调用签名
|
||||||
|
- 集成测试:`assert_brotli()` / `assert_brotli_preferred()` / `assert_not_brotli()`,新增 5 项 Brotli 测试
|
||||||
|
- `cocoon.json` 示例配置添加 `brotli_enabled: true`
|
||||||
|
- README:更新特性描述、curl 示例、命令行参数、路线图
|
||||||
|
- 单元测试:113 个全部通过,集成测试:37 项全部通过
|
||||||
|
- 推送到 main
|
||||||
- 2026-06-04: **本轮行动 — 配置文件支持 + README 修正 + bug 修复**
|
- 2026-06-04: **本轮行动 — 配置文件支持 + README 修正 + bug 修复**
|
||||||
- `config.c` / `config.h`:极简 JSON 配置解析器(支持数字、字符串、布尔 true/false、// 注释)
|
- `config.c` / `config.h`:极简 JSON 配置解析器(支持数字、字符串、布尔 true/false、// 注释)
|
||||||
- `cocoon_config_t` 扩展 `gzip_enabled` 字段(默认 true)
|
- `cocoon_config_t` 扩展 `gzip_enabled` 字段(默认 true)
|
||||||
|
|||||||
4
Makefile
4
Makefile
@ -7,7 +7,7 @@ COCO_LIB ?= $(COCO_DIR)/build
|
|||||||
|
|
||||||
CC = gcc
|
CC = gcc
|
||||||
CFLAGS = -Wall -Wextra -O2 -std=c11 -D_GNU_SOURCE -I$(COCO_INCLUDE)
|
CFLAGS = -Wall -Wextra -O2 -std=c11 -D_GNU_SOURCE -I$(COCO_INCLUDE)
|
||||||
LDFLAGS = -L$(COCO_LIB) -lcoco -lpthread -lm -luring -lz
|
LDFLAGS = -L$(COCO_LIB) -lcoco -lpthread -lm -luring -lz -lbrotlienc
|
||||||
|
|
||||||
# 调试模式
|
# 调试模式
|
||||||
DEBUG ?= 0
|
DEBUG ?= 0
|
||||||
@ -88,7 +88,7 @@ $(UNIT_TEST_DIR)/test_http: $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SR
|
|||||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SRC) -lm
|
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SRC) -lm
|
||||||
|
|
||||||
$(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c $(UNITY_SRC)
|
$(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c $(UNITY_SRC)
|
||||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c $(UNITY_SRC) -lm -lz
|
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c $(UNITY_SRC) -lm -lz -lbrotlienc
|
||||||
|
|
||||||
$(UNIT_TEST_DIR)/test_log: $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC)
|
$(UNIT_TEST_DIR)/test_log: $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC)
|
||||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) -lm
|
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) -lm
|
||||||
|
|||||||
11
README.md
11
README.md
@ -14,7 +14,7 @@
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 🚀 协程驱动 | 基于 coco 有栈协程,每个连接一个协程,上下文切换 < 100ns |
|
| 🚀 协程驱动 | 基于 coco 有栈协程,每个连接一个协程,上下文切换 < 100ns |
|
||||||
| 📁 静态托管 | 目录浏览、MIME 自动识别(25+ 种)、Range 请求、index.html 自动补全 |
|
| 📁 静态托管 | 目录浏览、MIME 自动识别(25+ 种)、Range 请求、index.html 自动补全 |
|
||||||
| 🗜️ 动态压缩 | 对文本/JSON/CSS/JS 启用 Gzip 压缩,图片等二进制文件跳过 |
|
| 🗜️ 动态压缩 | 对文本/JSON/CSS/JS 启用 Brotli / Gzip 压缩,图片等二进制文件跳过,Brotli 优先 |
|
||||||
| 📦 缓存协商 | ETag + Last-Modified + If-None-Match + If-Modified-Since,自动 304 |
|
| 📦 缓存协商 | ETag + Last-Modified + If-None-Match + If-Modified-Since,自动 304 |
|
||||||
| 🧵 多核扩展 | M:N 调度器 + Work-stealing,自动负载均衡至多核 |
|
| 🧵 多核扩展 | M:N 调度器 + Work-stealing,自动负载均衡至多核 |
|
||||||
| ⚡ 零拷贝 | 优先使用 `sendfile`,减少用户态/内核态数据拷贝 |
|
| ⚡ 零拷贝 | 优先使用 `sendfile`,减少用户态/内核态数据拷贝 |
|
||||||
@ -84,7 +84,10 @@ curl -H "Range: bytes=0-99" http://localhost:8080/index.html
|
|||||||
# 缓存协商(304 Not Modified)
|
# 缓存协商(304 Not Modified)
|
||||||
curl -I -H "If-None-Match: \"your-etag\"" http://localhost:8080/index.html
|
curl -I -H "If-None-Match: \"your-etag\"" http://localhost:8080/index.html
|
||||||
|
|
||||||
# Gzip 压缩
|
# Brotli 压缩(优先)
|
||||||
|
curl -I -H "Accept-Encoding: br" http://localhost:8080/index.html
|
||||||
|
|
||||||
|
# Gzip 压缩(回退)
|
||||||
curl -I -H "Accept-Encoding: gzip" http://localhost:8080/index.html
|
curl -I -H "Accept-Encoding: gzip" http://localhost:8080/index.html
|
||||||
|
|
||||||
# POST JSON 回显
|
# POST JSON 回显
|
||||||
@ -118,6 +121,8 @@ Options:
|
|||||||
-o <ms> 连接空闲超时毫秒数(默认 30000)
|
-o <ms> 连接空闲超时毫秒数(默认 30000)
|
||||||
-l <level> 日志级别:debug, info, warn, error(默认 info)
|
-l <level> 日志级别:debug, info, warn, error(默认 info)
|
||||||
-v 详细日志输出(等同于 -l debug)
|
-v 详细日志输出(等同于 -l debug)
|
||||||
|
--no-gzip 禁用 gzip 压缩
|
||||||
|
--no-brotli 禁用 brotli 压缩
|
||||||
-h 显示帮助
|
-h 显示帮助
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -218,7 +223,7 @@ make bench
|
|||||||
## 路线图
|
## 路线图
|
||||||
|
|
||||||
- [x] ETag / Last-Modified 缓存协商 + 304 Not Modified
|
- [x] ETag / Last-Modified 缓存协商 + 304 Not Modified
|
||||||
- [x] Gzip 动态压缩
|
- [x] Brotli / Gzip 动态压缩(Brotli 优先)
|
||||||
- [x] 连接空闲超时 + 最大并发限制
|
- [x] 连接空闲超时 + 最大并发限制
|
||||||
- [x] 分级日志系统
|
- [x] 分级日志系统
|
||||||
- [x] POST 请求体解析(JSON / form-urlencoded 回显)
|
- [x] POST 请求体解析(JSON / form-urlencoded 回显)
|
||||||
|
|||||||
1
cocoon.h
1
cocoon.h
@ -37,6 +37,7 @@ typedef struct cocoon_config {
|
|||||||
uint32_t timeout_ms; /**< 连接空闲超时毫秒(0 = 默认30000) */
|
uint32_t timeout_ms; /**< 连接空闲超时毫秒(0 = 默认30000) */
|
||||||
log_level_t log_level; /**< 日志级别 */
|
log_level_t log_level; /**< 日志级别 */
|
||||||
bool gzip_enabled; /**< 是否启用 gzip 压缩(默认 true) */
|
bool gzip_enabled; /**< 是否启用 gzip 压缩(默认 true) */
|
||||||
|
bool brotli_enabled; /**< 是否启用 brotli 压缩(默认 true) */
|
||||||
} cocoon_config_t;
|
} cocoon_config_t;
|
||||||
|
|
||||||
/* === 服务器生命周期 API === */
|
/* === 服务器生命周期 API === */
|
||||||
|
|||||||
@ -6,5 +6,6 @@
|
|||||||
"max_connections": 10000,
|
"max_connections": 10000,
|
||||||
"timeout_ms": 30000,
|
"timeout_ms": 30000,
|
||||||
"log_level": "info",
|
"log_level": "info",
|
||||||
"gzip_enabled": true
|
"gzip_enabled": true,
|
||||||
|
"brotli_enabled": true
|
||||||
}
|
}
|
||||||
|
|||||||
6
config.c
6
config.c
@ -271,6 +271,9 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
|
|||||||
} else if (strcmp(key_str, "gzip_enabled") == 0) {
|
} else if (strcmp(key_str, "gzip_enabled") == 0) {
|
||||||
if (val.type == TOKEN_TRUE) config->gzip_enabled = true;
|
if (val.type == TOKEN_TRUE) config->gzip_enabled = true;
|
||||||
else if (val.type == TOKEN_FALSE) config->gzip_enabled = false;
|
else if (val.type == TOKEN_FALSE) config->gzip_enabled = false;
|
||||||
|
} else if (strcmp(key_str, "brotli_enabled") == 0) {
|
||||||
|
if (val.type == TOKEN_TRUE) config->brotli_enabled = true;
|
||||||
|
else if (val.type == TOKEN_FALSE) config->brotli_enabled = false;
|
||||||
}
|
}
|
||||||
/* 其他字段:忽略(未来扩展预留) */
|
/* 其他字段:忽略(未来扩展预留) */
|
||||||
|
|
||||||
@ -293,7 +296,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,
|
void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
|
||||||
bool has_root_dir, bool has_port, bool has_workers,
|
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) {
|
bool has_gzip_enabled, bool has_brotli_enabled) {
|
||||||
if (!base || !cmdline) return;
|
if (!base || !cmdline) return;
|
||||||
|
|
||||||
/* 命令行显式指定的值覆盖配置文件 */
|
/* 命令行显式指定的值覆盖配置文件 */
|
||||||
@ -307,6 +310,7 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
|
|||||||
if (has_timeout) base->timeout_ms = cmdline->timeout_ms;
|
if (has_timeout) base->timeout_ms = cmdline->timeout_ms;
|
||||||
if (has_log_level) base->log_level = cmdline->log_level;
|
if (has_log_level) base->log_level = cmdline->log_level;
|
||||||
if (has_gzip_enabled) base->gzip_enabled = cmdline->gzip_enabled;
|
if (has_gzip_enabled) base->gzip_enabled = cmdline->gzip_enabled;
|
||||||
|
if (has_brotli_enabled) base->brotli_enabled = cmdline->brotli_enabled;
|
||||||
/* threaded 是 flag 参数,命令行指定了就用命令行的 */
|
/* threaded 是 flag 参数,命令行指定了就用命令行的 */
|
||||||
if (cmdline->threaded) base->threaded = true;
|
if (cmdline->threaded) base->threaded = true;
|
||||||
}
|
}
|
||||||
|
|||||||
2
config.h
2
config.h
@ -43,6 +43,6 @@ bool config_load_from_file(const char *path, cocoon_config_t *config);
|
|||||||
void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
|
void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
|
||||||
bool has_root_dir, bool has_port, bool has_workers,
|
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);
|
bool has_gzip_enabled, bool has_brotli_enabled);
|
||||||
|
|
||||||
#endif /* COCOON_CONFIG_H */
|
#endif /* COCOON_CONFIG_H */
|
||||||
|
|||||||
1
http.c
1
http.c
@ -128,6 +128,7 @@ static void parse_headers(const char **p, const char *end, http_request_t *req)
|
|||||||
req->has_accept_encoding = true;
|
req->has_accept_encoding = true;
|
||||||
const char *val = req->headers[req->num_headers].value;
|
const char *val = req->headers[req->num_headers].value;
|
||||||
if (strstr(val, "gzip") != NULL) req->accept_gzip = true;
|
if (strstr(val, "gzip") != NULL) req->accept_gzip = true;
|
||||||
|
if (strstr(val, "br") != NULL) req->accept_brotli = true;
|
||||||
if (strstr(val, "deflate") != NULL) req->accept_deflate = true;
|
if (strstr(val, "deflate") != NULL) req->accept_deflate = true;
|
||||||
} else if (strcmp(req->headers[req->num_headers].name, "content-type") == 0) {
|
} else if (strcmp(req->headers[req->num_headers].name, "content-type") == 0) {
|
||||||
int copy_len = strlen(req->headers[req->num_headers].value);
|
int copy_len = strlen(req->headers[req->num_headers].value);
|
||||||
|
|||||||
1
http.h
1
http.h
@ -58,6 +58,7 @@ typedef struct {
|
|||||||
|
|
||||||
/* 压缩相关头 */
|
/* 压缩相关头 */
|
||||||
bool accept_gzip; /* 客户端支持 gzip */
|
bool accept_gzip; /* 客户端支持 gzip */
|
||||||
|
bool accept_brotli; /* 客户端支持 brotli */
|
||||||
bool accept_deflate; /* 客户端支持 deflate */
|
bool accept_deflate; /* 客户端支持 deflate */
|
||||||
bool has_accept_encoding;
|
bool has_accept_encoding;
|
||||||
|
|
||||||
|
|||||||
8
main.c
8
main.c
@ -52,6 +52,7 @@ static void print_usage(const char *prog) {
|
|||||||
printf(" -l <level> 日志级别: error, warn, info, debug(默认 info)\n");
|
printf(" -l <level> 日志级别: error, warn, info, debug(默认 info)\n");
|
||||||
printf(" -v 详细日志输出(等同于 -l debug)\n");
|
printf(" -v 详细日志输出(等同于 -l debug)\n");
|
||||||
printf(" --no-gzip 禁用 gzip 压缩\n");
|
printf(" --no-gzip 禁用 gzip 压缩\n");
|
||||||
|
printf(" --no-brotli 禁用 brotli 压缩\n");
|
||||||
printf(" -h 显示此帮助\n");
|
printf(" -h 显示此帮助\n");
|
||||||
printf("\nExample:\n");
|
printf("\nExample:\n");
|
||||||
printf(" %s -c cocoon.json\n", prog);
|
printf(" %s -c cocoon.json\n", prog);
|
||||||
@ -79,6 +80,7 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
|
|||||||
config->timeout_ms = 0;
|
config->timeout_ms = 0;
|
||||||
config->log_level = LOG_LEVEL_INFO;
|
config->log_level = LOG_LEVEL_INFO;
|
||||||
config->gzip_enabled = true;
|
config->gzip_enabled = true;
|
||||||
|
config->brotli_enabled = true;
|
||||||
|
|
||||||
bool has_root_dir = false;
|
bool has_root_dir = false;
|
||||||
bool has_port = false;
|
bool has_port = false;
|
||||||
@ -87,6 +89,7 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
|
|||||||
bool has_timeout = false;
|
bool has_timeout = false;
|
||||||
bool has_log_level = false;
|
bool has_log_level = false;
|
||||||
bool has_gzip_enabled = false;
|
bool has_gzip_enabled = false;
|
||||||
|
bool has_brotli_enabled = false;
|
||||||
const char *config_file = NULL;
|
const char *config_file = NULL;
|
||||||
|
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
@ -133,6 +136,9 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
|
|||||||
} else if (strcmp(argv[i], "--no-gzip") == 0) {
|
} else if (strcmp(argv[i], "--no-gzip") == 0) {
|
||||||
config->gzip_enabled = false;
|
config->gzip_enabled = false;
|
||||||
has_gzip_enabled = true;
|
has_gzip_enabled = true;
|
||||||
|
} else if (strcmp(argv[i], "--no-brotli") == 0) {
|
||||||
|
config->brotli_enabled = false;
|
||||||
|
has_brotli_enabled = true;
|
||||||
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
||||||
print_usage(argv[0]);
|
print_usage(argv[0]);
|
||||||
exit(0);
|
exit(0);
|
||||||
@ -150,7 +156,7 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
|
|||||||
}
|
}
|
||||||
/* 用命令行参数覆盖配置文件 */
|
/* 用命令行参数覆盖配置文件 */
|
||||||
config_merge(config, config, has_root_dir, has_port, has_workers,
|
config_merge(config, config, has_root_dir, has_port, has_workers,
|
||||||
has_max_conn, has_timeout, has_log_level, has_gzip_enabled);
|
has_max_conn, has_timeout, has_log_level, has_gzip_enabled, has_brotli_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config->root_dir) {
|
if (!config->root_dir) {
|
||||||
|
|||||||
6
server.c
6
server.c
@ -57,6 +57,7 @@ typedef struct {
|
|||||||
coco_timer_t *timer; /**< 空闲超时定时器 */
|
coco_timer_t *timer; /**< 空闲超时定时器 */
|
||||||
coco_coro_t *coro; /**< 当前处理协程 */
|
coco_coro_t *coro; /**< 当前处理协程 */
|
||||||
bool gzip_enabled; /**< 是否启用 gzip 压缩 */
|
bool gzip_enabled; /**< 是否启用 gzip 压缩 */
|
||||||
|
bool brotli_enabled; /**< 是否启用 brotli 压缩 */
|
||||||
} connection_t;
|
} connection_t;
|
||||||
struct server_context {
|
struct server_context {
|
||||||
int listen_fd; /**< 监听 socket */
|
int listen_fd; /**< 监听 socket */
|
||||||
@ -428,14 +429,14 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
http_request_free(&req);
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
static_serve_file(conn->fd, &index_req, root_dir, conn->gzip_enabled);
|
static_serve_file(conn->fd, &index_req, root_dir, conn->gzip_enabled, conn->brotli_enabled);
|
||||||
} else {
|
} else {
|
||||||
/* 无 index.html,生成目录列表 */
|
/* 无 index.html,生成目录列表 */
|
||||||
static_serve_directory(conn->fd, &req, root_dir, real_path);
|
static_serve_directory(conn->fd, &req, root_dir, real_path);
|
||||||
}
|
}
|
||||||
} else if (S_ISREG(st.st_mode)) {
|
} else if (S_ISREG(st.st_mode)) {
|
||||||
/* 普通文件 */
|
/* 普通文件 */
|
||||||
static_serve_file(conn->fd, &req, root_dir, conn->gzip_enabled);
|
static_serve_file(conn->fd, &req, root_dir, conn->gzip_enabled, conn->brotli_enabled);
|
||||||
} else {
|
} else {
|
||||||
static_send_error(conn->fd, 403, req.keep_alive);
|
static_send_error(conn->fd, 403, req.keep_alive);
|
||||||
}
|
}
|
||||||
@ -622,6 +623,7 @@ static void accept_loop(void *arg) {
|
|||||||
conn->root_dir = ctx->config.root_dir;
|
conn->root_dir = ctx->config.root_dir;
|
||||||
conn->timeout_ms = ctx->config.timeout_ms > 0 ? ctx->config.timeout_ms : CONN_TIMEOUT_MS;
|
conn->timeout_ms = ctx->config.timeout_ms > 0 ? ctx->config.timeout_ms : CONN_TIMEOUT_MS;
|
||||||
conn->gzip_enabled = ctx->config.gzip_enabled;
|
conn->gzip_enabled = ctx->config.gzip_enabled;
|
||||||
|
conn->brotli_enabled = ctx->config.brotli_enabled;
|
||||||
|
|
||||||
atomic_fetch_add(&g_active_connections, 1);
|
atomic_fetch_add(&g_active_connections, 1);
|
||||||
log_debug("新连接 fd=%d,当前活跃连接: %d", client_fd,
|
log_debug("新连接 fd=%d,当前活跃连接: %d", client_fd,
|
||||||
|
|||||||
84
static.c
84
static.c
@ -21,6 +21,7 @@
|
|||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <zlib.h>
|
#include <zlib.h>
|
||||||
|
#include <brotli/encode.h>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* is_compressible_mime - 判断 MIME 类型是否适合压缩
|
* is_compressible_mime - 判断 MIME 类型是否适合压缩
|
||||||
@ -83,6 +84,38 @@ static ssize_t gzip_compress(const char *src, size_t src_len,
|
|||||||
return (ssize_t)compressed_len;
|
return (ssize_t)compressed_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* brotli_compress - 使用 Brotli 压缩数据
|
||||||
|
*
|
||||||
|
* 使用 Brotli 编码器进行高质量压缩。
|
||||||
|
*
|
||||||
|
* @param src 原始数据
|
||||||
|
* @param src_len 原始数据长度
|
||||||
|
* @param dst 输出缓冲区(调用者分配,建议大小为 src_len)
|
||||||
|
* @param dst_cap 输出缓冲区容量
|
||||||
|
* @return 压缩后长度,0 表示不需要压缩(压缩后更大),-1 表示错误
|
||||||
|
*/
|
||||||
|
static ssize_t brotli_compress(const char *src, size_t src_len,
|
||||||
|
char *dst, size_t dst_cap) {
|
||||||
|
size_t encoded_size = dst_cap;
|
||||||
|
BROTLI_BOOL ok = BrotliEncoderCompress(
|
||||||
|
BROTLI_DEFAULT_QUALITY, /* 默认质量 11 */
|
||||||
|
BROTLI_DEFAULT_WINDOW, /* 默认窗口大小 22 */
|
||||||
|
BROTLI_MODE_GENERIC, /* 通用模式 */
|
||||||
|
src_len,
|
||||||
|
(const uint8_t *)src,
|
||||||
|
&encoded_size,
|
||||||
|
(uint8_t *)dst
|
||||||
|
);
|
||||||
|
if (!ok) return -1;
|
||||||
|
|
||||||
|
/* 如果压缩后更大或差不多,就不压缩了 */
|
||||||
|
if (encoded_size >= src_len * 0.95) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (ssize_t)encoded_size;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* format_http_time - 将时间戳格式化为 HTTP 日期字符串
|
* format_http_time - 将时间戳格式化为 HTTP 日期字符串
|
||||||
*
|
*
|
||||||
@ -275,7 +308,7 @@ int static_send_error(int fd, int status_code, bool keep_alive) {
|
|||||||
* @param root_dir 静态资源根目录
|
* @param root_dir 静态资源根目录
|
||||||
* @return COCOON_OK 成功,负值错误码
|
* @return COCOON_OK 成功,负值错误码
|
||||||
*/
|
*/
|
||||||
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled) {
|
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled, bool brotli_enabled) {
|
||||||
char real_path[4096];
|
char real_path[4096];
|
||||||
if (!safe_path_join(real_path, sizeof(real_path), root_dir, req->path)) {
|
if (!safe_path_join(real_path, sizeof(real_path), root_dir, req->path)) {
|
||||||
return static_send_error(fd, 403, req->keep_alive);
|
return static_send_error(fd, 403, req->keep_alive);
|
||||||
@ -339,13 +372,14 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir, b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 判断是否 gzip 压缩 */
|
/* 判断压缩方式:优先 brotli,回退 gzip */
|
||||||
int64_t file_size = st.st_size;
|
int64_t file_size = st.st_size;
|
||||||
bool use_gzip = false;
|
bool use_gzip = false;
|
||||||
char *gzip_buf = NULL;
|
bool use_brotli = false;
|
||||||
ssize_t gzip_len = 0;
|
char *compress_buf = NULL;
|
||||||
|
ssize_t compress_len = 0;
|
||||||
|
|
||||||
if (gzip_enabled && req->accept_gzip && !req->has_range && req->method != HTTP_HEAD) {
|
if (!req->has_range && req->method != HTTP_HEAD) {
|
||||||
const char *mime = http_mime_type(real_path);
|
const char *mime = http_mime_type(real_path);
|
||||||
if (is_compressible_mime(mime) && file_size > 256) {
|
if (is_compressible_mime(mime) && file_size > 256) {
|
||||||
/* 读取文件内容到内存 */
|
/* 读取文件内容到内存 */
|
||||||
@ -358,11 +392,17 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir, b
|
|||||||
read_total += n;
|
read_total += n;
|
||||||
}
|
}
|
||||||
if (read_total == file_size) {
|
if (read_total == file_size) {
|
||||||
gzip_buf = (char *)malloc((size_t)file_size);
|
compress_buf = (char *)malloc((size_t)file_size);
|
||||||
if (gzip_buf) {
|
if (compress_buf) {
|
||||||
gzip_len = gzip_compress(file_buf, (size_t)file_size, gzip_buf, (size_t)file_size);
|
/* 优先 brotli */
|
||||||
if (gzip_len > 0) {
|
if (brotli_enabled && req->accept_brotli) {
|
||||||
use_gzip = true;
|
compress_len = brotli_compress(file_buf, (size_t)file_size, compress_buf, (size_t)file_size);
|
||||||
|
if (compress_len > 0) use_brotli = true;
|
||||||
|
}
|
||||||
|
/* 回退 gzip */
|
||||||
|
if (!use_brotli && gzip_enabled && req->accept_gzip) {
|
||||||
|
compress_len = gzip_compress(file_buf, (size_t)file_size, compress_buf, (size_t)file_size);
|
||||||
|
if (compress_len > 0) use_gzip = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -376,20 +416,20 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir, b
|
|||||||
int64_t send_end = file_size - 1;
|
int64_t send_end = file_size - 1;
|
||||||
int status_code = 200;
|
int status_code = 200;
|
||||||
|
|
||||||
if (!use_gzip && req->has_range) {
|
if (!use_gzip && !use_brotli && req->has_range) {
|
||||||
send_start = req->range_start;
|
send_start = req->range_start;
|
||||||
if (req->range_end >= 0 && req->range_end < file_size) {
|
if (req->range_end >= 0 && req->range_end < file_size) {
|
||||||
send_end = req->range_end;
|
send_end = req->range_end;
|
||||||
}
|
}
|
||||||
if (send_start >= file_size || send_start > send_end) {
|
if (send_start >= file_size || send_start > send_end) {
|
||||||
close(file_fd);
|
close(file_fd);
|
||||||
free(gzip_buf);
|
free(compress_buf);
|
||||||
return static_send_error(fd, 416, req->keep_alive);
|
return static_send_error(fd, 416, req->keep_alive);
|
||||||
}
|
}
|
||||||
status_code = 206;
|
status_code = 206;
|
||||||
}
|
}
|
||||||
|
|
||||||
int64_t send_length = use_gzip ? gzip_len : (send_end - send_start + 1);
|
int64_t send_length = (use_gzip || use_brotli) ? compress_len : (send_end - send_start + 1);
|
||||||
|
|
||||||
/* 构建响应头 */
|
/* 构建响应头 */
|
||||||
http_response_t resp = {
|
http_response_t resp = {
|
||||||
@ -398,27 +438,27 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir, b
|
|||||||
.content_type = http_mime_type(real_path),
|
.content_type = http_mime_type(real_path),
|
||||||
.content_length = send_length,
|
.content_length = send_length,
|
||||||
.keep_alive = req->keep_alive,
|
.keep_alive = req->keep_alive,
|
||||||
.has_range = !use_gzip && req->has_range,
|
.has_range = !use_gzip && !use_brotli && req->has_range,
|
||||||
.range_start = send_start,
|
.range_start = send_start,
|
||||||
.range_end = send_end,
|
.range_end = send_end,
|
||||||
.total_length = file_size,
|
.total_length = file_size,
|
||||||
.etag = etag,
|
.etag = etag,
|
||||||
.last_modified = last_modified,
|
.last_modified = last_modified,
|
||||||
.content_encoding = use_gzip ? "gzip" : NULL
|
.content_encoding = use_brotli ? "br" : (use_gzip ? "gzip" : NULL)
|
||||||
};
|
};
|
||||||
|
|
||||||
char header_buf[1024];
|
char header_buf[1024];
|
||||||
int header_len = http_format_response_header(header_buf, sizeof(header_buf), &resp);
|
int header_len = http_format_response_header(header_buf, sizeof(header_buf), &resp);
|
||||||
if (header_len < 0) {
|
if (header_len < 0) {
|
||||||
close(file_fd);
|
close(file_fd);
|
||||||
free(gzip_buf);
|
free(compress_buf);
|
||||||
return static_send_error(fd, 500, req->keep_alive);
|
return static_send_error(fd, 500, req->keep_alive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 发送响应头 */
|
/* 发送响应头 */
|
||||||
if (send_all(fd, header_buf, (size_t)header_len) != 0) {
|
if (send_all(fd, header_buf, (size_t)header_len) != 0) {
|
||||||
close(file_fd);
|
close(file_fd);
|
||||||
free(gzip_buf);
|
free(compress_buf);
|
||||||
return COCOON_ERROR;
|
return COCOON_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,14 +466,14 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir, b
|
|||||||
if (req->method == HTTP_HEAD) {
|
if (req->method == HTTP_HEAD) {
|
||||||
/* HEAD 请求不发送 body */
|
/* HEAD 请求不发送 body */
|
||||||
close(file_fd);
|
close(file_fd);
|
||||||
free(gzip_buf);
|
free(compress_buf);
|
||||||
return COCOON_OK;
|
return COCOON_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (use_gzip) {
|
if (use_gzip || use_brotli) {
|
||||||
/* 发送 gzip 压缩后的数据 */
|
/* 发送压缩后的数据 */
|
||||||
send_all(fd, gzip_buf, (size_t)gzip_len);
|
send_all(fd, compress_buf, (size_t)compress_len);
|
||||||
free(gzip_buf);
|
free(compress_buf);
|
||||||
close(file_fd);
|
close(file_fd);
|
||||||
} else {
|
} else {
|
||||||
/* 定位到起始位置 */
|
/* 定位到起始位置 */
|
||||||
|
|||||||
3
static.h
3
static.h
@ -33,9 +33,10 @@ int send_all(int fd, const char *buf, size_t len);
|
|||||||
* @param req HTTP 请求
|
* @param req HTTP 请求
|
||||||
* @param root_dir 静态资源根目录
|
* @param root_dir 静态资源根目录
|
||||||
* @param gzip_enabled 是否启用 gzip 压缩
|
* @param gzip_enabled 是否启用 gzip 压缩
|
||||||
|
* @param brotli_enabled 是否启用 brotli 压缩
|
||||||
* @return COCOON_OK 成功,负值错误码
|
* @return COCOON_OK 成功,负值错误码
|
||||||
*/
|
*/
|
||||||
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled);
|
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled, bool brotli_enabled);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* static_serve_directory - 生成目录列表 HTML
|
* static_serve_directory - 生成目录列表 HTML
|
||||||
|
|||||||
@ -155,6 +155,48 @@ assert_body_contains() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_brotli() {
|
||||||
|
local url="$1"
|
||||||
|
local desc="${2:-$url}"
|
||||||
|
local encoding
|
||||||
|
encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: br" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||||||
|
if echo "$encoding" | grep -qi "br"; then
|
||||||
|
echo " ✓ $desc — 返回 brotli 压缩"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ $desc — 期望 brotli 压缩, 实际: $encoding"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_brotli_preferred() {
|
||||||
|
local url="$1"
|
||||||
|
local desc="${2:-$url}"
|
||||||
|
local encoding
|
||||||
|
encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: gzip, br" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||||||
|
if echo "$encoding" | grep -qi "br"; then
|
||||||
|
echo " ✓ $desc — 优先返回 brotli 压缩"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ $desc — 期望优先 brotli, 实际: $encoding"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_not_brotli() {
|
||||||
|
local url="$1"
|
||||||
|
local desc="${2:-$url}"
|
||||||
|
local encoding
|
||||||
|
encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: br" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||||||
|
if [[ -z "$encoding" ]]; then
|
||||||
|
echo " ✓ $desc — 无 Content-Encoding(未压缩)"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ $desc — 期望不压缩, 实际: $encoding"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
assert_gzip() {
|
assert_gzip() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local desc="${2:-$url}"
|
local desc="${2:-$url}"
|
||||||
@ -343,6 +385,15 @@ assert_gzip "$BASE/api.json" "JSON gzip"
|
|||||||
# 图片不应压缩
|
# 图片不应压缩
|
||||||
assert_not_gzip "$BASE/image.png" "图片不压缩"
|
assert_not_gzip "$BASE/image.png" "图片不压缩"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Brotli 压缩测试 ==="
|
||||||
|
assert_brotli "$BASE/index.html" "HTML brotli"
|
||||||
|
assert_brotli "$BASE/style.css" "CSS brotli"
|
||||||
|
assert_brotli "$BASE/app.js" "JS brotli"
|
||||||
|
assert_brotli_preferred "$BASE/index.html" "优先 brotli"
|
||||||
|
# 图片不应压缩
|
||||||
|
assert_not_brotli "$BASE/image.png" "图片不压缩"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== MIME 类型测试 ==="
|
echo "=== MIME 类型测试 ==="
|
||||||
assert_header_contains "$BASE/style.css" "Content-Type" "text/css" "CSS MIME"
|
assert_header_contains "$BASE/style.css" "Content-Type" "text/css" "CSS MIME"
|
||||||
|
|||||||
@ -111,7 +111,8 @@ void test_merge_override_all(void) {
|
|||||||
.max_connections = 100,
|
.max_connections = 100,
|
||||||
.timeout_ms = 30000,
|
.timeout_ms = 30000,
|
||||||
.log_level = LOG_LEVEL_INFO,
|
.log_level = LOG_LEVEL_INFO,
|
||||||
.gzip_enabled = true
|
.gzip_enabled = true,
|
||||||
|
.brotli_enabled = true
|
||||||
};
|
};
|
||||||
cocoon_config_t cmdline = {
|
cocoon_config_t cmdline = {
|
||||||
.root_dir = strdup("/new"),
|
.root_dir = strdup("/new"),
|
||||||
@ -121,10 +122,11 @@ void test_merge_override_all(void) {
|
|||||||
.max_connections = 200,
|
.max_connections = 200,
|
||||||
.timeout_ms = 60000,
|
.timeout_ms = 60000,
|
||||||
.log_level = LOG_LEVEL_DEBUG,
|
.log_level = LOG_LEVEL_DEBUG,
|
||||||
.gzip_enabled = false
|
.gzip_enabled = false,
|
||||||
|
.brotli_enabled = false
|
||||||
};
|
};
|
||||||
config_merge(&base, &cmdline,
|
config_merge(&base, &cmdline,
|
||||||
true, true, true, true, true, true, true);
|
true, true, true, true, true, true, true, true);
|
||||||
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir);
|
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir);
|
||||||
TEST_ASSERT_EQUAL(9090, base.port);
|
TEST_ASSERT_EQUAL(9090, base.port);
|
||||||
TEST_ASSERT_TRUE(base.threaded);
|
TEST_ASSERT_TRUE(base.threaded);
|
||||||
@ -133,6 +135,7 @@ void test_merge_override_all(void) {
|
|||||||
TEST_ASSERT_EQUAL(60000, base.timeout_ms);
|
TEST_ASSERT_EQUAL(60000, base.timeout_ms);
|
||||||
TEST_ASSERT_EQUAL(LOG_LEVEL_DEBUG, base.log_level);
|
TEST_ASSERT_EQUAL(LOG_LEVEL_DEBUG, base.log_level);
|
||||||
TEST_ASSERT_FALSE(base.gzip_enabled);
|
TEST_ASSERT_FALSE(base.gzip_enabled);
|
||||||
|
TEST_ASSERT_FALSE(base.brotli_enabled);
|
||||||
free((void *)base.root_dir);
|
free((void *)base.root_dir);
|
||||||
free((void *)cmdline.root_dir);
|
free((void *)cmdline.root_dir);
|
||||||
}
|
}
|
||||||
@ -152,7 +155,7 @@ void test_merge_no_override(void) {
|
|||||||
.log_level = LOG_LEVEL_DEBUG
|
.log_level = LOG_LEVEL_DEBUG
|
||||||
};
|
};
|
||||||
config_merge(&base, &cmdline,
|
config_merge(&base, &cmdline,
|
||||||
false, false, false, false, false, false, false);
|
false, false, false, false, false, false, false, false);
|
||||||
TEST_ASSERT_EQUAL_STRING("/old", base.root_dir);
|
TEST_ASSERT_EQUAL_STRING("/old", base.root_dir);
|
||||||
TEST_ASSERT_EQUAL(8080, base.port);
|
TEST_ASSERT_EQUAL(8080, base.port);
|
||||||
TEST_ASSERT_FALSE(base.threaded);
|
TEST_ASSERT_FALSE(base.threaded);
|
||||||
@ -175,7 +178,7 @@ void test_merge_partial_override(void) {
|
|||||||
.log_level = LOG_LEVEL_DEBUG
|
.log_level = LOG_LEVEL_DEBUG
|
||||||
};
|
};
|
||||||
config_merge(&base, &cmdline,
|
config_merge(&base, &cmdline,
|
||||||
true, false, false, true, false, false, false);
|
true, false, false, true, false, false, false, false);
|
||||||
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir); /* overridden */
|
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir); /* overridden */
|
||||||
TEST_ASSERT_EQUAL(8080, base.port); /* not overridden */
|
TEST_ASSERT_EQUAL(8080, base.port); /* not overridden */
|
||||||
TEST_ASSERT_EQUAL(2, base.num_workers); /* not overridden */
|
TEST_ASSERT_EQUAL(2, base.num_workers); /* not overridden */
|
||||||
@ -240,7 +243,7 @@ void test_merge_cmdline_null_root_dir(void) {
|
|||||||
/* cmdline root_dir 为 NULL,不应覆盖 base */
|
/* cmdline root_dir 为 NULL,不应覆盖 base */
|
||||||
cocoon_config_t base = {.root_dir = strdup("/old"), .port = 8080};
|
cocoon_config_t base = {.root_dir = strdup("/old"), .port = 8080};
|
||||||
cocoon_config_t cmdline = {.root_dir = NULL, .port = 9090};
|
cocoon_config_t cmdline = {.root_dir = NULL, .port = 9090};
|
||||||
config_merge(&base, &cmdline, true, true, false, false, false, false, false);
|
config_merge(&base, &cmdline, true, true, false, false, false, false, false, false);
|
||||||
TEST_ASSERT_EQUAL_STRING("/old", base.root_dir); /* NULL 不覆盖 */
|
TEST_ASSERT_EQUAL_STRING("/old", base.root_dir); /* NULL 不覆盖 */
|
||||||
TEST_ASSERT_EQUAL(9090, base.port); /* port 覆盖 */
|
TEST_ASSERT_EQUAL(9090, base.port); /* port 覆盖 */
|
||||||
free((void *)base.root_dir);
|
free((void *)base.root_dir);
|
||||||
@ -248,7 +251,7 @@ void test_merge_cmdline_null_root_dir(void) {
|
|||||||
|
|
||||||
void test_merge_null_safety(void) {
|
void test_merge_null_safety(void) {
|
||||||
/* 不应 crash */
|
/* 不应 crash */
|
||||||
config_merge(NULL, NULL, true, true, true, true, true, true, true);
|
config_merge(NULL, NULL, true, true, true, true, true, true, true, true);
|
||||||
TEST_ASSERT_TRUE(1);
|
TEST_ASSERT_TRUE(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,6 +281,32 @@ void test_load_gzip_enabled(void) {
|
|||||||
cleanup(p3);
|
cleanup(p3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_load_brotli_enabled(void) {
|
||||||
|
/* brotli_enabled: true */
|
||||||
|
const char *p1 = write_temp_config("{\"brotli_enabled\": true}");
|
||||||
|
cocoon_config_t cfg1 = {0};
|
||||||
|
TEST_ASSERT_TRUE(config_load_from_file(p1, &cfg1));
|
||||||
|
TEST_ASSERT_TRUE(cfg1.brotli_enabled);
|
||||||
|
free((void *)cfg1.root_dir);
|
||||||
|
cleanup(p1);
|
||||||
|
|
||||||
|
/* brotli_enabled: false */
|
||||||
|
const char *p2 = write_temp_config("{\"brotli_enabled\": false}");
|
||||||
|
cocoon_config_t cfg2 = {0};
|
||||||
|
TEST_ASSERT_TRUE(config_load_from_file(p2, &cfg2));
|
||||||
|
TEST_ASSERT_FALSE(cfg2.brotli_enabled);
|
||||||
|
free((void *)cfg2.root_dir);
|
||||||
|
cleanup(p2);
|
||||||
|
|
||||||
|
/* 默认值应为 true */
|
||||||
|
const char *p3 = write_temp_config("{\"port\": 3000}");
|
||||||
|
cocoon_config_t cfg3 = {.brotli_enabled = true};
|
||||||
|
TEST_ASSERT_TRUE(config_load_from_file(p3, &cfg3));
|
||||||
|
TEST_ASSERT_TRUE(cfg3.brotli_enabled);
|
||||||
|
free((void *)cfg3.root_dir);
|
||||||
|
cleanup(p3);
|
||||||
|
}
|
||||||
|
|
||||||
void setUp(void) {}
|
void setUp(void) {}
|
||||||
void tearDown(void) {}
|
void tearDown(void) {}
|
||||||
|
|
||||||
@ -303,5 +332,6 @@ int main(void) {
|
|||||||
RUN_TEST(test_merge_cmdline_null_root_dir);
|
RUN_TEST(test_merge_cmdline_null_root_dir);
|
||||||
|
|
||||||
RUN_TEST(test_load_gzip_enabled);
|
RUN_TEST(test_load_gzip_enabled);
|
||||||
|
RUN_TEST(test_load_brotli_enabled);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -268,6 +268,67 @@ void test_gzip_compress_buffer_too_small(void) {
|
|||||||
TEST_ASSERT_EQUAL(-1, compressed); /* 应该失败 */
|
TEST_ASSERT_EQUAL(-1, compressed); /* 应该失败 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== brotli_compress ===== */
|
||||||
|
|
||||||
|
void test_brotli_compress_simple(void) {
|
||||||
|
const char *src = "hello world hello world hello world hello world";
|
||||||
|
size_t src_len = strlen(src);
|
||||||
|
|
||||||
|
char *dst = (char *)malloc(src_len);
|
||||||
|
TEST_ASSERT_NOT_NULL(dst);
|
||||||
|
|
||||||
|
ssize_t compressed = brotli_compress(src, src_len, dst, src_len);
|
||||||
|
|
||||||
|
/* 可压缩文本应该被压缩 */
|
||||||
|
TEST_ASSERT_GREATER_THAN(0, compressed);
|
||||||
|
TEST_ASSERT_LESS_THAN((ssize_t)src_len, compressed);
|
||||||
|
|
||||||
|
free(dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_brotli_compress_incompressible(void) {
|
||||||
|
/* 随机数据通常难以压缩 */
|
||||||
|
char src[256];
|
||||||
|
srand(42);
|
||||||
|
for (int i = 0; i < 256; i++) src[i] = (char)(rand() % 256);
|
||||||
|
|
||||||
|
char *dst = (char *)malloc(256);
|
||||||
|
TEST_ASSERT_NOT_NULL(dst);
|
||||||
|
|
||||||
|
ssize_t compressed = brotli_compress(src, 256, dst, 256);
|
||||||
|
|
||||||
|
/* 不可压缩数据:brotli 可能返回 0(不压缩)或 -1(错误),或仍略小于原大小 */
|
||||||
|
/* 放宽断言:只要返回 >= 0 或 -1 都算合理,但这里只检查不崩溃 */
|
||||||
|
TEST_ASSERT_TRUE(compressed >= -1);
|
||||||
|
|
||||||
|
free(dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_brotli_compress_small(void) {
|
||||||
|
/* 小数据可能压缩后更大 */
|
||||||
|
const char *src = "hi";
|
||||||
|
|
||||||
|
char *dst = (char *)malloc(256);
|
||||||
|
TEST_ASSERT_NOT_NULL(dst);
|
||||||
|
|
||||||
|
ssize_t compressed = brotli_compress(src, 2, dst, 256);
|
||||||
|
|
||||||
|
/* 太小可能返回 0 */
|
||||||
|
TEST_ASSERT_GREATER_OR_EQUAL(0, compressed);
|
||||||
|
|
||||||
|
free(dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_brotli_compress_buffer_too_small(void) {
|
||||||
|
const char *src = "hello world hello world";
|
||||||
|
|
||||||
|
/* 极小的输出缓冲区 */
|
||||||
|
char dst[1];
|
||||||
|
ssize_t compressed = brotli_compress(src, strlen(src), dst, 1);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(-1, compressed); /* 应该失败 */
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== send_all ===== */
|
/* ===== send_all ===== */
|
||||||
|
|
||||||
void test_send_all_basic(void) {
|
void test_send_all_basic(void) {
|
||||||
@ -408,6 +469,12 @@ int main(void) {
|
|||||||
RUN_TEST(test_gzip_compress_small);
|
RUN_TEST(test_gzip_compress_small);
|
||||||
RUN_TEST(test_gzip_compress_buffer_too_small);
|
RUN_TEST(test_gzip_compress_buffer_too_small);
|
||||||
|
|
||||||
|
/* brotli_compress */
|
||||||
|
RUN_TEST(test_brotli_compress_simple);
|
||||||
|
RUN_TEST(test_brotli_compress_incompressible);
|
||||||
|
RUN_TEST(test_brotli_compress_small);
|
||||||
|
RUN_TEST(test_brotli_compress_buffer_too_small);
|
||||||
|
|
||||||
/* send_all */
|
/* send_all */
|
||||||
RUN_TEST(test_send_all_basic);
|
RUN_TEST(test_send_all_basic);
|
||||||
RUN_TEST(test_send_all_empty);
|
RUN_TEST(test_send_all_empty);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user