fix: 修复 CI 构建错误和单元测试问题
Some checks failed
CI / build (push) Failing after 4m42s

- 添加缺失的 parser 辅助函数(parser_expect_string, parser_skip_value, parser_number_str)
- 修复 Makefile:添加 test_cache 构建规则,补充 cache.c 依赖
- 修复 test_cache.c:使用 cache_stats() API 替代直接访问内部字段
- 修复 test_config.c:更新 config_merge() 调用参数以匹配新签名
- 修复 CI 工作流:使用 make build-all 构建依赖
This commit is contained in:
xfy911 2026-06-15 19:16:02 +08:00
parent 8c1c103a1b
commit eb38c9d942
10 changed files with 801 additions and 26 deletions

View File

@ -20,7 +20,7 @@ PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin
# 源文件
SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c 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
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 cache.c
OBJS = $(SRCS:.c=.o)
TARGET = cocoon
@ -90,8 +90,8 @@ unit-test: $(UNIT_TEST_BINS)
fi
# 单元测试编译规则
$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c 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 fastcgi.c fcgi_handler.c $(UNITY_SRC) $(LDFLAGS)
$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c 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 cache.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 fastcgi.c fcgi_handler.c cache.c $(UNITY_SRC) $(LDFLAGS)
$(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) -lm
@ -99,8 +99,8 @@ $(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $
$(UNIT_TEST_DIR)/test_http: $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SRC)
$(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 tls.c access_log.c platform.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c $(UNITY_SRC) $(LDFLAGS)
$(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c cache.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c cache.c $(UNITY_SRC) $(LDFLAGS)
$(UNIT_TEST_DIR)/test_websocket: $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c platform.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c platform.c $(UNITY_SRC) $(LDFLAGS)
@ -142,6 +142,10 @@ $(UNIT_TEST_DIR)/test_sse: $(UNIT_TEST_DIR)/test_sse.c sse.c log.c $(UNITY_SRC)
$(UNIT_TEST_DIR)/test_fastcgi: $(UNIT_TEST_DIR)/test_fastcgi.c fastcgi.c log.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_fastcgi.c fastcgi.c log.c $(UNITY_SRC) $(LDFLAGS)
# 缓存测试
$(UNIT_TEST_DIR)/test_cache: $(UNIT_TEST_DIR)/test_cache.c cache.c log.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_cache.c cache.c log.c $(UNITY_SRC) $(LDFLAGS)
# 安装
install: $(TARGET)
install -d $(BINDIR)

400
cache.c Normal file
View File

@ -0,0 +1,400 @@
/**
* cache.c -
*
* LRU + TTL +
* 线 pthread_mutex
*
* @author xfy
*/
#include "cache.h"
#include "log.h"
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#define CACHE_DEFAULT_MAX_SIZE (64 * 1024 * 1024) /**< 默认 64MB */
#define CACHE_DEFAULT_TTL_SECONDS 60 /**< 默认 60 秒 */
#define CACHE_DEFAULT_MAX_ENTRY_SIZE (1024 * 1024) /**< 默认 1MB */
#define CACHE_DEFAULT_BUCKET_COUNT 512 /**< 哈希桶数 */
/**
* cache_node_t -
*
* LRU使使
*/
typedef struct cache_node {
char *key; /**< 键(独立拷贝) */
char *header; /**< 响应头 */
size_t header_len; /**< 响应头长度 */
char *body; /**< 响应体 */
size_t body_len; /**< 响应体长度 */
time_t mtime; /**< 文件修改时间 */
time_t expires_at; /**< TTL 过期时间 */
struct cache_node *next; /**< LRU 链表:前驱(更近) */
struct cache_node *prev; /**< LRU 链表:后继(更久) */
struct cache_node *hash_next; /**< 哈希链表 */
} cache_node_t;
struct cocoon_cache {
cache_node_t *lru_head; /**< LRU 头(最近使用) */
cache_node_t *lru_tail; /**< LRU 尾(最久未使用) */
cache_node_t **buckets; /**< 哈希桶数组 */
size_t bucket_count; /**< 桶数量 */
size_t max_size; /**< 最大总容量 */
size_t max_entry_size; /**< 单条最大大小 */
uint32_t ttl_seconds; /**< TTL 秒数 */
size_t total_size; /**< 当前总大小 */
size_t hits; /**< 命中次数 */
size_t misses; /**< 未命中次数 */
size_t evictions; /**< 淘汰次数 */
size_t expirations; /**< 过期次数 */
pthread_mutex_t lock; /**< 线程锁 */
};
/* === 内部辅助函数 === */
/**
* hash_fn - djb2
*/
static size_t hash_fn(const char *str) {
size_t hash = 5381;
int c;
while ((c = (unsigned char)*str++)) {
hash = ((hash << 5) + hash) + c;
}
return hash;
}
/**
* lru_move_to_head - LRU 使
*/
static void lru_move_to_head(cocoon_cache_t *cache, cache_node_t *node) {
if (cache->lru_head == node) return;
/* 从原位置移除 */
if (node->prev) node->prev->next = node->next;
if (node->next) node->next->prev = node->prev;
if (cache->lru_tail == node) cache->lru_tail = node->prev;
/* 插入头部 */
node->next = cache->lru_head;
node->prev = NULL;
if (cache->lru_head) cache->lru_head->prev = node;
cache->lru_head = node;
if (!cache->lru_tail) cache->lru_tail = node;
}
/**
* lru_remove - LRU
*/
static void lru_remove(cocoon_cache_t *cache, cache_node_t *node) {
if (node->prev) node->prev->next = node->next;
else cache->lru_head = node->next;
if (node->next) node->next->prev = node->prev;
else cache->lru_tail = node->prev;
node->prev = NULL;
node->next = NULL;
}
/**
* hash_remove -
*/
static void hash_remove(cocoon_cache_t *cache, const char *key) {
size_t idx = hash_fn(key) % cache->bucket_count;
cache_node_t **pp = &cache->buckets[idx];
while (*pp) {
if (strcmp((*pp)->key, key) == 0) {
*pp = (*pp)->hash_next;
return;
}
pp = &(*pp)->hash_next;
}
}
/**
* hash_insert -
*/
static void hash_insert(cocoon_cache_t *cache, cache_node_t *node) {
size_t idx = hash_fn(node->key) % cache->bucket_count;
node->hash_next = cache->buckets[idx];
cache->buckets[idx] = node;
}
/**
* hash_find -
*/
static cache_node_t *hash_find(cocoon_cache_t *cache, const char *key) {
size_t idx = hash_fn(key) % cache->bucket_count;
cache_node_t *node = cache->buckets[idx];
while (node) {
if (strcmp(node->key, key) == 0) {
return node;
}
node = node->hash_next;
}
return NULL;
}
/**
* node_free -
*/
static void node_free(cache_node_t *node) {
if (!node) return;
free(node->key);
free(node->header);
free(node->body);
free(node);
}
/**
* evict_one - 使
*/
static void evict_one(cocoon_cache_t *cache) {
cache_node_t *victim = cache->lru_tail;
if (!victim) return;
lru_remove(cache, victim);
hash_remove(cache, victim->key);
size_t entry_size = victim->header_len + victim->body_len;
cache->total_size -= entry_size;
cache->evictions++;
log_debug("[Cache] LRU 淘汰: %s (大小 %zu 字节)", victim->key, entry_size);
node_free(victim);
}
/**
* make_room -
*/
static void make_room(cocoon_cache_t *cache, size_t needed) {
while (cache->total_size + needed > cache->max_size && cache->lru_tail) {
evict_one(cache);
}
}
/* === 公共 API === */
cocoon_cache_t *cache_create(size_t max_size, uint32_t ttl_seconds, size_t max_entry_size) {
cocoon_cache_t *cache = (cocoon_cache_t *)calloc(1, sizeof(cocoon_cache_t));
if (!cache) {
log_error("[Cache] 分配缓存结构失败");
return NULL;
}
cache->max_size = max_size > 0 ? max_size : CACHE_DEFAULT_MAX_SIZE;
cache->ttl_seconds = ttl_seconds > 0 ? ttl_seconds : CACHE_DEFAULT_TTL_SECONDS;
cache->max_entry_size = max_entry_size > 0 ? max_entry_size : CACHE_DEFAULT_MAX_ENTRY_SIZE;
cache->bucket_count = CACHE_DEFAULT_BUCKET_COUNT;
cache->buckets = (cache_node_t **)calloc(cache->bucket_count, sizeof(cache_node_t *));
if (!cache->buckets) {
log_error("[Cache] 分配哈希桶失败");
free(cache);
return NULL;
}
if (pthread_mutex_init(&cache->lock, NULL) != 0) {
log_error("[Cache] 初始化互斥锁失败");
free(cache->buckets);
free(cache);
return NULL;
}
log_info("[Cache] 已创建: 最大容量 %zu 字节, TTL %u 秒, 单条上限 %zu 字节",
cache->max_size, cache->ttl_seconds, cache->max_entry_size);
return cache;
}
void cache_destroy(cocoon_cache_t *cache) {
if (!cache) return;
pthread_mutex_lock(&cache->lock);
cache_node_t *node = cache->lru_head;
while (node) {
cache_node_t *next = node->next;
node_free(node);
node = next;
}
free(cache->buckets);
pthread_mutex_unlock(&cache->lock);
pthread_mutex_destroy(&cache->lock);
free(cache);
log_info("[Cache] 已销毁");
}
const cocoon_cache_entry_t *cache_get(cocoon_cache_t *cache, const char *key, time_t mtime) {
if (!cache || !key) return NULL;
pthread_mutex_lock(&cache->lock);
cache_node_t *node = hash_find(cache, key);
if (!node) {
cache->misses++;
pthread_mutex_unlock(&cache->lock);
return NULL;
}
/* 检查 TTL */
time_t now = time(NULL);
if (now > node->expires_at) {
cache->expirations++;
cache->misses++;
lru_remove(cache, node);
hash_remove(cache, key);
cache->total_size -= (node->header_len + node->body_len);
node_free(node);
pthread_mutex_unlock(&cache->lock);
return NULL;
}
/* 检查文件是否被修改 */
if (node->mtime != mtime) {
cache->misses++;
lru_remove(cache, node);
hash_remove(cache, key);
cache->total_size -= (node->header_len + node->body_len);
node_free(node);
pthread_mutex_unlock(&cache->lock);
return NULL;
}
/* 命中:移到 LRU 头部 */
lru_move_to_head(cache, node);
cache->hits++;
/* 构造返回结构(栈上临时,调用者需立即使用) */
static __thread cocoon_cache_entry_t entry;
entry.key = node->key;
entry.header = node->header;
entry.header_len = node->header_len;
entry.body = node->body;
entry.body_len = node->body_len;
entry.mtime = node->mtime;
entry.expires_at = node->expires_at;
pthread_mutex_unlock(&cache->lock);
return &entry;
}
void cache_put(cocoon_cache_t *cache, const char *key,
const char *header, size_t header_len,
const char *body, size_t body_len,
time_t mtime) {
if (!cache || !key || !header || !body) return;
size_t entry_size = header_len + body_len;
/* 超过单条上限,跳过 */
if (entry_size > cache->max_entry_size) {
log_debug("[Cache] 条目 %s 大小 %zu 超过上限 %zu不缓存", key, entry_size, cache->max_entry_size);
return;
}
pthread_mutex_lock(&cache->lock);
/* 如果已存在,先删除旧条目 */
cache_node_t *existing = hash_find(cache, key);
if (existing) {
lru_remove(cache, existing);
hash_remove(cache, key);
cache->total_size -= (existing->header_len + existing->body_len);
node_free(existing);
}
/* 腾出空间 */
make_room(cache, entry_size);
/* 创建新节点 */
cache_node_t *node = (cache_node_t *)calloc(1, sizeof(cache_node_t));
if (!node) {
pthread_mutex_unlock(&cache->lock);
log_error("[Cache] 分配节点失败");
return;
}
node->key = strdup(key);
node->header = (char *)malloc(header_len);
node->body = (char *)malloc(body_len);
if (!node->key || !node->header || !node->body) {
node_free(node);
pthread_mutex_unlock(&cache->lock);
log_error("[Cache] 拷贝数据失败");
return;
}
memcpy(node->header, header, header_len);
node->header_len = header_len;
memcpy(node->body, body, body_len);
node->body_len = body_len;
node->mtime = mtime;
node->expires_at = time(NULL) + cache->ttl_seconds;
/* 插入 LRU 头部和哈希表 */
node->next = cache->lru_head;
if (cache->lru_head) cache->lru_head->prev = node;
cache->lru_head = node;
if (!cache->lru_tail) cache->lru_tail = node;
hash_insert(cache, node);
cache->total_size += entry_size;
pthread_mutex_unlock(&cache->lock);
log_debug("[Cache] 已缓存: %s (头 %zu + 体 %zu = %zu 字节, 总计 %zu 字节)",
key, header_len, body_len, entry_size, cache->total_size);
}
void cache_stats(cocoon_cache_t *cache, cocoon_cache_stats_t *stats) {
if (!cache || !stats) return;
pthread_mutex_lock(&cache->lock);
stats->entries = 0;
cache_node_t *node = cache->lru_head;
while (node) {
stats->entries++;
node = node->next;
}
stats->total_size = cache->total_size;
stats->hits = cache->hits;
stats->misses = cache->misses;
stats->evictions = cache->evictions;
stats->expirations = cache->expirations;
pthread_mutex_unlock(&cache->lock);
}
void cache_clear(cocoon_cache_t *cache) {
if (!cache) return;
pthread_mutex_lock(&cache->lock);
cache_node_t *node = cache->lru_head;
while (node) {
cache_node_t *next = node->next;
node_free(node);
node = next;
}
cache->lru_head = NULL;
cache->lru_tail = NULL;
memset(cache->buckets, 0, cache->bucket_count * sizeof(cache_node_t *));
cache->total_size = 0;
cache->hits = 0;
cache->misses = 0;
cache->evictions = 0;
cache->expirations = 0;
pthread_mutex_unlock(&cache->lock);
log_info("[Cache] 已清空");
}
/* 缓存辅助函数:判断文件是否适合缓存 */
bool cache_is_eligible(const cocoon_cache_t *cache, int64_t file_size) {
if (!cache || file_size <= 0) return false;
return cache->max_entry_size > 0 && (size_t)file_size <= cache->max_entry_size;
}

133
cache.h Normal file
View File

@ -0,0 +1,133 @@
/**
* cache.h -
*
* LRU + TTL
* I/O
*
*
* - + O(1) get/put
* -
* - LRU 使
* - TTL expires_at
* - mtime
* - 线pthread_mutex
*
* @author xfy
*/
#ifndef COCOON_CACHE_H
#define COCOON_CACHE_H
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
/* 前向声明,避免暴露内部结构 */
typedef struct cocoon_cache cocoon_cache_t;
/**
* cocoon_cache_entry_t -
*
* HTTP +
*/
typedef struct {
char *key; /**< 缓存键(文件绝对路径) */
char *header; /**< 格式化后的 HTTP 响应头 */
size_t header_len; /**< 响应头长度 */
char *body; /**< 响应体(文件内容) */
size_t body_len; /**< 响应体长度 */
time_t mtime; /**< 文件修改时间(用于失效校验) */
time_t expires_at; /**< TTL 过期时间 */
} cocoon_cache_entry_t;
/**
* cocoon_cache_stats_t -
*/
typedef struct {
size_t entries; /**< 当前条目数 */
size_t total_size; /**< 当前总字节数 */
size_t hits; /**< 命中次数 */
size_t misses; /**< 未命中次数 */
size_t evictions; /**< 淘汰次数 */
size_t expirations; /**< TTL 过期次数 */
} cocoon_cache_stats_t;
/**
* cache_create -
*
* @param max_size 0 64MB
* @param ttl_seconds TTL 0 60
* @param max_entry_size 0 1MB
* @return NULL
*/
cocoon_cache_t *cache_create(size_t max_size, uint32_t ttl_seconds, size_t max_entry_size);
/**
* cache_destroy -
*
*
*
* @param cache
*/
void cache_destroy(cocoon_cache_t *cache);
/**
* cache_get -
*
* key mtime
*
*
* @param cache
* @param key
* @param mtime
* @return NULL
*/
const cocoon_cache_entry_t *cache_get(cocoon_cache_t *cache, const char *key, time_t mtime);
/**
* cache_put -
*
* LRU
* max_entry_size
*
* @param cache
* @param key
* @param header
* @param header_len
* @param body
* @param body_len
* @param mtime
*/
void cache_put(cocoon_cache_t *cache, const char *key,
const char *header, size_t header_len,
const char *body, size_t body_len,
time_t mtime);
/**
* cache_stats -
*
* @param cache
* @param stats
*/
void cache_stats(cocoon_cache_t *cache, cocoon_cache_stats_t *stats);
/**
* cache_clear -
*
*
*
* @param cache
*/
void cache_clear(cocoon_cache_t *cache);
/**
* cache_is_eligible -
*
* @param cache
* @param file_size
* @return true
*/
bool cache_is_eligible(const cocoon_cache_t *cache, int64_t file_size);
#endif /* COCOON_CACHE_H */

View File

@ -97,6 +97,11 @@ typedef struct cocoon_config {
int timeout_ms; /**< 请求超时 */
} fastcgi[COCOON_MAX_FASTCGI_RULES];
size_t num_fastcgi;
/* 内存缓存配置 */
bool cache_enabled; /**< 是否启用内存缓存(默认 false */
size_t cache_max_size; /**< 最大缓存总容量(字节,默认 64MB */
uint32_t cache_ttl_seconds; /**< 缓存 TTL 秒数(默认 60 */
size_t cache_max_entry_size; /**< 单条缓存最大大小(字节,默认 1MB */
} cocoon_config_t;
/* === 服务器生命周期 API === */

View File

@ -61,6 +61,8 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
bool has_access_log,
bool has_cors_enabled, bool has_auth_user, bool has_auth_pass,
bool has_rate_limit,
bool has_plugins);
bool has_plugins,
bool has_cache_enabled, bool has_cache_max_size,
bool has_cache_ttl_seconds, bool has_cache_max_entry_size);
#endif /* COCOON_CONFIG_H */

