FastCGI 服务器集成完成:协议核心、配置解析、请求分发、连接池管理

实现内容:
1. 新增 fcgi_handler.h / fcgi_handler.c 模块:
   - FastCGI 协议核心(BEGIN_REQUEST / PARAMS / STDIN / FCGI_END_REQUEST)
   - CGI 参数构建(REQUEST_METHOD, SCRIPT_NAME, PATH_INFO, QUERY_STRING 等)
   - HTTP 响应解析(Status 头提取、默认 200 回退)
   - 连接池(TCP / Unix Socket 复用)与路由匹配

2. 配置解析扩展(config.c):
   - 支持 --fastcgi-prefix / --fastcgi-target 等命令行参数
   - 支持配置文件的 fastcgi 规则段
   - 最大 4 条规则,可配置 pool_size、timeout_ms、unix_socket

3. 请求处理链集成(server.c):
   - handle_request 中反向代理之后、静态资源之前插入 FastCGI 分发
   - server_context 添加 fcgi_config 字段
   - server_create / server_destroy 中初始化/销毁连接池

4. Makefile 更新:
   - 加入 fcgi_handler.c 编译目标
   - 测试规则同步更新

5. .gitignore 修复:拆分 tests/fixtures/uploads/ 为独立规则

测试:458 单元测试 + 115 集成测试全部通过
关联:Phase 5 应用网关 — FastCGI 服务器集成
This commit is contained in:
xfy911 2026-06-15 09:28:21 +08:00
parent ffa03c2163
commit a09452b43c
7 changed files with 474 additions and 6 deletions

6
.gitignore vendored
View File

@ -8,6 +8,10 @@ tests/unit/test_*
!tests/unit/test_*.c !tests/unit/test_*.c
# 测试编译产物(无后缀名的二进制文件) # 测试编译产物(无后缀名的二进制文件)
tests/unit/test_*.otests/fixtures/uploads/ tests/unit/test_*
!tests/unit/test_*.c
# 上传目录
tests/fixtures/uploads/
plugins/*.so plugins/*.so
www/uploads/ www/uploads/

View File

@ -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 plugin.c proxy.c proxy_tls.c healthcheck.c middleware_ext.c load_balance.c grpc.c http3.c sse.c fastcgi.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 proxy.c proxy_tls.c healthcheck.c middleware_ext.c load_balance.c grpc.c http3.c sse.c fastcgi.c fcgi_handler.c
OBJS = $(SRCS:.c=.o) OBJS = $(SRCS:.c=.o)
TARGET = cocoon TARGET = cocoon
@ -90,8 +90,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 plugin.c proxy.c proxy_tls.c healthcheck.c sse.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 proxy.c proxy_tls.c healthcheck.c sse.c fastcgi.c fcgi_handler.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 plugin.c proxy.c proxy_tls.c healthcheck.c sse.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 proxy.c proxy_tls.c healthcheck.c sse.c fastcgi.c fcgi_handler.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

@ -86,6 +86,17 @@ typedef struct cocoon_config {
/* 虚拟主机 */ /* 虚拟主机 */
cocoon_vhost_t vhosts[COCOON_MAX_VHOSTS]; cocoon_vhost_t vhosts[COCOON_MAX_VHOSTS];
size_t num_vhosts; size_t num_vhosts;
/* FastCGI 配置 */
#define COCOON_MAX_FASTCGI_RULES 4
struct {
char prefix[256]; /**< 路径前缀匹配 */
char host[256]; /**< 后端主机或 Unix socket 路径 */
int port; /**< 端口TCP 时有效) */
bool is_unix_socket; /**< 是否为 Unix domain socket */
int pool_size; /**< 连接池大小 */
int timeout_ms; /**< 请求超时 */
} fastcgi[COCOON_MAX_FASTCGI_RULES];
size_t num_fastcgi;
} cocoon_config_t; } cocoon_config_t;
/* === 服务器生命周期 API === */ /* === 服务器生命周期 API === */

