1. select函数介绍

为什么要有select?

read/recv 等 文件接口只有一个文件描述符
想要 让一个接口等待多个文件描述符,而read等接口是不具备这个能力的
操作系统就设计一个接口 select,用于多路复用


select 作用
1.等待多个文件描述符
2.只负责等(没有数据拷贝的能力)


select 接口

输入 man select

由于select只负责等待,不负责拷贝,所以没有缓冲区

第一个参数 nfds的理解

第一个参数 nfds,是一个输入型参数 ,表示 select等待的多个文件描述符(fd)数字层面 最大的+1
(文件描述符的本质为 数组下标,多个文件描述符中 数值最大的文件描述符值+1 即nfds )

什么是 输入 输出型参数

用户把数据交给操作系统,同样操作系统也要 通过这些输出型参数 把结果 交给用户
为了让 用户 和 操作系统之间进行信息传递,就把参数设置为 输入 输出型参数

最后一个参数 timeout 的理解

timeout 是一个 输入 输出型参数


timeout的数据类型 为struct timeval

可以一个时间结构体,tv_sec 表示 秒, tv_usec 表示 微秒


对于 struct timeval的对象 可设置三种值

第一种 对象被设为 NULL ,对于select来说 表示 阻塞等待
(多个文件描述符任何一个都不就绪,select就一直不返回)

第二种 struct timeval对象定义出来,并将其中变量都设为0
对于select来说 表示 非阻塞等待
(多个文件描述符任何一个都不就绪,select就会立马出错 并返回)

第三种 struct timeval对象定义出来,并将其中变量设为 5 和 0
表示 5s以内 阻塞等待,否则 就 timeout(非阻塞等待) 一次
若在第3s时 有一个文件描述符就绪,则select就会返回 其中参数 timeout 表示 剩余的时间 2s(5-3=2)


readfds writefds exceptfds 参数的理解

readfds writefds exceptfds 这三个参数 是同质的
readfds 表示 读事件
writefds 表示 写事件
excepttfds表示 异常事件

三者类型都为 fd_set


fd_set是一个位图结构,用其表示多个文件描述符
通过比特位的位置, 就代表文件描述符数值是谁


位图结构想要使用 按位与、按位或 这些操作,必须使用操作系统提供的接口

FD_CLR :将指定的文件描述符从 指定的集合中清除

FD_ISSET:判断文件描述符是否在该集合中被添加

FD_SET: 将一个文件描述符添加到对应的set集合中

FD_ZERO:将文件描述符整体清空


以readfds 读事件为例

若放入 readfds 集合中,用户告诉内核 ,那些文件描述符对应的读事件需要由 内核 来关心
返回时,内核要告诉用户,那些文件描述符的读事件已经就绪


假设想让操作系统去关心八个文件描述符对应的事件

用户想告诉内核时,用户需 定义 fd_set 对象 rfds ,其中八个比特位设置为1
比特位的位置表示几号文件描述符
比特位被置1,则操作系统就需要关心 对应的几号文件描述符
如:需要关心 1-8号文件描述符,即查看是否就绪


当select返回时, 内核会告诉用户,rfds重置,并将 就绪的文件描述符 对应 的 比特位位置 置1

如: 3号和5号就绪,则对应比特位 位置 置1 ,表示3号和5号文件描述符 对应的内容就绪


select的返回值

select的返回值 同样也有三种情况
第一种 大于0
表示有几个文件描述符 是就绪的

第二种 等于0
进入timeout状态 ,即 5s以内没有任何一个文件描述符 就绪

第三种 小于0
等待失败 返回-1
如:想要等待下标为1 和2的文件描述符,但是下标为2的文件描述符根本不存在,就会等待失败


2. select的使用

本次实现分为V1版本V2版本
V1版本 只能为 读事件
而V1版本 除了有读事件 还可以处理 写事件和异常事件
同时两者只有selectserver.hpp有区别,其他都是一样的


SelectServer_v1

start 最初版本

首先设置一个rfds集合,用来表示读集合
在使用 FD_ZERO 将读集合清空
将listensock套接字 添加到 读集合rfds中

