S/C模型(下),利用UDP、TCP协议和多线程、多路复用实现server的局域网搜索响应与定向实时通信连接

一、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,且每个客服都在不停的发送信息,那此时本文的思路是否还有足够的资源来进行信息的显示,是否有足够的资源来添加新的客户端呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值