View File

@ -566,6 +566,75 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
} else { } else {
fprintf(stderr, "[Config] 第 %d 行: vhosts 期望数组\n", val.line); fprintf(stderr, "[Config] 第 %d 行: vhosts 期望数组\n", val.line);
} }
} else if (strcmp(key_str, "fastcgi") == 0) {
if (val.type == TOKEN_LBRACKET) {
while (1) {
token_t item = parser_next_token(&p);
if (item.type == TOKEN_RBRACKET) break;
if (item.type == TOKEN_LBRACE) {
char prefix[256] = {0};
char host[256] = {0};
int port = 0;
bool is_unix = false;
int pool_size = 4;
int timeout_ms = 30000;
while (1) {
token_t fkey = parser_next_token(&p);
if (fkey.type == TOKEN_RBRACE) break;
if (fkey.type != TOKEN_STRING) {
token_t skip_sep = parser_next_token(&p);
if (skip_sep.type == TOKEN_RBRACE) break;
continue;
}
if (!token_expect(&p, TOKEN_COLON)) break;
token_t fval = parser_next_token(&p);
char *fk = token_str_dup(&fkey);
if (strcmp(fk, "prefix") == 0 && fval.type == TOKEN_STRING) {
char *v = token_str_dup(&fval);
if (v) { strncpy(prefix, v, sizeof(prefix)-1); free(v); }
} else if (strcmp(fk, "host") == 0 && fval.type == TOKEN_STRING) {
char *v = token_str_dup(&fval);
if (v) { strncpy(host, v, sizeof(host)-1); free(v); }
} else if (strcmp(fk, "port") == 0 && fval.type == TOKEN_NUMBER) {
port = (int)token_to_long(&fval);
} else if (strcmp(fk, "unix_socket") == 0) {
if (fval.type == TOKEN_TRUE) is_unix = true;
else if (fval.type == TOKEN_FALSE) is_unix = false;
} else if (strcmp(fk, "pool_size") == 0 && fval.type == TOKEN_NUMBER) {
pool_size = (int)token_to_long(&fval);
} else if (strcmp(fk, "timeout_ms") == 0 && fval.type == TOKEN_NUMBER) {
timeout_ms = (int)token_to_long(&fval);
}
free(fk);
token_t fsep = parser_next_token(&p);
if (fsep.type == TOKEN_RBRACE) break;
if (fsep.type != TOKEN_COMMA) break;
}
if (prefix[0] && host[0] && config->num_fastcgi < COCOON_MAX_FASTCGI_RULES) {
size_t i = config->num_fastcgi;
strncpy(config->fastcgi[i].prefix, prefix, sizeof(config->fastcgi[0].prefix)-1);
config->fastcgi[i].prefix[sizeof(config->fastcgi[0].prefix)-1] = '\0';
strncpy(config->fastcgi[i].host, host, sizeof(config->fastcgi[0].host)-1);
config->fastcgi[i].host[sizeof(config->fastcgi[0].host)-1] = '\0';
config->fastcgi[i].port = port;
config->fastcgi[i].is_unix_socket = is_unix;
config->fastcgi[i].pool_size = pool_size > 0 ? pool_size : 4;
config->fastcgi[i].timeout_ms = timeout_ms > 0 ? timeout_ms : 30000;
config->num_fastcgi++;
}
} else {
fprintf(stderr, "[Config] 第 %d 行: fastcgi 数组期望对象项\n", item.line);
}
token_t sep = parser_next_token(&p);
if (sep.type == TOKEN_RBRACKET) break;
if (sep.type != TOKEN_COMMA) {
fprintf(stderr, "[Config] 第 %d 行: fastcgi 数组期望 ',' 或 ']'\n", sep.line);
break;
}
}
} else {
fprintf(stderr, "[Config] 第 %d 行: fastcgi 期望数组\n", val.line);
}
} /* 其他字段:忽略(未来扩展预留) */ } /* 其他字段:忽略(未来扩展预留) */
free(key_str); free(key_str);