由于当前只看读集合,所以写 和异常集合都为空,同时将timeout设置为阻塞等待,即输入才返回
slect的返回值 用n接收 ,使用switch case 来区分 返回值


创建 HandlerEvent函数,来处理就绪事件
因为只有n大于0时,才有文件描述符就绪,所以将其放入default中


start 最终版本

但是这样是存在问题的,当不断会有listensock套接字就绪时,文件描述符会变多 即将套接字 放入 rfds集合中
当select返回时,rfds大部分位图可能会被清空,就没办法保证 对之前的文件描述符有持续的监控能力


select服务器,在使用的时候,需要程序员自己维护一个第三方数组,来进行已经获得的sock进行管理


先通过typedef 将int类型定义 为type_t
定义出一个整形数组 fdarray,并设置数组大小为N (位图大小)


将listensock套接字的文件描述符 作为fdarray 数组的第一个元素
由于select函数的第一个函数 是所有文件描述符 最大值+1
所以通过maxfd 记录最大值

每次都从数组下标为0的位置处开始 向后寻找
而初始化时,将数组的所有元素都设为-1
所以当寻找到不为-1的数时,就将对应的文件描述符 添加到rfds读集合中

这样就可以保证在查询时,可以寻找到历史的文件描述符


HandlerEvent函数——处理就绪事件

继续遍历fdarray数组,跳过数组元素为-1的

若为合法文件描述符 则有两种情况:
listensock套接字 或者 普通文件描述符


第一种情况

是listensock套接字 并且在rfds集合中
为了方便观看,所以写了一个Accepter函数用于获取新连接的动作

Accepter函数

此时就可以直接使用accept函数了,定义一个客户端IP和客户端端口号
用于获取客户端IP和端口号

定义sock 用于接收返回值,当返回值sock大于0时,即获取连接成功

因为下标为0处,已经被listensock套接字占用了,所以从下标为1位置开始
找到数组元素为-1(表示该位置没有被使用)
将返回值sock赋值给对应的数组元素


第二种情况

不是listensock套接字 但在rfds集合中 即普通文件描述符

使用recv函数 ,将文件描述符fd中的数据 发送到 buffer中

若返回值s大于0,则表示返回成功
在buffer数据的基础上,添加 select server echo ,通过 send 函数 重新发送给文件描述符fd中,被select管理

返回值的其他情况,都是打印信息
最后关闭文件

完整代码

SelectServer.hpp
//SelectServer  V1版本
#include<iostream>
#include<string>
#include<sys/select.h>
#include<cstring>
#include"Sock.hpp"
#include"Log.hpp"
#include"Err.hpp"
using namespace std;


const static int gport=8888;    

typedef int type_t;//定义一个整形数组

class SelectServer
{
  static const int N=sizeof(fd_set)*8;//N对应位图大小

 public:
      SelectServer(uint16_t port=gport)
      :port_(port)
      {}

      void InitServer()//初始化
      {
           listensock_.Socket();//创建套接字
           listensock_.Bind(port_);//绑定
           listensock_.Listen();//设置监听状态
           
           //对fdarray数组进行初始化
           for(int i=0;i<N;i++)
           {
             fdarray_[i]= defaultfd;
           }
      }


     void Accepter()//获取新连接的动作
     {
             //这里再使用accept 就不会阻塞了
             //listen套接字底层一定有就绪的事件 即连接已经到来了
             string clientip;
             uint16_t  clientport;
            int sock=listensock_.Accept(&clientip,&clientport);//获取客户端IP和端口号
            if(sock<0)
            {
              return;
            }
            
            //当得到对应新连接的sock套接字,是不能进行read/recv
            //并不知道sock上的数据是否就绪的
            //所以需要将sock交给select,由select进行管理
            logMessage(Debug,"[%s:%d],sock:%d",clientip.c_str(),clientport,sock );
             //只需把新获取的sock 添加到 数组中
             int pos=1;
             for(;pos<N;pos++)
             {
                if(fdarray_[pos]==defaultfd)//说明没有被占用
                {
                   break;
                }
             }
             if(pos>=N)//整个数组中的位置全被占用了
             {
                close(sock);
                logMessage(Warning,"sockfd[] array full");
             }
             else //找到了对应的位置
             {
                fdarray_[pos]=sock;
             }
     }
     
