feat(plugin): 实现动态插件系统(.so 加载 + 中间件注册)

新增插件系统:
- plugin.h / plugin.c:dlopen/dlsym 动态加载,最多8个插件
- 插件接口:cocoon_plugin_init / cocoon_plugin_shutdown / cocoon_plugin_version
- 插件可通过 cocoon_middleware_register() 注册中间件参与请求处理
- 命令行:--plugin <path>(可多次指定)
- 配置文件:plugins 字段(字符串,单插件路径)
- server.c 在 server_create 时加载,server_destroy 时逆序卸载
- plugins/hello.c:示例插件,演示中间件注册
- 集成测试:新增插件加载 + 日志验证测试(68项全部通过)
- 单元测试:127个全部通过
- 编译:Makefile 添加 plugin.c 和 -ldl
- .gitignore 排除编译产物和上传目录
This commit is contained in:
xfy911 2026-06-06 03:13:59 +08:00
parent dd41464b9c
commit 952cc663fe
14 changed files with 385 additions and 33 deletions

View File

@ -23,7 +23,7 @@
- [x] 访问日志Nginx combined 格式,含 User-Agent / Referer✅ 2026-06-05 - [x] 访问日志Nginx combined 格式,含 User-Agent / Referer✅ 2026-06-05
- [x] Gzip 压缩 ✅ 2026-06-03已接入响应流程 - [x] Gzip 压缩 ✅ 2026-06-03已接入响应流程
- [x] Brotli 压缩 ✅ 2026-06-04优先于 Gzip - [x] Brotli 压缩 ✅ 2026-06-04优先于 Gzip
- [x] 集成测试 suite61 项 curl/bash 测试全部通过)✅ 2026-06-05 - [x] 集成测试 suite66 项 curl/bash 测试全部通过)✅ 2026-06-06
- [x] 性能基准wrk: ~16.2K RPS / ~60μs 延迟)✅ 2026-06-03 - [x] 性能基准wrk: ~16.2K RPS / ~60μs 延迟)✅ 2026-06-03
- [x] 请求体解析POST 支持)✅ 2026-06-03 - [x] 请求体解析POST 支持)✅ 2026-06-03
- Content-Length 读取 - Content-Length 读取
@ -34,7 +34,7 @@
### Phase 3 — 扩展(已完成) ### Phase 3 — 扩展(已完成)
- [x] **配置文件支持** — JSON 配置替代纯命令行 ✅ 2026-06-04 - [x] **配置文件支持** — JSON 配置替代纯命令行 ✅ 2026-06-04
- `config.c` / `config.h`:极简 JSON 解析器(数字、字符串、布尔、注释) - `config.c` / `config.h`:极简 JSON 解析器(数字、字符串、布尔、注释)
- `cocoon_config_t` 结构体root_dir / port / threaded / num_workers / max_connections / timeout_ms / log_level / gzip_enabled / brotli_enabled / access_log - `cocoon_config_t` 结构体root_dir / port / threaded / num_workers / max_connections / timeout_ms / log_level / gzip_enabled / brotli_enabled / access_log / cors_enabled / auth_user / auth_pass / rate_limit
- `config_merge()`:命令行参数覆盖配置文件 - `config_merge()`:命令行参数覆盖配置文件
- `cocoon.json` 示例配置 - `cocoon.json` 示例配置
- `--no-gzip` / `--no-brotli` 命令行选项禁用压缩 - `--no-gzip` / `--no-brotli` 命令行选项禁用压缩
@ -48,8 +48,8 @@
### Phase 4 — 生态 ### Phase 4 — 生态
- [x] **WebSocket 广播/频道路由** — 全局连接注册表 + 广播/定向发送 API ✅ 2026-06-05 - [x] **WebSocket 广播/频道路由** — 全局连接注册表 + 广播/定向发送 API ✅ 2026-06-05
- [ ] 中间件机制 - [x] **中间件机制** — 注册表 + 链式执行,内置 CORS / Basic Auth / Rate Limit ✅ 2026-06-06
- [ ] 插件系统 - [ ] 插件系统 — 动态加载 .so/.dll 扩展
## 当前状态 ## 当前状态
@ -57,13 +57,20 @@
- `ws_broadcast()` 全局广播、`ws_broadcast_to_path()` 按频道广播 - `ws_broadcast()` 全局广播、`ws_broadcast_to_path()` 按频道广播
- `ws_connection_count()` 连接计数 - `ws_connection_count()` 连接计数
- 线程安全(链表 + mutex连接自动注册/注销 - 线程安全(链表 + mutex连接自动注册/注销
- **中间件框架已上线**
- `cocoon_middleware_register()` / `cocoon_middleware_execute()` 注册表 + 链式执行
- CORS 中间件OPTIONS 预检 204 + 跨域响应头
- Basic Auth 中间件HTTP 基础认证401 未授权
- Rate Limit 中间件:基于 IP 秒级限流429 Too Many Requests
- 命令行参数:`--cors` / `--auth-user` / `--auth-pass` / `--rate-limit`
- 配置文件支持:`cors_enabled` / `auth_user` / `auth_pass` / `rate_limit`
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示) - 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
- **61 项集成测试全部通过**GET/HEAD/POST/404/Range/304/gzip/brotli/MIME/目录浏览/路径防护/文件上传/TLS/HTTP/2/h2c/WebSocket/访问日志) - **66 项集成测试全部通过**GET/HEAD/POST/404/Range/304/gzip/brotli/MIME/目录浏览/路径防护/文件上传/TLS/HTTP/2/h2c/WebSocket/访问日志/中间件
- **127 个单元测试全部通过**Unity 框架) - **127 个单元测试全部通过**Unity 框架)
- 压测数据wrk -t4 -c100 -d10s → 16,179 RPS平均延迟 59.86μs单线程 - 压测数据wrk -t4 -c100 -d10s → 16,179 RPS平均延迟 59.86μs单线程
- 多线程模式(-t -w 4已修复主线程 accept + pollclient_handler 1MB 协程栈) - 多线程模式(-t -w 4已修复主线程 accept + pollclient_handler 1MB 协程栈)
- POST 支持JSON 和 form-urlencoded 回显multipart 文件上传Content-Length 解析8MB 上限 - POST 支持JSON 和 form-urlencoded 回显multipart 文件上传Content-Length 解析8MB 上限
- **配置文件支持**JSON 格式9 个字段(含 access_log命令行参数可覆盖 - **配置文件支持**JSON 格式,13 个字段,命令行参数可覆盖
- **访问日志**Nginx combined 格式,支持文件路径或 stdout`-`线程安全pthread_mutex记录 User-Agent / Referer / 状态码 - **访问日志**Nginx combined 格式,支持文件路径或 stdout`-`线程安全pthread_mutex记录 User-Agent / Referer / 状态码
- **TLS/HTTPS**OpenSSL 3.0 Memory BIO + coco 协程集成自签名证书支持ALPN 协商 h2/http1.1 - **TLS/HTTPS**OpenSSL 3.0 Memory BIO + coco 协程集成自签名证书支持ALPN 协商 h2/http1.1
- **HTTP/2**完整功能静态文件服务、目录浏览、缓存协商、压缩、HEAD 请求) - **HTTP/2**完整功能静态文件服务、目录浏览、缓存协商、压缩、HEAD 请求)
@ -73,11 +80,12 @@
## 待办池 ## 待办池
1. **[高] 中间件机制** — 请求处理前后 hook 系统允许插入自定义逻辑认证、限流、CORS 1. **[高] 插件系统** — 动态加载 .so/.dll 扩展,中间件注册 API 扩展
2. **[高] 插件系统** — 动态加载 .so/.dll 扩展 2. **[中] HTTP/2 压缩** — 接入 gzip/brotli 到 HTTP/2 响应
3. **[低] Doxygen 中文注释** — tls.c, config.c, multipart.c, access_log.c, main.c, websocket.c 等模块待补充 3. **[低] Doxygen 中文注释** — tls.c, config.c, multipart.c, access_log.c, main.c, websocket.c 等模块待补充
4. **[低] 性能优化** — 连接池复用、零拷贝优化、压缩预缓存 4. **[低] 性能优化** — 连接池复用、零拷贝优化、压缩预缓存
5. **[低] WebSocket 单元测试** — 为广播/注册表 API 添加 C 单元测试 5. **[低] WebSocket 单元测试** — 为广播/注册表 API 添加 C 单元测试
6. **[低] 配置文件 JSON Schema 验证** — 配置文件格式校验
## Windows 兼容性实现详情 ## Windows 兼容性实现详情
@ -100,16 +108,19 @@
## 最近行动记录 ## 最近行动记录
- 2026-06-04: **本轮行动 — Windows 兼容性** - 2026-06-06: **本轮行动 — 中间件框架CORS / Basic Auth / Rate Limit**
- `platform.h`:跨平台抽象层头文件,定义 `cocoon_socket_t`/`cocoon_file_t`/`cocoon_dir_iter_t` 类型 - `middleware.c` / `middleware.h`:注册表(最多 16 个)+ 链式执行 + 短路机制
- `platform.c`双平台实现POSIX 分支fcntl/sendfile/opendir+ Windows 分支ioctlsocket/WSAStartup/FindFirstFile - CORS 中间件OPTIONS 预检 204 + 跨域响应头Access-Control-Allow-Origin/Methods/Headers
- `server.c`:移除全部 POSIX 头文件,使用跨平台 API`int fd``cocoon_socket_t` - Basic Auth 中间件HTTP 基础认证Base64 解码401 Unauthorized + WWW-Authenticate 头
- `static.c`文件操作和目录遍历全部跨平台化sendfile 替代方案 64KB 缓冲区 - Rate Limit 中间件:基于 IP 秒级限流哈希表256 桶429 Too Many Requests
- `main.c`:信号处理跨平台化,新增 `cocoon_socket_init()`/`cocoon_socket_cleanup()` - `config.c` / `config.h`:新增 `cors_enabled` / `auth_user` / `auth_pass` / `rate_limit` 字段
- `CMakeLists.txt`:全新跨平台构建配置,自动检测 Windows/POSIX - `main.c`:新增 `--cors` / `--auth-user` / `--auth-pass` / `--rate-limit` CLI 选项;修复 `access_log_path` 未初始化段错误
- `Makefile`:添加 `platform.c`Windows 下自动链接 `ws2_32` - `server.c`:集成 `cocoon_middleware_init_builtin()``server_create()` 中初始化;修复 `server_destroy``main()` 双重释放字符串指针;新增 `mw_config` 字段避免栈变量悬空
- 单元测试127 个通过Linux 验证集成测试61 项通过 - 修复 `rate_limit_hash`:仅使用 IP 地址计算哈希,排除端口,避免同一 IP 不同 bucket
- 推送到 feature/windows-compat-impl - 修复 401/429 响应 Content-Length与 body 长度严格匹配,避免 curl 挂起等待
- 集成测试66 项全部通过(新增 CORS/Basic Auth/Rate Limit 5 项测试)
- 单元测试127 个全部通过(修复 `test_config.c``config_merge` 参数不匹配)
- 编译零警告,推送到 maindd41464
- 2026-06-05: **本轮行动 — WebSocket 广播/频道路由系统** - 2026-06-05: **本轮行动 — WebSocket 广播/频道路由系统**
- `websocket.c`:新增全局连接注册表(链表 + pthread_mutex + 原子计数) - `websocket.c`:新增全局连接注册表(链表 + pthread_mutex + 原子计数)
- `ws_registry_add/remove`: 连接自动注册/注销 - `ws_registry_add/remove`: 连接自动注册/注销

