#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <errno.h>
#include <signal.h> // for signal()
#define PORT 8080
#define BUFFER_SIZE 4096
#define MAX_PATH 256
#define WEB_ROOT "./web"
#define MAX_CLIENTS 100
// 服务器类型枚举
typedef enum {
SERVER_MULTITHREAD,
SERVER_MULTIPROCESS,
SERVER_IO_MULTIPLEXING
} server_type_t;
// 线程参数结构体 (仅多线程模式使用)
typedef struct {
int client_socket;
} thread_args_t;
// 客户端连接信息 (仅IO多路复用模式使用)
typedef struct {
int socket;
int active;
} client_info_t;
// --- 通用辅助函数 (所有模型共用) ---
// 获取文件MIME类型
const char* get_mime_type(const char* filename) {
const char* ext = strrchr(filename, '.');
if (!ext) return "text/plain";
if (strcmp(ext, ".html") == 0 || strcmp(ext, ".htm") == 0) return "text/html";
if (strcmp(ext, ".css") == 0) return "text/css";
if (strcmp(ext, ".js") == 0) return "application/javascript";
if (strcmp(ext, ".png") == 0) return "image/png";
if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0) return "image/jpeg";
if (strcmp(ext, ".gif") == 0) return "image/gif";
return "text/plain";
}
// 发送HTTP响应头
void send_response_header(int client_socket, int status_code, const char* status_text,
const char* content_type, long content_length) {
char header[BUFFER_SIZE];
int header_len = snprintf(header, sizeof(header),
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n"
"\r\n",
status_code, status_text, content_type, content_length);
send(client_socket, header, header_len, 0);
}
// 发送错误响应
void send_error(int client_socket, int status_code, const char* status_text) {
char body[100];
int body_len = snprintf(body, sizeof(body), "%d %s", status_code, status_text);
send_response_header(client_socket, status_code, status_text, "text/plain", body_len);
send(client_socket, body, body_len, 0);
}
// 发送文件内容
void send_file(int client_socket, const char* filepath) {
struct stat file_stat;
if (stat(filepath, &file_stat) == -1) {
send_error(client_socket, 404, "Not Found");
return;
}
int file_fd = open(filepath, O_RDONLY);
if (file_fd == -1) {
send_error(client_socket, 500, "Internal Server Error");
return;
}
// 发送响应头
const char* mime_type = get_mime_type(filepath);
send_response_header(client_socket, 200, "OK", mime_type, file_stat.st_size);
// 发送文件内容
char buffer[BUFFER_SIZE];
int bytes_read;
while ((bytes_read = read(file_fd, buffer, sizeof(buffer))) > 0) {
if (send(client_socket, buffer, bytes_read, 0) < 0) {
// 如果发送失败 (例如客户端关闭连接), 停止发送
break;
}
}
close(file_fd);
}
// 处理HTTP请求 (所有模型共用的核心逻辑)
void handle_request(int client_socket) {
char buffer[BUFFER_SIZE];
// 只读取一次,对于简单的GET请求足够了
int bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
if (bytes_received <= 0) {
// 客户端已关闭连接或发生错误
return;
}
buffer[bytes_received] = '\0';
char method[16], path[MAX_PATH];
if (sscanf(buffer, "%s %s", method, path) != 2) {
send_error(client_socket, 400, "Bad Request");
return;
}
if (strcmp(method, "GET") != 0) {
send_error(client_socket, 405, "Method Not Allowed");
return;
}
char filepath[MAX_PATH * 2];
if (strcmp(path, "/") == 0) {
snprintf(filepath, sizeof(filepath), "%s/index.html", WEB_ROOT);
} else {
snprintf(filepath, sizeof(filepath), "%s%s", WEB_ROOT, path);
}
send_file(client_socket, filepath);
}
// ===== 模型一:多线程服务器实现 =====
// 核心思想: 主线程负责监听和接受连接,每当有新连接到来,就创建一个新的子线程来专门处理这个连接。
// 优点: 线程间共享内存,通信方便,创建开销比进程小。
// 缺点: 需要处理线程同步问题(本例中不需要,因为线程间不共享数据),线程数量过多会消耗大量系统资源。
// 线程处理函数
void* thread_handler(void* arg) {
thread_args_t* args = (thread_args_t*)arg;
int client_socket = args->client_socket;
free(args); // 释放参数结构体
// 让线程可以被系统自动回收资源,主线程无需调用 pthread_join
pthread_detach(pthread_self());
printf("[线程 %lu] 开始处理客户端 socket %d\n", pthread_self(), client_socket);
handle_request(client_socket);
printf("[线程 %lu] 完成处理,关闭 socket %d\n", pthread_self(), client_socket);
close(client_socket);
return NULL;
}
void run_multithread_server() {
int server_socket;
struct sockaddr_in server_addr;
// --- 标准的Socket服务器设置 ---
server_socket = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(server_socket, 10);
printf("=== 多线程Web服务器启动,监听端口 %d ===\n", PORT);
while (1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
// 1. 主线程阻塞在 accept(),等待新连接
int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_len);
if (client_socket < 0) continue;
printf("接受新连接: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 2. 为新连接动态分配参数
thread_args_t* args = malloc(sizeof(thread_args_t));
args->client_socket = client_socket;
// 3. 【关键】创建新线程,让新线程去执行 handle_request
pthread_t thread_id;
if (pthread_create(&thread_id, NULL, thread_handler, args) != 0) {
perror("无法创建线程");
free(args);
close(client_socket);
}
// 主线程不等待子线程结束,立即返回到 accept() 等待下一个连接
}
close(server_socket);
}
// ===== 模型二:多进程服务器实现 =====
// 核心思想: 主进程(父进程)负责监听和接受连接,每当有新连接到来,就 fork() 一个子进程来专门处理这个连接。
// 优点: 进程间地址空间独立,一个进程崩溃不会影响其他进程,非常稳定。
// 缺点: 创建进程的开销远大于线程,进程间通信(IPC)复杂。
// 处理僵尸进程的信号处理器
void sigchld_handler(int sig) {
// 使用 waitpid 循环回收所有已结束的子进程
while (waitpid(-1, NULL, WNOHANG) > 0);
}
void run_multiprocess_server() {
int server_socket;
struct sockaddr_in server_addr;
// --- 标准的Socket服务器设置 ---
server_socket = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(server_socket, 10);
// 【关键】设置信号处理函数来处理子进程结束信号,防止僵尸进程
// 你的原代码使用 SIG_IGN 是一种简便方法,这里使用更健壮的 waitpid 方式
signal(SIGCHLD, sigchld_handler);
printf("=== 多进程Web服务器启动,监听端口 %d ===\n", PORT);
while (1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
// 1. 父进程阻塞在 accept(),等待新连接
int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_len);
if (client_socket < 0) continue;
printf("接受新连接: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 2. 【关键】创建子进程
pid_t pid = fork();
if (pid < 0) {
perror("无法创建进程");
close(client_socket);
} else if (pid == 0) {
// --- 子进程代码 ---
// a. 子进程不需要监听,关闭服务器socket
close(server_socket);
printf("[进程 %d] 开始处理客户端 socket %d\n", getpid(), client_socket);
// b. 子进程调用 handle_request 处理业务
handle_request(client_socket);
printf("[进程 %d] 完成处理,关闭 socket %d\n", getpid(), client_socket);
// c. 关闭连接并退出子进程
close(client_socket);
exit(0);
} else {
// --- 父进程代码 ---
// a. 父进程不处理具体连接,关闭客户端socket
close(client_socket);
// b. 父进程立即返回到 accept() 等待下一个连接
}
}
close(server_socket);
}
// ===== 模型三:IO多路复用服务器实现 (select) =====
// 核心思想: 使用一个进程(或线程)来同时监视多个文件描述符(socket)。当任何一个socket准备好被读/写时,`select` 调用就会返回,然后程序可以去处理那个就绪的socket。
// 优点: 用最少的进程/线程处理大量的连接,系统资源开销小。
// 缺点: 编码比前两者复杂。`select` 本身有最大文件描述符数量的限制(通常是1024),并且每次调用都需要在内核和用户空间之间复制整个文件描述符集合,效率在连接数非常多时会下降(`poll` 和 `epoll` 是更优的替代方案)。
void run_io_multiplexing_server() {
int server_socket, max_fd;
struct sockaddr_in server_addr;
fd_set master_fds, read_fds; // master_fds 保存所有要监控的fd,read_fds是每次传给select的临时集合
client_info_t clients[MAX_CLIENTS];
for (int i = 0; i < MAX_CLIENTS; i++) {
clients[i].socket = -1;
clients[i].active = 0;
}
// --- 标准的Socket服务器设置 ---
server_socket = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(server_socket, MAX_CLIENTS); // 监听队列可以大一点
printf("=== IO多路复用Web服务器启动,监听端口 %d ===\n", PORT);
// 1. 【关键】初始化文件描述符集合
FD_ZERO(&master_fds);
// 2. 将服务器监听socket加入监控集合
FD_SET(server_socket, &master_fds);
max_fd = server_socket;
while (1) {
// 每次循环前,都从 master_fds 复制一份到 read_fds,因为 select 会修改传入的集合
read_fds = master_fds;
// 3. 【关键】调用 select(),阻塞程序,直到有fd就绪
// select 会监控 read_fds 中的所有fd,当有fd可读时,它会返回
int activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
if (activity < 0 && errno != EINTR) {
perror("select 错误");
break;
}
// 4. 检查是哪个fd就绪了
// a. 检查是不是服务器监听socket就绪了 (意味着有新连接)
if (FD_ISSET(server_socket, &read_fds)) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int new_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_len);
printf("接受新连接: %s:%d on socket %d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), new_socket);
// 将新连接的socket也加入到监控集合中
int i;
for (i = 0; i < MAX_CLIENTS; i++) {
if (!clients[i].active) {
clients[i].socket = new_socket;
clients[i].active = 1;
FD_SET(new_socket, &master_fds);
if (new_socket > max_fd) max_fd = new_socket;
printf("客户端 socket %d 添加到槽位 %d\n", new_socket, i);
break;
}
}
if (i == MAX_CLIENTS) {
printf("连接数已满\n");
close(new_socket);
}
}
// b. 检查是不是某个已连接的客户端socket就绪了 (意味着客户端发来了数据)
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].active && FD_ISSET(clients[i].socket, &read_fds)) {
printf("处理客户端槽位 %d (socket %d) 的请求\n", i, clients[i].socket);
// 在这个简单的模型中,我们直接处理完请求并关闭连接
// 一个更复杂的IO多路复用服务器会在这里进行非阻塞读写
handle_request(clients[i].socket);
// 从监控集合中移除,并关闭socket
close(clients[i].socket);
FD_CLR(clients[i].socket, &master_fds);
clients[i].active = 0;
clients[i].socket = -1;
printf("客户端槽位 %d (socket %d) 连接已关闭\n", i, clients[i].socket);
}
}
}
close(server_socket);
}
// 主函数,用于选择服务器模型
int main(int argc, char* argv[]) {
server_type_t server_type = SERVER_MULTITHREAD; // 默认使用多线程
if (argc > 1) {
if (strcmp(argv[1], "thread") == 0) {
server_type = SERVER_MULTITHREAD;
} else if (strcmp(argv[1], "process") == 0) {
server_type = SERVER_MULTIPROCESS;
} else if (strcmp(argv[1], "select") == 0) {
server_type = SERVER_IO_MULTIPLEXING;
} else {
fprintf(stderr, "用法: %s [thread|process|select]\n", argv[0]);
fprintf(stderr, " thread - 多线程模式 (默认)\n");
fprintf(stderr, " process - 多进程模式\n");
fprintf(stderr, " select - IO多路复用模式\n");
return 1;
}
}
// 自动创建web根目录和index.html用于测试
system("mkdir -p " WEB_ROOT);
FILE* index_file = fopen(WEB_ROOT "/index.html", "w");
if (index_file) {
fprintf(index_file,
"<!DOCTYPE html><html><head><title>Test Page</title><meta charset='UTF-8'></head>"
"<body><h1>Web Server Test Page</h1><p>Server is running!</p></body></html>");
fclose(index_file);
}
// 根据选择启动对应的服务器
switch (server_type) {
case SERVER_MULTITHREAD:
run_multithread_server();
break;
case SERVER_MULTIPROCESS:
run_multiprocess_server();
break;
case SERVER_IO_MULTIPLEXING:
run_io_multiplexing_server();
break;
}
return 0;
}
最新发布