      void  HandlerEvent(fd_set  &rfds)//处理就绪事件
      { 
             for(int i=0;i<N;i++)
             {
               if(fdarray_[i]==defaultfd)
               {
                  continue;
               }
               //合法fd

               //若套接字为listensock套接字
               if(fdarray_[i]==listensock_.Fd() &&FD_ISSET(listensock_.Fd(),&rfds))
               {
                  Accepter();
               }
               //若套接字不是listensock套接字,但在rfds集合中
                else if ((fdarray_[i] != listensock_.Fd()) && FD_ISSET(fdarray_[i], &rfds)) 
               {
                   //普通文件描述符就绪
                   int fd=fdarray_[i];
                   char buffer[1024];
                   ssize_t s=recv(fd,buffer,sizeof(buffer)-1,0);
                   //读取不会被阻塞
                   if(s>0)//读取成功
                   {
                     buffer[s-1]=0;
                     cout<<"client# "<<buffer<<endl;

                     //发送回去 也要被select管理
                      string echo=buffer ;
                      echo+= "[select server echo ]";
                      send(fd,echo.c_str(),echo.size(),0);//发送消息 将echo内的数据 交给fd
                   }
                   else 
                   {
                     if(s==0)//读到文件结尾
                     {
                       logMessage(Info,"client quit...,fdarray_[i] -> defaultfd:%d->%d",fd,defaultfd);
                     }
                     else //读取失败 
                     {
                       logMessage(Warning,"recv error,client quit...,fdarray_[i] -> defaultfd:%d->%d",fd,defaultfd);
                     }  
                      close(fdarray_[i]);
                      fdarray_[i]=defaultfd;
                   }   
                } 
             } 
         
      }
      

        void DebugPrint()
       {
         cout<<"fdarray_[]:"<<endl;
         for(int i=0;i<N;i++)
         {
          if(fdarray_[i]==defaultfd)
          {
            continue;
          }
          cout<<fdarray_[i]<<" ";
         }
         cout<<"\n";
       }

      void Start() //启动
      {
        //在网络中,新连接到来被当作 读事件就绪
        //对应不同的事件就绪,做出不同的动作
        
        fdarray_[0]=listensock_.Fd();//先将listensock套接字添加到数组中
        while(true) 
        {
          //因为rfds是输入 输出型参数,就注定了每次都要对rfds进行重置
          //重置 就通过 fdarray数组知道 历史上有那些fd

          //因为服务器在运行中,sockfd的值一直在动态变化,所以maxfd也一直在变化
          //maxfd也要动态更新
            fd_set rfds;//作为读文件描述符集合
            FD_ZERO(&rfds);//将集合清空
            int maxfd=fdarray_[0];
            for(int i=0;i<N;i++)
            {
               if(fdarray_[i]==defaultfd)
               {
                 continue;
               }
               //将合法fd添加到rfds集合中
               FD_SET(fdarray_[i],&rfds);
               if( maxfd<fdarray_[i])
               {
                  maxfd=fdarray_[i];
               }
            }


            int n=select(  maxfd+1,&rfds,nullptr,nullptr,nullptr);//将listensock套接字添加到读文件描述符集合中
            //timeout 设为nullptr后,全部为阻塞等待

            switch(n)
            {
              case 0:   //表示没有任何一个文件描述符就绪 
               logMessage(Debug,"timeout,%d: %s",errno,strerror(errno));
               break;

             case -1:  //等待失败 返回-1
              logMessage(Warning,"%d: %s",errno,strerror(errno));
              break;

             default:  //大于0 ,则表示成功 返回有多少文件描述符就绪
                logMessage(Debug,"有一个就绪事件发生了:%d",n);
                 HandlerEvent(rfds);//处理就绪事件
                 DebugPrint();//打印数组内容
               break;
            }
        }
      }
       
