select函数--IO多路复用详解

本章基于网络编程,需要学习网络编程的可以看我这篇博客:
socket编程(超简单、详细、可运行)–实现一个简单的聊天程序

什么是多路复用IO呢?

多路io:允许同时对多个I/O进行控制
可能这样说还是有点抽象,stm32下面有GPIO口,如果我需要一个LED屏幕连接在32开发板上面,是不是又很多的接线,每个接线接着板子上面的对应引脚上面,既然我们接线这个引脚了,是不是首先需要让这个引脚处于使能状态,这就是开时钟,多个引脚我们是不是要开多个引脚的时钟。配置好后我们的开发板的处理器是不是会按照需要去对这些IO口进行操作。类比Linux,socket编程里面,一个服务器端可以连接多个客户端,但是仅仅socket编程只能实现1v1,我们如何实现1 v n呢,一个客户端连接多个客户端?
在这里插入图片描述
我们可以这样理解:
在这里插入图片描述
有这样一个表,存储所有客户的标识符,根据判断那个标识符出现动作,与之进行通信。
这就需要我们要学习的第一个函数select函数。通过select函数来检测表是否准备完成,简单的说就是检测表里面有没有事件发生,比如描述符的增加,请求通信等。如果没有动作就阻塞,有动作就返回。

所以我们先来看一下
还是以socket为例子,如果使用多路IO的话:我在服务器端,我创建了一个socket套接字,然后将服务器端socket套接字加入到表中。
当我们创建完socket后socket套接字文件描述符是3,为什么是3?因为Linux里面有三个标准文件,标准输入、标准输出、错误。他们的描述符已经占用了0、1、2所以在创建文件,文件描述符是从3开始的。
在这里插入图片描述
然后下面调用select函数,现在表里面没有异动(无动作),那么select就阻塞了,假如现在有一个客户来连接,那么被连接的服务器端的socket文件描述符出现了动作,也就是3发生了异动,因为有人向他发出连接请求,它是不是肯定要做些什么哇,就像你向你女神表白了,你女神肯定会有所动作吧,要么拒绝你,要么答应你那么select就检测到表里面发生了动作,就会解除阻塞,然后我们就可以接受连接请求,将这个客户端的套接字文件描述符加入表中:
在这里插入图片描述
如果我设计这样一个结构:

创建表;
将服务器端套接字描述符加入表;
while(1)
{
 select;
 接收accept连接请求
 将新客户文件描述符加入到表中。
}

这样是不是表里面服务器端socket描述符出现异动select才会解除阻塞,然后接受新的客户端连接请求,再将新的客户端socket文件描述符加入表中,客户A,客户B,客户C。。。。。
是不是实现了这个结构:
在这里插入图片描述

注意:

假如现在表里面有:
0 1 2 3 4 5 6
如果现在有新的客户来连接,是不是就是7,表在判断3出现动作时候
(FD_ISSET函数),会清除别的文件描述符,也就是表里面只剩下3,所以
我们要注意保存问题。实例中会仔细说解决方法,下面只是大概思路

我们可以创建这种结构:
*********************************************************
创建表1,表2;
将服务器端套接字描述符加入表2;
while(1)
{
 表1=表2   //每次将表2内容复制给表1
 select;//阻塞等待表异动
 FD_ISSET检测表1那个文件发生异动
 if(服务器端套接字描述符发生异动)
 {
接收accept连接请求
将新客户文件描述符加入到表2中。
 }
}
这样我们操作在表1里面,内容保存在表2里面。

接下面我们看一下需要的函数:

表操作函数

头文件:
       #include <sys/select.h>
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

1、创建表:
   fd_set yyy;
   yyy是表名称。
   例如:fd_set lisk;
   这就创建了一个名为lisk的表。
   
2、判断表里面那个文件描述符fd发生动作:
  int FD_ISSET(int fd, fd_set *set);
  参数1:要判断的文件描述符fd
  参数2:表指针
  返回值:当描述符fd在描述符集fdset中时返回非零值,否则,返回零。

3、将文件描述符fd添加到表中:
  void FD_SET(int fd, fd_set *set);
  参数1:要添加的文件描述符fd
  参数2:表指针
  返回指:无
  
4、清除表中的描述符
   void FD_CLR(int fd, fd_set *set);
   参数1:要清除的文件描述符fd
   参数2:表指针
   返回指:无
   
5、表清0(初始化)
   void FD_ZERO(fd_set *set);
   参数:要初始化的表指针;
   返回值:无

select函数