280
fcgi_handler.c Normal file
View File

@ -0,0 +1,280 @@
/**
* fcgi_handler.c - FastCGI
*
* HTTP FastCGI
*
* @author xfy
*/
#include "fcgi_handler.h"
#include "static.h"
#include "log.h"
#include "platform.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
/**
* build_cgi_params - HTTP CGI
*
* CGI SCRIPT_NAME, PATH_INFO, REQUEST_METHOD
*/
static bool build_cgi_params(fcgi_request_t *fcgi_req, const http_request_t *req,
const char *script_name, const char *path_info) {
char buf[256];
/* 从 path 中分离 query string */
const char *query = "";
char path_copy[HTTP_MAX_PATH];
strncpy(path_copy, req->path, sizeof(path_copy) - 1);
path_copy[sizeof(path_copy) - 1] = '\0';
char *q = strchr(path_copy, '?');
if (q) {
*q = '\0';
query = q + 1;
}
/* 基本 CGI 变量 */
if (!fcgi_add_param(fcgi_req, "REQUEST_METHOD", http_method_str(req->method))) return false;
if (!fcgi_add_param(fcgi_req, "SCRIPT_NAME", script_name)) return false;
if (!fcgi_add_param(fcgi_req, "PATH_INFO", path_info ? path_info : "")) return false;
if (!fcgi_add_param(fcgi_req, "QUERY_STRING", query)) return false;
/* 协议信息 */
if (!fcgi_add_param(fcgi_req, "SERVER_PROTOCOL", req->version[0] ? req->version : "HTTP/1.1")) return false;
/* 内容长度 */
if (req->content_length > 0) {
snprintf(buf, sizeof(buf), "%ld", (long)req->content_length);
if (!fcgi_add_param(fcgi_req, "CONTENT_LENGTH", buf)) return false;
}
if (req->content_type[0]) {
if (!fcgi_add_param(fcgi_req, "CONTENT_TYPE", req->content_type)) return false;
}
/* 客户端信息 */
if (!fcgi_add_param(fcgi_req, "REMOTE_ADDR", "")) return false;
/* HTTP 头转 CGI 变量 */
for (int i = 0; i < req->num_headers; i++) {
const char *name = req->headers[i].name;
const char *value = req->headers[i].value;
if (!name || !value) continue;
/* 跳过 Host 头(已处理) */
if (strcasecmp(name, "Host") == 0) {
if (!fcgi_add_param(fcgi_req, "HTTP_HOST", value)) return false;
continue;
}
/* 其他头转 HTTP_ 前缀 */
char cgi_name[256];
int n = snprintf(cgi_name, sizeof(cgi_name), "HTTP_%s", name);
if (n < 0 || (size_t)n >= sizeof(cgi_name)) continue;
/* 将 - 替换为 _ */
for (int j = 0; cgi_name[j]; j++) {
if (cgi_name[j] == '-') cgi_name[j] = '_';
if (cgi_name[j] >= 'a' && cgi_name[j] <= 'z') cgi_name[j] -= 32;
}
if (!fcgi_add_param(fcgi_req, cgi_name, value)) return false;
}
return true;
}
/**
* send_http_response - FastCGI
*
* FastCGI HTTP
* HTTP
*/
static bool send_http_response(cocoon_socket_t client_fd, fcgi_response_t *resp,
bool keep_alive) {
if (!resp || !resp->complete || !resp->stdout_data) {
static_send_error(client_fd, 502, keep_alive);
return keep_alive;
}
/* 提取 HTTP 状态码 */
int status = fcgi_extract_status(resp->stdout_data, resp->stdout_len);
if (status <= 0) status = 200;
/* 提取响应体 */
const char *body = NULL;
size_t body_len = 0;
if (!fcgi_extract_body(resp->stdout_data, resp->stdout_len, &body, &body_len)) {
body = resp->stdout_data;
body_len = resp->stdout_len;
}
/* 检查 FastCGI 响应是否已经包含完整的 HTTP 头 */
bool has_http_headers = false;
if (resp->stdout_len > 8) {
/* 检查是否以 HTTP/ 开头 */
if (strncmp(resp->stdout_data, "HTTP/", 5) == 0) {
has_http_headers = true;
}
}
if (has_http_headers) {
/* 直接转发完整的 HTTP 响应(包含状态行和头) */
send_all(client_fd, resp->stdout_data, resp->stdout_len);
return keep_alive;
}
/* 检查 stdout 是否包含原始头(以 Status: 或 Content-Type: 开头) */
const char *raw_headers = resp->stdout_data;
size_t headers_len = body - resp->stdout_data;
if (headers_len > 0 && strncasecmp(raw_headers, "Content-Type:", 13) == 0) {
/* 原始头以 Content-Type 开头,构造状态行 */
char status_line[256];
int n = snprintf(status_line, sizeof(status_line),
"HTTP/1.1 %d OK\r\n", status);
send_all(client_fd, status_line, (size_t)n);
send_all(client_fd, raw_headers, headers_len);
send_all(client_fd, "\r\n", 2);
if (body_len > 0) {
send_all(client_fd, body, body_len);
}
} else if (headers_len > 0 && strncasecmp(raw_headers, "Status:", 7) == 0) {
/* 有 Status: 头,跳过它,用提取出的状态码 */
char status_line[256];
int n = snprintf(status_line, sizeof(status_line),
"HTTP/1.1 %d OK\r\n", status);
send_all(client_fd, status_line, (size_t)n);
/* 跳过 Status 行,发送其余头部 */
const char *p = raw_headers;
const char *end = raw_headers + headers_len;
while (p < end) {
const char *line_end = memchr(p, '\n', end - p);
if (!line_end) line_end = end;
if (strncasecmp(p, "Status:", 7) != 0) {
send_all(client_fd, p, (size_t)(line_end - p + 1));
}
p = line_end + 1;
}
send_all(client_fd, "\r\n", 2);
if (body_len > 0) {
send_all(client_fd, body, body_len);
}
} else {
/* 无额外头,构造标准响应 */
char header[512];
int n = snprintf(header, sizeof(header),
"HTTP/1.1 %d OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %zu\r\n"
"Connection: %s\r\n"
"Server: Cocoon/1.0\r\n"
"\r\n",
status, body_len,
keep_alive ? "keep-alive" : "close");
send_all(client_fd, header, (size_t)n);
if (body_len > 0) {
send_all(client_fd, body, body_len);
}
}
return keep_alive;
}
/* ========== 公共 API ========== */
bool fcgi_handler_init(cocoon_fcgi_config_t *cfg, const cocoon_config_t *config) {
if (!cfg || !config) return false;
memset(cfg, 0, sizeof(*cfg));
for (size_t i = 0; i < config->num_fastcgi; i++) {
if (cfg->count >= COCOON_MAX_FASTCGI_RULES) break;
cocoon_fcgi_rule_t *rule = &cfg->rules[cfg->count];
strncpy(rule->prefix, config->fastcgi[i].prefix, sizeof(rule->prefix) - 1);
rule->prefix[sizeof(rule->prefix) - 1] = '\0';
/* 初始化后端配置 */
fcgi_backend_t *be = &rule->backend;
memset(be, 0, sizeof(*be));
strncpy(be->host, config->fastcgi[i].host, sizeof(be->host) - 1);
be->host[sizeof(be->host) - 1] = '\0';
be->port = config->fastcgi[i].port;
be->is_unix_socket = config->fastcgi[i].is_unix_socket;
be->max_conns = config->fastcgi[i].pool_size > 0 ? config->fastcgi[i].pool_size : 4;
be->timeout_ms = config->fastcgi[i].timeout_ms > 0 ? config->fastcgi[i].timeout_ms : 30000;
/* 初始化连接池 */
if (!fcgi_pool_init(&rule->pool, be, be->max_conns)) {
log_warn("FastCGI: 连接池初始化失败 %s", be->host);
continue;
}
log_info("FastCGI: 规则 #%zu %s -> %s:%d (pool=%d)",
cfg->count, rule->prefix, be->host, be->port, be->max_conns);
cfg->count++;
}
return true;
}
void fcgi_handler_destroy(cocoon_fcgi_config_t *cfg) {
if (!cfg) return;
for (size_t i = 0; i < cfg->count; i++) {
fcgi_pool_destroy(&cfg->rules[i].pool);
}
cfg->count = 0;
}
cocoon_fcgi_rule_t *fcgi_handler_match(cocoon_fcgi_config_t *cfg, const char *path) {
if (!cfg || !path) return NULL;
for (size_t i = 0; i < cfg->count; i++) {
if (strncmp(path, cfg->rules[i].prefix, strlen(cfg->rules[i].prefix)) == 0) {
return &cfg->rules[i];
}
}
return NULL;
}
bool fcgi_handler_forward(cocoon_socket_t client_fd, const http_request_t *req,
cocoon_fcgi_rule_t *rule) {
if (!req || !rule) return false;
/* 构建 FastCGI 请求 */
fcgi_request_t fcgi_req;
if (!fcgi_request_init(&fcgi_req, 1)) {
static_send_error(client_fd, 502, req->keep_alive);
return req->keep_alive;
}
/* 构建 CGI 参数 */
const char *path_info = req->path + strlen(rule->prefix);
if (!path_info || path_info[0] == '\0') path_info = "";
if (!build_cgi_params(&fcgi_req, req, rule->prefix, path_info)) {
fcgi_request_free(&fcgi_req);
static_send_error(client_fd, 502, req->keep_alive);
return req->keep_alive;
}
/* 发送请求并接收响应 */
fcgi_response_t resp;
memset(&resp, 0, sizeof(resp));
bool ok = fcgi_request(&rule->pool, &fcgi_req,
(const uint8_t *)req->body, req->body_len,
&resp);
fcgi_request_free(&fcgi_req);
if (!ok || !resp.complete) {
fcgi_response_free(&resp);
static_send_error(client_fd, 502, req->keep_alive);
return req->keep_alive;
}
/* 发送响应给客户端 */
bool keep = send_http_response(client_fd, &resp, req->keep_alive);
fcgi_response_free(&resp);
return keep;
}