      ~SelectServer()
       {
        listensock_.Close();
       }

 private:
     uint16_t port_;//端口号
      Sock listensock_;//创建Sock对象
      type_t fdarray_[N];//自己定义一个数组,与位图大小相同,来进行已经获得的sock进行管理
};

Err.hpp(错误信息)
#pragma once 

enum
{
  USAGE_ERR=1,
  SOCKET_ERR,//2
  BIND_ERR,//3
  LISTEN_ERR,//4
  SETSID_ERR,//5
  OPEN_ERR//6
};





Log.hpp(日志打印)
#pragma once 
#include<iostream>
#include<string.h>
#include<cstdio>
#include<cstring>
#include<cstdarg>
#include<unistd.h>
#include<sys/types.h>
#include<time.h>

const std::string  filename="tecpserver.log";

//日志等级
enum{
 Debug=0, // 用于调试
 Info  ,  //1 常规
 Warning, //2 告警
 Error ,  //3  一般错误
 Tatal ,  //4 致命错误
 Uknown//未知错误
};

static  std::string tolevelstring(int level)//将数字转化为字符串
{
  switch(level)
  {
     case  Debug : return "Debug";
     case Info : return "Info";
     case Warning : return "Warning";
     case  Error : return "Error";
     case Tatal : return "Tatal";
     default: return "Uknown";
  }
}
std::string gettime()//获取时间
{
   time_t curr=time(nullptr);//获取time_t
   struct tm *tmp=localtime(&curr);//将time_t 转换为 struct tm结构体
   char buffer[128];
   snprintf(buffer,sizeof(buffer),"%d-%d-%d %d:%d:%d",tmp->tm_year+1900,tmp->tm_mon+1,tmp->tm_mday,
   tmp->tm_hour,tmp->tm_min,tmp->tm_sec);
   return buffer;

}
void logMessage(int level, const char*format,...)
{
   //日志左边部分的实现
   char logLeft[1024];
   std::string level_string=tolevelstring(level);
   std::string curr_time=gettime();
   snprintf(logLeft,sizeof(logLeft),"%s %s %d",level_string.c_str(),curr_time.c_str());

   //日志右边部分的实现
   char logRight[1024]; 
   va_list p;//p可以看作是1字节的指针
   va_start(p,format);//将p指向最开始
   vsnprintf(logRight,sizeof(logRight),format,p);
   va_end(p);//将指针置空
   
   //打印日志 
   printf("%s%s\n",logLeft,logRight);

   //保存到文件中
   FILE*fp=fopen( filename.c_str(),"a");//以追加的方式 将filename文件打开
   //fopen打开失败 返回空指针
   if(fp==nullptr)
   {
      return;
   }
   fprintf(fp,"%s%s\n",logLeft,logRight);//将对应的信息格式化到流中
   fflush(fp);//刷新缓冲区
   fclose(fp);
}



main.cc(主函数调用)
#include"SelectServer.hpp"
#include<memory>
int main()
{
    std::unique_ptr<SelectServer> svr(new SelectServer());
    svr->InitServer();//初始化
    svr->Start();//启动
    return 0;
}




makefile
SelectServer:main.cc
	g++ -o $@ $^ -std=c++11
	
.PHONY:clean
clean:
	rm -f SelectServer
Sock.hpp(套接字接口实现)


#include<iostream>
#include<cstring>
#include<cstdlib>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
#include"Log.hpp"
#include"Err.hpp"

static const int  gbacklog=32;
static const int defaultfd=-1;
class Sock
{
 public:
 Sock() //构造
 :_sock(defaultfd)
 {
 }

 void  Socket()//创建套接字
 {
  _sock=socket(AF_INET,SOCK_STREAM,0);
  if(_sock<0)//套接字创建失败
  {
    logMessage( Tatal,"socket error,code:%s,errstring:%s",errno,strerror(errno));
    exit(SOCKET_ERR);
  }
 }