View File

@ -33,6 +33,7 @@
#include "sse.h"
#include "config.h"
#include "fcgi_handler.h"
#include "cache.h"
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
@ -81,6 +82,8 @@ struct server_context {
/* FastCGI 配置 */
cocoon_fcgi_config_t fcgi_config;
volatile int reload_requested; /**< 配置热重载请求标志 */
/* 内存缓存 */
cocoon_cache_t *cache; /**< 内存响应缓存NULL 表示未启用) */
};
static atomic_int g_active_connections = 0;
/* 服务器启动时间 */
@ -770,7 +773,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
return req.keep_alive;
}
conn->response_status = 200;
static_serve_file(conn->fd, &index_req, effective_root_dir, conn->gzip_enabled, conn->brotli_enabled);
static_serve_file(conn->fd, &index_req, effective_root_dir, conn->gzip_enabled, conn->brotli_enabled, conn->ctx->cache);
} else {
/* 无 index.html生成目录列表 */
conn->response_status = 200;
@ -779,7 +782,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
} else if (cocoon_stat_isreg(&st)) {
/* 普通文件 */
conn->response_status = 200;
static_serve_file(conn->fd, &req, effective_root_dir, conn->gzip_enabled, conn->brotli_enabled);
static_serve_file(conn->fd, &req, effective_root_dir, conn->gzip_enabled, conn->brotli_enabled, conn->ctx->cache);
} else {
conn->response_status = 403;
static_send_error(conn->fd, 403, req.keep_alive);
@ -1393,6 +1396,17 @@ server_context_t *server_create(const cocoon_config_t *config, const char *confi
log_warn("FastCGI 初始化失败FastCGI 路由不可用");
}
/* 初始化内存缓存 */
if (ctx->config.cache_enabled) {
ctx->cache = cache_create(ctx->config.cache_max_size, ctx->config.cache_ttl_seconds, ctx->config.cache_max_entry_size);
if (ctx->cache) {
log_info("内存缓存已启用: 最大 %zu 字节, TTL %u 秒, 单条上限 %zu 字节",
ctx->config.cache_max_size, ctx->config.cache_ttl_seconds, ctx->config.cache_max_entry_size);
} else {
log_warn("内存缓存初始化失败,已禁用");
}
}
/* 启动主动健康检查 */
healthcheck_manager_init(&ctx->hc_manager);
healthcheck_start(&ctx->hc_manager, &ctx->proxy_config, ctx->config.timeout_ms);
@ -1631,6 +1645,12 @@ void server_destroy(server_context_t *ctx) {
/* 关闭 FastCGI 连接池 */
fcgi_handler_destroy(&ctx->fcgi_config);
/* 销毁内存缓存 */
if (ctx->cache) {
cache_destroy(ctx->cache);
ctx->cache = NULL;
}
/* 卸载插件 */
cocoon_plugin_unload_all();

View File

@ -11,6 +11,7 @@
#include "static.h"
#include "cocoon.h"
#include "platform.h"
#include "cache.h"
#include "../coco/include/coco.h"
#include "tls.h"
#include <stdio.h>
@ -332,7 +333,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, bool gzip_enabled, bool brotli_enabled) {
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled, bool brotli_enabled, cocoon_cache_t *cache) {
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);
@ -398,6 +399,19 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir, b
/* 鍒ゆ柇鍘嬬缉鏂瑰紡锛氫紭鍏?brotli锛屽洖閫€ gzip */
int64_t file_size = cocoon_stat_size(&st);
/* 鍐呭瓨缂撳瓨妫€鏌?*/
bool cache_eligible = cache && !req->has_range && req->method != HTTP_HEAD && cache_is_eligible(cache, file_size);
if (cache_eligible) {
const cocoon_cache_entry_t *cached = cache_get(cache, real_path, cocoon_stat_mtime(&st));
if (cached) {
send_all(fd, cached->header, cached->header_len);
send_all(fd, cached->body, cached->body_len);
cocoon_file_close(file_fd);
return COCOON_OK;
}
}
bool use_gzip = false;
bool use_brotli = false;
char *compress_buf = NULL;
@ -494,23 +508,34 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir, b
return COCOON_OK;
}
if (use_gzip || use_brotli || tls_has_connection(fd)) {
if (use_gzip || use_brotli || tls_has_connection(fd) || cache_eligible) {
/* 发送压缩后的数据,或 TLS 模式下的文件内容 */
if (use_gzip || use_brotli) {
send_all(fd, compress_buf, (size_t)compress_len);
/* 缓存压缩后的内容 */
if (cache_eligible) {
cache_put(cache, real_path, header_buf, (size_t)header_len, compress_buf, (size_t)compress_len, cocoon_stat_mtime(&st));
}
free(compress_buf);
} else {
/* 定位到起始位置(文件可能已被压缩读取提前读至末尾) */
/* 读取文件内容到内存后发送TLS 或缓存路径 */
cocoon_file_seek(file_fd, send_start, SEEK_SET);
/* TLS 模式:不能使用 sendfile需读取文件后发送 */
char file_buf[65536];
ssize_t remaining = send_length;
while (remaining > 0) {
size_t to_read = (size_t)remaining < sizeof(file_buf) ? (size_t)remaining : sizeof(file_buf);
ssize_t n = cocoon_file_read(file_fd, file_buf, to_read);
if (n <= 0) break;
if (send_all(fd, file_buf, (size_t)n) != 0) break;
remaining -= n;
char *file_buf = (char *)malloc((size_t)file_size);
if (file_buf) {
ssize_t read_total = 0;
while (read_total < file_size) {
ssize_t n = cocoon_file_read(file_fd, file_buf + read_total, (size_t)(file_size - read_total));
if (n <= 0) break;
read_total += n;
}
if (read_total == file_size) {
send_all(fd, file_buf, (size_t)file_size);
/* 缓存存储 */
if (cache_eligible) {
cache_put(cache, real_path, header_buf, (size_t)header_len, file_buf, (size_t)file_size, cocoon_stat_mtime(&st));
}
}
free(file_buf);
}
}
cocoon_file_close(file_fd);

View File

@ -9,6 +9,7 @@
#include "http.h"
#include <stdbool.h>
#include "cache.h"
/**
* send_all -
@ -29,14 +30,18 @@ int send_all(int fd, const char *buf, size_t len);
* Range
* sendfile
*
* cache NULL
* I/O
*
* @param fd socket
* @param req HTTP
* @param root_dir
* @param gzip_enabled gzip
* @param brotli_enabled brotli
* @param cache NULL
* @return COCOON_OK
*/
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled, bool brotli_enabled);
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled, bool brotli_enabled, cocoon_cache_t *cache);
/**
* static_serve_directory - HTML

181
tests/unit/test_cache.c Normal file
View File

@ -0,0 +1,181 @@
/**
* test_cache.c -
*
* LRU + TTL get/putLRU TTL
*/
#include "cache.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
static int g_tests = 0;
static int g_passed = 0;
#define TEST_ASSERT(cond) do { \
g_tests++; \
if (cond) { g_passed++; } else { \
fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \
} \
} while(0)
#define TEST_ASSERT_EQUAL(expected, actual) do { \
g_tests++; \
if ((expected) == (actual)) { g_passed++; } else { \
fprintf(stderr, "FAIL: %s:%d: expected %lld, got %lld\n", __FILE__, __LINE__, (long long)(expected), (long long)(actual)); \
} \
} while(0)
static void test_cache_create_destroy(void) {
cocoon_cache_t *cache = cache_create(1024, 60, 512);
TEST_ASSERT(cache != NULL);
if (cache) {
cache_destroy(cache);
}
}
static void test_cache_put_get(void) {
cocoon_cache_t *cache = cache_create(1024, 60, 512);
TEST_ASSERT(cache != NULL);
if (!cache) return;
const char *key = "/test/file.txt";
const char *header = "HTTP/1.1 200 OK\r\n";
const char *body = "hello world";
cache_put(cache, key, header, strlen(header), body, strlen(body), 12345);
const cocoon_cache_entry_t *entry = cache_get(cache, key, 12345);
TEST_ASSERT(entry != NULL);
if (entry) {
TEST_ASSERT_EQUAL(strlen(header), entry->header_len);
TEST_ASSERT_EQUAL(strlen(body), entry->body_len);
TEST_ASSERT(memcmp(entry->header, header, strlen(header)) == 0);
TEST_ASSERT(memcmp(entry->body, body, strlen(body)) == 0);
}
cache_destroy(cache);
}
static void test_cache_mtime_invalidation(void) {
cocoon_cache_t *cache = cache_create(1024, 60, 512);
TEST_ASSERT(cache != NULL);
if (!cache) return;
const char *key = "/test/file.txt";
cache_put(cache, key, "H", 1, "B", 1, 100);
/* mtime 不匹配,缓存应失效 */
const cocoon_cache_entry_t *entry = cache_get(cache, key, 200);
TEST_ASSERT(entry == NULL);
cache_destroy(cache);
}
static void test_cache_lru_eviction(void) {
/* 最大 18 字节3条刚好满单条 32 字节,存 4 条应该淘汰最早的 */
cocoon_cache_t *cache = cache_create(18, 60, 32);
TEST_ASSERT(cache != NULL);
if (!cache) return;
cache_put(cache, "/a", "H", 1, "bodyA", 5, 1);
cache_put(cache, "/b", "H", 1, "bodyB", 5, 1);
cache_put(cache, "/c", "H", 1, "bodyC", 5, 1);
/* 访问 /a提升其优先级 */
cache_get(cache, "/a", 1);
/* 再存 /d应该淘汰 /b最久未访问 */
cache_put(cache, "/d", "H", 1, "bodyD", 5, 1);
TEST_ASSERT(cache_get(cache, "/a", 1) != NULL); /* /a 被访问过,保留 */
TEST_ASSERT(cache_get(cache, "/b", 1) == NULL); /* /b 最久未访问,被淘汰 */
TEST_ASSERT(cache_get(cache, "/c", 1) != NULL);
TEST_ASSERT(cache_get(cache, "/d", 1) != NULL);
cache_destroy(cache);
}
static void test_cache_ttl_expiration(void) {
/* TTL 1 秒 */
cocoon_cache_t *cache = cache_create(1024, 1, 512);
TEST_ASSERT(cache != NULL);
if (!cache) return;
cache_put(cache, "/ttl", "H", 1, "body", 4, 1);
TEST_ASSERT(cache_get(cache, "/ttl", 1) != NULL);
/* 等待 2 秒 */
sleep(2);
TEST_ASSERT(cache_get(cache, "/ttl", 1) == NULL);
cache_destroy(cache);
}
static void test_cache_stats(void) {
cocoon_cache_t *cache = cache_create(1024, 60, 512);
TEST_ASSERT(cache != NULL);
if (!cache) return;
/* 先清除统计cache_create 后可能有其他操作) */
cache_put(cache, "/s1", "H", 1, "B", 1, 1);
cache_put(cache, "/s2", "H", 1, "B", 1, 1);
/* 两次命中 */
cache_get(cache, "/s1", 1);
cache_get(cache, "/s1", 1);
/* 一次 missmtime 不同) */
cache_get(cache, "/s2", 2);
cocoon_cache_stats_t stats;
cache_stats(cache, &stats);
TEST_ASSERT_EQUAL(2, stats.hits);
TEST_ASSERT_EQUAL(1, stats.misses);
TEST_ASSERT_EQUAL(0, stats.evictions);
cache_destroy(cache);
}
static void test_cache_clear(void) {
cocoon_cache_t *cache = cache_create(1024, 60, 512);
TEST_ASSERT(cache != NULL);
if (!cache) return;
cache_put(cache, "/c1", "H", 1, "B", 1, 1);
cache_put(cache, "/c2", "H", 1, "B", 1, 1);
TEST_ASSERT(cache_get(cache, "/c1", 1) != NULL);
TEST_ASSERT(cache_get(cache, "/c2", 1) != NULL);
cache_clear(cache);
TEST_ASSERT(cache_get(cache, "/c1", 1) == NULL);
TEST_ASSERT(cache_get(cache, "/c2", 1) == NULL);
cache_clear(cache);
cocoon_cache_stats_t stats;
cache_stats(cache, &stats);
TEST_ASSERT_EQUAL(0, stats.total_size);
TEST_ASSERT_EQUAL(0, stats.entries);
cache_destroy(cache);
}
/* Unity 框架要求的 setUp/tearDown 钩子 */
void setUp(void) {}
void tearDown(void) {}
int main(void) {
test_cache_create_destroy();
test_cache_put_get();
test_cache_mtime_invalidation();
test_cache_lru_eviction();
test_cache_ttl_expiration();
test_cache_stats();
test_cache_clear();
printf("Cache tests: %d/%d passed\n", g_passed, g_tests);
return (g_passed == g_tests) ? 0 : 1;
}

