select函数详解及使用案例

本文详细介绍了select函数的工作原理,包括其原型和参数解释,阐述了select函数在处理文件描述符集合时的阻塞机制。同时,分析了select的优缺点,如较好的可移植性和较高的时间精度,以及文件描述符数量限制和拷贝开销等问题。此外,通过一个简单的高并发服务器案例展示了select的实际应用,演示了如何处理连接请求和数据读写。

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

1、select函数原型

int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);

参数解释:

 maxfdp——传入参数,集合中所有文件描述符的范围,即最大文件描述符值+1
 readfds——传入传出参数,select调用时传入要监听的可读文件描述符集合,select返回时传出发生可读事件的文件描述符集合
 writefds——传入传出参数,select调用时传入要监听的可写文件描述符集合,select返回时传出发生可写事件的文件描述符集合
 errorfds——传出参数,select返回时传出发生事件(包括可读和可写)中异常事件的文件描述符集合
 timeout——传入参数,设置select阻塞的时间。若设置为NULL,则select一直阻塞直到有事件发生;
                                      若设置为0,则select为非阻塞模式,执行后立即返回;
                                      若设置为一个大于0的数,即select的阻塞时间,若阻塞时间内有事件发生就返回,否则时间到了立即返回

fd_set是自定义的一个数据结构,可看作一个集合,存放可读、可写或异常事件的文件描述符。fd_set集合通常有以下四个宏来操作:

void FD_ZERO(fd_set *fdset);  //清空fdset中所有文件描述符
void FD_SET(int fd,fd_set *fdset);  //添加文件描述符fd到集合fdset中
void FD_CLR(int fd,fd_set *fdset); //将文件描述符fd从集合fdset中去除
int FD_ISSET(int fd,fd_set *fdset);  //判断文件描述符fd是否在集合fdset中

select工作原理:传入要监听的文件描述符集合(可读、可写或异常)开始监听,select处于阻塞状态,当有事件发生或设置的等待时间timeout到了就会返回,返回之前自动去除集合中无事件发生的文件描述符,返回时传出有事件发生的文件描述符集合。但select传出的集合并没有告诉用户集合中包括哪几个就绪的文件描述符,需要用户后续进行遍历操作。

2、select优缺点

优点:

(1)select的可移植性较好,可以跨平台;
(2)select可设置的监听时间timeout精度更好,可精确到微秒,而poll为毫秒。

缺点:

(1)select支持的文件描述符数量上限为1024,不能根据用户需求进行更改;
(2)select每次调用时都要将文件描述符集合从用户态拷贝到内核态,开销较大;
(3)select返回的就绪文件描述符集合,需要用户循环遍历所监听的所有文件描述符是否在该集合中,当监听描述符数量很大时效率较低。

3、select使用经典案例

用select函数编写一个简单的高并发服务器,且假设服务器启动时处于无连接状态,满足以下功能:
a)可处理来自一个新客户端的连接请求;
b)监听可读事件,若已连接客户端的已连接描述符发生可读事件,服务器从客户端读取数据并处理;

服务器端代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<ctype.h>

#define SERV_PORT 6666