  void Bind(uint16_t port)//绑定
  {
   struct sockaddr_in local;
   memset(&local,0,sizeof(local));//清空
   local.sin_family=AF_INET;//16位地址类型
   local.sin_port= htons(port); //端口号
   local.sin_addr.s_addr= INADDR_ANY;//IP地址
   
   //若小于0,则绑定失败
   if(bind(_sock,(struct sockaddr*)&local,sizeof(local))<0)
   {
      logMessage( Tatal,"bind error,code:%s,errstring:%s",errno,strerror(errno));
      exit(BIND_ERR);
   }
  }
   
   void Listen()//将套接字设置为监听状态
   {
      //小于0则监听失败
      if(listen(_sock,gbacklog)<0)
      {
        logMessage( Tatal,"listen error,code:%s,errstring:%s",errno,strerror(errno));
        exit(LISTEN_ERR);
      }
   }

   int Accept(std::string *clientip,uint16_t * clientport)//获取连接
   {
        struct sockaddr_in temp;
        socklen_t len=sizeof(temp);
        int sock=accept(_sock,(struct sockaddr*)&temp,&len);

        if(sock<0)
        {
             logMessage(Warning,"accept error,code:%s,errstring:%s",errno,strerror(errno));
        }
        else 
        {
            //inet_ntoa 4字节风格IP转化为字符串风格IP
            *clientip = inet_ntoa(temp.sin_addr) ; //客户端IP地址
            //ntohs 网络序列转主机序列
            *clientport= ntohs(temp.sin_port);//客户端的端口号
            

        }
        return sock;//返回新获取的套接字
   }

   int Connect(const std::string&serverip,const uint16_t &serverport )//发起链接
   {
      struct sockaddr_in server;
      memset(&server,0,sizeof(server));//清空
      server.sin_family=AF_INET;//16位地址类型
      server.sin_port=htons(serverport);//端口号
      //inet_addr  字符串风格IP转化为4字节风格IP
      server.sin_addr.s_addr=inet_addr(serverip.c_str());//IP地址
      //成功返回0,失败返回-1
      return  connect(_sock, (struct sockaddr*)&server,sizeof(server));
    
    }

    int Fd()
    {
      return _sock;
    }
    void Close()
    {
      if(_sock!=defaultfd)
     {
       close(_sock);
     }

    }
    
 ~Sock()//析构
 {
    
 }
 private:
 int _sock;

};


SelectServer_v2

相比于V1版本,V2版本的 type_t 类型从 int整形 变为 结构体


由于数组的每一个元素都是结构体,所以就需要都每一个成员进行初始化


同时分别通过第一个、第二个、第三个比特位 来表示读事件、写事件、异常事件

start的改写

由于数组的类型是一个结构体,对应的下标为0位置处的元素 也是要说明 结构体对应的成员

将下标为0位置处的元素 (lisetnsock套接字) 设置为读事件

在循环中,设置rfds(读集合)、wfds(写集合)
分别将两者清空


同样与V1相同,需要设置maxfd,用于slect的第一个参数

还需判断,当前的数组元素 是读集合还是写集合
最终将两者添加到select函数中
由于没有异常集合,所以设置为nullptr


HandlerEvent函数的改写

相比于V1,多了写事件,所以参数也应该多传一个


由于有读事件和写事件,所以 if 判断 else if 判断 区分

若为读事件,并且数组的元素 在rfds读集合中
则进入 if 判断
进行V1操作 即判断是 listensock套接字 还是 普通文件描述符
这里为了方便观看,所以把 普通文件描述符代码放入 Recver中


若为写事件,并且数组的元素在wfds写集合中
则进入else if 判断


完整代码

#include <iostream>
#include <string>
#include <sys/select.h>
#include <cstring>
#include "Sock.hpp"
#include "Log.hpp"
#include "Err.hpp"
using namespace std;

const static int gport = 8888;
const static int defaultevent=0;

// 通过第一个 第二个 第三个比特位 来表示 读 写 异常
#define READ_EVENT (0x1)        // 读事件
#define WRITE_EVENT (0x1 << 1)  // 写事件
#define EXCEPT_EVENT (0x1 << 2) // 异常事件

