socket网络编程select函数处理多个IO并发

select函数处理socket多IO并发


此文接上篇文章
https://blog.csdn.net/liufeng_06/article/details/105007836

简介

网络编程的服务端程序在大多数的情况下,并不只是与一个客户端进行通讯。在嵌入式行业中,设备通常被被要求至少同时需要与5-10个客户端同时通信,而对于嵌入式设备来说,其内部资源是非常有限的,我们不可能使用多进程来实现该功能,在Linux设备中通常是使用多线程和select函数或者poll函数进行处理。这里作者之所以选择select函数,是因为这种处理方式不仅仅在Linux系统中可以使用,在资源更加宝贵的MCU中,我们通过使用 lwip 协议栈可以使用select函数。

select函数

在Linux下使用该函数需要加上以下头文件
include <sys/select.h>
函数原型为:
int select (int maxfd + 1,fd_set *readset,fd_set *writeset, fd_set *exceptset,const struct timeval * timeout);

参数一 :
最大的文件描述符 + 1。是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!

参数二 :
用于检查可读性,是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

参数三:
用于检查可写性,具体的解释同参数二一致。

参数四:
用于检查文件错误异常,具体的解释同参数二一致。

参数五
用于指定select函数运行到此处时等待的时间。这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
注意点
参数五经过select函数使用以后,消耗的时间一直是递减的。所以在select使用前,每次都需要重新给其赋值。如果我在其初始化时赋值为20s,第一次使用消耗了3s,那么下次使用如果任然想让其等待20s(注意这里并不是一定等20s,在等待期间有描述符可操作的话会立即返回的)就必须重新给其赋值为20s

返回值
>0 集合中就绪的个数
-1 出错
0 函数超时

与该函数相关的一些结构体和宏

    ```
    ----------------------------------------------------------------
    struct timeval
    {
		long tv_sec;   //秒
		long tv_usec; //微妙  
	}
   ------------------------------------------------------------------
   ------------------------------------------------------------------
   fd_set fd;
   void FD_ZERO (fd_set *fdset); //清除整个描述符集
   void FD_SET (int fd,fd_set *fdset); //往描述符集里添加一个描述符
   void FD_CLR (int fd,fd_set *fdset); // 删除一个描述符集里的特定描述符
   int FD_ISSET(int fd,fd_set *fdset); // 检查一个描述集的一个特定描述符是否可读或者可写
   ------------------------------------------------------------------

    ```

代码如下

想实现的状态是:TCP服务端可以同时接受多个客户端的连接,并实现回显功能。
实现的大概思路如下:
  1、定义一个fds[MAX_CLIENT_NUM]数组,其第0个元素 fds[0]固定为监听是否有新的客户端请求连接,其他元素则为通过accept函数创建的新的套接字。
  2、由于可读、可写、出错描述符集的功能大概相同,为了简单,我只对可读描述符集进行监测。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/un.h>
#include <fcntl.h>
#include <sys/select.h>