2
.gitignore vendored
View File

@ -10,3 +10,5 @@ tests/unit/test_multipart
tests/unit/test_static tests/unit/test_static
tests/unit/test_server tests/unit/test_server
tests/fixtures/uploads/ tests/fixtures/uploads/
plugins/*.so
www/uploads/

View File

@ -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 -lbrotlienc -lssl -lcrypto -lnghttp2 LDFLAGS = -L$(COCO_LIB) -lcoco -lpthread -lm -luring -lz -lbrotlienc -lssl -lcrypto -lnghttp2 -ldl
# 调试模式 # 调试模式
DEBUG ?= 0 DEBUG ?= 0
@ -20,7 +20,7 @@ PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin BINDIR = $(PREFIX)/bin
# 源文件 # 源文件
SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c
OBJS = $(SRCS:.c=.o) OBJS = $(SRCS:.c=.o)
TARGET = cocoon TARGET = cocoon
@ -83,8 +83,8 @@ unit-test: $(UNIT_TEST_BINS)
fi 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 http2.c access_log.c websocket.c platform.c middleware.c $(UNITY_SRC) $(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 access_log.c websocket.c platform.c middleware.c plugin.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 access_log.c websocket.c platform.c middleware.c $(UNITY_SRC) $(LDFLAGS) $(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 access_log.c websocket.c platform.c middleware.c plugin.c $(UNITY_SRC) $(LDFLAGS)
$(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) $(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 $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) -lm

View File

@ -27,7 +27,8 @@
* cocoon_config - * cocoon_config -
* *
* *
*/ */#define COCOON_MAX_PLUGINS 8 /**< 最大插件数量 */
typedef struct cocoon_config { typedef struct cocoon_config {
const char *root_dir; /**< 静态资源根目录 */ const char *root_dir; /**< 静态资源根目录 */
uint16_t port; /**< 监听端口 */ uint16_t port; /**< 监听端口 */
@ -47,6 +48,9 @@ typedef struct cocoon_config {
const char *auth_user; /**< Basic Auth 用户名 */ const char *auth_user; /**< Basic Auth 用户名 */
const char *auth_pass; /**< Basic Auth 密码 */ const char *auth_pass; /**< Basic Auth 密码 */
uint32_t rate_limit; /**< 每秒最大请求数0 表示禁用) */ uint32_t rate_limit; /**< 每秒最大请求数0 表示禁用) */
/* 插件配置 */
const char *plugins[COCOON_MAX_PLUGINS]; /**< 插件路径列表 */
size_t num_plugins; /**< 插件数量 */
} cocoon_config_t; } cocoon_config_t;
/* === 服务器生命周期 API === */ /* === 服务器生命周期 API === */

View File

@ -8,5 +8,6 @@
"log_level": "info", "log_level": "info",
"gzip_enabled": true, "gzip_enabled": true,
"brotli_enabled": true, "brotli_enabled": true,
"access_log": "-" "plugins": "plugins/hello.so",
"access_log": "-"
} }

