Files
C-exp-collection/server-exp4/webserver.c

1150 lines
40 KiB
C
Raw Normal View History

2026-07-18 16:39:01 +08:00
/**
* webserver.c 4: Web服务器
*
* ======================== ========================
* 1. epoll I/O
* 2. 线 (Thread Pool)
* 3. LRU (Web Cache)
* 4. GET POST
* 5. CGI
* 6.
*
* ======================== ========================
*
*
* Main Thread (epoll)
*
* acceptepoll_waitread 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;
}