feat(cache): 实现 HTTP 缓存协商(ETag + Last-Modified + 304)
- static: 添加 format_http_time / generate_etag / match_etag / parse_http_time 辅助函数 - static: 静态文件服务生成 ETag(文件大小+修改时间)和 Last-Modified - static: 实现 If-None-Match 和 If-Modified-Since 缓存协商,命中时返回 304 Not Modified - http: 请求结构体添加 if_none_match / if_modified_since 字段 - http: 响应头格式化支持 ETag 和 Last-Modified 输出 - http: 重构响应头构建,先输出固定头再追加缓存头 测试验证:curl 带 If-None-Match / If-Modified-Since 均返回 304
This commit is contained in:
parent
413288d837
commit
f5563381b8
31
http.c
31
http.c
@ -227,30 +227,37 @@ int http_format_response_header(char *buf, size_t buf_size, const http_response_
|
||||
"Content-Type: %s\r\n"
|
||||
"Content-Length: %ld\r\n"
|
||||
"Accept-Ranges: bytes\r\n"
|
||||
"Content-Range: bytes %ld-%ld/%ld\r\n"
|
||||
"Connection: %s\r\n"
|
||||
"Server: Cocoon/1.0\r\n"
|
||||
"\r\n",
|
||||
"Content-Range: bytes %ld-%ld/%ld\r\n",
|
||||
resp->status_code, resp->status_text,
|
||||
resp->content_type ? resp->content_type : "application/octet-stream",
|
||||
(long)resp->content_length,
|
||||
(long)resp->range_start, (long)resp->range_end, (long)resp->total_length,
|
||||
resp->keep_alive ? "keep-alive" : "close");
|
||||
(long)resp->range_start, (long)resp->range_end, (long)resp->total_length);
|
||||
} else {
|
||||
n += snprintf(buf + n, buf_size - n,
|
||||
"HTTP/1.1 %d %s\r\n"
|
||||
"Content-Type: %s\r\n"
|
||||
"Content-Length: %ld\r\n"
|
||||
"Accept-Ranges: bytes\r\n"
|
||||
"Connection: %s\r\n"
|
||||
"Server: Cocoon/1.0\r\n"
|
||||
"\r\n",
|
||||
"Accept-Ranges: bytes\r\n",
|
||||
resp->status_code, resp->status_text,
|
||||
resp->content_type ? resp->content_type : "application/octet-stream",
|
||||
(long)resp->content_length,
|
||||
resp->keep_alive ? "keep-alive" : "close");
|
||||
(long)resp->content_length);
|
||||
}
|
||||
|
||||
/* 缓存头 */
|
||||
if (resp->etag && resp->etag[0]) {
|
||||
n += snprintf(buf + n, buf_size - n, "ETag: %s\r\n", resp->etag);
|
||||
}
|
||||
if (resp->last_modified && resp->last_modified[0]) {
|
||||
n += snprintf(buf + n, buf_size - n, "Last-Modified: %s\r\n", resp->last_modified);
|
||||
}
|
||||
|
||||
/* 连接头 + 空行 */
|
||||
n += snprintf(buf + n, buf_size - n,
|
||||
"Connection: %s\r\n"
|
||||
"Server: Cocoon/1.0\r\n"
|
||||
"\r\n",
|
||||
resp->keep_alive ? "keep-alive" : "close");
|
||||
|
||||
if ((size_t)n >= buf_size) return -1;
|
||||
return n;
|
||||
}
|
||||
|
||||
116
static.c
116
static.c
@ -22,6 +22,75 @@
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
|
||||
/**
|
||||
* format_http_time - 将时间戳格式化为 HTTP 日期字符串
|
||||
*
|
||||
* HTTP 日期格式: "Wed, 21 Oct 2015 07:28:00 GMT"
|
||||
*
|
||||
* @param t 时间戳(秒)
|
||||
* @param buf 输出缓冲区
|
||||
* @param buf_size 缓冲区大小
|
||||
*/
|
||||
static void format_http_time(time_t t, char *buf, size_t buf_size) {
|
||||
struct tm *gmt = gmtime(&t);
|
||||
if (gmt) {
|
||||
strftime(buf, buf_size, "%a, %d %b %Y %H:%M:%S GMT", gmt);
|
||||
} else {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* generate_etag - 基于文件元数据生成 ETag
|
||||
*
|
||||
* 格式: "大小-修改时间十六进制"
|
||||
* 示例: "1024-647a3b2f"
|
||||
*
|
||||
* @param st 文件状态结构体
|
||||
* @param buf 输出缓冲区
|
||||
* @param buf_size 缓冲区大小
|
||||
*/
|
||||
static void generate_etag(const struct stat *st, char *buf, size_t buf_size) {
|
||||
snprintf(buf, buf_size, "\"%lx-%lx\"", (unsigned long)st->st_size, (unsigned long)st->st_mtime);
|
||||
}
|
||||
|
||||
/**
|
||||
* match_etag - 比较 ETag 值是否匹配
|
||||
*
|
||||
* 支持 W/ 弱匹配前缀和 * 通配符。
|
||||
*
|
||||
* @param etag 服务器 ETag
|
||||
* @param if_none_match 客户端 If-None-Match 值
|
||||
* @return true 匹配
|
||||
*/
|
||||
static bool match_etag(const char *etag, const char *if_none_match) {
|
||||
if (!etag || !if_none_match) return false;
|
||||
/* 通配符匹配 */
|
||||
if (strcmp(if_none_match, "*") == 0) return true;
|
||||
/* 去除 W/ 前缀比较 */
|
||||
const char *client = if_none_match;
|
||||
if (strncmp(client, "W/", 2) == 0) client += 2;
|
||||
return strcmp(client, etag) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse_http_time - 解析 HTTP 日期字符串为时间戳
|
||||
*
|
||||
* 支持 RFC 1123 / RFC 850 / ANSI C 格式。
|
||||
*
|
||||
* @param str HTTP 日期字符串
|
||||
* @return 时间戳,解析失败返回 -1
|
||||
*/
|
||||
static time_t parse_http_time(const char *str) {
|
||||
struct tm tm = {0};
|
||||
if (strptime(str, "%a, %d %b %Y %H:%M:%S GMT", &tm) != NULL ||
|
||||
strptime(str, "%A, %d-%b-%y %H:%M:%S GMT", &tm) != NULL ||
|
||||
strptime(str, "%a %b %d %H:%M:%S %Y", &tm) != NULL) {
|
||||
return timegm(&tm);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* safe_path_join - 安全路径拼接
|
||||
*
|
||||
@ -166,6 +235,49 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir) {
|
||||
return static_send_error(fd, 403, req->keep_alive);
|
||||
}
|
||||
|
||||
/* 生成 ETag 和 Last-Modified */
|
||||
char etag[64];
|
||||
char last_modified[64];
|
||||
generate_etag(&st, etag, sizeof(etag));
|
||||
format_http_time(st.st_mtime, last_modified, sizeof(last_modified));
|
||||
|
||||
/* 检查缓存协商 */
|
||||
if (req->has_if_none_match && match_etag(etag, req->if_none_match)) {
|
||||
close(file_fd);
|
||||
http_response_t resp = {
|
||||
.status_code = 304,
|
||||
.status_text = "Not Modified",
|
||||
.content_type = http_mime_type(real_path),
|
||||
.content_length = 0,
|
||||
.keep_alive = req->keep_alive,
|
||||
.etag = etag,
|
||||
.last_modified = last_modified
|
||||
};
|
||||
char header_buf[1024];
|
||||
int header_len = http_format_response_header(header_buf, sizeof(header_buf), &resp);
|
||||
if (header_len > 0) send_all(fd, header_buf, (size_t)header_len);
|
||||
return COCOON_OK;
|
||||
}
|
||||
if (req->has_if_modified_since) {
|
||||
time_t client_time = parse_http_time(req->if_modified_since);
|
||||
if (client_time >= 0 && st.st_mtime <= client_time) {
|
||||
close(file_fd);
|
||||
http_response_t resp = {
|
||||
.status_code = 304,
|
||||
.status_text = "Not Modified",
|
||||
.content_type = http_mime_type(real_path),
|
||||
.content_length = 0,
|
||||
.keep_alive = req->keep_alive,
|
||||
.etag = etag,
|
||||
.last_modified = last_modified
|
||||
};
|
||||
char header_buf[1024];
|
||||
int header_len = http_format_response_header(header_buf, sizeof(header_buf), &resp);
|
||||
if (header_len > 0) send_all(fd, header_buf, (size_t)header_len);
|
||||
return COCOON_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/* 计算发送范围 */
|
||||
int64_t file_size = st.st_size;
|
||||
int64_t send_start = 0;
|
||||
@ -196,7 +308,9 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir) {
|
||||
.has_range = req->has_range,
|
||||
.range_start = send_start,
|
||||
.range_end = send_end,
|
||||
.total_length = file_size
|
||||
.total_length = file_size,
|
||||
.etag = etag,
|
||||
.last_modified = last_modified
|
||||
};
|
||||
|
||||
char header_buf[1024];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user