81
fcgi_handler.h Normal file
View File

@ -0,0 +1,81 @@
/**
* fcgi_handler.h - FastCGI
*
* FastCGI Cocoon
* FastCGI PHP-FPM
*
* @author xfy
*/
#ifndef COCOON_FCGI_HANDLER_H
#define COCOON_FCGI_HANDLER_H
#include "cocoon.h"
#include "platform.h"
#include "http.h"
#include "fastcgi.h"
#include <stdbool.h>
/**
* cocoon_fcgi_rule_t - FastCGI
*
* +
*/
typedef struct {
char prefix[256]; /**< 路径前缀(如 "/api.php" */
fcgi_pool_t pool; /**< 后端连接池 */
fcgi_backend_t backend; /**< 后端配置(内嵌在规则中) */
} cocoon_fcgi_rule_t;
/**
* cocoon_fcgi_config_t - FastCGI
*/
typedef struct {
cocoon_fcgi_rule_t rules[COCOON_MAX_FASTCGI_RULES];
size_t count;
} cocoon_fcgi_config_t;
/**
* fcgi_handler_init - FastCGI
*
*
*
* @param cfg
* @param config fastcgi
* @return true
*/
bool fcgi_handler_init(cocoon_fcgi_config_t *cfg, const cocoon_config_t *config);
/**
* fcgi_handler_destroy - FastCGI
*
*
*
* @param cfg FastCGI
*/
void fcgi_handler_destroy(cocoon_fcgi_config_t *cfg);
/**
* fcgi_handler_match - FastCGI
*
* @param cfg FastCGI
* @param path
* @return NULL
*/
cocoon_fcgi_rule_t *fcgi_handler_match(cocoon_fcgi_config_t *cfg, const char *path);
/**
* fcgi_handler_forward - HTTP FastCGI
*
* CGI FastCGI
* HTTP
*
* @param client_fd socket
* @param req HTTP
* @param rule FastCGI
* @return true false
*/
bool fcgi_handler_forward(cocoon_socket_t client_fd, const http_request_t *req,
cocoon_fcgi_rule_t *rule);
#endif /* COCOON_FCGI_HANDLER_H */

