note/tech/simp_httpd_m.c
2025-11-19 10:16:05 +08:00

476 lines
16 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.

/**
* @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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>
#include <fcntl.h>
#include <jansson.h>
#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);
}