(三)TCP用select函数处理多个客户端连接(非阻塞模式)

本文介绍如何在服务器端实现非阻塞IO,并利用Select函数监控多个客户端连接。通过修改文件描述符属性实现非阻塞模式,使用Select函数同步监听多客户端数据,附带完整代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这个程序,客户端们通过服务器进行群聊。
主要讲讲两点:1.怎么弄非阻塞模式 。 2.select的粗略讲解(心急的可以跳过,直接看后面代码)


首先,看看这个程序服务端设计的基本逻辑,其实非常简单,就在一个while(1)循环里面不停地轮询 accept 和 select函数。
有人可能问,accept不是会阻塞,直到有客户端连接进来的吗?
其实当你的socket套接字设置成非阻塞模式,那么accept也不会阻塞。

1.那怎么弄非阻塞呢?
这就涉及到 fcntl函数。fcntl能改变文件的属性。看看他的原型:int fcntl(int fd, int cmd);失败返回-1(当然这个函数还有很多用途,但这里只谈谈他如何实现非阻塞)
三行代码:
long val = fcntl(sockfd,F_GETFL); //把sockfd套接字的属性拿出来给val
val|=O_NONBLOCK;		  //把非阻塞的属性O_NONBLOCK加进去,用或运算
fcntl(sockfd,F_SETFL,val);	  //再把做好的属性val,再加到sockfd中去
这时候 把sockfd 作为参数给 accept,accept就不会再阻塞。


2.select()函数:select函数能够同时监听多个文件描述符,若其中一个或多个文件描述符有反应(读或写),select就会返回。
原型: int select(nfds, readfds, writefds, errfds, timeout)  

fd_set *readfds, *writefds, *errfds;   //第2,3,4个参数都是文件描述符集,对Socket编程比较有用的是 readfds。  
struct timeval *timeout;               //控制select()如何返回,是非阻塞,还是阻塞一定时间返回,还是直接有文件描述符有响应才返回。

函数参数:
    1.nfs:最大的文件描述符+1,这个不能错,若不能确定,写一个算法找出来,下面提供的代码有
    2.可读文件描述符集。什么意思呢,就是这个集中的文件描述符随时可能给服务器写数据,那作为服务器,就要监测这些文件描述符,若不关心可读,填NULL。
    3.可写文件描述符集。这里不用到,就不说了。若不关心,可填NULL
    4.异常文件描述符集。若不关心,可填NULL。
    5.时间控制结构体。这个结构体里面有2个成员。一个代表秒,一个代表毫秒。两个都设成0表示select将会非阻塞返回。

对此,还有一些列的宏提供给select用:
     1.FD_SET();  用来把文件描述符加到文件描述符集中
     2.FD_ZERO(); 清空文件描述符集中的所有描述符
     3.FD_ISSET();判断某个文件描述符有没有响应。
     注意:select每返回一次后,都要重新清空文件描述符集,和重新把文件描述符加到文件描述符集中。

下面给出服务端的代码:
//TCP服务端
#include"myhead.h"

struct client_list
{
	int sock;
	struct client_list *next;
};


struct client_list *head = NULL;

struct client_list *init_list(struct client_list*head)
{
	head = malloc(sizeof(struct client_list));
	head->sock = -1;
	head->next = NULL;
	return head;
}

//新的客户端加到客户端队列中
int add_sock(struct client_list*head,int new_sock)
{
	struct client_list *p = head;
	struct client_list *new_node = malloc(sizeof(struct client_list));
	new_node->sock = new_sock;
	new_node->next = NULL;

	while(p->next!=NULL)
	{
		p = p->next;
	}
	p->next = new_node;
	return 0;
}

//找出最大的文件描述符
int find_max(struct client_list*head)
{
	struct client_list *p = head->next;
	if(p==NULL)
		return 0;
	int max_sd = p->sock;
	for(p;p!=NULL;p=p->next)
	{
		if(max_sd < p->sock)
			max_sd = p->sock;
	}
	return max_sd;
}

//信息转发给其他客户端
int write_to_client(struct client_list*head,char *wbuf,int size)
{
	struct client_list *p=head;
	for(p=head->next;p!=NULL;p=p->next)
	{
		write(p->sock,wbuf,size);
	}
	return 0;
}

//当有新的客户端作为新的文件描述符加进来时,显示客户端列表中的所有客户端文件描述符
void show_client_list(struct client_list*head)
{
	struct client_list *p = head;
	if(p->next == NULL)
	{
		printf("IS A EMPTY LIST!\n");
		return ;
	}
	else
	{
		puts("client_list is :");
		for(p =head->next; p!=NULL;p = p->next)
		{
			printf("%d ",p->sock);
		}
		printf("\n");
	}
}
//取消退出客户端的结点。
int del_node(struct client_list*head,int sock)
{
	struct client_list *p = head->next;
	struct client_list *q = head;
	while(p!=NULL)
	{
		if(p->sock == sock)
		{
			q->next = p->next;
			free(p);
			p = NULL;
		}
		else
		{
			p = p->next;
			q = q->next;
		}
	}
	return 0;
}

