/** * 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 /* 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), "\n" "\n" "\n" "%d %s\n" "\n" "\n" "

%d %s

\n" "

%s

\n" "
Experiment4-WebServer v%d.0\n" "\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, "\n" "\n" "\n" "POST Request Received\n" "\n" "\n" "

✓ POST Request Processed Successfully

\n" "

Form Data Received:

\n" "
%s
\n" "

Raw Body (%d bytes):

\n" "
%.*s
\n" "

← Back to Index

\n" "
Experiment4-WebServer v%d.0\n" "\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; }