View File

@ -313,6 +313,11 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
} else if (strcmp(key_str, "rate_limit") == 0 && val.type == TOKEN_NUMBER) { } else if (strcmp(key_str, "rate_limit") == 0 && val.type == TOKEN_NUMBER) {
long v = token_to_long(&val); long v = token_to_long(&val);
if (v >= 0 && v < 1000000) config->rate_limit = (uint32_t)v; if (v >= 0 && v < 1000000) config->rate_limit = (uint32_t)v;
} else if (strcmp(key_str, "plugins") == 0 && val.type == TOKEN_STRING) {
char *v = token_str_dup(&val);
if (v && config->num_plugins < COCOON_MAX_PLUGINS) {
config->plugins[config->num_plugins++] = v;
}
} }
/* 其他字段:忽略(未来扩展预留) */ /* 其他字段:忽略(未来扩展预留) */
@ -339,7 +344,8 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
bool has_tls_cert, bool has_tls_key, bool has_tls_enabled, bool has_tls_cert, bool has_tls_key, bool has_tls_enabled,
bool has_access_log, bool has_access_log,
bool has_cors_enabled, bool has_auth_user, bool has_auth_pass, bool has_cors_enabled, bool has_auth_user, bool has_auth_pass,
bool has_rate_limit) { bool has_rate_limit,
bool has_plugins) {
if (!base || !cmdline) return; if (!base || !cmdline) return;
/* 命令行显式指定的值覆盖配置文件 */ /* 命令行显式指定的值覆盖配置文件 */
@ -377,6 +383,13 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
base->auth_pass = strdup(cmdline->auth_pass); base->auth_pass = strdup(cmdline->auth_pass);
} }
if (has_rate_limit) base->rate_limit = cmdline->rate_limit; if (has_rate_limit) base->rate_limit = cmdline->rate_limit;
if (has_plugins) {
for (size_t i = 0; i < cmdline->num_plugins && i < COCOON_MAX_PLUGINS; i++) {
if (base->num_plugins < COCOON_MAX_PLUGINS) {
base->plugins[base->num_plugins++] = strdup(cmdline->plugins[i]);
}
}
}
/* threaded 是 flag 参数,命令行指定了就用命令行的 */ /* threaded 是 flag 参数,命令行指定了就用命令行的 */
if (cmdline->threaded) base->threaded = true; if (cmdline->threaded) base->threaded = true;
} }

