/** * @file simp_httpd_m.c * @author maxwell@void * @version 0.2 * @date 2024-07-12 * * @copyright Copyright (c) 2024 * 功能: * 1. 实现一个简单的HTTP服务器,可以处理GET请求,并返回指定文件的内容。 * 2. 支持命令行参数,可以指定端口号和目录。 * 3. 支持线程池,可以处理多个客户端连接。 * 4. 支持SIGPIPE信号,可以忽略客户端断开连接时产生的错误。 * 5. 这个版本支持连接池和持久连接 * 编译:gcc -o sim_httpd_m simp_httpd_m.c -lpthread * 程序的流程是: * 1. 解析命令行参数,获取端口号和目录;如果没有指定,则使用默认目录和端口号 * 2. 改变当前工作目录,使得服务器可以读取指定目录下的文件 * 3. 忽略 SIGPIPE 信号,忽略客户端断开连接时产生的错误。否则会导致程序崩溃 * 4. 初始化线程池,创建线程,并设置互斥锁、条件变量、任务队列、任务数量、关闭标志 * 5. 创建服务器套接字,绑定到指定端口,监听连接 * 6. 接受并处理客户端连接,向线程池添加任务 * 7. 关闭服务器套接字,关闭线程池 * 8. 线程池中的线程会从任务队列中获取任务,并处理任务,并通知其他线程 * 9. 线程池中的线程在shutdown标志为1时退出循环 * 10. 线程池中的线程在接收到SIGINT、SIGTERM信号时,会关闭线程池,并等待其他线程退出 */ #include #include #include #include #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 // 缓冲区大小 #define THREAD_POOL_SIZE 10 // 线程池大小 #define CONFIG_FILE "config.json" // 配置文件名 // 定义处理客户端连接的函数 void handle_client(int client_socket); // 定义向客户端发送文件内容的函数 void send_file(int client_socket, const char *filename); // 定义线程池相关函数 void *thread_worker(void *arg); int read_config(int *port, char **directory, char *config_file); int create_default_config(char *config_file); // 线程池结构 typedef struct { pthread_t threads[THREAD_POOL_SIZE]; // 线程数组 pthread_mutex_t lock; // 互斥锁 pthread_cond_t cond; // 条件变量 int client_sockets[BUFFER_SIZE]; // 客户端套接字数组 int head, tail; // 头尾指针 int count; // 任务数量 int shutdown; // 关闭标志 } thread_pool_t; // 全局线程池变量 thread_pool_t pool; /** * @brief * 初始化线程池 * 初始化线程池,创建线程,并设置互斥锁、条件变量、任务队列、任务数量、关闭标志 */ void init_thread_pool() { pthread_mutex_init(&pool.lock, NULL); // 初始化互斥锁 pthread_cond_init(&pool.cond, NULL); // 初始化条件变量 pool.head = pool.tail = pool.count = 0; // 初始化头尾指针和任务数量 pool.shutdown = 0; // 初始化关闭标志 // 创建线程 for (int i = 0; i < THREAD_POOL_SIZE; i++) { pthread_create(&pool.threads[i], NULL, thread_worker, NULL); } } /** * @brief * 向线程池添加任务 * 向线程池中添加一个任务,任务是客户端套接字 * 线程池中的线程会从任务队列中获取任务,并处理任务 * @param client_socket * 客户端套接字 * @return void */ void add_task(int client_socket) { pthread_mutex_lock(&pool.lock); // 加锁 pool.client_sockets[pool.tail] = client_socket; // 添加任务 pool.tail = (pool.tail + 1) % BUFFER_SIZE; // 更新尾指针 pool.count++; // 任务数量加1 pthread_cond_signal(&pool.cond); // 通知线程 pthread_mutex_unlock(&pool.lock); // 解锁 } /** * @brief 线程工作函数 * 线程工作函数, 循环处理任务 * 线程池中的线程调用这个函数,等待任务,处理任务,并通知其他线程 * 线程池中的线程在shutdown标志为1时退出循环 * 线程池中的线程在接收到SIGINT、SIGTERM信号时,会关闭线程池,并等待其他线程退出 * @param arg * @return void* */ void *thread_worker(void *arg) { (void)arg; // 没有用到arg参数,但为了保持接口一致性,这里保留 while (1) { pthread_mutex_lock(&pool.lock); // 加锁 while (pool.count == 0 && !pool.shutdown) { // 等待任务 pthread_cond_wait(&pool.cond, &pool.lock); // 等待条件变量 } // 关闭线程池 if (pool.shutdown) { pthread_mutex_unlock(&pool.lock); break; } // 处理任务 int client_socket = pool.client_sockets[pool.head]; // 获取任务 pool.head = (pool.head + 1) % BUFFER_SIZE; // 更新头指针 pool.count--; // 任务数量减1 pthread_mutex_unlock(&pool.lock); // 解锁 handle_client(client_socket); // 处理客户端连接 close(client_socket); // 关闭客户端套接字 } return NULL; } /** * @brief * 关闭线程池 * 关闭线程池,设置关闭标志,通知所有线程,等待线程退出 */ void shutdown_thread_pool() { pthread_mutex_lock(&pool.lock); // 加锁 pool.shutdown = 1; // 设置关闭标志 pthread_cond_broadcast(&pool.cond); // 通知所有线程 pthread_mutex_unlock(&pool.lock); // 解锁 // 等待线程退出 for (int i = 0; i < THREAD_POOL_SIZE; i++) { pthread_join(pool.threads[i], NULL); } } /** * @brief * 读取配置文件。 * 读取默认配置文件时,如果不存在,则创建默认配置文件。 * @param port * @param directory * @param config_file * @return int */ int read_config(int *port, char **directory, char *config_file) { // 判断config_file是否为json格式 if (strstr(config_file, ".json") == NULL) { fprintf(stderr, "Config file is not a json file\n"); return -1; } // 读取配置文件 json_error_t error; json_t *root = json_load_file(config_file, 0, &error); if (!root) { fprintf(stderr, "Config file failed: %s\n", error.text); return -1; } json_t *port_json = json_object_get(root, "port"); if (!json_is_integer(port_json)) { fprintf(stderr, "port is not an integer\n"); json_decref(root); return -1; } *port = json_integer_value(port_json); json_t *directory_json = json_object_get(root, "directory"); if (!json_is_string(directory_json)) { fprintf(stderr, "directory is not a string\n"); json_decref(root); return -1; } *directory = strdup(json_string_value(directory_json)); json_decref(root); return 0; } /** * @brief * */ int create_default_config(char *config_file) { json_t *root = json_object(); json_object_set_new(root, "port", json_integer(8080)); json_object_set_new(root, "directory", json_string(".")); // 写入配置文件 if (json_dump_file(root, config_file, JSON_INDENT(4)) < 0) { perror(" Failed to creat json file. \n"); json_decref(root); return -1; }; json_decref(root); // 释放内存 return 0; } /** * @brief * 主函数 * 解析命令行参数,创建服务器套接字,绑定到指定端口,监听连接,接受并处理客户端连接 * @param argc * 程序参数个数 * @param argv * 程序参数数组 * @return int * 返回值 */ int main(int argc, char *argv[]) { int server_socket, client_socket; // 服务器套接字、客户端套接字 struct sockaddr_in server_addr, client_addr; // 服务器地址、客户端地址 socklen_t client_addr_len = sizeof(client_addr); // 客户端地址长度 int port = 8080; // 默认端口 char *directory = "."; // 默认目录 char *config_file = NULL; // 配置文件路径 // 如果不存在config.json,则创建 if (access(CONFIG_FILE, F_OK) == -1) { if (create_default_config(CONFIG_FILE) == -1) { perror("Create default config file failed. \n"); } printf("Create a default config file. \n"); } // 解析命令行参数 // 如果有-c 参数,则忽略其他参数,读取配置文件 // 如果没有-c 参数,则读取命令行参数 // 如果没有命令行参数,则读取默认配置文件 // 如果没有默认配置文件,则创建默认配置文件 int opt; while ((opt = getopt(argc, argv, "p:d:c:")) != -1) { switch (opt) { case 'p': port = atoi(optarg); break; case 'd': directory = optarg; break; case 'c': config_file = optarg; break; default: fprintf(stderr, "Usage: %s [-p port] [-d directory]\n", argv[0]); exit(EXIT_FAILURE); } } // 如果指定了配置文件或什么参数都没有,则读取配置文件 if (optind < argc || config_file != NULL) { if (read_config(&port, &directory, config_file) == -1){ perror("Read config file failed"); exit(EXIT_FAILURE); } } // 改变当前工作目录,目的在于使得服务器可以读取指定目录下的文件 if (chdir(directory) != 0) { perror("chdir failed"); exit(EXIT_FAILURE); } // 忽略 SIGPIPE 信号,目的在于忽略客户端断开连接时产生的错误 signal(SIGPIPE, SIG_IGN); // 初始化线程池,目的在于处理多个客户端连接 init_thread_pool(); // 创建服务器套接字,目的在于监听客户端连接 server_socket = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字, TCP, IPv4, 0表示默认协议 if (server_socket == -1) { perror("Socket creation failed"); exit(EXIT_FAILURE); } // For MacOS only,判断系统是否为MacOS #ifdef __APPLE__ // 创建SO_REUSEADDR套接字选项,使得服务器可以重用端口号 int optval = 1; if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) { perror("setsockopt failed"); exit(EXIT_FAILURE); } #endif // 设置服务器地址 server_addr.sin_family = AF_INET; // IPv4 server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有地址。如果监听本地地址,可以改为INADDR_LOOPBACK server_addr.sin_port = htons(port); // 端口号 // 绑定套接字到指定端口,目的在于监听客户端连接 if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { // 绑定地址, 地址长度 perror("Bind failed"); close(server_socket); exit(EXIT_FAILURE); } // 监听连接,10表示最大连接数 if (listen(server_socket, 10) == -1) { perror("Listen failed"); close(server_socket); exit(EXIT_FAILURE); } printf("Server is listening on port %d, serving directory %s...\n", port, directory); // 接受并处理客户端连接 while (1) { client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len); // 接受连接, 地址, 地址长度 if (client_socket == -1) { perror("Accept failed"); continue; } printf("Client connected: %s\n", inet_ntoa(client_addr.sin_addr)); add_task(client_socket); // 向线程池添加任务 } // 关闭服务器套接字 close(server_socket); // 关闭线程池 shutdown_thread_pool(); return 0; } /** * @brief * 处理客户端连接 * 处理客户端连接,接收请求,解析请求,发送响应 * @param client_socket * 客户端套接字 */ void handle_client(int client_socket) { char buffer[BUFFER_SIZE]; // 缓冲区 int received; // 接收到的字节数 while ((received = recv(client_socket, buffer, BUFFER_SIZE - 1, 0)) > 0) { // 接收请求, 缓冲区, 缓冲区大小, 0表示不等待 buffer[received] = '\0'; // 结尾加上'\0' printf("Request: %s\n", buffer); // 简单解析HTTP GET请求 char method[16], path[256], protocol[16]; // 方法、路径、协议 sscanf(buffer, "%s %s %s", method, path, protocol); // 解析请求 // 判断是否为GET请求 if (strcmp(method, "GET") == 0) { // 去掉路径中的'/' if (path[0] == '/') { memmove(path, path + 1, strlen(path)); } send_file(client_socket, path); // 发送文件内容 } else { const char *response = "HTTP/1.1 405 Method Not Allowed\r\n\r\n"; send(client_socket, response, strlen(response), 0); // 发送响应 } // 检查是否为持久连接, 如果是持久连接,则继续接收请求,否则断开连接 if (strstr(buffer, "Connection: keep-alive") == NULL) { break; // 非持久连接,断开 } } } /** * @brief * 向客户端发送文件内容 * 向客户端发送文件内容,读取文件内容,并发送给客户端 * 发送响应头,包括Content-Length,并发送响应内容 * 关闭客户端套接字 * @param client_socket * @param filename */ void send_file(int client_socket, const char *filename) { // 判断filename的长度,如果大于256,则返回414 Request-URI Too Long if (strlen(filename) > 256) { const char *response = "HTTP/1.1 414 Request-URI Too Long\r\n\r\n"; send(client_socket, response, strlen(response), 0); close(client_socket); return; } // 判断filename是否为空,如果为空,则返回index.html if (strlen(filename) == 0) { filename = "index.html"; } // 判断filename是否合法,如果不合法,则返回403 Forbidden if (filename[0] == '.' || strstr(filename, "..") != NULL) { const char *response = "HTTP/1.1 403 Forbidden\r\n\r\n"; send(client_socket, response, strlen(response), 0); close(client_socket); return; } // 打开文件,如果文件不存在,则返回404 Not Found FILE *file = fopen(filename, "rb"); if (file == NULL) { const char *response = "HTTP/1.1 404 Not Found\r\n\r\n"; send(client_socket, response, strlen(response), 0); close(client_socket); return; } // 发送响应头,包括Content-Length fseek(file, 0, SEEK_END); // 定位到文件末尾 long file_size = ftell(file); // 获取文件大小 fseek(file, 0, SEEK_SET); // 定位到文件开头 char header[BUFFER_SIZE]; // 响应头 snprintf(header, BUFFER_SIZE, "HTTP/1.1 200 OK\r\nContent-Length: %ld\r\n\r\n", file_size); send(client_socket, header, strlen(header), 0); // 发送响应内容 char file_buffer[BUFFER_SIZE]; // 文件缓冲区 size_t bytes_read; // 读取字节数 // 循环读取文件内容,并发送给客户端 while ((bytes_read = fread(file_buffer, 1, BUFFER_SIZE, file)) > 0) { // 读取文件内容, 缓冲区, 缓冲区大小, 文件 if (send(client_socket, file_buffer, bytes_read, 0) == -1) { // 发送响应内容, 字节数, 0表示不等待 perror("Send failed"); break; } } fclose(file); }