int main()
{
  int i,j,n,maxi;
  int maxfd,listenfd,connfd,sockfd;
  int nready,client[FD_SETSIZE-1];  //FD_SETSIZE=1024,定义数组client来储存已连接描述符,最多1023个
  char buf[BUFSIZ], str;

  struct sockaddr_in clie_addr,serv_addr;
  socklen_t clie_addr_len;
  fd_set allset,readset;  //定义监听描述符集合allset和发生事件描述符集合readset
 
  bzero(&serv_addr,sizeof(serv_addr));
  serv_addr.sin_family=AF_INET;
  serv_addr.sin_port=htons(SERV_PORT):  //端口号,将无符号短整型转换为网络字节顺序
  serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);  //一个主机可能有多个网卡,所以是本机的任意IP地址
 
  listenfd=socket(AF_INET,SOCK_STREAM,0); //AF_INET表示使用32位IP地址,SOCK_STREAM表示使用TCP连接
  bind(listenfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));  //将服务器套接字地址与套接字描述符联系起来
  listen(listenfd,1024); //设置可监听的连接数量为1024

  maxfd=listenfd; //初始化最大文件描述符为监听描述符listenfd
 
  //初始化client数组,将数组所有元素置为-1
  int maxi=-1;  //数组client储存的文件描述符的个数,初始化为-1
  for (i=0;i<FD_SETSIZE;i++)
      client[i]=-1

  //初始化select监听文件描述符的集合
  FD_ZERO(&allset);     //初始化监听集合
  FD_SET(listenfd,&allset);  //将监听描述符listenfd添加到集合中
 
  while(1)
  {
    readset=allset;
    nready=select(maxfd+1,&readset,NULL,NULL,NULL); //select只监听可读事件,且为永久阻塞直到有事件发生
    if (nready<0)
       perr_exit("select error");

    //判断listenfd是否发生事件,若发生,则处理新客户端连接请求
    if (FD_ISSET(listenfd,&readset))
    {
       clie_addr_len=sizeof(clie_addr);
       connfd=accept(listenfd,(struct sockaddr *)&clie_addr,&clie_addr_len);//与请求客户端建立连接
       printf(“received from %s at port %d\n”,
             inet_ntop(AF_INET,&clie_addr.sin_addr.s_addr,&str,sizeof(str)),
             ntohs(clie_add.sin_port));  //打印该客户端的IP地址和端口号
           
      //将connfd赋值给client数组中第一个为-1的元素位置
      for (i=0;i<FD_SETSIZE;i++)
      {
         if (client[i]<0)
         {
            client[i]=connfd;
            break;
         }
      }
 
      //判断select监听的文件描述符的个数是否超过上限
      if (i == FD_SIZE-1)   //减1的原因是要考虑监听描述符listenfd也属于select监控
      {
         fputs("too many clients\n",stderr);
         exit(1);
      }

      FD_SET(connfd,&allset);  //向监控的文件描述符集合allset中添加新的描述符connfd
      if (connfd>maxfd)
         maxfd=connfd;   //更新最大文件描述符值
  
      //保证maxi永远是client数组中最后一个非-1的元素的位置
      if(i>maxi)
         maxi=i;
       
      //如果nready=1,即只有一个发生事件的描述符,在此条件下必为listenfd,则返回循环位置,继续调用select监控;否则继续向下执行
      --nready;
      if (nready==0continue;
    }

    //找到client数组中发生事件的已连接描述符,并读取、处理数据
    for (i=0;i<=maxi;i++)
    {
        sockfd=client[i];
       if (sockfd<0)  //已连接描述符失效,重新开始循环
          continue;  
        
       if (FD_ISSET(sockfd,&readset))
       {
          n=read(sockfd,buf,sizeof(buf));
          if (n==0) //当客户端关闭连接,服务端也关闭连接
          {
             colse(sockfd);
             FD_CLR(sockfd,&allset);  //解除select对该已连接文件描述符的监控
             client[i]=-1;
          }
          else if (n>0)
          {
            for (j=0;j<n;j++)
                buf[j]=toupper(buf[j]);
            sleep(2);
            write(sockfd,buf,n);
          }

          --nready;
          if (nready==0)
              break;  //跳出for循环,还在while中
       }
    }
  }
  close(listenfd);
  return 0;
} 

客户端代码

#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>

#define SERV_IP "127.0.0.1"  //客户端、服务端都在一台主机上,所以直接用本机IP地址
#define SERV_PORT 6666

int main()
{
   int cfd;
   struct sockaddr_in serv_addr;
   char buf[BUFSIZ];
   int n;

   cfd=socket(AF_INET,SOCK_STREAM,0);
   
   memset(&serv_addr,0,sizeof(serv_addr));
   serv_addr.sin_family=AF_INET;
   serv_addr.sin_port=htons(SERV_PORT);
   inet_pton(AF_INET,SERV_IP,&serv_addr.sin_addr.s_addr);  //将点十进制字节串转换为网络字节序

   connect(cfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));

   while(1)
   {
      fgets(buf,sizeof(buf),stdin);
      write(cfd,buf,strlen(buf));
      n=Read(cfd,buf,sizeof(buf));
      write(STDOUT_FILENO,buf,n);
   }
   close(cfd);

   return 0;
}

将服务端、客户端生成可执行文件之后,先启动服务器,再启动客户端与服务器建立连接,用户输入字符,服务器将读取到的字符转换为大写,再写回客户端的屏幕上,测试结果如下所示:
服务器会显示客户端的IP地址以及端口号,可发现客户端与服务器来自同一台主机,这是没有问题的
在这里插入图片描述
客户端会显示用户输入的字符串以及转为大写后写回的字符串,如图:
在这里插入图片描述
注意:数据传输完毕后,一定要客户端先断开连接,避免服务器出现TIME_WAIT状态,从而占用服务器资源。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值