一、S/C模型主要功能流程图
与上篇对应,这篇主要来实现server端各项功能,不同于client的大体单线程流程,server主要分为三个大线程来实现UDP搜索响应,TCP的连接与接收数据的控制和键盘键入的定向连接数据发送,当然,我这边设计的三个线程都是常驻运行的(无限循环,没有出口,有阻断),除非报错,毕竟服务器嘛,辛苦一点也是应该的吧(~ ̄▽ ̄)~。还有注意的一点是这次本文把tcp接收accpet放到了epoll下,其实是我上一篇画错了( •̀ ω •́ )y(上一篇也已经改正了),以免误会特此声明😏
二、预定义宏
2.1介绍及使用
在正式开始server构建流程之前,先介绍c语言的预定义宏这一优化功能,虽然现在的代码和流程量不足以完全发挥它的性能,甚至没必要使用,但当工程量复杂起来它的作用就越发重要,在后期的日志系统我们也会更加频繁的见到它。那么预定义宏是什么呢?预定义宏是编译器在编译过程中自动定义的特殊标识符,它们提供了关于编译环境、编译时间、文件信息等的内置信息,无需#define而可以直接使用,这些宏通常用于调试、日志记录、条件编译等场景
#include <stdio.h>
/*****************************************************************************
函数名称 : errorposition
功能描述 : 在报错时调用,打印报错的具体信息
输入参数 : function调用的函数名预定义宏,line调用的行数预定义宏
输出参数 : 无
返 回 值 : int
*****************************************************************************/
int errorposition(const char *function, int line)
{
printf("Present date: %s\n", __DATE__); // 当前日期
printf("Present time: %s\n", __TIME__); // 当前时间
printf("File Fame: %s\n", __FILE__); // 文件名
printf("Present Function: %s\n", function); // 函数名
printf("Present Line: %d\n", line); // 所在行
return 0;
}
//argc为传入参数的个数count,argv为传入的参数值value,char **argv也可以
int main(int argc, char *argv[])
{
for(int i = 0; i < argc; i++)
{
printf("cmd input argv%d :%s\n", i, argv[i]);
}
errorposition(__func__,__LINE__);
}
预定义宏类似于C语言stdio.h内置了全局变量int a = 0;可以直接调用a一样,是不是很简单?😎所以这里还对一个更简单的main函数的int argc, char *argv[]作了解释和打印,那么就让我们一起看一下吧
2.2展示
1为代码运行时传入的参数*agcv,2是参数0默认指向本身的运行指令。后面就是预定义宏打印了,时间所在行等一目了然,这样如果哪里出现了问题,就能快速定位问题所在的具体函数和行数了。接下来正式进入流程模块
三、UDP响应
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#define PORT 8989
#define MESS "I am here!"
extern int errorposition();
/*****************************************************************************
函数名称 : UDPlink
功能描述 : 监听所有网络接口和特定端口,在接收到UDP广播消息后回应口令
输入参数 : 无
输出参数 : 无
返 回 值 : int
*****************************************************************************/
void * UDPlink(void *arg)
{
// 创建UDP套接字socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("udp socket error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
// 绑定监听自身所有地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
servaddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
printf("selfip: %s\n", inet_ntoa(servaddr.sin_addr));
if (ret < 0)
{
perror("udp bind error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
printf("UDP broadcast client is listening on port %d\n", PORT);
// 设置数据储存位置
char recvline[1024];
struct sockaddr_in cliaddr;
bzero(&cliaddr, sizeof(cliaddr)); //同menset
socklen_t addrlen = sizeof(cliaddr);
// 接收广播数据并原路发送识别口令MESS
while (1)
{
int n = recvfrom(sockfd, recvline, sizeof(recvline), 0, (struct sockaddr *)&cliaddr, &addrlen);
if (n > 0)
{
printf("recv sockfd= [%s], cliIP =%s, cliPort =%d\n",
recvline, inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port);
int ret = sendto(sockfd, MESS, sizeof(MESS), 0, (struct sockaddr *)&cliaddr, addrlen);
if (ret < 0)
{
perror("sendto error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
}
else
{
perror("recvfrom error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
}
return 0;
}
突然发现这里没什么重要点可以讲,索性直接贴源码了,下面未解释的地方也是同理,不懂的可以直接跳转上一章😜
S/C模型(上),利用UDP广播,TCP协议和多线程实现client的局域网设备搜索与实时通信连接
四、TCP连接与多路复用
4.1 TCP初始化
// 创建TCP socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0)
{
perror("tcp socket error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
// 设置服务器地址
struct sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定socket
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0)
{
perror("tcp bind error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
// 监听连接请求
if (listen(server_fd, LISTEN_BACKLOG) < 0)
{
perror("tcp listen error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
4.2 epoll初始化
/*****************************************************************************
函数原型 : #include <sys/epoll.h>
int epoll_create1(int flags)
功能描述 : 创建 epoll 实例
输入参数 : int flags: flags 参数为 0时,则 epoll_create1 的行为与 epoll_create 相同
也可以设置“执行时关闭”(FD_CLOEXEC)标志
输出参数 : 无
返 回 值 : int 成功返回非负值的文件描述符,失败返回-1
*****************************************************************************/
int epoll_fd = epoll_create1(0);
if (epoll_fd < 0)
{
perror("epoll_createl error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
/*****************************************************************************
函数原型 : #include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
功能描述 : epoll 机制的一部分,用于控制和修改 epoll 实例中的文件描述符(FD)的监控状态
输入参数 : int epfd: epoll 实例的文件描述符
int op: 指定要执行的操作类型
EPOLL_CTL_ADD: 添加新的 FD 到 epfd 监控的集合中。
EPOLL_CTL_MOD: 修改已经在 epfd 监控集合中的 FD 的事件。
EPOLL_CTL_DEL: 从 epfd 监控集合中删除 FD。
int fd: 要监听操作的文件描述符
struct epoll_event *event: 指向 epoll_event 结构体的指针
struct epoll_event
{
uint32_t events;
epoll_data_t data;
};
uint32_t events: 指定要监听的事件类型,常见的事件标志包括:
EPOLLIN:表示文件描述符可读(有数据可读)。
EPOLLOUT:表示文件描述符可写(发送缓冲区有足够空间)。
EPOLLERR:表示文件描述符发生错误。
EPOLLHUP:表示文件描述符被挂断。
EPOLLET:将 epoll 实例设置为边缘触发模式。
EPOLLONESHOT:表示只监听一次事件,之后需要重新设置。
使用:可以通过按位或操作(|)组合多个事件标志,例如 EPOLLIN | EPOLLOUT。
epoll_data_t data: 联合体,用于存储与事件相关联的用户数据
void* ptr:指向用户定义数据的指针。
int fd:文件描述符。
uint32_t u32:32位无符号整数。
uint64_t u64:64位无符号整数。
输出参数 : 无
返 回 值 : int 成功返回0,失败返回-1
*****************************************************************************/
struct epoll_event event;
event.data.fd = server_fd;
event.events = EPOLLIN;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0)
{
perror("epoll_ctl error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
此处为创建epoll实例,将服务器socket添加到epoll监控,进行event.events = EPOLLIN输入到服务器socket事件的监听初始化。
4.3 epoll事件处理
4.3.1 TCP连接确认
// 事件循环
struct epoll_event events[MAX_EVENTS];
struct sockaddr_in clientaddr;
socklen_t clientaddr_len = sizeof(clientaddr);
int new_fd;
char buffer[1024];
while (1)
{
/*****************************************************************************
函数原型 : #include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
功能描述 : 等待 epoll 实例中的 I/O 事件,并将发生的事件填充到events数组中
输入参数 : int epfd: epoll 实例的文件描述符
struct epoll_event *events: 指向 epoll_event 结构体数组的指针,用于接收 epoll 实例中发生的事件
int maxevents: events 数组的最大长度,即可以接收的最大事件数量
int timeout: 等待事件的最长时间,单位为毫秒
-1:无限期等待,直到有事件发生。
0:非阻塞模式,立即返回,如果有事件已经发生则返回,否则返回 0。
> 0:等待指定的超时时间。
输出参数 : 无
返 回 值 : int 成功返回 events 数组中填充的事件数量,失败返回-1
*****************************************************************************/
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (n < 0)
{
perror("epoll_wait error");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
for (int i = 0; i < n; i++)
{
// 检测是否为服务器套接字(server_fd)的输入事件(connect连接请求)
if (events[i].data.fd == server_fd)
{
// 接受新的连接,socklen_t 是一个无符号整数类型,第三参数为指向socklen_t变量的指针
new_fd = accept(server_fd, (struct sockaddr *)&clientaddr, &clientaddr_len);
if (new_fd < 0)
{
perror("accept error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
printf("client%d连接成功\n", new_fd);
// 为新连接设置epoll监听的输入事件
event.data.fd = new_fd;
event.events = EPOLLIN;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_fd, &event) < 0)
{
perror("epoll_ctl new_fd error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
}
4.3.2 通信的接收与打印
else if (events[i].events & EPOLLIN)
{
// 接收数据
memset(buffer, 0, sizeof(buffer));
int count = recv(events[i].data.fd, buffer, sizeof(buffer), 0);
if (count < 0)
{
perror("read error\n");
errorposition(__func__,__LINE__);
exit(EXIT_FAILURE);
}
// 客户端关闭连接
else if (count == 0)
{
printf("client%d已断开连接\n", events[i].data.fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
// 当文件描述符被关闭后,操作系统会自动释放与该文件描述符相关的资源,包括内存、文件句柄等
close(events[i].data.fd);
}
else
{
printf("recevie from client%d: %s", events[i].data.fd, buffer);
}
}
}
}
这里可能大家会疑问if (events[i].data.fd == server_fd)与else if (events[i].events & EPOLLIN)并不互斥,为何触发的连接输入与通信输入事件不会混淆?
因为一开始的连接输入事件是未知名的,所以会走accept路线,但是accept之后为其赋予new_fd句柄并加入到epoll_fd的event中了,所以后续触发的通信输入事件便成为了有名分new_fd(event.data.fd储存的int值)的事件,故if (events[i].data.fd == server_fd)不成立,转而进行else if (events[i].events & EPOLLIN)的判断。
五、指定目标client发送
// 获取标准输入并发送指定目标
void *get_thread(void *arg)
{
int sendname;
printf("send start\n");
char send_buf[1024];
while (1)
{
if (strlen(send_buf) == 0)
{
fgets(send_buf, sizeof(send_buf), stdin);
//"***\n\0"——>"***\0\0"
send_buf[strcspn(send_buf, "\n")] = 0;
}
if (strlen(send_buf) != 0)
{
printf("input send number: \n");
scanf("%d", &sendname);
int bytes_sent = send(sendname, send_buf, sizeof(send_buf), 0);
if (bytes_sent < 0)
{
printf("Client%d send failed.\n", sendname);
}
memset(send_buf, 0, sizeof(send_buf));
}
}
pthread_exit(NULL);
}
特地分成两个if是防止send_buf出现残留,指定client即accpet接收的套接字句柄printf("client%d连接成功\n", new_fd)
六、展示
6.1 server服务端
6.2 client6服务端
6.2 client7服务端
七、总结
可以看出本篇测试的都是同一台服务器的不同窗口(端口),所以ip地址都一样,当然用多台设备也可以,不过要交叉编译和传输比较麻烦,原版代码已经多机验证成功,但本篇的代码是重制版,如有问题或更好思路也可以交流讨论😋,那么最后本文再提一个问题,如果客户端数量达到C10K,且每个客服都在不停的发送信息,那此时本文的思路是否还有足够的资源来进行信息的显示,是否有足够的资源来添加新的客户端呢?