/*
* @copyright: copyright (c) 2025 Chengdu TP-Link Technologies Co.Ltd.
* @fileName: server.h
* @description: function declaration file of server.c
* @Author: Wang ZHiheng
* @email: wangzhiheng@tp-link.com.hk
* @version: 1.0.0
* @Date: 2025-08-07
* @history: \arg 1.0.0, 25Aug07, Wang Zhiheng, Create the file
*/
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <dirent.h>
#include <stdlib.h>
#include <pthread.h>
#include <ctype.h>
#include <fcntl.h>
#include <math.h>
#include "server.h" /* 函数声明文件 */
/* 错误处理 */
#define handle_error(cmd, result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
}
/**
* @function: int init_listen_fd(unsigned short port)
* @description: 新建一个用于TCP监听的socket文件描述符,并返回
* @return 返回监听socket的文件描述符
* @note:
* @param {unsigned short} port 监听端口号
*/
int init_listen_fd(unsigned short port)
{
int listen_sockfd; /* 监听socket */
struct sockaddr_in listen_addr; /* 监听地址 */
memset(&listen_addr, 0, sizeof(listen_addr)); /* 初始化 */
int temp_result; /* 存储临时变量用于debug */
/* 创建监听socket */
listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
handle_error("socket", listen_sockfd);
/* 设置端口复用 */
int opt = -1;
int ret = setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);
handle_error("setsockopt", listen_sockfd);
/* 绑定端口和IP */
listen_addr.sin_family = AF_INET;
listen_addr.sin_addr.s_addr = INADDR_ANY;
listen_addr.sin_port = htons(port);
/* 绑定地址 */
temp_result = bind(listen_sockfd, (struct sockaddr *)&listen_addr, sizeof(listen_addr));
handle_error("bind", temp_result);
printf("bind success\n");
/* 进入监听模式 */
if (listen(listen_sockfd, 128) < 0)
{
perror("listen failed");
close(listen_sockfd);
exit(EXIT_FAILURE);
}
printf("listen success\n");
return listen_sockfd;
}
/**
* @function: int epoll_run(int listen_sockfd)
* @description: 启动epoll
* @return 返回值无实际作用,因为会处于循环状态监听
* @note:
* @param {int} listen_sockfd 监听socket的文件描述符
*/
int threads_run(int listen_sockfd)
{
int temp_result; /* 存储临时变量用于debug */
struct sockaddr_in client_addr; /* 用于存储客户端地址 */
memset(&client_addr, 0, sizeof(client_addr));
/* 接收client连接 */
socklen_t client_addr_len = sizeof(client_addr);
while (1)
{
pthread_t pid; /* 子线程, 用于从客户端读取数据*/
int *client_fd_ptr = (int *)malloc(sizeof(int));
int client_fd = accept(listen_sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
*client_fd_ptr = client_fd;
if (client_fd < 0)
{
perror("accept failed");
close(listen_sockfd);
exit(EXIT_FAILURE);
}
printf("a client from ip:%s at port %d, socket %d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd);
/* 调用子线程接收客户端数据, 并发送响应*/
if ((pthread_create(&pid, NULL, recv_http_request, client_fd_ptr)) != 0)
{
// perror("pthread_create");
// pthread_exit(&pid);
perror("pthread_create failed");
close(client_fd);
free(client_fd_ptr);
continue;
}
/* 使子线程处于detached状态,使其终止时自动回收资源,同时不阻塞线程 */
// pthread_join(pid, NULL);
pthread_detach(pid);
}
printf("close socket");
close(listen_sockfd);
return 0;
}
/**
* @function: void* recv_http_request(void* arg)
* @description: 接收http请求, 读取数据
* @return
* @note: conn_fd表示连接socket, epoll_fd表示epoll树
* @param {void*} arg
*/
void *recv_http_request(void *arg)
{
printf("进入recv\n");
int client_fd = *(int *)arg;
int len = 0; /* 用于接收recv的返回值 */
char buf[4096] = {0}; /* 接收客户端请求数据 */
len = recv(client_fd, buf, sizeof(buf), 0);
if (len <= 0)
{
printf("Client disconnected\n");
close(client_fd);
free(arg);
return NULL;
}
printf("len%d\n", len);
parse_request(buf, (int *)arg);
free(arg);
printf("退出 recv\n");
close(client_fd);
}
/**
* @function: int parse_request(const char *line, int conn_fd)
* @description: 解析HTTP的请求行,并调用send_http_response
* @return {*}
* @note:
* @param {char} *line
* @param {int} conn_fd
*/
int parse_request(const char *message, int *conn_fd)
{
/* 解析请求行 */
char method[12]; /* 存储客户端请求方法,如GET/POST */
char path[1024];
char *filename = NULL; /* 处理客户端请求的静态资源(目录或文件) */
char version[1024];
printf("获取方法和路径\n");
sscanf(message, "%s %s %s", method, path, version);
printf("请求内容: \nmethod:%s, path:%s\n\n", method, path);
/* 不区分大小写的比较,如果不等于0,则返回-1 */
if (strcasecmp(method, "get") == 0)
{
/* 处理客户端请求的静态资源(目录或文件),因为当前获得的/xxx/1.jpg是相对于工作路径的,所以需要转换为./xxx/1.jpg或者xxx/1.jpg */
if (strcmp(path, "/") == 0)
{ /* 如果get当前的工作目录 */
filename = "./";
}
else
{
filename = path + 1; /* get工作目录中的一个资源 */
}
send_http_response(*conn_fd, filename);
}
else if(strcasecmp(method, "post") == 0)
{
// 检查是否为前端指定的 /data/contact.json 路径
if (strcmp(path, "/data/contact.json") == 0) {
// 查找请求体起始位置
const char *body_start = strstr(message, "\r\n\r\n");
if (body_start) {
body_start += 4; // 跳过空行
// 解析表单数据 (application/x-www-form-urlencoded)
char *name = NULL, *email = NULL, *message_content = NULL;
char *token = strtok((char *)body_start, "&");
while (token != NULL) {
if (strstr(token, "name=") == token) {
name = token + 5; // 跳过 "name="
} else if (strstr(token, "email=") == token) {
email = token + 6; // 跳过 "email="
} else if (strstr(token, "message=") == token) {
message_content = token + 8; // 跳过 "message="
}
token = strtok(NULL, "&");
}
// 打印解码后的表单数据
printf("收到联系表单数据:\n");
printf("姓名: %s\n", url_decode(name));
printf("邮箱: %s\n", url_decode(email));
printf("留言: %s\n", url_decode(message_content));
// 发送前端期望的JSON响应
const char *response =
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 27\r\n"
"Connection: close\r\n"
"\r\n"
"{\"callback\":\"提交成功\"}";
send(*conn_fd, response, strlen(response), 0);
} else {
// 没有找到请求体
const char *response =
"HTTP/1.1 400 Bad Request\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 39\r\n"
"Connection: close\r\n"
"\r\n"
"{\"error\":\"请求体缺失\"}";
send(*conn_fd, response, strlen(response), 0);
}
}
}
else
{
return -1;
}
return 0;
}
/**
* @function: void send_http_response(int sockfd, const char* file_path)
* @description: 发送http响应报文, 包括 响应头 响应文件数据 等
* @return
* @note:
* @param {int*} sockfd 与客户端连接的socket文件描述符
* @param {char*} file_path 需要发送的文件路径
*/
void send_http_response(int sockfd, const char *file_path)
{
struct stat file_stat; /* stat函数的传出采纳数 */
char buffer[(int)pow(2, 17)]; /* 用于读取文件和发送数据的缓冲区 */
ssize_t bytes_read; /* 用于接收read返回的数据长度 */
if (stat(file_path, &file_stat) != 0)
{
/* 文件不存在或无法获取状态 */
const char *response_404 =
"HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/html\r\n"
"Content-Length: 133\r\n"
"Connection: close\r\n"
"\r\n"
"<html><body><h1>404 Not Found</h1><p>The requested file was not found on this server.</p></body></html>";
send(sockfd, response_404, strlen(response_404), 0);
close(sockfd);
return;
}
int fd = open(file_path, O_RDONLY);
if (fd == -1)
{
/* 打开文件失败 */
const char *response_500 =
"HTTP/1.1 500 Internal Server Error\r\n"
"Content-Type: text/html\r\n"
"Content-Length: 151\r\n"
"Connection: close\r\n"
"\r\n"
"<html><body><h1>500 Internal Server Error</h1><p>Failed to open the requested file.</p></body></html>";
send(sockfd, response_500, strlen(response_500), 0);
close(sockfd);
return;
}
printf("打开文件%s\n", file_path);
const char *mime_type = get_mime_type(file_path);
/* 发送HTTP响应头 */
char response_header[2048];
snprintf(response_header, sizeof(response_header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Connection: keep-alive\r\n"
"\r\n",
mime_type, file_stat.st_size);
send(sockfd, response_header, strlen(response_header), 0);
/* 循环读取并发送文件内容 */
while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0)
{
ssize_t sent = send(sockfd, buffer, bytes_read, 0);
if (sent < 0)
{
perror("send failed");
break;
}
}
printf("bytes_read: %zd\n", bytes_read);
close(fd); /* 关闭文件描述符 */
}
/**
* @function: char *url_decode(const char *src)
* @description: 将post请求解码为姓名、邮箱、留言三项
* @return
* @note:
* @param {int*} sockfd 与客户端连接的socket文件描述符
* @param {char*} file_path 需要发送的文件路径
*/
char *url_decode(const char *src) {
if (!src) return NULL;
size_t src_len = strlen(src);
char *decoded = malloc(src_len + 1);
char *dst = decoded;
while (*src) {
if (*src == '+') {
*dst++ = ' ';
src++;
} else if (*src == '%' && isxdigit(src[1]) && isxdigit(src[2])) {
char hex[3] = {src[1], src[2], '\0'};
*dst++ = (char)strtol(hex, NULL, 16);
src += 3;
} else {
*dst++ = *src++;
}
}
*dst = '\0';
return decoded;
}
/**
* @function: const char* get_mime_type(const char* filename)
* @description: MIME类型映射, 获取发送给客户端文件名称的后缀名, 以生成对应Content-Type参数
* @return 返回存储Content-Type参数的指针
* @note: 根据提供的web页面, 只涉及了其中三种Content-Type参数
* @param {char*} filename 要发送给客户端的路径名或文件名
*/
const char *get_mime_type(const char *filename)
{
const char *dot = NULL; /* 接收strrchr返回参数,存储文件后缀名 */
dot = strrchr(filename, '.'); /* strrchr获取指定字符的最后一次出现 */
/* 根据文件后缀名生成对应Content-Type参数 */
if (dot == NULL)
return "application/octet-stream";
if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
return "text/html";
else if (strcmp(dot, ".css") == 0)
return "text/css";
else if (strcmp(dot, ".png") == 0 || strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
return "image/jpeg"; /* 注意:这里为了简化,JPEG和PNG都使用了image/jpeg,实际应分开处理 */
return "application/octet-stream";
}
没有问题了,不改代码,就统一一下格式,将对Post和get的处理分别封装为一个函数,完善函数注释,注释统一为/**/格式,记住,不要修改代码逻辑
最新发布