【计算机网络】高级IO——select

文章目录

  • 1. select函数介绍
    • 为什么要有select?
    • select 接口
      • 第一个参数 nfds的理解
      • 什么是 输入 输出型参数
      • 最后一个参数 timeout 的理解
      • readfds writefds exceptfds 参数的理解
      • select的返回值
  • 2. select的使用
    • SelectServer_v1
      • start 最初版本
      • start 最终版本
      • HandlerEvent函数——处理就绪事件
        • 第一种情况
          • Accepter函数
        • 第二种情况
      • 完整代码
        • SelectServer.hpp
        • Err.hpp(错误信息)
        • Log.hpp(日志打印)
        • main.cc(主函数调用)
        • makefile
        • Sock.hpp(套接字接口实现)
    • SelectServer_v2
      • start的改写
      • HandlerEvent函数的改写
      • 完整代码
  • 3. select 特点
  • 4. select 缺点

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 最初版本

【计算机网络】高级IO——select_第1张图片

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

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


【计算机网络】高级IO——select_第2张图片

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


start 最终版本

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


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

【计算机网络】高级IO——select_第3张图片
【计算机网络】高级IO——select_第4张图片

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


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

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

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


HandlerEvent函数——处理就绪事件

【计算机网络】高级IO——select_第5张图片

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

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


第一种情况

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

Accepter函数
【计算机网络】高级IO——select_第6张图片

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

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

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


第二种情况

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

【计算机网络】高级IO——select_第7张图片

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

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

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

完整代码

SelectServer.hpp
//SelectServer  V1版本
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
#include
#include

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
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
#include
#include
#include
#include
#include
#include
#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

【计算机网络】高级IO——select_第8张图片

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


【计算机网络】高级IO——select_第9张图片

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


【计算机网络】高级IO——select_第10张图片

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

start的改写

【计算机网络】高级IO——select_第11张图片

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

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

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


【计算机网络】高级IO——select_第12张图片

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

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


HandlerEvent函数的改写

【计算机网络】高级IO——select_第13张图片

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


【计算机网络】高级IO——select_第14张图片

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

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


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


完整代码

#include 
#include 
#include 
#include 
#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 特点

【计算机网络】高级IO——select_第15张图片
  • 文件描述符是有上限的,云服务器是1024

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


4. select 缺点

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

你可能感兴趣的:(计算机网络)