头文件:
#include <sys/select.h>
函数:
int select(int nfds, fd_set *readfds, fd_set *writefds,
          fd_set *exceptfds, struct timeval *timeout)
参数:
    nfds : 就是你表中的文件描述符的最大值+1
    readfds: 读事件需要结构体
    writefds:写事件需要结构体
    exceptfds:异常事件需要结构体
    timeval:(结构体)
             struct timeval
            {
            long tv_sec; /* second */ //秒
            long tv_usec; /* microsecond */ //微秒
            };
            一般写NULL  无限等待,就是阻塞
            
     timeval参数解释:
     第一:若将NULL以形参传入,即不传入时间结构,就是将 select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;

    第二:若将时间值设为00毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;

    第三:timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

返回值:>0返回已经准备好并包含在fd_set结构中的描述符的总数
        =0时间限制过期则返回0
        <0出错    

接下来就是示例演示,实现一个1 v n的服务器对客户端
我们进行分段演示,完整代码在最下面:
服务器端:
首先是socket创建等步骤:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#define PORT 55555//1024~49050
#define IP "192.168.0.103"
//一下是表部分变量
int MAX,i,flag;
fd_set lisk,lock;//创建表
int comfd;//客户端描述符
int w_flag=0;

int main()
{
  char r_buf[100]={0};
  char w_buf[100]={0};
  struct sockaddr_in addr,cli_addr;
  int len=sizeof(addr);
  int sockfd=socket(AF_INET,SOCK_STREAM,0);//IPV4,tcp
  if(sockfd==-1)
  {
    perror("error socket");
    return -1;
  
  }
 printf("连接套接字创建成功\n");
 addr.sin_family=AF_INET;
 addr.sin_port=htons(PORT);//转换
 addr.sin_addr.s_addr=inet_addr(IP);//转换
 int ret=bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
 if(ret==-1)
 {
	perror("bind error");
        return -1;
 }
  printf("绑定成功\n");
 ret=listen(sockfd,3);
 if(ret==0)
 {
   printf("监听成功\n");
 }
 else
 {
  return -1;
 }

接下来是不是accept了,如果常规的直接只使用accept函数。那么只能实现1v1。我们搭配多路IO。

//一些定义在socket创建等步骤部分,这里我拿下来
/*  全局变量
int MAX,i,flag;
fd_set lisk,lock;//创建表
int comfd;//客户端描述符
int w_flag=0;
*/
  FD_ZERO(&lock);//清空表
  FD_SET(sockfd,&lock);//像表里面添加内容
  MAX=sockfd+1;
 while(1)
 {
   lisk=lock;//备份表
   flag=select(MAX,&lisk,NULL,NULL,NULL);//调用select阻塞,产生异动后解除阻塞
   if(flag<0)
   {
     perror("select");
   }
   else
   {
      for(i=0;i<MAX;i++)
      {
        if(FD_ISSET(i,&lisk))//判断那个描述符出现异动
        {
          if(i==sockfd)//如果是server端套接字异动说明有人请求连接
          {
            comfd = accept(sockfd,(struct sockaddr *)&cli_addr,&len);//接受连接
            FD_SET(comfd,&lock);//新客户端描述符加入表
            if(comfd>MAX-1)
            {
              MAX=comfd+1;
            }
            printf("%d 已连接\n",comfd);
          }
          else//如果是服务器端描述符以外的客户端描述符异动,说明客户端请求通信或者退出
          {
            memset(r_buf,0,sizeof(r_buf));
            int col=read(i,r_buf,sizeof(r_buf));

            memset(w_buf,0,sizeof(w_buf));
            strcpy(w_buf,r_buf);

            if(col==0)//读到的字节数为0说明不是请求通信,是退出
            {
              printf("%d 已经退出\n",i);
              close(i);//关闭对应套接字文件
              FD_CLR(i,&lock);//清除表中退出的描述符
              if(i==MAX-1)//如果是最大的描述符
              {
                MAX=MAX-1;
              }
            }
            else
            { //显示接收到的内容
              printf("%d 说:%s\n",i, r_buf);
              w_flag=1;
            }
            
          }
          
        }//显示给所有用户看消息内容
      if(w_flag==1)
      {
        for(int n=4;n<MAX;n++)
        {
        write(n,w_buf,sizeof(r_buf));        
        }

      }
      w_flag=0; 
      }
   }
   
  
 }
  return 0;
}

