diff --git a/Makefile b/Makefile index d315a5b..64548c7 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/cache.c b/cache.c new file mode 100644 index 0000000..59d303e --- /dev/null +++ b/cache.c @@ -0,0 +1,400 @@ +/** + * cache.c - 内存响应缓存实现 + * + * LRU + TTL 缓存,哈希表 + 双向链表。 + * 线程安全:所有操作受 pthread_mutex 保护。 + * + * @author xfy + */ + +#include "cache.h" +#include "log.h" +#include +#include +#include + +#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; +} diff --git a/cache.h b/cache.h new file mode 100644 index 0000000..0363030 --- /dev/null +++ b/cache.h @@ -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 +#include +#include +#include + +/* 前向声明,避免暴露内部结构 */ +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 */ diff --git a/cocoon.h b/cocoon.h index 668f42a..66a4eef 100644 --- a/cocoon.h +++ b/cocoon.h @@ -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 === */ diff --git a/config.h b/config.h index 2b65e51..fdcf7e5 100644 --- a/config.h +++ b/config.h @@ -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 */ diff --git a/server.c b/server.c index 08f27dd..5d44294 100644 --- a/server.c +++ b/server.c @@ -33,6 +33,7 @@ #include "sse.h" #include "config.h" #include "fcgi_handler.h" +#include "cache.h" #include #include #include @@ -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(); diff --git a/static.c b/static.c index f2d8a96..3a09140 100644 --- a/static.c +++ b/static.c @@ -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 @@ -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); diff --git a/static.h b/static.h index 998eb3e..97fabbb 100644 --- a/static.h +++ b/static.h @@ -9,6 +9,7 @@ #include "http.h" #include +#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 diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c new file mode 100644 index 0000000..0761637 --- /dev/null +++ b/tests/unit/test_cache.c @@ -0,0 +1,181 @@ +/** + * test_cache.c - 内存缓存单元测试 + * + * 测试 LRU + TTL 缓存的 get/put、LRU 淘汰、TTL 失效、统计等功能。 + */ + +#include "cache.h" +#include +#include +#include +#include + +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); + + /* 一次 miss(mtime 不同) */ + 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; +} diff --git a/tests/unit/test_config.c b/tests/unit/test_config.c index fb0defd..a9fa341 100644 --- a/tests/unit/test_config.c +++ b/tests/unit/test_config.c @@ -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); }