typedef struct FdEvent
{
  int fd;
  uint8_t event;       // 表示关心这个文件描述符上的什么事件
  string clientip;    // 该文件描述符是那个客户端ip地址
  uint16_t clientport; ////该文件描述符是那个客户端的端口号
} type_t;

class SelectServer
{ 
  static const int N = sizeof(fd_set) * 8; // N对应位图大小

public:
  SelectServer(uint16_t port = gport)
      : port_(port)
  {
  }

  void InitServer() // 初始化
  {
    listensock_.Socket();    // 创建套接字
    listensock_.Bind(port_); // 绑定
    listensock_.Listen();    // 设置监听状态

    // 对fdarray数组进行初始化
    for (int i = 0; i < N; i++)
    {
      fdarray_[i].fd = defaultfd;
      fdarray_[i].event = defaultevent; // 默认为0 表示一个事件都不关心
      fdarray_[i].clientport = 0;
    }
  }

  void Accepter() // 获取新连接的动作
  {
    // 这里再使用accept 就不会阻塞了
    // listen套接字底层一定有就绪的事件 即连接已经到来了
    string clientip;
    uint16_t clientport;
    int sock = listensock_.Accept(&clientip, &clientport); // 获取客户端IP和端口号
    if (sock < 0)
    {
      return;
    }

    // 当得到对应新连接的sock套接字,是不能进行read/recv
    // 并不知道sock上的数据是否就绪的
    // 所以需要将sock交给select,由select进行管理
    logMessage(Debug, "[%s:%d],sock:%d", clientip.c_str(), clientport, sock);
    // 只需把新获取的sock 添加到 数组中
    int pos = 1;
    for (; pos < N; pos++)
    {
      if (fdarray_[pos].fd == defaultfd) // 说明没有被占用
      {
        break;
      }
    }
    if (pos >= N) // 整个数组中的位置全被占用了
    {
      close(sock);
      logMessage(Warning, "sockfd[] array full");
    }
    else // 找到了对应的位置
    {
      fdarray_[pos].fd = sock;
      fdarray_[pos].event= READ_EVENT;
      fdarray_[pos].clientip=clientip;
      fdarray_[pos].clientport=clientport;

    }
  }