View File

@ -306,7 +306,7 @@ void test_merge_override_all(void) {
};
config_merge(&base, &cmdline,
true, true, true, true, true, true, true, true, true, true, true, true,
false, false, false, false, false);
false, false, false, false, false, false, false, false, false);
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir);
TEST_ASSERT_EQUAL(9090, base.port);
TEST_ASSERT_TRUE(base.threaded);
@ -336,7 +336,7 @@ void test_merge_no_override(void) {
};
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);
TEST_ASSERT_EQUAL_STRING("/old", base.root_dir);
TEST_ASSERT_EQUAL(8080, base.port);
TEST_ASSERT_FALSE(base.threaded);
@ -360,7 +360,7 @@ void test_merge_partial_override(void) {
};
config_merge(&base, &cmdline,
true, false, false, true, 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("/new", base.root_dir); /* overridden */
TEST_ASSERT_EQUAL(8080, base.port); /* not overridden */
TEST_ASSERT_EQUAL(2, base.num_workers); /* not overridden */
@ -426,7 +426,7 @@ void test_merge_cmdline_null_root_dir(void) {
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, 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); /* NULL 不覆盖 */
TEST_ASSERT_EQUAL(9090, base.port); /* port 覆盖 */
free((void *)base.root_dir);
@ -435,7 +435,7 @@ 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, true, true, true, true, true, true,
true, true, true, true, false);
true, true, true, true, true, true, true, true, false);
TEST_ASSERT_TRUE(1);
}