int main(int argc, char const *argv[])
{
	char rbuf[50]={0};
	char wbuf[50]={0};

	int sockfd,size,on=1;
	int new_sock;
	int max_sd;
	struct client_list *pos;
	struct timeval timeout = {0,0};  //设置select为非阻塞返回
	fd_set fdset;
	long val;

	head = init_list(head);   //初始化客户端链表。
	pos = head;

	struct sockaddr_in saddr;
	struct sockaddr_in caddr;
	size = sizeof(struct sockaddr_in);
	bzero(&saddr,size);

	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(8888);
	saddr.sin_addr.s_addr = htonl(INADDR_ANY);

	sockfd = socket(AF_INET,SOCK_STREAM,0);
	setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));//设置socket套接字为复用,不设也可以
	
	//把sockfd设置为非阻塞
	val = fcntl(sockfd,F_GETFL);
	val|=O_NONBLOCK;
	fcntl(sockfd,F_SETFL,val);

	bind(sockfd,(struct sockaddr*)&saddr,size);
	listen(sockfd,10);
	while(1)
	{
		new_sock = accept(sockfd,(struct sockaddr*)&caddr,&size);//循环接受新连接的客户端
		if (new_sock!= -1)
		{
			puts("new node come!\n");
			printf("new_sock = %d\n",new_sock);
			add_sock(head,new_sock);

			show_client_list(head);
		}

		max_sd = find_max(head);     //从客户端队列中,找出最大的文件描述符
		FD_ZERO(&fdset);             //清空文件描述符集
		pos = head;
		//把每个套接字加入到集合中
		if(pos->next != NULL)   //若套接字列表不是空    
		{
			for(pos=head->next;pos!=NULL;pos=pos->next)
			{
				FD_SET(pos->sock,&fdset);
			}
		}

		select(max_sd+1,&fdset,NULL,NULL,&timeout); //等待描述符

		for(pos=head->next;pos!=NULL;pos = pos->next) //检查哪个套接字有响应
		{
			if(FD_ISSET(pos->sock,&fdset))           //判断pos->sock这个文件描述符指向的客户端有没有数据写过来
			{
				bzero(rbuf,50);
				read(pos->sock,rbuf,50);
				printf("%s\n",rbuf);
				if(strcmp(rbuf,"quit")==0)  //若客户端发来的信息为quit,则取消这个客户端的结点。
				{
					del_node(head,pos->sock);	
				}
				write_to_client(head,rbuf,50);     //把写过来的信息转发给队列中的其他客户端。		

			}
		}
	}
	return 0;
}



客户端代码:
//客户端
#include"myhead.h"

int main(int argc, char const *argv[])
{
	int sockfd;
	char rbuf[50]={0};
	char wbuf[50]={0};
	char ipbuf[50]={0};
	int port;
	int max_sd;

	int size,on=1;
	int ret;
	fd_set fdset;
	struct sockaddr_in saddr;
	size = sizeof(struct sockaddr_in);

	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(8888);
	saddr.sin_addr.s_addr = inet_addr("192.168.152.128");

	sockfd = socket(AF_INET,SOCK_STREAM,0);
	setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));

	ret = connect(sockfd,(struct sockaddr*)&saddr,size);
	if(ret ==0)
	{
		printf("connect sucess\n");
		inet_ntop(AF_INET,(void*)&saddr.sin_addr.s_addr,ipbuf,50);
		port = ntohs(saddr.sin_port);
		printf("ip:%s,port:%d\n",ipbuf,port);
	}
	else if(ret == -1)
	{
		printf("failed to connect\n");
		return -1;
	}
	
	while(1)
	{
		FD_ZERO(&fdset);
		FD_SET(sockfd,&fdset); //把监测服务端的描述符集放到集合中
		FD_SET(STDIN_FILENO,&fdset); //STDIN_FILENO这个文件描述符用于监测标准输入(键盘)
		max_sd = sockfd>STDIN_FILENO?sockfd:STDIN_FILENO;
		select(max_sd+1,&fdset,NULL,NULL,NULL); //这里的select是设置为一直阻塞到有文件描述符发生响应

		if(FD_ISSET(sockfd,&fdset)) //判断是否服务端有数据发过来
		{
			bzero(rbuf,50);
			read(sockfd,rbuf,50);
			printf("%s\n",rbuf);
		}
		if(FD_ISSET(STDIN_FILENO,&fdset))  //判断键盘是否有数据传过来
		{
			bzero(wbuf,50);
			scanf("%s",wbuf);
			write(sockfd,wbuf,50);         //把键盘传过来的数据发给服务端
			if(strcmp(wbuf,"quit")==0)    //若键盘过来的数据为quit,则关闭这个客户端
			{
				printf("quit!\n");
				return 0;
			}
		}
	}
	return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值