  void Recver(int index)
  {
    // 普通文件描述符就绪
    int fd = fdarray_[index].fd;
    char buffer[1024];
    ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0);
    // 读取不会被阻塞
    if (s > 0) // 读取成功
    {
      buffer[s - 1] = 0;
      cout <<fdarray_[index].clientip <<":" <<fdarray_[index].clientport<< buffer << endl;

      // 发送回去 也要被select管理
      string echo = buffer;
      echo += "[select server echo ]";
      send(fd, echo.c_str(), echo.size(), 0); // 发送消息 将echo内的数据 交给fd
    }
    else
    {
      if (s == 0) // 读到文件结尾
      {
        logMessage(Info, "client quit...,fdarray_[i] -> defaultfd:%d->%d", fd, defaultfd);
      }
      else // 读取失败
      {
        logMessage(Warning, "recv error,client quit...,fdarray_[i] -> defaultfd:%d->%d", fd, defaultfd);
      }
      close(fdarray_[index].fd);
      fdarray_[index].fd = defaultfd;
      fdarray_[index].event=defaultevent;
      fdarray_[index].clientip.resize(0);
      fdarray_[index].clientport=0;
    }
  }

      
  void HandlerEvent(fd_set &rfds, fd_set &wfds) // 处理就绪事件
  {
    for (int i = 0; i < N; i++)
    {
      if (fdarray_[i].fd == defaultfd)
      {
        continue;
      }
      // 合法fd

      // 若为读事件,并且读事件已经在rfds集合中
      if ((fdarray_[i].event & READ_EVENT) && FD_ISSET(fdarray_[i].fd, &rfds))
      {
        // 处理读取
        //  1.accept  2.recv
     
        // 若套接字为listensock套接字
        if (fdarray_[i].fd == listensock_.Fd())
        {
          Accepter();
        }
        // 若套接字不是listensock套接字,但在rfds集合中
        else if ((fdarray_[i].fd != listensock_.Fd()) )
        {
          Recver(i);
        }
        else 
        {}
      }
      //若为写事件,并且写事件已经在rfds集合中
      else  if ((fdarray_[i].event & WRITE_EVENT) && FD_ISSET(fdarray_[i].fd, &wfds))
      {
              
      }
      else 
      {
        //异常事件
      }
    }
  }
  

  void DebugPrint()
  {
    cout << "fdarray_[]:" << endl;
    for (int i = 0; i < N; i++)
    {
      if (fdarray_[i].fd == defaultfd)
      {
        continue;
      }
      cout << fdarray_[i].fd << " ";
    }
    cout << "\n";
  }



  void Start() // 启动
  {
    // 在网络中,新连接到来被当作 读事件就绪
    // 对应不同的事件就绪,做出不同的动作

    fdarray_[0].fd = listensock_.Fd(); // 先将listensock套接字添加到数组中
    fdarray_[0].event = READ_EVENT;    // 设置为 读事件

    while (true)
    {
      // 因为rfds是输入 输出型参数,就注定了每次都要对rfds进行重置
      // 重置 就通过 fdarray数组知道 历史上有那些fd

      // 因为服务器在运行中,sockfd的值一直在动态变化,所以maxfd也一直在变化
      // maxfd也要动态更新
      fd_set rfds;    // 作为读文件描述符集合
      fd_set wfds;    // 作为写文件描述符集合
      FD_ZERO(&rfds); // 将读集合清空
      FD_ZERO(&wfds); // 将写集合清空

      int maxfd = fdarray_[0].fd;
      for (int i = 0; i < N; i++)
      {
        if (fdarray_[i].fd == defaultfd)
        {
          continue;
        }
        // 将合法fd添加到rfds集合中

        if (fdarray_[i].event & READ_EVENT) // 若当前为读,则添加到读集合中
        {
          FD_SET(fdarray_[i].fd, &rfds);
        }
        if (fdarray_[i].event & WRITE_EVENT) // 若当前为写,则添加到写集合中
        {
          FD_SET(fdarray_[i].fd, &wfds);
        }
        if (maxfd < fdarray_[i].fd)
        {
          maxfd = fdarray_[i].fd;
        }
      }

      int n = select(maxfd + 1, &rfds, &wfds, nullptr, nullptr); // 将listensock套接字添加到读文件描述符集合中
      // timeout 设为nullptr后,全部为阻塞等待

      switch (n)
      {
      case 0: // 表示没有任何一个文件描述符就绪
        logMessage(Debug, "timeout,%d: %s", errno, strerror(errno));
        break;

      case -1: // 等待失败 返回-1
        logMessage(Warning, "%d: %s", errno, strerror(errno));
        break;

      default: // 大于0 ,则表示成功 返回有多少文件描述符就绪
        logMessage(Debug, "有一个就绪事件发生了:%d", n);
        HandlerEvent(rfds, wfds); // 处理就绪事件
        DebugPrint();             // 打印数组内容
        break;
      }
    }
  }
  
private:
  uint16_t port_;     // 端口号
  Sock listensock_;   // 创建Sock对象
  type_t fdarray_[N]; // 自己定义一个数组,与位图大小相同,来进行已经获得的sock进行管理
};

3. select 特点

  • 文件描述符是有上限的,云服务器是1024

  • 需要使用一个array数组保存slect中的文件描述符 (start最终版本有详细说)


4. select 缺点

  • 每次调用select时,都需要手动设置fd集合
    (如:每次都需要设置一个maxfd, 通过不断判断后 作为第一个参数)
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,在fd很多时开销会很大
    (每次都需要用户需要告诉内核,那些文件描述符的那些事件需要关心)
  • 每次调用select,都需要在内核遍历传递过来的所有fd,在fd很多时 开销会很大
    (每次内核都需要告诉用户,那些关心的文件描述符的那些事件就绪)
  • select支持的文件描述符太小
    (如:V1中设置fdarray数组的类型为int,大小为固定长度N)

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