View File

@ -47,6 +47,7 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
bool has_tls_cert, bool has_tls_key, bool has_tls_enabled, bool has_tls_cert, bool has_tls_key, bool has_tls_enabled,
bool has_access_log, bool has_access_log,
bool has_cors_enabled, bool has_auth_user, bool has_auth_pass, bool has_cors_enabled, bool has_auth_user, bool has_auth_pass,
bool has_rate_limit); bool has_rate_limit,
bool has_plugins);
#endif /* COCOON_CONFIG_H */ #endif /* COCOON_CONFIG_H */

21
main.c
View File

@ -61,7 +61,7 @@ static void print_usage(const char *prog) {
printf(" --auth-user <user> Basic Auth 用户名\n"); printf(" --auth-user <user> Basic Auth 用户名\n");
printf(" --auth-pass <pass> Basic Auth 密码\n"); printf(" --auth-pass <pass> Basic Auth 密码\n");
printf(" --rate-limit <n> 每秒最大请求数(限流)\n"); printf(" --rate-limit <n> 每秒最大请求数(限流)\n");
printf(" -h 显示此帮助\n"); printf(" --plugin <path> 加载插件(可多次指定)\n");
printf("\nExample:\n"); printf("\nExample:\n");
printf(" %s -c cocoon.json\n", prog); printf(" %s -c cocoon.json\n", prog);
printf(" %s -r ./www -p 8080\n", prog); printf(" %s -r ./www -p 8080\n", prog);
@ -100,6 +100,8 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
config->auth_pass = NULL; config->auth_pass = NULL;
config->rate_limit = 0; config->rate_limit = 0;
config->num_plugins = 0;
bool has_root_dir = false; bool has_root_dir = false;
bool has_port = false; bool has_port = false;
bool has_workers = false; bool has_workers = false;
@ -116,6 +118,7 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
bool has_auth_user = false; bool has_auth_user = false;
bool has_auth_pass = false; bool has_auth_pass = false;
bool has_rate_limit = false; bool has_rate_limit = false;
bool has_plugins = 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++) {
@ -195,6 +198,15 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
if (++i >= argc) return false; if (++i >= argc) return false;
config->rate_limit = (uint32_t)atoi(argv[i]); config->rate_limit = (uint32_t)atoi(argv[i]);
has_rate_limit = true; has_rate_limit = true;
} else if (strcmp(argv[i], "--plugin") == 0) {
if (++i >= argc) return false;
if (config->num_plugins < COCOON_MAX_PLUGINS) {
config->plugins[config->num_plugins++] = strdup(argv[i]);
has_plugins = true;
} else {
fprintf(stderr, "Error: 最多支持 %d 个插件\n", COCOON_MAX_PLUGINS);
return false;
}
} 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);
@ -217,7 +229,8 @@ static bool parse_args(int argc, char *argv[], cocoon_config_t *config) {
has_tls_cert, has_tls_key, has_tls_enabled, has_tls_cert, has_tls_key, has_tls_enabled,
has_access_log, has_access_log,
has_cors_enabled, has_auth_user, has_auth_pass, has_cors_enabled, has_auth_user, has_auth_pass,
has_rate_limit); has_rate_limit,
has_plugins);
} }
if (!config->root_dir) { if (!config->root_dir) {
@ -283,6 +296,10 @@ int main(int argc, char *argv[]) {
access_log_close(); access_log_close();
/* 释放配置文件分配的内存 */ /* 释放配置文件分配的内存 */
/* 释放插件路径 */
for (size_t i = 0; i < config.num_plugins; i++) {
free((void *)config.plugins[i]);
}
if (config.root_dir) free((void *)config.root_dir); if (config.root_dir) free((void *)config.root_dir);
if (config.tls_cert) free((void *)config.tls_cert); if (config.tls_cert) free((void *)config.tls_cert);
if (config.tls_key) free((void *)config.tls_key); if (config.tls_key) free((void *)config.tls_key);

121
plugin.c Normal file
View File

@ -0,0 +1,121 @@
/**
* plugin.c -
*
* 使 dlopen / dlsym
* 线/线
*
* @author xfy
*/
#include "plugin.h"
#include "log.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
/* dlopen 需要 RTLD_NOW 和 RTLD_LOCAL */
#include <dlfcn.h>
/* === 插件条目 === */
typedef struct {
void *handle; /**< dlopen 句柄 */
char path[256]; /**< 插件路径 */
cocoon_plugin_shutdown_func_t shutdown; /**< 关闭函数 */
cocoon_plugin_version_func_t version; /**< 版本函数 */
} plugin_entry_t;
static plugin_entry_t g_plugins[MAX_PLUGINS];
static size_t g_plugin_count = 0;
/**
* cocoon_plugin_load -
*/
int cocoon_plugin_load(const char *path) {
if (!path || !path[0]) {
log_error("plugin: 空路径");
return -1;
}
if (g_plugin_count >= MAX_PLUGINS) {
log_error("plugin: 注册表已满(最多 %d 个)", MAX_PLUGINS);
return -1;
}
void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
if (!handle) {
log_error("plugin: 无法加载 %s: %s", path, dlerror());
return -1;
}
/* 查找符号 */
cocoon_plugin_init_func_t init = (cocoon_plugin_init_func_t)dlsym(handle, "cocoon_plugin_init");
cocoon_plugin_shutdown_func_t shutdown = (cocoon_plugin_shutdown_func_t)dlsym(handle, "cocoon_plugin_shutdown");
cocoon_plugin_version_func_t version = (cocoon_plugin_version_func_t)dlsym(handle, "cocoon_plugin_version");
const char *ver_str = version ? version() : "unknown";
/* 如果存在 init则调用 */
if (init) {
int ret = init();
if (ret != 0) {
log_error("plugin: %s 初始化失败(返回 %d已卸载", path, ret);
dlclose(handle);
return -1;
}
} else {
log_warn("plugin: %s 缺少 cocoon_plugin_init仅加载", path);
}
/* 保存到注册表 */
plugin_entry_t *entry = &g_plugins[g_plugin_count];
entry->handle = handle;
entry->shutdown = shutdown;
entry->version = version;
strncpy(entry->path, path, sizeof(entry->path) - 1);
entry->path[sizeof(entry->path) - 1] = '\0';
g_plugin_count++;
log_info("plugin: 已加载 %s (v%s, #%zu)", path, ver_str, g_plugin_count);
return 0;
}
/**
* cocoon_plugin_unload_all -
*/
void cocoon_plugin_unload_all(void) {
/* 逆序卸载 */
for (size_t i = g_plugin_count; i > 0; i--) {
plugin_entry_t *entry = &g_plugins[i - 1];
if (entry->shutdown) {
entry->shutdown();
}
dlclose(entry->handle);
log_info("plugin: 已卸载 %s", entry->path);
}
g_plugin_count = 0;
}
/**
* cocoon_plugin_count -
*/
size_t cocoon_plugin_count(void) {
return g_plugin_count;
}
/**
* cocoon_plugin_get_version -
*/
const char *cocoon_plugin_get_version(size_t index) {
if (index >= g_plugin_count) return "unknown";
if (g_plugins[index].version) {
return g_plugins[index].version();
}
return "unknown";
}
/**
* cocoon_plugin_get_path -
*/
const char *cocoon_plugin_get_path(size_t index) {
if (index >= g_plugin_count) return NULL;
return g_plugins[index].path;
}

89
plugin.h Normal file
View File

@ -0,0 +1,89 @@
/**
* plugin.h -
*
* .so/.dll
*
*
*
* - cocoon_plugin_init() 0
* - cocoon_plugin_shutdown()
* - cocoon_plugin_version()
*
* cocoon_middleware_register()
*
* @author xfy
*/
#ifndef COCOON_PLUGIN_H
#define COCOON_PLUGIN_H
#include "middleware.h"
#include <stdbool.h>
#include <stddef.h>
/* === 插件接口(插件需要实现) === */
/**
* cocoon_plugin_init_func_t -
*
* cocoon_middleware_register()
*
*
* @return 0 0
*/
typedef int (*cocoon_plugin_init_func_t)(void);
/**
* cocoon_plugin_shutdown_func_t -
*
*
*/
typedef void (*cocoon_plugin_shutdown_func_t)(void);
/**
* cocoon_plugin_version_func_t -
*
* @return "1.0.0"
*/
typedef const char *(*cocoon_plugin_version_func_t)(void);
/* === 插件加载器 API === */
#define MAX_PLUGINS 8
/**
* cocoon_plugin_load -
*
* 使 dlopen/dlsym
* cocoon_plugin_init
*
* @param path .so / .dll
* @return 0 -1
*/
int cocoon_plugin_load(const char *path);
/**
* cocoon_plugin_unload_all -
*
* shutdown dlclose
*/
void cocoon_plugin_unload_all(void);
/**
* cocoon_plugin_count -
*/
size_t cocoon_plugin_count(void);
/**
* cocoon_plugin_get_version -
*
* @param index 0 ~ count-1
* @return "unknown"
*/
const char *cocoon_plugin_get_version(size_t index);
/**
* cocoon_plugin_get_path -
*/
const char *cocoon_plugin_get_path(size_t index);
#endif /* COCOON_PLUGIN_H */

50
plugins/hello.c Normal file
View File

@ -0,0 +1,50 @@
/**
* hello.c -
*
*
* X-Hello-Plugin
*/
#include "../middleware.h"
#include "../http.h"
#include "../platform.h"
#include <stdio.h>
#include <string.h>
/**
* hello_middleware -
*
*
*/
static int hello_middleware(http_request_t *req, cocoon_socket_t fd, void *user_data) {
(void)req;
(void)fd;
(void)user_data;
/* 仅返回 0 继续后续处理,不短路 */
return 0;
}
/**
* cocoon_plugin_init -
*/
int cocoon_plugin_init(void) {
/* 注册中间件 */
if (cocoon_middleware_register("hello", hello_middleware, NULL) != 0) {
return -1;
}
return 0;
}
/**
* cocoon_plugin_shutdown -
*/
void cocoon_plugin_shutdown(void) {
cocoon_middleware_unregister("hello");
}
/**
* cocoon_plugin_version -
*/
const char *cocoon_plugin_version(void) {
return "1.0.0";
}

View File

@ -16,6 +16,7 @@
*/ */
#include "server.h" #include "server.h"
#include "plugin.h"
#include "http.h" #include "http.h"
#include "static.h" #include "static.h"
#include "cocoon.h" #include "cocoon.h"
@ -1048,6 +1049,13 @@ server_context_t *server_create(const cocoon_config_t *config) {
ctx->mw_config.rate_limit = ctx->config.rate_limit; ctx->mw_config.rate_limit = ctx->config.rate_limit;
cocoon_middleware_init_builtin(&ctx->mw_config); cocoon_middleware_init_builtin(&ctx->mw_config);
/* 加载插件 */
for (size_t i = 0; i < ctx->config.num_plugins; i++) {
if (cocoon_plugin_load(ctx->config.plugins[i]) != 0) {
log_error("插件加载失败: %s", ctx->config.plugins[i]);
}
}
return ctx; return ctx;
} }
@ -1121,6 +1129,9 @@ void server_stop(server_context_t *ctx) {
void server_destroy(server_context_t *ctx) { void server_destroy(server_context_t *ctx) {
if (!ctx) return; if (!ctx) return;
/* 卸载插件 */
cocoon_plugin_unload_all();
cocoon_middleware_cleanup(); cocoon_middleware_cleanup();
tls_destroy_context(); tls_destroy_context();

View File

@ -817,6 +817,38 @@ kill_server
sleep 1 sleep 1
start_server start_server
echo ""
echo "=== 插件测试 ==="
# 编译示例插件
if gcc -Wall -Wextra -fPIC -shared -I. -Icoco/include -o "$TMPDIR/hello_plugin.so" plugins/hello.c middleware.c http.c log.c platform.c 2>/dev/null; then
kill_server
sleep 1
$SERVER -r "$ROOT" -p 9999 --plugin "$TMPDIR/hello_plugin.so" > "$TMPDIR/server_plugin.log" 2>&1 &
for i in {1..30}; do
if nc -z localhost 9999 2>/dev/null; then break; fi
sleep 0.1
done
plugin_http=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/")
if [[ "$plugin_http" == "200" ]]; then
echo " ✓ 插件加载 — 服务器正常响应 HTTP 200"
pass
else
echo " ✗ 插件加载 — 期望 200, 实际 $plugin_http"
fail
fi
# 检查日志中是否包含插件加载信息
if grep -q "plugin: 已加载" "$TMPDIR/server_plugin.log"; then
echo " ✓ 插件日志 — 加载日志正确输出"
pass
else
echo " ✗ 插件日志 — 未找到加载日志"
fail
fi
else
echo " ⊘ 插件编译跳过(无编译器)"
fi
echo "" echo ""
echo "=== WebSocket 测试 ===" echo "=== WebSocket 测试 ==="

View File

@ -127,7 +127,7 @@ void test_merge_override_all(void) {
}; };
config_merge(&base, &cmdline, config_merge(&base, &cmdline,
true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true,
false, false, false, false); false, false, false, false, false);
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);
@ -157,7 +157,7 @@ void test_merge_no_override(void) {
}; };
config_merge(&base, &cmdline, config_merge(&base, &cmdline,
false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 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);
@ -181,7 +181,7 @@ void test_merge_partial_override(void) {
}; };
config_merge(&base, &cmdline, config_merge(&base, &cmdline,
true, false, false, true, false, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, false, false, false, false,
false, false, false, false); false, 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 */
@ -247,7 +247,7 @@ void test_merge_cmdline_null_root_dir(void) {
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, false, false, false, false, false, config_merge(&base, &cmdline, true, true, false, false, false, false, false, false, false, false, false, false,
false, false, false, 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);
@ -256,7 +256,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, true, true, true, true, true, config_merge(NULL, NULL, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true, true); true, true, true, true, false);
TEST_ASSERT_TRUE(1); TEST_ASSERT_TRUE(1);
} }