服务器端完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#define PORT 55555//1024~49050
#define IP "192.168.0.103"
int MAX,i,flag;
fd_set lisk,lock;//创建表
int comfd;//客户端描述符
int w_flag=0;
int main()
{
  char r_buf[100]={0};
  char w_buf[100]={0};
  struct sockaddr_in addr,cli_addr;
  int len=sizeof(addr);
  int sockfd=socket(AF_INET,SOCK_STREAM,0);//IPV4,tcp
  if(sockfd==-1)
  {
    perror("error socket");
    return -1;
  
  }
 printf("连接套接字创建成功\n");
 addr.sin_family=AF_INET;
 addr.sin_port=htons(PORT);//转换
 addr.sin_addr.s_addr=inet_addr(IP);//转换
 int ret=bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
 if(ret==-1)
 {
	perror("bind error");
        return -1;
 }
  printf("绑定成功\n");
 ret=listen(sockfd,3);
 if(ret==0)
 {
   printf("监听成功\n");
 }
 else
 {
  return -1;
 }
  FD_ZERO(&lock);//清空表
  FD_SET(sockfd,&lock);//像表里面添加内容
  MAX=sockfd+1;
 while(1)
 {
   lisk=lock;
   flag=select(MAX,&lisk,NULL,NULL,NULL);
   if(flag<0)
   {
     perror("select");
   }
   else
   {
      for(i=0;i<MAX;i++)
      {
        if(FD_ISSET(i,&lisk))//成功返回非0值
        {
          if(i==sockfd)
          {
            comfd = accept(sockfd,(struct sockaddr *)&cli_addr,&len);
            FD_SET(comfd,&lock);
            if(comfd>MAX-1)
            {
              MAX=comfd+1;
            }
            printf("%d 已连接\n",comfd);
          }
          else
          {
            memset(r_buf,0,sizeof(r_buf));
            int col=read(i,r_buf,sizeof(r_buf));

            memset(w_buf,0,sizeof(w_buf));
            strcpy(w_buf,r_buf);

            if(col==0)
            {
              printf("%d 已经退出\n",i);
              close(i);
              FD_CLR(i,&lock);
              if(i==MAX-1)
              {
                MAX=MAX-1;
              }
            }
            else
            {
              printf("%d 说:%s\n",i, r_buf);
              w_flag=1;
            }
            
          }
          
        }
      if(w_flag==1)
      {
        for(int n=4;n<MAX;n++)
        {
        write(n,w_buf,sizeof(r_buf));        
        }

      }
      w_flag=0; 
      }
   }
   
  
 }
  return 0;
}


客户端:
客户端就写的比较简单了
read函数可以说一下:
读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define PORT 55555
#define IP "192.168.0.103"

void * client_fun1(void * arg);
void * client_fun2(void * arg);

int sockfd=0;
char r_buf[100]={0};
char w_buf[100]={0};
int main()
{

  pthread_t pthrea_id[2]={0};
  struct sockaddr_in addr;
  int len=sizeof(addr);
  sockfd=socket(AF_INET,SOCK_STREAM,0);
  if(sockfd==-1)
  {
    perror("error socket");
    return -1;
  
  }
 printf("连接套接字创建成功\n");
 addr.sin_family=AF_INET;
 addr.sin_port=htons(PORT);
 addr.sin_addr.s_addr=inet_addr(IP);


 connect(sockfd,(struct sockaddr *)&addr,len);

 pthread_create(&pthrea_id[0],NULL,client_fun1,NULL);
 pthread_create(&pthrea_id[1],NULL,client_fun2,NULL);

 pthread_join(pthrea_id[0],NULL);
 pthread_join(pthrea_id[1],NULL);
 
 close(sockfd);
 return 0;
}
void * client_fun1(void * arg)
{
  while(1)
  {
    read(sockfd,r_buf,sizeof(r_buf));
    printf("server :%s\n",r_buf);
    memset(r_buf,0,sizeof(r_buf));
  }
}
void * client_fun2(void * arg)
{
 while(1)
 {
  scanf("%s", w_buf);
  getchar();
  write(sockfd,w_buf,sizeof(w_buf));
  memset(w_buf,0,sizeof(w_buf));
 }
}

运行截图

服务器端:
在这里插入图片描述
在这里插入图片描述

客户端:
在这里插入图片描述
可能看起来有点懵,写的比较粗糙没有优化,主要实现就是:服务器端可以看到所有人的消息,每个用户也可以看到其他用户发送的消息,类似群聊。后续会优化一下代码。


感谢评阅,感谢指正。欢迎在评论区交流或者私信我。
_
禁止转载。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值