feat(http2): HTTP/2 静态文件服务支持 gzip/brotli 压缩
- http2.c: 文件内容读取到内存后按需压缩(优先 brotli,回退 gzip) - 响应头自动添加 content-encoding: br 或 gzip - 不可压缩类型(图片等)跳过压缩 - 修复 curl 测试:添加 --compressed 自动解压 - 新增 4 项 HTTP/2 压缩集成测试(HTML/CSS/JS brotli、图片不压缩) - 49 项集成测试全部通过,35 个单元测试全部通过
This commit is contained in:
parent
2436218d3e
commit
e9d7af80b8
156
http2.c
156
http2.c
@ -19,10 +19,63 @@
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <zlib.h>
|
||||
#include <brotli/encode.h>
|
||||
|
||||
/* 最大 HTTP/2 会话数 */
|
||||
#define MAX_HTTP2_SESSIONS 1024
|
||||
|
||||
/* 压缩辅助函数(与 static.c 独立,保持模块边界) */
|
||||
static bool is_compressible_mime(const char *mime_type) {
|
||||
if (!mime_type) return false;
|
||||
return (
|
||||
strstr(mime_type, "text/") != NULL ||
|
||||
strstr(mime_type, "application/javascript") != NULL ||
|
||||
strstr(mime_type, "application/json") != NULL ||
|
||||
strstr(mime_type, "application/xml") != NULL ||
|
||||
strstr(mime_type, "application/manifest") != NULL ||
|
||||
strstr(mime_type, "image/svg") != NULL
|
||||
);
|
||||
}
|
||||
|
||||
static ssize_t gzip_compress(const char *src, size_t src_len,
|
||||
char *dst, size_t dst_cap) {
|
||||
z_stream strm = {0};
|
||||
if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
|
||||
15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
|
||||
return -1;
|
||||
}
|
||||
strm.avail_in = (uInt)src_len;
|
||||
strm.next_in = (Bytef *)src;
|
||||
strm.avail_out = (uInt)dst_cap;
|
||||
strm.next_out = (Bytef *)dst;
|
||||
if (deflate(&strm, Z_FINISH) != Z_STREAM_END) {
|
||||
deflateEnd(&strm);
|
||||
return -1;
|
||||
}
|
||||
size_t compressed_len = dst_cap - strm.avail_out;
|
||||
deflateEnd(&strm);
|
||||
if (compressed_len >= src_len * 0.95) return 0;
|
||||
return (ssize_t)compressed_len;
|
||||
}
|
||||
|
||||
static ssize_t brotli_compress(const char *src, size_t src_len,
|
||||
char *dst, size_t dst_cap) {
|
||||
size_t encoded_size = dst_cap;
|
||||
BROTLI_BOOL ok = BrotliEncoderCompress(
|
||||
BROTLI_DEFAULT_QUALITY,
|
||||
BROTLI_DEFAULT_WINDOW,
|
||||
BROTLI_MODE_GENERIC,
|
||||
src_len,
|
||||
(const uint8_t *)src,
|
||||
&encoded_size,
|
||||
(uint8_t *)dst
|
||||
);
|
||||
if (!ok) return -1;
|
||||
if (encoded_size >= src_len * 0.95) return 0;
|
||||
return (ssize_t)encoded_size;
|
||||
}
|
||||
|
||||
static http2_session_t *g_sessions[MAX_HTTP2_SESSIONS];
|
||||
static int g_session_count = 0;
|
||||
|
||||
@ -463,19 +516,26 @@ static ssize_t http2_data_source_read_callback(
|
||||
nghttp2_data_source *source,
|
||||
void *user_data __attribute__((unused))) {
|
||||
|
||||
if (source->fd < 0) {
|
||||
http2_stream_data_t *stream = (http2_stream_data_t *)source->ptr;
|
||||
if (!stream || !stream->response_body) {
|
||||
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||||
return 0;
|
||||
}
|
||||
|
||||
ssize_t n = read(source->fd, buf, length);
|
||||
if (n < 0) {
|
||||
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
|
||||
size_t remaining = stream->response_len - stream->response_sent;
|
||||
if (remaining == 0) {
|
||||
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||||
return 0;
|
||||
}
|
||||
if (n == 0) {
|
||||
|
||||
size_t to_send = length < remaining ? length : remaining;
|
||||
memcpy(buf, stream->response_body + stream->response_sent, to_send);
|
||||
stream->response_sent += to_send;
|
||||
|
||||
if (stream->response_sent >= stream->response_len) {
|
||||
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||||
}
|
||||
return (ssize_t)n;
|
||||
return (ssize_t)to_send;
|
||||
}
|
||||
|
||||
static void http2_serve_static(http2_session_t *h2, http2_stream_data_t *stream) {
|
||||
@ -593,6 +653,72 @@ static void http2_serve_static(http2_session_t *h2, http2_stream_data_t *stream)
|
||||
return;
|
||||
}
|
||||
|
||||
/* 读取文件内容到内存 */
|
||||
int64_t file_size = st.st_size;
|
||||
char *file_buf = (char *)malloc((size_t)file_size);
|
||||
if (!file_buf) {
|
||||
close(file_fd);
|
||||
nghttp2_nv hdrs[] = {
|
||||
{(uint8_t *)":status", (uint8_t *)"500", 7, 3, 0}};
|
||||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
ssize_t read_total = 0;
|
||||
while (read_total < file_size) {
|
||||
ssize_t n = read(file_fd, file_buf + read_total, (size_t)(file_size - read_total));
|
||||
if (n <= 0) break;
|
||||
read_total += n;
|
||||
}
|
||||
close(file_fd);
|
||||
|
||||
if (read_total != file_size) {
|
||||
free(file_buf);
|
||||
nghttp2_nv hdrs[] = {
|
||||
{(uint8_t *)":status", (uint8_t *)"500", 7, 3, 0}};
|
||||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 判断是否需要压缩 */
|
||||
bool use_gzip = false;
|
||||
bool use_brotli = false;
|
||||
const char *mime = http_mime_type(real_path);
|
||||
|
||||
if (stream->request.method != HTTP_HEAD && is_compressible_mime(mime) && file_size > 256) {
|
||||
char *compress_buf = (char *)malloc((size_t)file_size);
|
||||
if (compress_buf) {
|
||||
if (h2->brotli_enabled) {
|
||||
ssize_t cl = brotli_compress(file_buf, (size_t)file_size, compress_buf, (size_t)file_size);
|
||||
if (cl > 0) {
|
||||
free(file_buf);
|
||||
file_buf = compress_buf;
|
||||
file_size = cl;
|
||||
use_brotli = true;
|
||||
} else {
|
||||
free(compress_buf);
|
||||
}
|
||||
} else if (h2->gzip_enabled) {
|
||||
ssize_t cl = gzip_compress(file_buf, (size_t)file_size, compress_buf, (size_t)file_size);
|
||||
if (cl > 0) {
|
||||
free(file_buf);
|
||||
file_buf = compress_buf;
|
||||
file_size = cl;
|
||||
use_gzip = true;
|
||||
} else {
|
||||
free(compress_buf);
|
||||
}
|
||||
} else {
|
||||
free(compress_buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 存储到流 */
|
||||
stream->response_body = file_buf;
|
||||
stream->response_len = (size_t)file_size;
|
||||
stream->response_sent = 0;
|
||||
|
||||
/* 构建响应头 */
|
||||
nghttp2_nv hdrs[16];
|
||||
int num_hdrs = 0;
|
||||
@ -600,15 +726,22 @@ static void http2_serve_static(http2_session_t *h2, http2_stream_data_t *stream)
|
||||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||||
(uint8_t *)":status", (uint8_t *)"200", 7, 3, 0};
|
||||
|
||||
const char *mime = http_mime_type(real_path);
|
||||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||||
(uint8_t *)"content-type", (uint8_t *)mime, 12, strlen(mime), 0};
|
||||
|
||||
char content_length_str[32];
|
||||
snprintf(content_length_str, sizeof(content_length_str), "%ld", (long)st.st_size);
|
||||
snprintf(content_length_str, sizeof(content_length_str), "%ld", (long)file_size);
|
||||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||||
(uint8_t *)"content-length", (uint8_t *)content_length_str, 14, strlen(content_length_str), 0};
|
||||
|
||||
if (use_brotli) {
|
||||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||||
(uint8_t *)"content-encoding", (uint8_t *)"br", 16, 2, 0};
|
||||
} else if (use_gzip) {
|
||||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||||
(uint8_t *)"content-encoding", (uint8_t *)"gzip", 16, 4, 0};
|
||||
}
|
||||
|
||||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||||
(uint8_t *)"etag", (uint8_t *)etag, 4, strlen(etag), 0};
|
||||
|
||||
@ -620,14 +753,15 @@ static void http2_serve_static(http2_session_t *h2, http2_stream_data_t *stream)
|
||||
|
||||
/* HEAD 请求:不发送 body */
|
||||
if (stream->request.method == HTTP_HEAD) {
|
||||
close(file_fd);
|
||||
free(stream->response_body);
|
||||
stream->response_body = NULL;
|
||||
stream->response_len = 0;
|
||||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, NULL);
|
||||
} else {
|
||||
nghttp2_data_provider provider;
|
||||
provider.source.fd = file_fd;
|
||||
provider.source.ptr = stream;
|
||||
provider.read_callback = http2_data_source_read_callback;
|
||||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, &provider);
|
||||
stream->file_fd = file_fd;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -147,7 +147,7 @@ assert_body_contains() {
|
||||
local expect="$2"
|
||||
local desc="${3:-$url}"
|
||||
local body
|
||||
body=$(curl -s -k "$url")
|
||||
body=$(curl -s -k --compressed "$url")
|
||||
if echo "$body" | grep -q "$expect"; then
|
||||
echo " ✓ $desc — 响应体包含 '$expect'"
|
||||
pass
|
||||
@ -316,6 +316,48 @@ assert_post_form() {
|
||||
fi
|
||||
}
|
||||
|
||||
assert_http2_brotli() {
|
||||
local url="$1"
|
||||
local desc="${2:-$url}"
|
||||
local encoding
|
||||
encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||||
if echo "$encoding" | grep -qi "br"; then
|
||||
echo " ✓ $desc — HTTP/2 返回 brotli 压缩"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — HTTP/2 期望 brotli 压缩, 实际: $encoding"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_http2_gzip() {
|
||||
local url="$1"
|
||||
local desc="${2:-$url}"
|
||||
local encoding
|
||||
encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||||
if echo "$encoding" | grep -qi "gzip"; then
|
||||
echo " ✓ $desc — HTTP/2 返回 gzip 压缩"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — HTTP/2 期望 gzip 压缩, 实际: $encoding"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_http2_not_compressed() {
|
||||
local url="$1"
|
||||
local desc="${2:-$url}"
|
||||
local encoding
|
||||
encoding=$(curl --http2 -k -s -D - -o /dev/null "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||||
if [[ -z "$encoding" ]]; then
|
||||
echo " ✓ $desc — HTTP/2 无压缩"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — HTTP/2 期望无压缩, 实际: $encoding"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_status_405() {
|
||||
local url="$1"
|
||||
local desc="${2:-$url}"
|
||||
@ -484,7 +526,7 @@ else
|
||||
fi
|
||||
|
||||
# HTTP/2 响应体
|
||||
h2_body=$(curl --http2 -k -s "https://$HOST/")
|
||||
h2_body=$(curl --http2 -k -s --compressed "https://$HOST/")
|
||||
if echo "$h2_body" | grep -q "Cocoon"; then
|
||||
echo " ✓ HTTP/2 响应体 — 包含 'Cocoon'"
|
||||
pass
|
||||
@ -526,6 +568,12 @@ if [[ -n "$H2_ETAG" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# HTTP/2 压缩测试
|
||||
assert_http2_brotli "https://$HOST/index.html" "HTTP/2 HTML brotli"
|
||||
assert_http2_brotli "https://$HOST/style.css" "HTTP/2 CSS brotli"
|
||||
assert_http2_brotli "https://$HOST/app.js" "HTTP/2 JS brotli"
|
||||
assert_http2_not_compressed "https://$HOST/image.png" "HTTP/2 图片不压缩"
|
||||
|
||||
# 停止 HTTP/2 服务器,恢复 HTTP 服务器
|
||||
kill_server
|
||||
sleep 1
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user