#define MAX_CLIENT_NUM  5    //最大监听客户端连接数量为5
int main(int argc,char *argv[])
{
	char buf[512];
	int sfd,iflgs;
	int max_fd;
	int fds[MAX_CLIENT_NUM];
	fd_set rdset;
	
	if(argc != 3)
	{
		printf("please input PORT and IP ADDR!\n");
		return 0;
	}
	
	sfd = socket(AF_INET,SOCK_STREAM,0);
	if (sfd == -1)
		printf("socker create faild!\n");
	struct sockaddr_in serveraddr;
	bzero(&serveraddr,sizeof(struct sockaddr_in));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(atoi(argv[2]));
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //此处IP地址必须为linux下网口ip地址
	                           
	//因为socket默认为阻塞,这里需要将其设置为非阻塞模式
	iflgs = fcntl(sfd,F_GETFL,0);
	fcntl(sfd,F_SETFL,iflgs|O_NONBLOCK);

	if(bind(sfd,(struct sockaddr*)&serveraddr,sizeof(struct sockaddr)) == -1)
	{
		printf("bind!\n");
		close(sfd);
	}
	
	if(listen(sfd,MAX_CLIENT_NUM) == -1)
	{
		printf("listen!\n");
		close(sfd);
		
	}
	
	//fds[MAX_CLIENT_NUM]初始化为 -1
	for(int i=0; i<MAX_CLIENT_NUM;i++)
	{
		fds[i] = -1;
	}
	
	max_fd = sfd;
	fds[0] = sfd;   //fds[0]固定为创建socket时返回的套接字描述符
	
	int client_fd;
	struct sockaddr_in client_addr;
	int socket_len = sizeof(struct sockaddr_in);
	bzero(&client_addr,socket_len);
	
	struct timeval timeout;
	
	while(1)
	{
		FD_ZERO(&rdset);  //每个循序都必须清除可读描述符集
		
		for(int i=0; i<MAX_CLIENT_NUM;i++)
		{
			if(fds[i] != -1)
			{
				printf("fds[%d] = %d\n",i,fds[i]);
				FD_SET(fds[i],&rdset);
				
				if(fds[i] > max_fd)     //找到所有描述符中的最大描述符
				{
					max_fd = fds[i];
				}
			}
		}
		//若select没有检测到有可读的描述符,则3s后退出该函数,并返回 0 表示 时间超时
	
		timeout.tv_sec = 3;
		timeout.tv_usec = 0;
		
		switch(select(max_fd+1,&rdset,NULL,NULL,&timeout))
		{
			case -1:
				printf("select error!\n");
				break;
			case 0:
			    printf("wait timeout !\n");
				break;
			default:           //有准备就绪的可读描述符
				for(int i=0; i<MAX_CLIENT_NUM;i++)
				{
					if(fds[i] == sfd && FD_ISSET(sfd,&rdset))   //有新的客户端请求连接
					{
						client_fd = accept(sfd,(struct sockaddr*)&client_addr,&socket_len);
						if(client_fd == -1)
	                    {
							close(sfd);
		                    printf("--accept error-----------");
		                    return 0;
	                    }
	                    else      //打印请求连接的客户端IP地址和端口号
	                    {
		                    char *ptr = inet_ntoa(client_addr.sin_addr);  //IP地址,inet_ntoa函数将32位无符号的IP地址转化为X.X.X.X的形式
		                    unsigned short port_tmp = ntohs(client_addr.sin_port);
		                    printf("------------------------------------------------------\n");
		                    printf("client ip :%s---port:%d\n",ptr,port_tmp);
		                    printf("------------------------------------------------------\n");
	                    }
						//将新的套接字描述符加载到fds[MAX_CLIENT_NUM]中
						for(int j=0; j<MAX_CLIENT_NUM;j++)
						{
							if(fds[j] == -1)
							{
								fds[j] = client_fd;
								break;
							}
						}
					}
					else if(fds[i] != -1 && FD_ISSET(fds[i],&rdset))
					{
						//数据回显功能
		                int rev_num = recv(fds[i],buf,sizeof(buf),0);
		                 //客户端主动关闭网络连接
		                if(rev_num == 0)
		                {
			                close(fds[i]);
			                fds[i] = -1;
							break;
		                }
		
		                send(fds[i],buf,rev_num,0);
					}
				}
				break;
			    
			
		}
			
		
	}
	
	close(sfd);
	
	
}


功能测试

1、在Linux下编译并运行
编译成功

2、程序在Linux下运行时,在win下通过多个网络调试助手去连接。
连接成功以后,程序打印如下:
Linux下程序打印
==注意:==其IP地址虽然一致,但是端口号并不同,所以是两个TCP客户端
3、测试两个客户端的回显功能
两个客户端的回显功能


总结:

关于上文说的有关 timeout的注意点,大家可以把对timeout的赋值放到 while(1)外,就可以发现,若是没有新的客户端连接请求(fds[0]一直在监测)或者新的可读数据时,程序会一直打印 wait timeout并没有按照设想的,我只赋值一次,每次都会等待3s。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值