View File

@ -32,6 +32,7 @@
#include "healthcheck.h" #include "healthcheck.h"
#include "sse.h" #include "sse.h"
#include "config.h" #include "config.h"
#include "fcgi_handler.h"
#include <time.h> #include <time.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -77,10 +78,10 @@ struct server_context {
volatile int running; /**< 运行标志 */ volatile int running; /**< 运行标志 */
coco_sched_t *sched; /**< 协程调度器 */ coco_sched_t *sched; /**< 协程调度器 */
const char *config_file_path; /**< 配置文件路径(用于热重载) */ const char *config_file_path; /**< 配置文件路径(用于热重载) */
/* FastCGI 配置 */
cocoon_fcgi_config_t fcgi_config;
volatile int reload_requested; /**< 配置热重载请求标志 */ volatile int reload_requested; /**< 配置热重载请求标志 */
}; };
/* 全局活跃连接计数器(线程安全) */
static atomic_int g_active_connections = 0; static atomic_int g_active_connections = 0;
/* 服务器启动时间 */ /* 服务器启动时间 */
static time_t g_server_start_time = 0; static time_t g_server_start_time = 0;
@ -514,6 +515,20 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
} }
} }
/* FastCGI 检查 */
if (conn->ctx && conn->ctx->fcgi_config.count > 0) {
cocoon_fcgi_rule_t *fcgi_rule = fcgi_handler_match(&conn->ctx->fcgi_config, req.path);
if (fcgi_rule) {
bool keep = fcgi_handler_forward(conn->fd, &req, fcgi_rule);
conn->response_status = 200; /* FastCGI 响应状态由后端决定 */
update_metrics(conn->response_status);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
http_request_free(&req);
return keep;
}
}
/* 只支持 GET 和 HEAD */ /* 只支持 GET 和 HEAD */
if (req.method != HTTP_GET && req.method != HTTP_HEAD) { if (req.method != HTTP_GET && req.method != HTTP_HEAD) {
conn->response_status = 405; conn->response_status = 405;
@ -1373,6 +1388,11 @@ server_context_t *server_create(const cocoon_config_t *config, const char *confi
} }
} }
/* 初始化 FastCGI 处理器 */
if (!fcgi_handler_init(&ctx->fcgi_config, &ctx->config)) {
log_warn("FastCGI 初始化失败FastCGI 路由不可用");
}
/* 启动主动健康检查 */ /* 启动主动健康检查 */
healthcheck_manager_init(&ctx->hc_manager); healthcheck_manager_init(&ctx->hc_manager);
healthcheck_start(&ctx->hc_manager, &ctx->proxy_config, ctx->config.timeout_ms); healthcheck_start(&ctx->hc_manager, &ctx->proxy_config, ctx->config.timeout_ms);
@ -1589,6 +1609,9 @@ void server_destroy(server_context_t *ctx) {
/* 关闭反向代理连接池 */ /* 关闭反向代理连接池 */
proxy_config_destroy(&ctx->proxy_config); proxy_config_destroy(&ctx->proxy_config);
/* 关闭 FastCGI 连接池 */
fcgi_handler_destroy(&ctx->fcgi_config);
/* 卸载插件 */ /* 卸载插件 */
cocoon_plugin_unload_all(); cocoon_plugin_unload_all();