目录
前言
为了降低文章冗余度,编程环境以及编译命令在此篇中不再提及,可翻看本系列的前面两篇博文进行了解:
网络编程 · 代码笔记1
网络编程 · 代码笔记2
0161epoll_更高效的IO复用技术_条件触发_服务端
相关代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define BUF_SIZE 100
#define EPOLL_SIZE 50
int main(int argc, char const *argv[])
{
// 创建一个TCP套接字
int serv_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
printf("serv_sock:%d\n", serv_sock);
// 初始化服务器地址结构
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); // 将地址结构清零
serv_addr.sin_family = AF_INET; // 设置地址家族为IPv4
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置服务器地址为任意地址
serv_addr.sin_port = htons(1234); // 设置服务器端口号为1234
// 将套接字与服务器地址绑定
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
{
perror("bind函数执行失败,绑定服务器信息失败!"); // 绑定失败,打印错误信息
close(serv_sock); // 关闭套接字
return 0; // 返回0表示程序执行失败
}
// 在套接字上监听,设置最大连接数为20
if (listen(serv_sock, 20) == -1)
{
perror("listen 侦听失败!"); // 监听失败,打印错误信息
close(serv_sock); // 关闭套接字
return 0; // 返回0表示程序执行失败
}
// 创建一个epoll实例
int epoll_fd = epoll_create(EPOLL_SIZE);
printf("epoll_fd:%d\n", epoll_fd);
// 初始化一个epoll事件结构
struct epoll_event event;
event.events = EPOLLIN; // 设置事件类型为可读
event.data.fd = serv_sock; // 设置事件相关的文件描述符为服务器套接字
// 将服务器套接字添加到epoll实例中,以便监听其事件
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, serv_sock, &event);
// 分配epoll事件数组空间,用来存放有活动的文件描述符的信息
struct epoll_event *epoll_events_p = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
memset(epoll_events_p, 0, (sizeof(struct epoll_event) * EPOLL_SIZE));
while (1)
{
// 等待epoll事件,当有事件发生时返回事件的个数,-1表示无限等待
int event_cnt = epoll_wait(epoll_fd, epoll_events_p, EPOLL_SIZE, -1);
// 如果epoll_wait执行出错,打印错误信息并退出循环
if (event_cnt == -1)
{
printf("epoll_wait() 函数执行错误!\n");
break;
}
// 遍历所有的事件
for (int i = 0; i < event_cnt; i++)
{
// 如果是服务器套接字的事件,表示有新的连接
if (epoll_events_p[i].data.fd == serv_sock)
{
struct sockaddr_in clnt_addr;
// 初始化客户端地址结构
memset(&clnt_addr, 0, sizeof(clnt_addr));
socklen_t clnt_addr_size = sizeof(clnt_addr);
// 接受新的连接,并获取客户端的套接字
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
event.events = EPOLLIN;
event.data.fd = clnt_sock;
// 将新的客户端套接字添加到epoll的事件集中
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client:%d\n", clnt_sock);
}
// 如果是已连接客户端的事件
else
{
char buf[BUF_SIZE] = {0};
// 读取客户端发送的数据
int str_len = read(epoll_events_p[i].data.fd, buf, BUF_SIZE);
// 如果读取的字节数为0,表示客户端关闭了连接
if (str_len == 0)
{
// 从epoll的事件集中移除该客户端套接字
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events_p[i].data.fd, NULL);
// 关闭客户端套接字
close(epoll_events_p[i].data.fd);
printf("已关闭连接的客户端:%d\n", epoll_events_p[i].data.fd);
}
// 如果读取到数据,回显给客户端
else
{
write(epoll_events_p[i].data.fd, buf, str_len);
}
}
}
}
free(epoll_events_p);
close(serv_sock);
close(epoll_fd);
return 0;
}
0162epoll_更高效的IO复用技术_条件触发_客户端
相关代码:
#include <stdio.h> // 引入标准输入输出库
#include <string.h> // 引入字符串操作库
#include <stdlib.h> // 引入标准库
#include <unistd.h> // 引入Unix标准函数库
#include <arpa/inet.h> // 引入IP地址转换库
#include <sys/socket.h> // 引入套接字库
#define BUF_SIZE 1024 // 定义缓冲区大小
int main(int argc, char const *argv[])
{
// 创建客户端套接字
int clnt_sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr; // 定义服务器地址结构体
memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构体
serv_addr.sin_family = AF_INET; // 设置地址家族为IPv4
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置服务器IP地址为本地回环地址
serv_addr.sin_port = htons(1234); // 设置服务器端口号为1234
// 连接服务器
connect(clnt_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
while (1)
{
// 提示用户输入信息
fputs("请输入信息,输入“Q”退出:", stdout);
char message[BUF_SIZE] = {0}; // 初始化消息缓冲区
fgets(message, BUF_SIZE, stdin); // 从标准输入读取一行数据到消息缓冲区
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) // 判断用户是否输入"Q"或"q"
break; // 如果是,跳出循环
write(clnt_sock, message, strlen(message)); // 向服务器发送消息
int result = read(clnt_sock, message, BUF_SIZE - 1); // 从服务器接收消息
message[result] = 0; // 设置消息结束符
printf("来自服务器的信息为:%s\n", message); // 打印接收到的消息
}
close(clnt_sock); // 关闭客户端套接字
return 0; // 程序结束
}
运行结果:
0171epoll_更高效的IO复用技术_边缘触发_服务端
相关代码:
#include <stdio.h> // 引入标准输入输出库
#include <stdlib.h> // 引入标准库,提供多种函数
#include <string.h> // 引入字符串操作库
#include <unistd.h> // 引入Unix标准函数库
#include <arpa/inet.h> // 引入IP地址转换库
#include <sys/socket.h> // 引入套接字库
#include <sys/epoll.h> // 引入epoll库
#include <fcntl.h> // 引入文件控制库
#include <errno.h> // 引入错误号库
#define BUF_SIZE 4 // 定义缓冲区大小
#define EPOLL_SIZE 50 // 定义epoll事件表大小
int main(int argc, char const *argv[])
{
int serv_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建TCP套接字
struct sockaddr_in serv_addr; // 定义服务器地址结构体
memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构体为0
serv_addr.sin_family = AF_INET; // 设置地址家族为IPv4
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置服务器IP地址为任意地址
serv_addr.sin_port = htons(1234); // 设置服务器端口号为1234
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) // 绑定服务器信息
{
perror("bind函数执行失败,绑定服务器信息失败!"); // 输出错误信息
close(serv_sock); // 关闭套接字
return 0; // 程序退出
}
if (listen(serv_sock, 20) == -1) // 设置监听套接字
{
perror("listen 侦听失败!"); // 输出错误信息
close(serv_sock); // 关闭套接字
return 0; // 程序退出
}
struct epoll_event event; // 定义epoll事件结构体
event.events = EPOLLIN; // 设置事件为可读
event.data.fd = serv_sock; // 设置事件相关的文件描述符为服务器套接字
int epfd = epoll_create(EPOLL_SIZE); // 创建epoll实例
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); // 将服务器套接字添加到epoll事件表中
// 分配内存,用于存储epoll事件表
struct epoll_event *epoll_events_p = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
while (1)
{
// 等待epoll事件发生
int event_cnt = epoll_wait(epfd, epoll_events_p, EPOLL_SIZE, -1);
// 如果epoll_wait执行出错,输出错误信息并退出循环
if (event_cnt == -1)
{
printf("epoll_wait() 函数执行错误!\n");
break;
}
printf("epoll_wait 执行成功!\n"); // 如果epoll_wait执行成功,输出成功信息
for (int i = 0; i < event_cnt; i++)
{
// 如果事件发生在服务器套接字上,表示有新的客户端连接
if (epoll_events_p[i].data.fd == serv_sock)
{
struct sockaddr_in clnt_addr; // 定义客户端地址结构体
socklen_t adr_sz = sizeof(clnt_addr); // 获取地址结构体大小
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &adr_sz); // 接受客户端连接
/* 设置为非阻塞模式,即使没有数据可读,read 调用也会立即返回,而不是阻塞。 */
int flag = fcntl(clnt_sock, F_GETFL, 0); // 获取客户端套接字文件状态标志
fcntl(clnt_sock, F_SETFL, flag | O_NONBLOCK); // 设置客户端套接字为非阻塞模式
event.events = EPOLLIN | EPOLLET; // 设置事件为可读且边缘触发模式
event.data.fd = clnt_sock; // 设置事件相关的文件描述符为客户端套接字
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); // 将客户端套接字添加到epoll事件表中
printf("connected client:%d\n", clnt_sock); // 输出客户端连接信息
}
// 如果事件发生在其他套接字上,表示有数据可读或连接已关闭
else
{
while (1)
{
char buf[BUF_SIZE] = {0}; // 定义缓冲区并初始化为0
int str_len = read(epoll_events_p[i].data.fd, buf, BUF_SIZE); // 从套接字读取数据
if (str_len < 0)
{
if (errno == EAGAIN)
{
break; // 如果读取操作会阻塞,跳出循环
}
}
if (str_len == 0)
{
epoll_ctl(epfd, EPOLL_CTL_DEL, epoll_events_p[i].data.fd, NULL); // 从epoll事件表中删除套接字
close(epoll_events_p[i].data.fd); // 关闭套接字
printf("已关闭连接的客户端:%d\n", epoll_events_p[i].data.fd); // 输出关闭连接信息
break;
}
else
{
write(epoll_events_p[i].data.fd, buf, str_len); // 将读取的数据写回套接字
}
}
}
}
}
free(epoll_events_p);
close(serv_sock);
close(epfd);
return 0;
}
0172epoll_更高效的IO复用技术_边缘触发_客户端
相关代码:
#include <stdio.h> // 引入标准输入输出库
#include <string.h> // 引入字符串操作库
#include <stdlib.h> // 引入标准库,提供多种函数
#include <unistd.h> // 引入unistd库,提供对POSIX操作系统的API
#include <arpa/inet.h> // 引入arpa/inet.h库,提供网络编程相关的函数和结构体
#include <sys/socket.h> // 引入sys/socket.h库,提供socket相关的函数
#include <fcntl.h> // 引入文件控制头文件,提供文件控制相关的函数
#include <errno.h> // 引入errno.h库,提供错误号相关的宏定义
#define BUF_SIZE 1024 // 定义缓冲区大小为1024字节
int main(int argc, char const *argv[])
{
int sock = socket(AF_INET, SOCK_STREAM, 0); // 创建一个IPv4的TCP流式socket
struct sockaddr_in serv_addr; // 定义一个IPv4的socket地址结构
memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化serv_addr结构体为0
serv_addr.sin_family = AF_INET; // 设置地址家族为IPv4
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置服务器的IP地址为本地回环地址
serv_addr.sin_port = htons(1234); // 设置服务器的端口号为1234,并进行网络字节序的转换
// 尝试连接到服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
{
printf("connect() 函数执行错误,连接服务器失败!\n"); // 连接失败,打印错误信息
close(sock); // 关闭socket
return -1; // 返回-1表示程序执行失败
}
int flag = fcntl(sock, F_GETFL, 0); // 获取当前socket的文件状态标志
fcntl(sock, F_SETFL, flag | O_NONBLOCK); // 设置socket为非阻塞模式
while (1)
{
fputs("输入信息,输入'Q'退出程序:", stdout); // 提示用户输入信息
char message[BUF_SIZE] = {0}; // 初始化消息缓冲区
fgets(message, BUF_SIZE, stdin); // 从标准输入读取一行数据到message
// 如果用户输入的是'q'或'Q',则退出循环
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
write(sock, message, strlen(message)); // 将用户输入的消息写入socket,发送给服务器
sleep(2); // 程序暂停2秒
printf("服务器的信息为:"); // 打印提示信息,准备接收服务器响应
while (1)
{
memset(message, 0, sizeof(message)); // 清空消息缓冲区
int str_len = read(sock, message, BUF_SIZE - 1); // 从socket读取数据到message
if (str_len <= 0) // 如果读取的字节数小于等于0
{
if (errno == EAGAIN) // 如果错误号是EAGAIN,表示没有数据可读
{
break; // 退出内层循环
}
}
else
{
printf("%s", message); // 打印从服务器接收到的消息
}
}
}
close(sock);
return 0;
}
运行结果:
注意!客户端发送信息给服务端后会进入阻塞状态 2 秒,期间服务器发送数据过来会存储在客户端的输入缓冲区中,等客户端线程重新获取到cpu时,进入运行状态后才将数据读取出来,并显示到终端中
0180epoll模拟HTTP服务器
在当前文件夹下需要有两个文件,一个是error.html,另一个是home.html,其相关代码参考 网络编程 · 代码笔记2 的 “0120HTTP服务器”内容。
相关代码:
#include <stdio.h> // 引入标准输入输出库
#include <string.h> // 引入字符串操作库
#include <stdlib.h> // 引入标准库,提供多种函数
#include <unistd.h> // 引入unistd库,提供对POSIX操作系统的API
#include <arpa/inet.h> // 引入arpa/inet.h库,提供网络编程相关的函数和结构体
#include <sys/socket.h> // 引入sys/socket.h库,提供socket相关的函数
#include <sys/epoll.h> // 引入sys/epoll.h库,提供epoll相关的函数和结构体
#include <pthread.h> // 引入pthread.h库,提供多线程编程相关的函数和结构体
#include <stdbool.h> // 引入stdbool.h库,提供布尔类型的定义
#include <fcntl.h> // 引入文件控制头文件,提供文件控制相关的函数
#include <errno.h> // 引入errno.h库,提供错误号相关的宏定义
#define BUFFLEN 1024 // 定义缓冲区的大小为1024字节
#define SERVER_PORT 1234 // 定义服务器的端口号为1234
#define HTTP_FILENAME_LEN 256 // 定义HTTP文件名的长度限制为256字节
#define EPOLL_SIZE 1000 // 定义epoll事件表的大小为1000
/**
* @brief HTTP响应头模板
*
* 用于构造HTTP响应头,包含状态行、服务器信息、内容类型等。
*/
char *http_res_hdr_tmpl = "HTTP/1.1 200 OK\nServer: bianchengbang\n"
"Accept-Ranges: bytes\nContent-Length: %d\nConnection: closed\n"
"Content-Type: %s\n\n";
/**
* @brief 客户端信息结构体
*
* 用于存储每个客户端的epoll文件描述符和socket文件描述符。
*/
typedef struct info
{
int epoll_fd; // epoll文件描述符
int fd; // socket文件描述符
} info;
/**
* @brief 文档类型结构体
*
* 用于存储文件后缀名和对应的MIME类型。
*/
struct doc_type
{
char *suffix; // 文件后缀名
char *type; // MIME类型
};
/**
* @brief 文件类型数组
*
* 存储了常见的文件后缀名及其对应的MIME类型。
*/
struct doc_type file_type[] = {
{"html", "text/html"},
{"ico", "image/x-icon"},
{"png", "image/png"},
{"js", "application/x-javascript"},
{"css", "text/css"},
{"jpg", "image/jpeg"},
{"gif", "image/gif"},
{NULL, NULL}};
/**
* @brief 错误处理函数
*
* @param str 错误信息
*/
void error_die(const char *str)
{
perror(str); // 打印错误信息
exit(-1); // 非正常退出程序
}
/**
* @brief 将文件描述符添加到epoll的事件表中
*
* 这个函数用于将一个文件描述符添加到epoll的事件表中。
* epoll是Linux内核中的一种I/O多路复用机制,可以高效地处理大量的文件描述符。
*
* @param epfd epoll文件描述符
* @param fd 需要添加的文件描述符
* @param events 监听的事件类型
*/
void addfd(int epfd, int fd, int events)
{
struct epoll_event event; // 定义一个epoll事件结构体
event.events = events; // 设置要监听的事件类型
event.data.fd = fd; // 设置文件描述符
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); // 将事件添加到epoll的事件表中
}
/**
* @brief 解析HTTP请求中的命令
*
* 这个函数用于解析HTTP请求中的命令,提取文件名和文件后缀。
*
* @param buf HTTP请求缓冲区
* @param file_name 存放文件名的缓冲区
* @param suffix 存放文件后缀的缓冲区
*/
void http_parse_request_cmd(char *buf, char *file_name, char *suffix)
{
int file_length = 0, suffix_length = 0; // 定义文件名和后缀的长度变量
char *begin = strchr(buf, ' '); // 查找第一个空格,HTTP请求方法后通常跟有一个空格
begin += 1; // 移动到空格后的位置,即URL的开始
char *end = strchr(begin, ' '); // 查找第二个空格,URL后通常跟有一个空格
*end = 0; // 将第二个空格置为字符串结束符,以便获取完整的URL
file_length = end - begin - 1; // 计算文件名的长度
memcpy(file_name, begin + 1, file_length); // 将文件名复制到file_name缓冲区
file_name[file_length] = 0; // 确保文件名以null字符结束
char *bias = strrchr(begin, '/'); // 从URL中查找最后一个'/',用于确定文件后缀的位置
suffix_length = end - bias; // 计算文件后缀的长度
// 如果最后一个'/'后面直接是文件名,没有路径
if (*bias == '/')
{
bias++; // 移动到文件名的开始
suffix_length--; // 减去'/'的长度
}
// 如果找到了文件后缀
if (suffix_length > 0)
{
begin = strchr(file_name, '.'); // 在文件名中查找'.',以确定后缀的开始
if (begin) // 如果找到了'.'
strcpy(suffix, begin + 1); // 复制文件后缀到suffix缓冲区
}
}
/**
* @brief 根据文件后缀名获取HTTP内容类型
*
* @param suffix 文件后缀名
* @return char* 对应的HTTP内容类型(MIME类型),如果未找到则返回NULL
*/
char *http_get_type_by_suffix(const char *suffix)
{
struct doc_type *type = NULL; // 初始化类型指针为NULL
for (type = file_type; type->suffix; type++) // 遍历文件类型数组
{
if (strcmp(type->suffix, suffix) == 0) // 如果找到匹配的后缀名
return type->type; // 返回对应的MIME类型
}
return NULL; // 如果没有找到匹配的后缀名,返回NULL
}
/**
* @brief 初始化服务器
*
* @return int 返回服务器socket的文件描述符,如果初始化失败则返回-1
*/
int serv_init(void)
{
int serv_sock; // 服务器socket文件描述符
if ((serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) // 创建TCP套接字
{
error_die("套接字创建失败:"); // 如果创建失败,打印错误信息并退出
}
struct sockaddr_in serv_addr; // 服务器socket地址结构
memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构为0
serv_addr.sin_family = AF_INET; // 设置地址家族为IPv4
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置服务器地址为任意可用的IP地址
serv_addr.sin_port = htons(SERVER_PORT); // 设置服务器端口号,并进行网络字节序的转换
// 设置SO_REUSEADDR来允许端口复用
int reuse = 1;
if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&reuse, sizeof(reuse)) < 0)
{
perror("设置SO_REUSEADDR 1 失败");
return -1;
}
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) // 绑定地址到socket
{
error_die("bind() 执行失败:"); // 如果绑定失败,打印错误信息并关闭socket
close(serv_sock); // 关闭socket
}
if (listen(serv_sock, SOMAXCONN) == -1) // 监听socket,等待客户端连接
{
error_die("listen() 执行失败:"); // 如果监听失败,打印错误信息并关闭socket
close(serv_sock); // 关闭socket
}
return serv_sock; // 返回服务器socket的文件描述符
}
/**
* @brief 处理客户端HTTP请求的多线程函数
* 此函数用于网络编程中的HTTP服务器,负责接收客户端的请求,解析请求内容,
* 根据请求的资源发送相应的文件或错误页面给客户端,并管理socket连接。
* 函数首先将线程设置为分离状态,然后在一个无限循环中读取客户端socket的数据,
* 根据读取结果进行不同的处理,包括错误处理、连接断开处理和HTTP请求处理。
* 在HTTP请求处理中,函数将解析请求中的文件名和后缀,根据后缀获取MIME类型,
* 如果文件类型未找到或文件不存在,则发送错误页面给客户端。最后,函数将发送
* HTTP响应头和文件内容给客户端,并在发送完毕后关闭socket连接。
*
* @param args 指向info结构体的指针,包含epoll描述符和客户端socket描述符
* @return void* 返回NULL
*/
void *threadFun(void *args)
{
// 将当前线程设置为分离状态,线程结束后自动释放资源
pthread_detach(pthread_self());
// 将传入的参数转换为info结构指针,并获取epoll文件描述符和客户端socket文件描述符
info *fds = (info *)args;
int epoll_fd = fds->epoll_fd;
int clnt_sock = fds->fd;
// 释放info结构占用的内存
free(fds);
// 循环读取客户端socket的数据
while (1)
{
char buff[BUFFLEN] = {0};
// 清空缓冲区
memset(buff, 0, BUFFLEN);
// 从客户端socket读取数据到缓冲区
int str_len = read(clnt_sock, buff, BUFFLEN);
if (str_len < 0)
{
// 如果读取错误是由于没有数据可读,打印提示信息
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
printf("循环读完所有数据\n");
}
// 关闭客户端socket
close(clnt_sock);
// 退出循环
break;
}
// 如果读取到的数据长度为0,表示客户端已断开连接
else if (str_len == 0)
{
// 从epoll实例中移除客户端socket,关闭客户端socket
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, clnt_sock, NULL);
close(clnt_sock);
printf("和客户端 %d 断开连接\n", clnt_sock);
}
else
{
// 定义文件名和后缀名的字符数组,并初始化为0
char file_name[HTTP_FILENAME_LEN] = {0}, suffix[16] = {0};
// 解析HTTP请求中的命令,获取文件名和后缀名
http_parse_request_cmd(buff, file_name, suffix);
// 定义文件指针,初始化为NULL
FILE *fp = NULL;
// 定义标志变量,用于指示文件是否存在(1存在,0不存在)
int fp_has = 1;
// 定义标志变量,用于指示文件类型是否正确(1正确,0不正确)
int fp_type = 1;
// 根据文件后缀名获取对应的MIME类型
char *mime_type = http_get_type_by_suffix(suffix);
// 如果MIME类型为NULL,表示文件类型不匹配
if (mime_type == NULL)
{
// 设置文件类型错误标志
fp_type = 0;
// 打印文件类型不匹配的信息
printf("访问的文件类型(后缀名)不匹配\n");
// 获取默认的HTML文件的MIME类型
mime_type = http_get_type_by_suffix("html");
// 打开默认的错误页面文件
fp = fopen("error.html", "rb");
}
// 如果MIME类型不为NULL,尝试打开请求的文件
else
{
// 尝试打开请求的文件
fp = fopen(file_name, "rb");
// 如果文件指针为NULL,表示文件不存在
if (fp == NULL)
{
// 设置文件不存在标志
fp_has = 0;
// 打开默认的错误页面文件
fp = fopen("error.html", "rb");
}
}
// 将文件指针移动到文件末尾,用于获取文件长度
fseek(fp, 0, SEEK_END);
// 获取文件的长度
int file_len = ftell(fp);
// 将文件指针移动到文件开头,准备读取文件内容
fseek(fp, 0, SEEK_SET);
// 定义用于存储HTTP响应头的字符数组,并初始化为0
char http_header[BUFFLEN] = {0};
// sprintf函数将文件长度和MIME类型格式化到http_header中,并返回填充的字符数,即响应头的长度
int response_header_len = sprintf(http_header, http_res_hdr_tmpl, file_len, mime_type);
// 将http_header中的响应头发送到客户端,发送的长度为response_header_len
write(clnt_sock, http_header, response_header_len);
// 检查文件类型是否正确
if (fp_type == 1)
{
// 检查文件是否存在
if (fp_has == 0)
{
// 如果文件不存在,打印信息并准备发送errno.html文件
printf("----服务器不存在 %s 文件, error.html 文件发送中...\n", file_name);
// 将文件名替换为errno.html
strcpy(file_name, "error.html");
}
else
{
// 如果文件存在,打印信息并准备发送文件
printf("----服务器存在 %s 文件,发送中...\n", file_name);
}
}
// 清空缓冲区
memset(buff, 0, BUFFLEN);
// 定义用于存储每次读取字节数的变量
int nCount = 0;
// 循环读取文件内容到缓冲区,并写入到客户端socket
while ((nCount = fread(buff, 1, BUFFLEN, fp)) > 0)
{
// 将缓冲区中的内容写入到客户端socket
write(clnt_sock, buff, nCount);
// 清空缓冲区,准备下一次读取
memset(buff, 0, BUFFLEN);
}
// 打印文件发送完毕的信息
printf("----文件 %s 发送完毕\n", file_name);
// 关闭文件指针
fclose(fp);
// 关闭客户端socket的写方向,表示不再发送数据
shutdown(clnt_sock, SHUT_WR);
// 读取客户端可能发送的任何数据(通常是连接关闭的通知)
read(clnt_sock, buff, sizeof(buff));
// 从epoll实例中移除客户端socket
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, clnt_sock, NULL);
// 关闭客户端socket
close(clnt_sock);
// 打印主动断开连接的信息
printf("主动和客户端 %d 断开连接\n", clnt_sock);
}
}
return NULL;
}
/**
* @brief 处理客户端连接请求的主循环函数
* 此函数用于创建epoll实例,监听服务端socket上的活动,接受新的客户端连接,
* 并为每个新的客户端连接创建一个线程来处理HTTP请求。
*
* @param serv_sock 服务端socket的文件描述符
*/
void handle_connect(int serv_sock)
{
// 创建一个epoll实例,并返回epoll文件描述符
int epoll_fd = epoll_create(1);
// 将服务端socket添加到epoll实例中,监听EPOLLIN事件(可读事件)
addfd(epoll_fd, serv_sock, EPOLLIN);
// 分配一个epoll事件结构体数组,大小为EPOLL_SIZE,用于存储epoll_wait返回的事件
struct epoll_event *epoll_events_p = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
while (1)
{
// 等待epoll事件发生,超时时间为10000毫秒(10秒)
// 存储事件的数组为epoll_events_p,数组大小为EPOLL_SIZE
// 返回值event_cnt表示发生事件的数量
int event_cnt = epoll_wait(epoll_fd, epoll_events_p, EPOLL_SIZE, 10000);
// 如果epoll_wait返回-1,表示发生错误,打印错误信息并退出循环
if (event_cnt == -1)
{
perror("epoll_wait() 运行失败:");
break;
}
// 如果event_cnt为0,表示没有事件发生,打印监听中的信息并继续下一次循环
if (event_cnt == 0)
{
continue;
}
// 遍历epoll_wait返回的事件数组
for (int i = 0; i < event_cnt; i++)
{
// 如果事件发生在服务端socket上,表示有新的客户端连接请求
if (epoll_events_p[i].data.fd == serv_sock)
{
struct sockaddr_in clnt_addr;
// 定义客户端地址结构大小
socklen_t clnt_addr_size = sizeof(clnt_addr);
// 接受客户端连接,创建客户端socket,并获取客户端地址信息
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
// 设置客户端socket为非阻塞模式
// 在非阻塞模式下,read() 和 write() 系统调用会立即返回,
// 即使没有数据可读或缓冲区没有空间可写。
fcntl(clnt_sock, F_SETFL, O_NONBLOCK);
// 将客户端socket添加到epoll实例中,并设置监听EPOLLIN | EPOLLET | EPOLLONESHOT事件
// EPOLLIN:监听socket的可读事件,即当socket上有数据可读时触发通知
// EPOLLET:使用边缘触发(ET)模式,epoll仅在文件描述符状态改变时触发一次通知
// EPOLLONESHOT:一次触发后关闭事件,防止多线程同时处理同一个socket的事件,避免竞态条件
addfd(epoll_fd, clnt_sock, EPOLLIN | EPOLLET | EPOLLONESHOT);
// 打印客户端连接成功的信息
printf("客户端%d连接成功\n", clnt_sock);
}
// 如果事件发生在其他已连接的客户端socket上,为这个客户端创建线程处理HTTP请求
else
{
// 分配info结构体内存,用于存储epoll文件描述符和客户端socket文件描述符
info *fds = (info *)malloc(sizeof(info));
fds->epoll_fd = epoll_fd;
fds->fd = epoll_events_p[i].data.fd;
pthread_t threadID;
// 创建线程,执行threadFun函数处理客户端请求
int ret = pthread_create(&threadID, NULL, threadFun, (void *)fds);
// 如果线程创建失败,打印错误信息并退出程序
if (ret != 0)
{
printf("线程创建失败\n");
exit(0);
}
}
}
}
free(epoll_events_p);
}
int main()
{
int serv_sock = serv_init();
handle_connect(serv_sock);
close(serv_sock);
return 0;
}
运行结果:
在浏览器中输入以下网址:
http://127.0.0.1:1234/home.html
显示界面:
服务器输出:
0191基于UDP的多播_接收端
相关代码:
#include <stdio.h> // 引入标准输入输出头文件
#include <stdlib.h> // 引入标准库定义头文件
#include <string.h> // 引入字符串操作头文件
#include <unistd.h> // 引入Unix标准函数头文件
#include <sys/types.h> // 引入系统数据类型头文件
#include <sys/socket.h> // 引入套接字API头文件
#include <arpa/inet.h> // 引入网络地址操作头文件
#define MAX_BUF_SIZE 1024 // 定义最大缓冲区大小
#define PORT 1234 // 定义端口号
#define MULTICAST_GROUP "224.1.1.2" // 定义多播组地址
int main(int argc, char const *argv[])
{
// 创建UDP socket
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0); // 使用IPv4协议,UDP传输协议创建套接字
if (sock_fd < 0)
{
perror("socket"); // 输出创建socket失败的原因
exit(1); // 非正常退出程序
}
// 设置socket端口复用
int reuse = 1;
if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse)) < 0)
{
perror("setsockopt SO_REUSEADDR"); // 输出设置socket选项失败的原因
exit(1); // 非正常退出程序
}
// 绑定socket到本地地址和端口
struct sockaddr_in local_addr; // 定义IPv4的socket地址结构
memset(&local_addr, 0, sizeof(local_addr)); // 初始化地址结构为0
local_addr.sin_family = AF_INET; // 设置地址家族为IPv4
local_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置地址为任意可用的接口地址
local_addr.sin_port = htons(PORT); // 设置端口为之前定义的端口号
if (bind(sock_fd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0)
{
perror("bind"); // 输出绑定socket失败的原因
exit(1); // 非正常退出程序
}
// 加入多播组
struct ip_mreq mreq; // 定义多播组结构
memset(&mreq, 0, sizeof(mreq)); // 初始化多播组结构为0
mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_GROUP); // 设置多播组地址
mreq.imr_interface.s_addr = htonl(INADDR_ANY); // 设置加入多播组的接口为任意接口
if (setsockopt(sock_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
{
perror("setsockopt IP_ADD_MEMBERSHIP"); // 输出设置多播组失败的原因
exit(1); // 非正常退出程序
}
// 接收数据
char buf[MAX_BUF_SIZE]; // 定义接收数据缓冲区
int recv_len = recvfrom(sock_fd, buf, sizeof(buf), 0, NULL, 0); // 从socket接收数据
if (recv_len < 0)
{
perror("recvfrom"); // 输出接收数据失败的原因
exit(1); // 非正常退出程序
}
buf[recv_len] = '\0'; // 确保接收到的数据以null字符结尾
printf("%s\n", buf); // 打印接收到的数据
// 关闭socket
close(sock_fd); // 关闭socket文件描述符
return 0; // 正常退出程序
}
0192基于UDP的多播_发送端
相关代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define MULTICAST_GROUP "224.1.1.2"
#define PORT 1234
#define TTL 64
#define BUF_SIZE 1024
int main()
{
struct sockaddr_in addr;
char buf[BUF_SIZE] = "这是来源于发送端的新消息。";
// 创建UDP套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
perror("socket");
exit(1);
}
// 设置多播TTL
int ttl = TTL;
if ((setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl))) < 0)
{
perror("setsockopt");
exit(1);
}
// 设置目标地址和端口
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(MULTICAST_GROUP);
addr.sin_port = htons(PORT);
// 发送数据
if ((sendto(sock, buf, strlen(buf), 0, (struct sockaddr *)&addr, sizeof(addr))) < 0)
{
perror("sendto");
exit(1);
}
// 关闭套接字
close(sock);
return 0;
}
运行结果:
打开一个发送端和多个接收端,当发送端发送信息的时候,多个接收端将会接收到相同的信息