Files
C-exp-collection/server-exp4/webserver.c
2026-07-18 16:39:01 +08:00

1150 lines
40 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* webserver.c — 实验4: 高性能Web服务器
*
* ======================== 实现特性 ========================
* 1. 基于 epoll 的 I/O 多路复用模型
* 2. 线程池 (Thread Pool) 并发处理
* 3. LRU 页面缓存 (Web Cache)
* 4. 支持 GET 和 POST 方法
* 5. 支持 CGI 动态内容
* 6. 详细的性能统计与日志
*
* ======================== 架构设计 ========================
*
* ┌─────────────────────────────────────────┐
* │ Main Thread (epoll) │
* │ ┌─────┐ ┌──────────┐ ┌───────────┐ │
* │ │accept│→│epoll_wait│→│read req │ │
* │ └─────┘ └──────────┘ └────┬──────┘ │
* └───────────────────────────────┼─────────┘
* │ dispatch
* ┌──────────────┼──────────────┐
* ▼ ▼ ▼
* ┌──────────┐ ┌──────────┐ ┌──────────┐
* │ Worker 1 │ │ Worker 2 │ │ Worker N │ Thread Pool
* └────┬─────┘ └────┬─────┘ └────┬─────┘
* │ │ │
* ┌────┴────────────┴────────────┴────┐
* │ LRU Cache │
* │ (Hash Table + Doubly Linked List) │
* └────────────────────────────────────┘
* │
* ┌─────┴─────┐
* │ File I/O │ (cache miss)
* └───────────┘
*
* ======================== epoll 事件循环 ========================
*
* 使用边缘触发 (EPOLLET) 模式:
* - listen socket: 接受新连接,设置非阻塞,加入 epoll
* - client socket: 读取 HTTP 请求头,读取完成后提交到线程池
*
* ======================== 缓存 LRU 淘汰策略 ========================
*
* 哈希表 (DJB2) + 双向链表:
* - O(1) 查找: URL → 哈希桶 → 冲突链遍历
* - O(1) 淘汰: 移除链表尾部 (LRU)
* - O(1) 更新: 命中节点移到头部 (MRU)
*
* ======================== 编译与运行 ========================
*
* 编译: make
* 运行: ./webserver <端口> <网站根目录> [线程数] [缓存大小]
* 示例: ./webserver 8088 . 4 128
* 测试: ab -n 10000 -c 100 http://localhost:8088/index.html
* wrk -t4 -c100 -d30s http://localhost:8088/index.html
*/
#include "common.h"
#include "thpool.h"
#include "cache.h"
#include <sys/uio.h> /* writev */
/* ======================== 常量定义 ======================== */
#define VERSION 40 /* 服务器版本号 (实验4) */
#define BUFSIZE 8192 /* 缓冲区大小 */
#define MAX_EVENTS 1024 /* epoll 最大事件数 */
#define DEFAULT_THREADS 4 /* 默认线程数 */
#define DEFAULT_CACHE_SIZE 128 /* 默认缓存条目数 */
/* 日志类型 */
#define ERROR 42
#define LOG 44
#define FORBIDDEN 403
#define NOTFOUND 404
/* HTTP 请求解析状态 */
#define REQ_STATE_METHOD 0
#define REQ_STATE_URI 1
#define REQ_STATE_VERSION 2
#define REQ_STATE_HEADER 3
#define REQ_STATE_BODY 4
#define REQ_STATE_DONE 5
#ifndef SIGCLD
#define SIGCLD SIGCHLD
#endif
/* ======================== 数据结构 ======================== */
/* 支持的文件扩展名及其对应的MIME类型 */
struct {
char *ext;
char *filetype;
} extensions[] = {
{"gif", "image/gif" },
{"jpg", "image/jpg" },
{"jpeg", "image/jpeg" },
{"png", "image/png" },
{"ico", "image/ico" },
{"zip", "image/zip" },
{"gz", "image/gz" },
{"tar", "image/tar" },
{"htm", "text/html" },
{"html", "text/html" },
{"css", "text/css" },
{"js", "text/javascript"},
{"txt", "text/plain" },
{0, 0}
};
/* 请求参数 (提交给线程池的任务) */
typedef struct {
int fd; /* 客户端 socket */
int hit; /* 请求编号 */
char method[16]; /* GET / POST */
char uri[512]; /* 请求 URI */
char version[16]; /* HTTP 版本 */
char headers[2048]; /* 请求头 */
char body[BUFSIZE]; /* POST 请求体 */
int body_len; /* 请求体长度 */
struct sockaddr_in addr; /* 客户端地址 */
} request_t;
/* 全局缓存指针 */
static cache_t *web_cache = NULL;
static threadpool thpool = NULL;
/* 线程池工作线程的 thread-local ID定义在 thpool.c */
extern __thread int thpool_worker_id;
/* 每个线程的请求处理计数 */
#define MAX_THREADS 64
volatile int thread_req_count[MAX_THREADS];
volatile int thread_cgi_count[MAX_THREADS];
volatile int thread_file_count[MAX_THREADS];
static int g_nthreads = 0;
/* 服务器运行标志 */
static volatile int server_running = 1;
static int listen_fd = -1;
/* ======================== 函数声明 ======================== */
void ctrlc_handler(int sig);
long current_time_ms(void);
void logger(int type, const char *s1, const char *s2, int socket_fd);
int set_nonblocking(int fd);
void get_filetype(const char *filename, char *filetype);
int parse_request(const char *raw, int raw_len, request_t *req);
int read_http_request(int fd, char *buf, int *buf_len);
void process_request(void *arg);
void send_response(int fd, int status, const char *content_type,
const char *body, long body_len);
void send_file_response(int fd, const char *filetype,
const char *data, long size, int head_only, int cache_hit);
void send_error(int fd, int status, const char *shortmsg,
const char *description);
void handle_get(int fd, int hit, const char *uri, int head_only);
void handle_post(int fd, int hit, request_t *req);
/* ======================== 工具函数 ======================== */
/**
* ctrlc_handler — 优雅关闭服务器
*/
void ctrlc_handler(int sig)
{
(void)sig;
printf("\n[Ctrl+C] 正在关闭服务器...\n");
server_running = 0;
if (listen_fd >= 0) {
shutdown(listen_fd, SHUT_RDWR);
close(listen_fd);
}
}
/**
* current_time_ms — 获取当前毫秒级时间戳
*/
long current_time_ms(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
/**
* set_nonblocking — 设置文件描述符为非阻塞模式
*/
int set_nonblocking(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
/**
* get_filetype — 根据文件扩展名获取MIME类型
*/
void get_filetype(const char *filename, char *filetype)
{
strcpy(filetype, "text/html"); /* 默认类型 */
int i, len;
for (i = 0; extensions[i].ext != 0; i++) {
len = strlen(extensions[i].ext);
int flen = strlen(filename);
if (flen >= len &&
!strcasecmp(&filename[flen - len], extensions[i].ext)) {
strcpy(filetype, extensions[i].filetype);
return;
}
}
}
/**
* logger — 日志记录函数
* 记录请求处理过程中的各类事件
*/
void logger(int type, const char *s1, const char *s2, int socket_fd)
{
#ifdef DEBUG
int fd;
char logbuffer[BUFSIZE * 2];
char timebuffer[64];
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm *tm_info = localtime(&tv.tv_sec);
strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", tm_info);
char full_timebuffer[256];
snprintf(full_timebuffer, sizeof(full_timebuffer),
"[%s.%03ld]", timebuffer, tv.tv_usec / 1000);
switch (type) {
case ERROR:
snprintf(logbuffer, sizeof(logbuffer),
"%s [ERROR] %s:%s Errno=%d pid=%d",
full_timebuffer, s1, s2, errno, getpid());
break;
case FORBIDDEN:
snprintf(logbuffer, sizeof(logbuffer),
"%s [FORBIDDEN] %s:%s", full_timebuffer, s1, s2);
break;
case NOTFOUND:
snprintf(logbuffer, sizeof(logbuffer),
"%s [NOT FOUND] %s:%s", full_timebuffer, s1, s2);
break;
case LOG:
snprintf(logbuffer, sizeof(logbuffer),
"%s [INFO] %s:%s:%d", full_timebuffer, s1, s2, socket_fd);
break;
}
if ((fd = open("webserver.log", O_CREAT | O_WRONLY | O_APPEND, 0644)) >= 0) {
if (write(fd, logbuffer, strlen(logbuffer)) < 0) {}
if (write(fd, "\n", 1) < 0) {}
close(fd);
}
#else
(void)type; (void)s1; (void)s2; (void)socket_fd;
#endif
}
/* ======================== HTTP 请求解析 ======================== */
/**
* parse_request — 解析原始 HTTP 请求
* @raw: 原始请求数据
* @raw_len: 数据长度
* @req: 输出解析后的请求结构
* @return: 0=成功, -1=格式错误
*/
int parse_request(const char *raw, int raw_len, request_t *req)
{
char buf[BUFSIZE];
int buf_len = raw_len < BUFSIZE - 1 ? raw_len : BUFSIZE - 1;
memcpy(buf, raw, buf_len);
buf[buf_len] = '\0';
/* 解析请求行: METHOD URI VERSION */
char *line_start = buf;
char *line_end = strstr(buf, "\r\n");
if (line_end == NULL) return -1;
*line_end = '\0';
char *saveptr;
char *method = strtok_r(line_start, " ", &saveptr);
char *uri = strtok_r(NULL, " ", &saveptr);
char *ver = strtok_r(NULL, " ", &saveptr);
if (method == NULL || uri == NULL) return -1;
strncpy(req->method, method, sizeof(req->method) - 1);
strncpy(req->uri, uri, sizeof(req->uri) - 1);
if (ver)
strncpy(req->version, ver, sizeof(req->version) - 1);
else
strcpy(req->version, "HTTP/1.1");
/* 解析请求头 */
req->headers[0] = '\0';
char *hdr_start = line_end + 2; /* 跳过 \r\n */
char *body_sep = strstr(hdr_start, "\r\n\r\n");
if (body_sep) {
int hdr_len = body_sep - hdr_start;
if (hdr_len > 0 && hdr_len < (int)sizeof(req->headers) - 1) {
memcpy(req->headers, hdr_start, hdr_len);
req->headers[hdr_len] = '\0';
}
/* 解析 POST 请求体 */
if (strcasecmp(method, "POST") == 0) {
char *body_start = body_sep + 4;
int remaining = raw_len - (body_start - buf);
if (remaining > 0) {
req->body_len = remaining < BUFSIZE - 1
? remaining : BUFSIZE - 1;
memcpy(req->body, body_start, req->body_len);
req->body[req->body_len] = '\0';
} else {
req->body_len = 0;
req->body[0] = '\0';
}
}
}
return 0;
}
/**
* read_http_request — 从 socket 非阻塞读取 HTTP 请求
* 持续读取直到收到完整的 \r\n\r\n 或缓冲区满
* 对于 POST 请求,还读取 Content-Length 指定的请求体
* @return: 0=请求完整读取, -1=数据不完整或错误
*/
int read_http_request(int fd, char *buf, int *buf_len)
{
int total = *buf_len;
int n;
/* 读取请求头和请求体 */
while (total < BUFSIZE - 1) {
n = recv(fd, buf + total, BUFSIZE - 1 - total, 0);
if (n > 0) {
total += n;
buf[total] = '\0';
/* 检查是否收到完整请求头 */
char *header_end = strstr(buf, "\r\n\r\n");
if (header_end == NULL) {
/* 还没收到完整请求头,继续读取 */
if (total >= BUFSIZE - 1) break; /* 缓冲区满 */
continue;
}
/* 对于 GET 请求: 请求头完整即请求完成 */
if (strncmp(buf, "GET ", 4) == 0) {
*buf_len = total;
return 0;
}
/* 对于 POST 请求: 需要读取 Content-Length 指定的请求体 */
if (strncmp(buf, "POST ", 5) == 0) {
/* 查找 Content-Length */
char *cl = strcasestr(buf, "Content-Length:");
int expected_body = 0;
if (cl) {
cl += 15; /* 跳过 "Content-Length:" */
while (*cl == ' ') cl++;
expected_body = atoi(cl);
}
/* 计算已收到的请求体长度 */
int header_len = (header_end + 4) - buf; /* 包括 \r\n\r\n */
int received_body = total - header_len;
if (received_body >= expected_body) {
*buf_len = total;
return 0;
}
/* 否则继续读取请求体 */
}
} else if (n == 0) {
/* 连接关闭 */
return -1;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* 没有更多数据可读,返回当前已读数据 */
*buf_len = total;
return (total > 0) ? 1 : -1; /* 1=可能有更多数据 */
}
return -1; /* 真正的错误 */
}
}
*buf_len = total;
return 0;
}
/* ======================== HTTP 响应发送 ======================== */
/**
* send_response — 发送通用 HTTP 响应
*/
void send_response(int fd, int status, const char *content_type,
const char *body, long body_len)
{
char header[BUFSIZE];
const char *status_str;
switch (status) {
case 200: status_str = "200 OK"; break;
case 403: status_str = "403 Forbidden"; break;
case 404: status_str = "404 Not Found"; break;
case 405: status_str = "405 Method Not Allowed"; break;
case 500: status_str = "500 Internal Server Error"; break;
default: status_str = "500 Internal Server Error"; break;
}
int hdr_len = snprintf(header, sizeof(header),
"HTTP/1.1 %s\r\n"
"Server: Experiment4-WebServer/%d.0\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n"
"Content-Type: %s\r\n"
"X-Cache: MISS\r\n"
"Cache-Control: max-age=3600\r\n"
"\r\n",
status_str, VERSION, body_len, content_type);
/* 发送响应头和内容 (使用 write 而非 rio_writen因为已在阻塞模式) */
if (write(fd, header, hdr_len) < 0) {
logger(ERROR, "Failed to write response header", "", fd);
return;
}
if (body && body_len > 0) {
if (write(fd, body, body_len) < 0) {
logger(ERROR, "Failed to write response body", "", fd);
}
}
}
/**
* send_file_response — 发送文件响应 (用于缓存命中场景)
*/
void send_file_response(int fd, const char *filetype,
const char *data, long size, int head_only, int cache_hit)
{
char header[BUFSIZE];
int hdr_len = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Server: Experiment4-WebServer/%d.0\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n"
"Content-Type: %s\r\n"
"X-Cache: %s\r\n"
"Cache-Control: max-age=3600\r\n"
"\r\n",
VERSION, size, filetype, cache_hit ? "HIT" : "MISS");
/* HEAD请求: 只发送头,不发送体 */
if (head_only) {
if (write(fd, header, hdr_len) < 0) {
logger(ERROR, "Failed to write HEAD response", "", fd);
}
return;
}
/* GET请求: 使用 writev 合并发送头部和数据,减少系统调用 */
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = hdr_len;
iov[1].iov_base = (void *)data;
iov[1].iov_len = size;
if (writev(fd, iov, 2) < 0) {
logger(ERROR, "Failed to write file response", "", fd);
}
}
/**
* send_error — 发送错误响应
*/
void send_error(int fd, int status, const char *shortmsg,
const char *description)
{
char body[1024];
snprintf(body, sizeof(body),
"<!DOCTYPE html>\n"
"<html><head>\n"
"<meta charset=\"UTF-8\">\n"
"<title>%d %s</title>\n"
"<style>body{font-family:Arial;margin:40px;}"
"h1{color:#c00;}p{color:#666;}</style>\n"
"</head><body>\n"
"<h1>%d %s</h1>\n"
"<p>%s</p>\n"
"<hr><em>Experiment4-WebServer v%d.0</em>\n"
"</body></html>\n",
status, shortmsg, status, shortmsg, description, VERSION);
send_response(fd, status, "text/html; charset=UTF-8", body, strlen(body));
}
/* ======================== GET 请求处理 ======================== */
/**
* handle_get — 处理 GET 请求
*
* 流程:
* 1. 安全检查 (路径遍历)
* 2. 缓存查找
* 3. 缓存命中 → 直接返回
* 4. 缓存未命中 → 读取文件 → 插入缓存 → 返回
*/
void handle_get(int fd, int hit, const char *uri, int head_only)
{
logger(LOG, "GET request", uri, hit);
/* 安全检查: 防止路径遍历攻击 */
if (strstr(uri, "..") != NULL) {
send_error(fd, 403, "Forbidden",
"Parent directory traversal not allowed");
return;
}
/* 构造文件路径 */
char filename[512];
if (strcmp(uri, "/") == 0) {
strcpy(filename, "index.html");
} else {
/* 移除开头的 / */
const char *path = (*uri == '/') ? uri + 1 : uri;
/* 复制路径部分(遇到 ? 截止,去掉查询字符串) */
int i;
for (i = 0; i < (int)sizeof(filename) - 1 && path[i] && path[i] != '?'; i++)
filename[i] = path[i];
filename[i] = '\0';
}
/* 获取 MIME 类型 */
char filetype[64];
get_filetype(filename, filetype);
/* ---- CGI 处理(不缓存,直接执行) ---- */
if (strstr(uri, "cgi-bin") != NULL) {
char cgiargs[256] = "";
char *q = strchr(uri, '?');
if (q) strncpy(cgiargs, q + 1, sizeof(cgiargs) - 1);
/* 检查 CGI 程序是否存在且可执行 */
if (access(filename, X_OK) != 0) {
send_error(fd, 404, "Not Found",
"CGI program not found or not executable");
return;
}
/* 发送响应状态行和服务器头 (不含 \r\n\r\n 结尾,由 CGI 补全) */
char header[256];
snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Server: Experiment4-WebServer/%d.0\r\n"
"Connection: close\r\n",
VERSION);
write(fd, header, strlen(header));
/* 执行 CGI */
int pfd[2];
if (pipe(pfd) < 0) {
send_error(fd, 500, "Internal Server Error",
"Pipe creation failed");
return;
}
pid_t pid = fork();
if (pid < 0) {
close(pfd[0]); close(pfd[1]);
send_error(fd, 500, "Internal Server Error",
"Fork failed");
return;
}
if (pid == 0) {
/* 子进程: 执行 CGI 程序 */
close(pfd[1]);
dup2(pfd[0], STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
close(pfd[0]);
if (strlen(cgiargs) > 0)
setenv("QUERY_STRING", cgiargs, 1);
setenv("REQUEST_METHOD", "GET", 1);
char *emptylist[] = { NULL };
execve(filename, emptylist, environ);
/* execve 失败 — 发送错误信息 */
printf("Content-Type: text/plain; charset=UTF-8\r\n\r\n");
printf("Error: CGI program '%s' execution failed.\n",
filename);
fflush(stdout);
_exit(1);
}
/* 父进程: 向 CGI 传递参数,然后等待 */
close(pfd[0]);
if (strlen(cgiargs) > 0)
write(pfd[1], cgiargs, strlen(cgiargs) + 1);
close(pfd[1]); /* 先关闭写端,再等待子进程,避免死锁 */
waitpid(pid, NULL, 0);
logger(LOG, "CGI executed", uri, hit);
if (thpool_worker_id >= 0 && thpool_worker_id < MAX_THREADS)
thread_cgi_count[thpool_worker_id]++;
return;
}
/* ---- 步骤1: 尝试从缓存获取 ---- */
char *cached_data = NULL;
long cached_size = 0;
if (cache_lookup(web_cache, uri, &cached_data, &cached_size, filetype)) {
/* 缓存命中! */
#ifdef DEBUG
printf("[Cache HIT] %s (%ld bytes) for request #%d\n",
uri, cached_size, hit);
#endif
send_file_response(fd, filetype, cached_data, cached_size, head_only, 1); /* cache_hit=1 */
free(cached_data);
logger(LOG, "Cache HIT served", uri, hit);
if (thpool_worker_id >= 0 && thpool_worker_id < MAX_THREADS)
thread_file_count[thpool_worker_id]++;
return;
}
/* ---- 步骤2: 缓存未命中,读取文件 ---- */
#ifdef DEBUG
printf("[Cache MISS] %s for request #%d\n", uri, hit);
#endif
/* 打开文件 */
int file_fd = open(filename, O_RDONLY);
if (file_fd == -1) {
send_error(fd, 404, "Not Found",
"The requested file was not found on this server");
return;
}
/* 读取文件内容 */
long filesize = (long)lseek(file_fd, 0, SEEK_END);
lseek(file_fd, 0, SEEK_SET);
char *filebuf = (char *)malloc(filesize + 1);
if (filebuf == NULL) {
close(file_fd);
send_error(fd, 500, "Internal Server Error",
"Memory allocation failed");
return;
}
ssize_t total_read = 0;
while (total_read < filesize) {
ssize_t n = read(file_fd, filebuf + total_read,
filesize - total_read);
if (n <= 0) break;
total_read += n;
}
close(file_fd);
filebuf[total_read] = '\0';
/* ---- 步骤3: 插入缓存 ---- */
cache_insert(web_cache, uri, filebuf, filesize, filetype);
/* ---- 步骤4: 发送响应 ---- */
send_file_response(fd, filetype, filebuf, filesize, head_only, 0); /* cache_hit=0 */
/* 注意: filebuf 不需要 free 因为数据已缓存在 cache 中...
但实际上这里的 filebuf 和 cache 中的 data 是两份拷贝。
所以需要释放 filebuf。*/
free(filebuf);
logger(LOG, "File served (cache MISS)", uri, hit);
if (thpool_worker_id >= 0 && thpool_worker_id < MAX_THREADS)
thread_file_count[thpool_worker_id]++;
}
/* ======================== POST 请求处理 ======================== */
/**
* parse_post_form — 解析 POST 表单数据 (application/x-www-form-urlencoded)
*/
static void parse_post_form(const char *body, int body_len,
char *result, int result_size)
{
result[0] = '\0';
/* URL 解码并格式化显示 */
char decoded[BUFSIZE];
int j = 0;
for (int i = 0; i < body_len && i < BUFSIZE - 1; i++) {
if (body[i] == '+') {
decoded[j++] = ' ';
} else if (body[i] == '%' && i + 2 < body_len) {
char hex[3] = {body[i+1], body[i+2], '\0'};
decoded[j++] = (char)strtol(hex, NULL, 16);
i += 2;
} else if (body[i] == '&') {
decoded[j++] = '\n';
} else {
decoded[j++] = body[i];
}
}
decoded[j] = '\0';
snprintf(result, result_size,
"<!DOCTYPE html>\n"
"<html><head>\n"
"<meta charset=\"UTF-8\">\n"
"<title>POST Request Received</title>\n"
"<style>body{font-family:Arial;margin:40px;}"
"h1{color:#2a7;}pre{background:#f5f5f5;padding:15px;"
"border-radius:5px;border:1px solid #ddd;}</style>\n"
"</head><body>\n"
"<h1>✓ POST Request Processed Successfully</h1>\n"
"<h2>Form Data Received:</h2>\n"
"<pre>%s</pre>\n"
"<h2>Raw Body (%d bytes):</h2>\n"
"<pre>%.*s</pre>\n"
"<p><a href=\"/\">← Back to Index</a></p>\n"
"<hr><em>Experiment4-WebServer v%d.0</em>\n"
"</body></html>\n",
decoded, body_len, body_len, body, VERSION);
}
/**
* handle_post — 处理 POST 请求
*
* 支持:
* - application/x-www-form-urlencoded (表单提交)
* - multipart/form-data (文件上传,基础支持)
* - CGI 转发到 cgi-bin 目录下的程序
*/
void handle_post(int fd, int hit, request_t *req)
{
logger(LOG, "POST request", req->uri, hit);
#ifdef DEBUG
printf("[POST] #%d %s (body: %d bytes)\n",
hit, req->uri, req->body_len);
#endif
/* CGI 转发 */
if (strstr(req->uri, "cgi-bin") != NULL) {
char filename[512];
/* 构造 CGI 程序路径 (去掉查询字符串) */
const char *path = req->uri;
if (*path == '/') path++;
int i;
for (i = 0; i < (int)sizeof(filename) - 1 && path[i] && path[i] != '?'; i++)
filename[i] = path[i];
filename[i] = '\0';
/* 检查 CGI 程序是否存在且可执行 */
if (access(filename, X_OK) != 0) {
send_error(fd, 404, "Not Found",
"CGI program not found or not executable");
return;
}
/* 发送响应状态行和服务器头 (不含 \r\n\r\n 结尾,由 CGI 补全) */
char header[256];
snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Server: Experiment4-WebServer/%d.0\r\n"
"Connection: close\r\n",
VERSION);
write(fd, header, strlen(header));
int pfd[2];
if (pipe(pfd) < 0) {
send_error(fd, 500, "Internal Server Error",
"Pipe creation failed");
return;
}
pid_t pid = fork();
if (pid < 0) {
close(pfd[0]); close(pfd[1]);
send_error(fd, 500, "Internal Server Error",
"Fork failed");
return;
}
if (pid == 0) {
/* 子进程: 执行 CGI 程序 */
close(pfd[1]);
dup2(pfd[0], STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
close(pfd[0]);
setenv("REQUEST_METHOD", "POST", 1);
char cl_buf[32];
snprintf(cl_buf, sizeof(cl_buf), "%d", req->body_len);
setenv("CONTENT_LENGTH", cl_buf, 1);
char *emptylist[] = { NULL };
execve(filename, emptylist, environ);
/* execve 失败 */
printf("Content-Type: text/plain; charset=UTF-8\r\n\r\n");
printf("Error: CGI program '%s' execution failed.\n",
filename);
fflush(stdout);
_exit(1);
}
/* 父进程 */
close(pfd[0]);
if (req->body_len > 0)
write(pfd[1], req->body, req->body_len);
close(pfd[1]); /* 先关闭写端,再等待子进程,避免死锁 */
waitpid(pid, NULL, 0);
if (thpool_worker_id >= 0 && thpool_worker_id < MAX_THREADS)
thread_cgi_count[thpool_worker_id]++;
return;
}
/* 处理普通 POST 请求: 解析并显示表单数据 */
char response_body[BUFSIZE * 2];
parse_post_form(req->body, req->body_len,
response_body, sizeof(response_body));
send_response(fd, 200, "text/html; charset=UTF-8",
response_body, strlen(response_body));
}
/* ======================== 线程池工作函数 ======================== */
/**
* process_request — 线程池工作函数
* 解析请求并根据方法 (GET/POST) 分发处理
*/
void process_request(void *arg)
{
request_t *req = (request_t *)arg;
int fd = req->fd;
int hit = req->hit;
int tid = thpool_worker_id; /* 0-based 线程编号 */
/* 更新线程请求计数 */
if (tid >= 0 && tid < MAX_THREADS)
thread_req_count[tid]++;
#ifdef DEBUG
printf("[线程 %d] 处理请求 #%d: %s %s\n",
tid, hit, req->method, req->uri);
#endif
logger(LOG, "Processing request", req->uri, hit);
/* 支持 GET、POST 和 HEAD 方法 */
if (strcasecmp(req->method, "GET") == 0) {
handle_get(fd, hit, req->uri, 0); /* head_only=0 */
} else if (strcasecmp(req->method, "HEAD") == 0) {
handle_get(fd, hit, req->uri, 1); /* head_only=1, 只发响应头 */
} else if (strcasecmp(req->method, "POST") == 0) {
handle_post(fd, hit, req);
} else {
char desc[128];
snprintf(desc, sizeof(desc),
"Method '%s' is not supported. Only GET, POST and HEAD are allowed.",
req->method);
send_error(fd, 405, "Method Not Allowed", desc);
}
/* 清理 */
close(fd);
free(req);
}
/* ======================== 主函数 ======================== */
int main(int argc, char **argv)
{
int i, port, nthreads, cache_size;
int epoll_fd;
struct sockaddr_in cli_addr, serv_addr;
socklen_t cli_len;
/* 解析命令行参数 */
if (argc < 3 || !strcmp(argv[1], "-?") || !strcmp(argv[1], "-h")) {
printf("实验4 高性能Web服务器 v%d.0\n\n", VERSION);
printf("特性: epoll I/O多路复用 | 线程池 | LRU缓存 | GET/POST\n\n");
printf("用法: %s <端口> <网站根目录> [线程数] [缓存大小]\n", argv[0]);
printf("示例: %s 8088 . 4 128\n\n", argv[0]);
printf("测试: ab -n 10000 -c 100 http://localhost:8088/index.html\n");
printf(" wrk -t4 -c100 -d30s http://localhost:8088/index.html\n");
for (i = 0; extensions[i].ext != 0; i++)
printf(" %s", extensions[i].ext);
printf("\n");
exit(0);
}
port = atoi(argv[1]);
nthreads = (argc >= 4) ? atoi(argv[3]) : DEFAULT_THREADS;
cache_size = (argc >= 5) ? atoi(argv[4]) : DEFAULT_CACHE_SIZE;
g_nthreads = nthreads;
/* 初始化线程计数器 */
for (i = 0; i < MAX_THREADS; i++) {
thread_req_count[i] = 0;
thread_cgi_count[i] = 0;
thread_file_count[i] = 0;
}
/* 安全目录检查 */
if (!strncmp(argv[2], "/", 2) || !strncmp(argv[2], "/etc", 5) ||
!strncmp(argv[2], "/bin", 5) || !strncmp(argv[2], "/lib", 5) ||
!strncmp(argv[2], "/tmp", 5) || !strncmp(argv[2], "/usr", 5) ||
!strncmp(argv[2], "/dev", 5) || !strncmp(argv[2], "/sbin", 6)) {
printf("ERROR: 不允许使用系统目录 %s\n", argv[2]);
exit(3);
}
if (chdir(argv[2]) == -1) {
perror("切换目录失败");
exit(4);
}
/* 设置信号处理 */
if (signal(SIGINT, ctrlc_handler) == SIG_ERR)
printf("警告: 无法设置 SIGINT 信号处理\n");
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
printf("警告: 无法忽略 SIGPIPE 信号\n");
/* ---- 初始化缓存 ---- */
web_cache = cache_init(cache_size);
if (web_cache == NULL) {
fprintf(stderr, "缓存初始化失败\n");
exit(5);
}
/* ---- 初始化线程池 ---- */
thpool = thpool_init(nthreads);
if (thpool == NULL) {
fprintf(stderr, "线程池初始化失败\n");
cache_free(web_cache);
exit(6);
}
/* ---- 创建监听 socket ---- */
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
exit(7);
}
int optval = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,
(const void *)&optval, sizeof(int));
set_nonblocking(listen_fd); /* 非阻塞模式 */
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons((unsigned short)port);
if (bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("bind");
exit(8);
}
if (listen(listen_fd, SOMAXCONN) < 0) {
perror("listen");
exit(9);
}
/* ---- 创建 epoll 实例 ---- */
epoll_fd = epoll_create1(0);
if (epoll_fd < 0) {
perror("epoll_create1");
exit(10);
}
/* 将监听 socket 加入 epoll */
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN; /* 监听可读事件 */
ev.data.fd = listen_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) {
perror("epoll_ctl: listen_fd");
exit(11);
}
/* ---- 启动信息 ---- */
printf("╔══════════════════════════════════════════════════════════╗\n");
printf("║ 实验4 高性能Web服务器 v%d.0 ║\n", VERSION);
printf("╠══════════════════════════════════════════════════════════╣\n");
printf("║ 端口: %-6d 根目录: %-30s ║\n", port, argv[2]);
printf("║ 工作线程: %-2d 缓存容量: %-4d 条目 ║\n",
nthreads, cache_size);
printf("║ I/O模型: epoll (%s) ║\n",
"Level-Triggered");
printf("║ 支持方法: GET, POST ║\n");
printf("║ 缓存策略: LRU (哈希表+双向链表) ║\n");
printf("╚══════════════════════════════════════════════════════════╝\n");
printf("\n[启动] 服务器就绪,等待连接... (Ctrl+C 停止)\n\n");
/* ---- epoll 事件循环 ---- */
int hit = 0;
long start_time = current_time_ms();
while (server_running) {
/* epoll_wait 等待事件超时1秒以便检查 server_running */
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, 1000);
if (nfds < 0) {
if (errno == EINTR) continue;
perror("epoll_wait");
break;
}
for (i = 0; i < nfds; i++) {
int fd = events[i].data.fd;
/* ---- 新连接 ---- */
if (fd == listen_fd) {
while (1) {
cli_len = sizeof(cli_addr);
int conn_fd = accept(listen_fd,
(struct sockaddr *)&cli_addr,
&cli_len);
if (conn_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
break; /* 没有更多连接 */
logger(ERROR, "accept", "", 0);
break;
}
hit++;
#ifdef DEBUG
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cli_addr.sin_addr,
ip, sizeof(ip));
printf("[连接 #%d] 来自 %s:%d\n",
hit, ip, ntohs(cli_addr.sin_port));
#endif
/* 设置非阻塞并加入 epoll */
set_nonblocking(conn_fd);
ev.events = EPOLLIN | EPOLLET; /* 边缘触发 */
ev.data.fd = conn_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) < 0) {
logger(ERROR, "epoll_ctl add conn_fd", "", conn_fd);
close(conn_fd);
}
}
}
/* ---- 客户端数据 ---- */
else {
/* 从 epoll 移除该 fd (请求处理由线程池负责) */
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
/* 读取 HTTP 请求 */
char *raw_buf = (char *)malloc(BUFSIZE);
int raw_len = 0;
int ret = read_http_request(fd, raw_buf, &raw_len);
if (ret < 0 || raw_len == 0) {
/* 读取失败或空请求 */
free(raw_buf);
close(fd);
continue;
}
/* 创建请求对象并提交到线程池 */
request_t *req = (request_t *)calloc(1, sizeof(request_t));
req->fd = fd;
req->hit = hit;
if (parse_request(raw_buf, raw_len, req) < 0) {
send_error(fd, 400, "Bad Request",
"Failed to parse HTTP request");
free(raw_buf);
free(req);
close(fd);
continue;
}
free(raw_buf);
/* 提交工作到线程池 */
thpool_add_work(thpool, process_request, req);
}
}
}
/* ---- 关闭统计 ---- */
long end_time = current_time_ms();
long runtime = end_time - start_time;
printf("\n[关闭] 等待线程池任务完成...\n");
thpool_wait(thpool);
thpool_destroy(thpool);
/* ---- 打印总体统计 ---- */
int total_cgi = 0, total_file = 0;
for (i = 0; i < g_nthreads; i++) {
total_cgi += thread_cgi_count[i];
total_file += thread_file_count[i];
}
printf("\n");
printf("╔══════════════════════════════════════════════════════════════╗\n");
printf("║ 服务器已关闭 - 运行统计 ║\n");
printf("╠══════════════════════════════════════════════════════════════╣\n");
printf("║ 总请求数: %-6d 运行时间: %ld ms ║\n",
hit, runtime);
printf("║ 平均吞吐量: %.1f req/s ║\n",
runtime > 0 ? 1000.0 * hit / runtime : 0);
printf("║ 文件请求: %-6d CGI请求: %-6d ║\n",
total_file, total_cgi);
printf("╠══════════════════════════════════════════════════════════════╣\n");
printf("║ 各线程任务分配统计 ║\n");
printf("╠══════════════════════════════════════════════════════════════╣\n");
printf("║ 线程ID │ 请求总数 │ CGI │ 文件 │ 占比 │ 分配可视化 ║\n");
printf("╠═════════╪══════════╪═════╪══════╪═════════╪═════════════════╣\n");
int max_req = 1; /* 避免除零 */
for (i = 0; i < g_nthreads; i++) {
if (thread_req_count[i] > max_req)
max_req = thread_req_count[i];
}
for (i = 0; i < g_nthreads; i++) {
int cnt = thread_req_count[i];
float pct = hit > 0 ? 100.0 * cnt / hit : 0;
/* 用 # 字符画柱状图,最大宽度 15 个字符 */
int bar_len = max_req > 0 ? (cnt * 15 + max_req / 2) / max_req : 0;
char bar[16];
int j;
for (j = 0; j < bar_len && j < 15; j++)
bar[j] = '#';
bar[j] = '\0';
printf("║ 线程%-2d │ %5d │ %3d │ %4d │ %5.1f%% │ %-15s ║\n",
i, cnt, (int)thread_cgi_count[i], (int)thread_file_count[i],
pct, bar);
}
printf("╚═════════╧══════════╧═════╧══════╧═════════╧═════════════════╝\n");
cache_free(web_cache);
close(epoll_fd);
printf("[退出] 服务器正常退出\n");
return 0;
}