Apache自定义缓存模块:如何拦截请求、获取内容并保存至文件
实现Apache独立缓存模块的可行方案
核心思路
你需要结合请求早期钩子和输出过滤器钩子来实现需求:
- 早期钩子:在请求处理流程的极早阶段检查缓存文件,若存在则直接返回缓存内容并终止后续流程,避免触发PHP等内容生成脚本。
- 输出过滤器钩子:当缓存不存在时,拦截Apache向客户端发送的响应数据,将其写入本地缓存文件,完成缓存生成。
具体钩子选择与实现
1. 请求早期钩子:ap_hook_map_to_storage
选择这个钩子的原因是它在Apache处理请求的早期阶段执行(早于handler、fixups),此时还未触发PHP等内容生成逻辑。实现逻辑:
- 根据当前请求的URL生成对应的缓存文件路径(比如用MD5/SHA1哈希URL作为文件名,或映射到特定目录结构)。
- 用APR的文件API检查缓存文件是否存在且有效(可额外添加过期时间判断)。
- 如果缓存有效,直接调用
ap_send_fd发送缓存文件内容,设置r->status为HTTP_OK并返回OK终止后续流程。 - 如果缓存不存在或无效,返回
DECLINED让Apache继续执行后续流程。
示例代码片段:
static int cache_check_handler(request_rec *r) { // 跳过非GET请求(按需调整) if (strcmp(r->method, "GET") != 0) { return DECLINED; } // 基于URL生成缓存文件路径 char cache_path[APR_PATH_MAX]; apr_sha1_ctx_t ctx; unsigned char digest[APR_SHA1_DIGESTSIZE]; char hex_digest[APR_SHA1_DIGESTSIZE * 2 + 1]; apr_sha1_init(&ctx); apr_sha1_update(&ctx, (const unsigned char *)r->uri, strlen(r->uri)); apr_sha1_final(digest, &ctx); apr_hex_encode(hex_digest, (const char *)digest, APR_SHA1_DIGESTSIZE); apr_snprintf(cache_path, sizeof(cache_path), "/path/to/your/cache/dir/%s.cache", hex_digest); // 检查缓存文件是否存在 apr_file_t *cache_fd; apr_status_t rv = apr_file_open(&cache_fd, cache_path, APR_READ, APR_OS_DEFAULT, r->pool); if (rv == APR_SUCCESS) { // 设置响应头(按需调整内容类型) ap_set_content_type(r, "text/html"); r->status = HTTP_OK; // 发送缓存文件 ap_send_fd(cache_fd, r); apr_file_close(cache_fd); return OK; // 终止后续流程 } // 缓存不存在,继续执行 return DECLINED; } // 注册钩子,优先级设为APR_HOOK_EARLY,确保早于其他handler执行 static void register_hooks(apr_pool_t *p) { ap_hook_map_to_storage(cache_check_handler, NULL, NULL, APR_HOOK_EARLY); }
2. 输出过滤器钩子:ap_register_output_filter
当缓存不存在时,需要捕获响应内容并写入缓存文件。输出过滤器可以拦截Apache发送给客户端的每一块数据,我们可以在过滤器中收集数据并写入缓存。
实现逻辑:
- 定义过滤器上下文结构体,存储缓存文件句柄、缓存路径等信息。
- 首次收到数据时,创建并打开缓存文件。
- 每收到一块数据,先写入缓存文件,再传递给下一个过滤器(确保客户端能正常接收响应)。
- 请求结束时关闭缓存文件。
示例代码片段:
// 过滤器上下文结构体 typedef struct { apr_file_t *cache_fd; char *cache_path; } cache_filter_ctx; static apr_status_t cache_filter(ap_filter_t *f, apr_bucket_brigade *bb) { request_rec *r = f->r; cache_filter_ctx *ctx = f->ctx; apr_status_t rv; // 跳过非GET请求或已初始化过的上下文 if (strcmp(r->method, "GET") != 0 || ctx != NULL) { return ap_pass_brigade(f->next, bb); } // 生成缓存文件路径(和早期钩子逻辑一致) char cache_path[APR_PATH_MAX]; // ... 此处省略路径生成代码,与cache_check_handler中逻辑相同 ... // 递归创建缓存目录(如果不存在) apr_dir_make_recursive("/path/to/your/cache/dir", APR_OS_DEFAULT, r->pool); // 打开缓存文件用于写入 rv = apr_file_open(&ctx->cache_fd, cache_path, APR_WRITE | APR_CREATE | APR_TRUNCATE, APR_OS_DEFAULT, r->pool); if (rv != APR_SUCCESS) { // 缓存创建失败,不影响响应发送 return ap_pass_brigade(f->next, bb); } ctx->cache_path = apr_pstrdup(r->pool, cache_path); f->ctx = ctx; // 遍历数据块,写入缓存并传递给下一个过滤器 apr_bucket *e; for (e = APR_BRIGADE_FIRST(bb); e != APR_BRIGADE_SENTINEL(bb); e = APR_BUCKET_NEXT(e)) { const char *data; apr_size_t len; if (APR_BUCKET_IS_EOS(e)) { // 请求结束,关闭缓存文件 apr_file_close(ctx->cache_fd); return ap_pass_brigade(f->next, bb); } rv = apr_bucket_read(e, &data, &len, APR_BLOCK_READ); if (rv != APR_SUCCESS) { return rv; } // 写入缓存文件 apr_file_write(ctx->cache_fd, data, &len); } return ap_pass_brigade(f->next, bb); } // 在register_hooks中注册输出过滤器 static void register_hooks(apr_pool_t *p) { ap_hook_map_to_storage(cache_check_handler, NULL, NULL, APR_HOOK_EARLY); ap_register_output_filter("CACHE_FILTER", cache_filter, NULL, AP_FTYPE_RESOURCE); }
关键注意事项
- 缓存有效性:可在缓存文件名中加入时间戳,或在缓存文件头部写入过期时间,在早期钩子中判断是否过期。
- 并发安全:用APR的
apr_file_lock机制避免多个请求同时生成同一缓存文件的竞争问题。 - 缓存清理:可通过定时任务(如cron)清理过期缓存,或在早期钩子中检查到过期缓存时直接删除。
- 模块独立性:所有逻辑基于Apache和APR核心API实现,不依赖第三方模块,满足独立自包含需求。
内容的提问来源于stack exchange,提问作者user171453
相关产品推荐
相关产品推荐

