476 lines
16 KiB
C
476 lines
16 KiB
C
/**
|
||
* @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);
|
||
}
|