Files
C-exp-collection/server-exp3/webserver_pool.c
2026-06-18 18:13:12 +08:00

375 lines
13 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.
#include <arpa/inet.h> // 提供网络地址转换函数
#include <errno.h> // 定义错误码
#include <fcntl.h> // 文件操作相关函数
#include <netinet/in.h> // 定义网络协议的结构体和函数
#include <signal.h> // 提供信号处理函数
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 标准库函数如exit()
#include <string.h> // 字符串操作函数
#include <sys/socket.h> // 套接字相关函数
#include <sys/time.h> // 提供 gettimeofday 函数
#include <sys/types.h> // 定义数据类型
#include <time.h> // 时间相关函数
#include <unistd.h> // 提供POSIX API如read()、write()
#include "thpool.h"
#define VERSION 23 // 服务器版本号
#define BUFSIZE 8096 // 缓冲区大小
#define ERROR 42 // 错误日志类型
#define LOG 44 // 普通日志类型
#define FORBIDDEN 403 // HTTP状态码禁止访问
#define NOTFOUND 404 // HTTP状态码未找到资源
#define NTHREADS 4
#define SBUFSIZE 16
#ifndef SIGCLD
#define SIGCLD SIGCHLD // 为兼容不同系统定义信号别名
#endif
// 支持的文件扩展名及其对应的MIME类型
struct {
char *ext; // 文件扩展名
char *filetype; // MIME类型
} 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"},
{0, 0} // 结束标志
};
typedef struct _param {
int hit;
int socketfd;
} param;
/* Ctrl+C信号处理函数 */
void ctrlc_handler(int sig)
{
printf("您按下了Ctrl+C终止了Web服务器\n");
exit(0);
}
// 计算两个时间点之间的时间差(以毫秒为单位)
long calculate_time_diff(struct timespec start, struct timespec end) {
long diff_sec = end.tv_sec - start.tv_sec; // 秒部分差值
long diff_nsec = end.tv_nsec - start.tv_nsec; // 纳秒部分差值
return diff_sec * 1000 + diff_nsec / 1000000.0; // 转换为毫秒
}
void logger(int type, const char *s1, const char *s2, int socket_fd) {
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 exiting pid=%d",
full_timebuffer, s1, s2, errno, getpid());
break;
case FORBIDDEN: // 403 禁止访问
if (write(socket_fd,
"HTTP/1.1 403 Forbidden\nContent-Length: 185\nConnection: "
"close\nContent-Type: text/html\n\n"
"<html><head>\n<title>403 "
"Forbidden</title>\n</head><body>\n<h1>Forbidden</h1>\n"
"The requested URL, file type or operation is not allowed on this simple "
"static file webserver.\n"
"</body></html>\n",
271) < 0) {
perror("Failed to send 403 Forbidden response");
}
snprintf(logbuffer, sizeof(logbuffer), "%s [FORBIDDEN] %s:%s", full_timebuffer, s1, s2);
break;
case NOTFOUND: // 404 未找到
if (write(socket_fd,
"HTTP/1.1 404 Not Found\nContent-Length: 136\nConnection: "
"close\nContent-Type: text/html\n\n"
"<html><head>\n<title>404 Not Found</title>\n</head><body>\n<h1>Not "
"Found</h1>\n"
"The requested URL was not found on this server.\n</body></html>\n",
224) < 0) {
perror("Failed to send 404 Not Found response");
}
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;
}
// 打开日志文件并写入日志内容
#ifdef DEBUG
if ((fd = open("webserver.log", O_CREAT | O_WRONLY | O_APPEND, 0644)) >= 0) {
if (write(fd, logbuffer, strlen(logbuffer)) < 0) {
perror("Failed to write log to file");
}
if (write(fd, "\n", 1) < 0) {
perror("Failed to write newline to log file");
}
close(fd);
} else {
perror("Failed to open log file");
}
#endif
}
// 处理HTTP请求的函数
void web(void *arg) {
int j, file_fd, buflen;
long i, ret, len;
char *fstr;
char buffer[BUFSIZE + 1]; // 改为线程局部栈缓冲区,避免多线程共享同一缓冲区造成竞争
param* datap=(param *) arg;
int fd = datap->socketfd;
int hit = datap->hit;
// 定义计时变量
struct timespec start, end;
long duration;
logger(LOG, "Process new web request", "", fd);
// 记录开始时间(读取请求)
clock_gettime(CLOCK_MONOTONIC, &start);
ret = read(fd, buffer, BUFSIZE); // 读取客户端请求
if (ret <= 0) { // 读取失败
logger(FORBIDDEN, "Failed to read browser request", "", fd);
return;
}
buffer[ret]='\0';
#ifdef DEBUG
printf("第%d个客户请求:%s\n",hit,buffer);
#endif
// 记录结束时间并计算耗时
clock_gettime(CLOCK_MONOTONIC, &end);
duration = calculate_time_diff(start, end);
logger(LOG, "Time taken for request reading", "", duration);
// 清除换行符和回车符
for (i = 0; i < ret; i++) {
if (buffer[i] == '\r' || buffer[i] == '\n') buffer[i] = '*';
}
// 记录开始时间(解析请求)
clock_gettime(CLOCK_MONOTONIC, &start);
if (strncmp(buffer, "GET ", 4)) { // 检查是否为GET请求
logger(FORBIDDEN, "Only simple GET operation supported", buffer, fd);
return;
}
logger(LOG, "request", buffer, hit);
for (i = 4; i < BUFSIZE; i++) { // 截取文件路径
if (buffer[i] == ' ') {
buffer[i] = 0;
break;
}
}
clock_gettime(CLOCK_MONOTONIC, &end);
duration = calculate_time_diff(start, end);
logger(LOG, "Time taken for request parsing", "", duration);
// 检查路径是否合法
for (j = 0; j < i - 1; j++) {
if (buffer[j] == '.' && buffer[j + 1] == '.') {
logger(FORBIDDEN, "Parent directory (..) path names not supported", buffer, fd);
return;
}
}
// 默认返回 index.html
if (!strncmp(&buffer[0], "GET /\0", 6)) strcpy(buffer, "GET /index.html");
// 文件类型解析
buflen = strlen(buffer);
fstr = 0;
clock_gettime(CLOCK_MONOTONIC, &start);
for (i = 0; extensions[i].ext != 0; i++) {
len = strlen(extensions[i].ext);
if (!strncmp(&buffer[buflen - len], extensions[i].ext, len)) {
fstr = extensions[i].filetype;
break;
}
}
clock_gettime(CLOCK_MONOTONIC, &end);
duration = calculate_time_diff(start, end);
logger(LOG, "Time taken for file type detection", "", duration);
if (fstr == 0) {
logger(FORBIDDEN, "File extension type not supported", buffer, fd);
return;
}
// 文件打开
clock_gettime(CLOCK_MONOTONIC, &start);
if ((file_fd = open(&buffer[5], O_RDONLY)) == -1) {
logger(NOTFOUND, "Failed to open file", &buffer[5], fd);
return;
}
clock_gettime(CLOCK_MONOTONIC, &end);
duration = calculate_time_diff(start, end);
logger(LOG, "Time taken for file opening", "", duration);
// 文件读取与传输
logger(LOG, "SEND", &buffer[5], hit);
clock_gettime(CLOCK_MONOTONIC, &start);
len = (long)lseek(file_fd, 0, SEEK_END);
lseek(file_fd, 0, SEEK_SET);
sprintf(buffer,
"HTTP/1.1 200 OK\nServer: nweb/%d.0\nContent-Length: %ld\nConnection: "
"close\nContent-Type: %s\n\n",
VERSION, len, fstr);
logger(LOG, "Header", buffer, hit);
if (write(fd, buffer, strlen(buffer)) < 0) {
logger(ERROR, "Failed to write HTTP header", "", fd);
close(file_fd);
return;
}
// 分块读取文件内容并发送
while ((ret = read(file_fd, buffer, BUFSIZE)) > 0) {
if (write(fd, buffer, ret) < 0) {
logger(ERROR, "Failed to write file content", "", fd);
break;
}
}
clock_gettime(CLOCK_MONOTONIC, &end);
duration = calculate_time_diff(start, end);
logger(LOG, "Time taken for file transfer", "", duration);
#ifdef DEBUG
printf("第%d个客户服务器完成\n",hit);
#endif
// 关闭文件和套接字
free(arg);
close(file_fd);
close(fd);
}
// 主函数,设置服务器并监听客户端连接
int main(int argc, char **argv) {
int i, port, listenfd, socketfd, hit;
socklen_t length;
static struct sockaddr_in cli_addr; // 客户端地址
static struct sockaddr_in serv_addr; // 服务器地址
// 检查命令行参数是否正确
if (argc < 3 || argc > 3 || !strcmp(argv[1], "-?")) {
(void)printf(
"hint: webserver Port-Number Top-Directory\t\tversion %d\n\n"
"\twebserver is a small and very safe mini web server\n"
"\twebserver only servers out file/web pages with extensions named below\n"
"\t and only from the named directory or its sub-directories.\n"
"\tThere is no fancy features = safe and secure.\n\n"
"\tExample: webserver 8181 /home/oslab &\n\n"
"\tOnly Supports:",
VERSION);
for (i = 0; extensions[i].ext != 0; i++) (void)printf(" %s", extensions[i].ext);
(void)printf(
"\n\tNot Supported: URLs including \"..\", Java, Javascript, CGI\n"
"\tNot Supported: directories / /etc /bin /lib /tmp /usr /dev /sbin \n"
"\tNo warranty given or implied\n\tNigel Griffiths nag@uk.ibm.com\n");
exit(0);
}
/* 设置Ctrl+C信号处理 */
if(signal(SIGINT,ctrlc_handler)==SIG_ERR)
(void)printf("错误: 无法设置SIGINT信号处理\n");
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)) {
(void)printf("ERROR: Bad top directory %s, see nweb -?\n", argv[2]);
exit(3);
}
// 验证并切换到指定目录
if (chdir(argv[2]) == -1) {
perror("Failed to change directory");
exit(4);
}
port = atoi(argv[1]);
if (port < 1 || port > 60000) {
fprintf(stderr, "Invalid port number: %d\n", port);
exit(1);
}
// 设置服务器监听套接字
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation failed");
exit(1);
} serv_addr.sin_family = AF_INET;
/* Eliminates "Address already in use" error from bind. */
int optval=1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
(const void *)&optval , sizeof(int)) < 0)
return -1;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(port);
if (bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Bind failed");
exit(1);
}
if (listen(listenfd, 64) < 0) {
perror("Listen failed");
exit(1);
}
printf("start main\n");
// 初始化线程池
thpool_* pool = thpool_init(NTHREADS);
// 接受客户端连接
for (hit = 1;; hit++) {
length = sizeof(cli_addr);
if ((socketfd = accept(listenfd, (struct sockaddr *)&cli_addr, &length)) < 0) {
logger(ERROR, "system call", "accept", 0);
}
param *new_param = (param*) malloc(sizeof(param));
new_param->hit = hit;
new_param->socketfd=socketfd;
thpool_add_work(pool,web, (void*)new_param);
}
// 等待线程池中的所有任务执行完毕
thpool_wait(pool);
// 销毁线程池
thpool_destroy(pool);
logger(LOG, "webserver exit noramlly", "", 0);
return 0;
}