1150 lines
40 KiB
C
1150 lines
40 KiB
C
/**
|
||
* 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;
|
||
}
|