0
点赞
收藏
分享

微信扫一扫

IO多路转接之select(万字长文详解)

IO多路转接之select

select概念

我们要记住一个概念IO == 等待 + 拷贝!

select只负责等待,而且可以一次性等待多个数据!select本身是没有拷贝能力的!拷贝要read或者write来完成!

select函数

image-20231114114535797

==参数nfds==因为select一次可以等待多个文件描述符!而每一次文件描述符都是数据下标!所以多个文件描述符的数字大小肯定不一样!因此肯定存在一定存在最大/最小值!——==所以第一个参数的含义就是我们想要监视的多个文件描述符中的最大值+1==

==RETURN VALUE==

如果==返回值是大于0==,值是几就是表明有几个fd就绪了!例如:5,就表明有5个文件描述符就绪了!但是这个大小不会大于我们监视的文件描述符数!(监视10个不可能返回11个)

==如果返回值等于0==,代表的就是超时返回了!即:如果我们时间设置5s就说明超过5s返回了!如果是设置0s,也是超过0s后超时返回!

==如果返回值小于0==说明select调用失败了!例如:我们现在服务器中只打开了3,4,5三个文件描述符!只有这三个合法的文件描述符!但是现在我们要让把10,20号的文件描述符都管理起来!但是这两个文件描述符在进程里面压根没有被打开!那么此时select就会调用失败!

==当调用失败就会返回-1,错误码会被设置!==

==select中间的三个参数才是最重要的三个参数!==

select在等什么呢?select在等待文件描述符上的事件就绪!

事件一般分为三类

读事件就绪——表示这个文件描述符有数据可以读取了!

写事件就绪——缓冲区里面有空间了!可以写入了!

==读,写事件其实就是我们常说的IO——所以也统称为IO事件就绪!==

但是我们正常读写的时候,经常会发现各种各样的意外!例如:我们正在给对方进行写入!但是对方一下子把文件描述符关闭!那么我们就是给已经关闭的客户端写入!那么就有可能出现异常!或者是我们在读取的时候,传入的时候传入了一个异常的文件描述符!4.5.6都是正常的!但是我们传入了一个9,那么select在等待时候就可能会出错!

==所以从宏观上来说,时间还分正常事件(IO事件)和异常事件==

==select未来关心的事件只有三类,读事件,写事件,异常事件(无论读写)任何一个fd都是这三种情况!==

image-20231114162419081

但是不是说select能管理多个文件描述符的读写事件!可是现在除了第一个参数能给我们有多个文件描述符的感受之外,好像没有见到有多个文件描述符啊?

==我们就要了解一下fd_set这个类型!==

image-20231114163018699

因为文件描述符的本质是一个数组下标!所以文件描述符之间肯定是不同且连续的!所以我们可以使用位图的结构来表示

==所以我们select想要管理多个文件描述符,我们就要想办法把文件描述符添加进这些参数的位图里面!==

==而且这三个参数都是输入输出型参数!——我们也要认识一下他们输入输出的含义是什么!==

==我们以读事件为例==

image-20231114170825379

对同一个参数进行修改,我们就可以让用户和内核之间互相沟通,互相知晓对方要的或者关系的!——因为我们的参数是readfds!所以沟通的内容也就只停留在读事件上!凡是在readfds这个位图上的!都是只与读事件相关的!

如果想要关心写事件,我们只要向writefds传入相对应的位图即可!

我们可以定义两个位图!一个关心读,一个关心写!然后同时上传!那么操作系统就会同时去关心读写事件!如果想要先关心读,后关心写,那么只要调用两次select!先上传读位图,再上传写位图

==那么我们应该如何操作这些位图呢?==

我们可以直接用按位与或者按位或直接设置它吗?——不可以!也不建议!

为了更好的支撑这些操作所以操作系统也给我们提供了一系列的位图操作函数!

image-20231114171651305

==FD_CLR(fd_clear)——把一个文件描述符从一个集合里面清除==

==FD_ISSET——判断一个文件描述符是否在一个集合里面!==

==FD_SET——把一个文件描述符设置进一个集合里面!==

==FD_ZERO——清空整个文件描述符集合==

select的就绪条件

读就绪

select版本服务器

普通的阻塞服务器

//err.hpp
#pragma once
#include<iostream>
enum
{
 USAGE_ERR = -100,
 SOCKET_ERR,
 BIND_ERR,
 LISTEN_ERR,
 ACCEPT_ERR
};
//log.hpp

#pragma once
#include<iostream>
#include<string>
#include<cstdio>
#include<stdarg.h>
#include<ctime>
#include<unistd.h>
#include<sys/types.h>

#define LOG_ERR "log.errorr" 
#define LOG_NORMAL "log.normal" 

//日志是有日志等级的!来表示那些事情重要!那些事情不重要!
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3//这个的意思是虽然出错了!但是不影响我们后续的运行!
#define FATAL 4 //这个是致命的!意思是不仅仅是出错了!而且出错很严重!

const char* to_levelstr(int level)
{
    switch(level)
    {
        case DEBUG :return "DEBUG";
        case NORMAL :return "NORMAL";
        case WARNING :return "WARNING";
        case ERROR:return "ERROR";
        case FATAL :return "FATAL";
        default: return nullptr;
    }
}


void LogMessage(int level,const char* format,...)//这是一个可变参数列表!
{
    //[日志等级][时间搓/时间][pid][message]——日志格式
    //[WARNING][2023-05-11 18:08:08][123][调用socket失败!]
    va_list start;
    va_start(start,format);//初始化start,让其指向第一个可变参数的地址!
    //此时离可变参数列表最近的就是message
    #define NUM 1024
    char logprefix[NUM];
    snprintf(logprefix,sizeof(logprefix),"[%s][%ld][pid:%d]",to_levelstr(level),(long int)time(nullptr),getpid());
    //获取时间有time和gettimeofday和这两个的函数
    //localtime这个可以将时间转化为本地时间

    char logcontent[NUM];
    vsnprintf(logcontent,sizeof(logcontent),format,start);
    std::cout << logprefix << logcontent << std::endl;
}

//Sock.hpp
#pragma once

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>
#include<cstring>
#include<string>
#include"log.hpp"
#include"err.hpp"

class Sock//封装所有关于套接字的函数
{
public:
    const static int gbacklog = 5;

    static int Socket()//创建套接字
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0); // 因为我们使用的是udp协议!所以是SOCK_STREAM——字节流!
        if (sock == -1)
        {
            LogMessage(FATAL, "creat socket fail! ");
            exit(SOCKET_ERR);
        }

        LogMessage(NORMAL, "creat socket sucess! "); // 这是一个正常的常规日志!

        //我们要防止服务器挂掉从而处于timewait的状态!而不能立刻重启
        //设置地址复用
        int opt = 1;
        setsockopt(sock,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT,&opt,sizeof(opt));

        return sock;

    }

    static void Bind(int sock,int port)//绑定套接字
    {
        struct sockaddr_in peer;
        bzero(&peer, sizeof(peer));
        peer.sin_family = AF_INET;         // PF_INET也是可以的!两个是同一个的值不同的宏名字
        peer.sin_addr.s_addr = INADDR_ANY; // 服务器ip不需要一个固定的地址!
        peer.sin_port = htons(port);
        int n = bind(sock, (struct sockaddr *)&peer, sizeof(peer));
        if (n == -1)
        {
            LogMessage(FATAL, "bind socket fail! "); // 这是一个FATAL的日志
            exit(BIND_ERR);
        }

        LogMessage(NORMAL, "bind socket sucess! "); // 这是一个正常的常规日志!
    }


    static void Listen(int sock)//将套接字设置为监听状态
    {
        if (listen(sock, gbacklog) < 0)
        {
            LogMessage(FATAL, "listen socket fail! "); // 这是一个FATAL的日志
            exit(LISTEN_ERR);
        }
        LogMessage(NORMAL, "listen socket sucess! "); // 这是一个正常的常规日志!
    }


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

        std::cout << sock << std::endl;
        if (sock < 0)
        {
            LogMessage(ERROR, "accept fail! next"); // 这是一个错误!但是不是一个很严重的错误!
            // 不影响进程的接下来的正常运行!
            return ACCEPT_ERR;
        }
        LogMessage(NORMAL, "accept a link success!"); // 这是一个错误!但是不是一个很严重的错误!
        *clientip = inet_ntoa(peer.sin_addr);         // 获取ip
        *clientport = ntohs(peer.sin_port);           // 获取端口号


        return sock;
    }

};
//selectServer.hpp
#pragma once

#include<iostream>
#include"Sock.hpp"

namespace select_ns
{
  class SelectServer
  {
        static const int default_port = 8080;
    public:
        SelectServer(int port = default_port)
            : port_(port),listensock_(-1)
            {}

        void initServer()
        {
            listensock_ = Sock::Socket();//创建套接字
            Sock::Bind(listensock_,port_);//绑定套接字
            Sock::Listen(listensock_);    // 把套接字设置为监听状态!
        }

        void start()
        {
            for(;;)
            {
                std::string ClientIp;
                uint16_t ClientPort = 0;
                int sock = Sock::Accept(listensock_,&ClientIp,&ClientPort);//获取新连接

                if(sock < 0)continue;

            }

        }
        ~SelectServer()
        {
            if(listensock_< 0)close(listensock_);
        }

    private:
        int port_;
        int listensock_;
  };
}


#include"selectServer.hpp"
#include<memory>

using namespace std;
using namespace select_ns;
static void Usage(std::string proc)
{
    std::cerr<<"Usage:\n\t" <<proc <<" port"<<std::endl;
}

// ./select_server 8080
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return USAGE_ERR;
    }
    unique_ptr<SelectServer> svr(new SelectServer(atoi(argv[1])));
 svr->initServer();
    svr->start();
 return 0;
}

这样子我们就完成了一个普通服务器的编写!

但是我们在开始使用select之前我们要思考一个问题——select是用于管理多个文件描述符的!刚开始的服务器哪里有什么多个文件描述符!——我们只有一个listensock这个套接字!

但是我们经验告诉我们!我们后续多出来的所有的套接字其实都是来自listensock这个套接字!——==所以select想要监管多个套接字的话!首先就是把listensock交给select监管!==

==但是select的参数里面只有关心,读事件,写事件和异常事件!并没有所谓的监听事件!——其实listensock的连接就绪事件!其实本质就是一个读事件就绪!连接从底层来了!然后我们上层拿走它!这不就是一种读取数据吗?说的直白一点!这就是客户端向服务器发起的连接请求!从而触发三次握手!这是客户端向服务器发消息!那么服务器就要拿消息!==

==所以使用select这个代码我们就不能在start函数中进行循环使用accept函数来获取连接!!——因为accept函数,在没有连接的时候也在等待!accept函数也等于 等待+ 获取!==

==我们不知道底层的连接是否就绪!==

void start()
{
for(;;)
 {
     std::string ClientIp;
     uint16_t ClientPort = 0;
      int sock = Sock::Accept(listensock_,&ClientIp,&ClientPort);//获取新连接
      if(sock < 0)continue;
    }
}
//这种写法本质就是一种阻塞式的写法! 

我们想要的是底层连接已经就绪了!然后再来通知我们去获取!

select的用法细节

==select的用法细节其一——timeout参数==
void start()
{
 fd_set readfds;
 FD_ZERO(&readfds);//初始化
 FD_SET(listensock_,&readfds);//将listensock置入readfds中
 struct timeval timeout = {1,0};
 for(;;)
 {
       int n = select(listensock_+1,&readfds,nullptr,nullptr,&timeout);
       //listensock+1是不正确的写法!因为这是一个常数!
       switch(n)
       {
            case 0://如果返回0说明超时了!
                LogMessage(NORMAL,"timeout....");
                break;
            case -1:
                LogMessage(WARNING,"select error,code: %d,err string: %s",errno,strerror(errno));
                break;
            default:
                LogMessage(NORMAL,"get a new link ....");
                //说明事件就绪了!但是目前只有一个监听时间!
                break;
       }
 }

}

==我们发现怎么写会停1s后就直接疯狂打印!==

image-20231115110911665

==为什么会发生这样的事情呢?——因为我们上面说过timeout参数是一个输入输出型参数!==

开始的时候,select在里面等待的时候会慢慢的对timeout参数进行--,然后到0之后就超时返回!==然后当我们进入下一次循环的时候,select就已经被修改过了!==

==后面就全部变成非阻塞了!==

void start()
{
 fd_set readfds;
 FD_ZERO(&readfds);//初始化
 FD_SET(listensock_,&readfds);//将listensock置入readfds中
 for(;;)q
 {
       /////////////////////////////////////////////////
       struct timeval timeout = {1, 0};
       //正确的写法是将timeout放在for循环的内部!每次循环都要重新设置!
       //如果改成{0,0}那么就是非阻塞等待
       //////////////////////////////////////////////

       int n = select(listensock_+1,&readfds,nullptr,nullptr,&timeout);
       //listensock+1是不正确的写法!因为这是一个常数!

       //////////////////////////////////////////////////
       //int n = select(listensock_+1,&readfds,nullptr,nullptr,nullptr);
       //timeout参数设置成nullptr就是阻塞式等待!
       //////////////////////////////////////////////

       switch(n)
       {
            case 0://如果返回0说明超时了!
                LogMessage(NORMAL,"timeout....");
                break;
            case -1:
                LogMessage(WARNING,"select error,code: %d,err string: %s",errno,strerror(errno));
                break;
            default:
                LogMessage(NORMAL,"get a new link ....");
                //说明事件就绪了!但是目前只有一个监听时间!
                break;
       }
 }
}

image-20231115111859953

==select的用法细节其二——readfds参数==

==但是我们发现,我们连接的时候没有任何反应!==

image-20231115150326458

为什么呢?——因为我们select函数的readfds参数也是一个输入输出型参数!==当等待超时返回后!readfds也被置成全0了所以这样写是有问题的!==我们要把设置这样子也放进循环里面才可以!否则select一旦返回我们的readfds参数的内容就被修改了!

void start()
{
    for(;;)
    {
        //////////////////////////////////////
        //设置也要放进循环里面!
        fd_set readfds;
        FD_ZERO(&readfds);             // 初始化
        FD_SET(listensock_, &readfds); // 将listensock置入readfds中
        ///////////////////////////////////

        struct timeval timeout = {1, 0};
        int n = select(listensock_+1,&readfds,nullptr,nullptr,&timeout);
        //listensock+1是不正确的写法!因为这是一个常数!
        switch(n)
        {
            case 0://如果返回0说明超时了!
                LogMessage(NORMAL,"timeout....");
                break;
            case -1:
                LogMessage(WARNING,"select error,code: %d,err string: %s",errno,strerror(errno));
                break;
            default:
                LogMessage(NORMAL,"get a new link ....");
                //说明事件就绪了!但是目前只有一个监听时间!
                break;
        }
    }
}

image-20231115150857871

==我们发现,我们现在select就可以监视listensock,链接的时候,也可以成功获取!但是为什么会出现不断的循环打印呢?——因为此时我们没有把连接从底层拿起来!==

==select的用法细节其三——select只是负责等待的!我们要自己去将连接从底层拿起来!==
void HandlerEvent(fd_set& rfds)
{
    // 这里目前只能是listensock!
    if(FD_ISSET(listensock_,&rfds))
    {
        //select告诉我们,listensock读事件就绪了!
        std::string ClientIp;
        uint16_t ClientPort = 0;
        int sock = Sock::Accept(listensock_, &ClientIp, &ClientPort); // 获取新连接
        //走到这里accept函数不会阻塞!直接读取!
        if(sock == ACCEPT_ERR)return;
    }
}
void start()
{
    for(;;)
    {
        fd_set readfds;
        FD_ZERO(&readfds);             // 初始化
        FD_SET(listensock_, &readfds); // 将listensock置入readfds中

        struct timeval timeout = {1, 0};
        int n = select(listensock_+1,&readfds,nullptr,nullptr,&timeout);
        //listensock+1是不正确的写法!因为这是一个常数!
        switch(n)
        {
            case 0://如果返回0说明超时了!
                LogMessage(NORMAL,"timeout....");
                break;
            case -1:
                LogMessage(WARNING,"select error,code: %d,err string: %s",errno,strerror(errno));
                break;
            default:
                LogMessage(NORMAL,"get a new link ....");
                ////////////////////////////////////////////////
                HandlerEvent(readfds);
                //开始处理就绪事件!
                /////////////////////////////////////////////////
                break;
        }

    }

}

image-20231115151926161

==这样子我们就能把连接从底层拿出来了!select就不会一直通知我们了!==

void HandlerEvent(fd_set& rfds)
{
    // 这里目前只能是listensock!
    if(FD_ISSET(listensock_,&rfds))
    {
        //select告诉我们,listensock读事件就绪了!
        std::string ClientIp;
        uint16_t ClientPort = 0;
        int sock = Sock::Accept(listensock_, &ClientIp, &ClientPort); // 获取新连接
        //走到这里accept函数不会阻塞!
        if(sock == ACCEPT_ERR)return;

        //这里我们也能直接read/recv么?——不能!
    }
}

==为什么不能呢?——因为如果我们在这里直接直接开始创建线程/创建进程然后read/recv,派发任务的话,那么和我们上面的阻塞式代码其实没有什么区别!而且更加复杂了!==

==而且我们确实是成功建立连接了!如果直接进行read和recv,我们能保证底层有数据么?我们是不清楚的!——也就是说read/recv还是要进行等待+拷贝!如果进入等待,我们服务器如果是单进程就直接挂起了!就重新变成阻塞式调用了!==

==我们的整个代码中只有select才有资格检测事件是否就绪!==

void HandlerEvent(fd_set& rfds)
{
    // 这里目前只能是listensock!
    if(FD_ISSET(listensock_,&rfds))
    {
        //select告诉我们,listensock读事件就绪了!
        std::string ClientIp;
        uint16_t ClientPort = 0;
        int sock = Sock::Accept(listensock_, &ClientIp, &ClientPort); // 获取新连接
        //走到这里accept函数不会阻塞!
        if(sock == ACCEPT_ERR)return;


        //这里我们也能直接read/write么?——不能!
        //我们这里要将新的sock托管给select!让select关心新的连接是否就绪!
        //我们可以把select直接返回,然后进行处理吗?可以!但是这样很麻烦!
        //一般而言,要使用select要程序员自己维护一个保存所有合法fd的数组!
        //因为随着连接越来越多,我们的fd也会越来越多!有的连接断开后我们怎么知道该fd是合法的?
        //如果不保存!select每次都会修改readfds的内容!下一次我们怎么知道要设置什么合法fd呢?
        //同时要怎么知道最大文件描述符是谁?
    }
}
class SelectServer
{
    static const int default_port = 8080;
public:
    //......
private:
    int port_;
    int listensock_;
    int *fdarray_;//多一个数组
};
SelectServer(int port = default_port)
    : port_(port),listensock_(-1),fdarray_(nullptr)
    {}
void initServer()
{
    listensock_ = Sock::Socket();//创建套接字
    Sock::Bind(listensock_,port_);//绑定套接字
    Sock::Listen(listensock_);    // 把套接字设置为监听状态!

    // fdarray_ = new int[];
    //这个数组多大合适呢?
    //我们在select中使用的fd_set这个是一个自定义类型!
    //既然是类型,就必有大小!且一定是固定的!
    //所以fd_set这个位图能添加的fd个数一定是有上限的!
}
int main()
{
    std::cout << "fd_set_size: " <<sizeof(fd_set) <<std::endl;
}

image-20231115165307441

select服务器的简单实现

d#pragma once
#include<iostream>
#include"Sock.hpp"
namespace select_ns
{
    class SelectServer
    {
        static const int default_port = 8080;
        ////////////////////////////////////////////////
        static const int fd_num = sizeof(fd_set)*8;//数组的大小
        static const int defaultfd = -1;//用于初始化数组!
        ////////////////////////////////////////////////
    public:
        SelectServer(int port = default_port)
            : port_(port),listensock_(-1),fdarray_(nullptr)
            {}

        void initServer()
        {
            listensock_ = Sock::Socket();//创建套接字
            Sock::Bind(listensock_,port_);//绑定套接字
            Sock::Listen(listensock_);    // 把套接字设置为监听状态!
            ///////////////////////////////////////////
            fdarray_ = new int[fd_num];
            for(int i = 0;i<fd_num;i++) fdarray_[i] = defaultfd;
            // 初始化该数组!
            fdarray_[0] =listensock_;
            ////////////////////////////////////////////
        }
        void Print()
        {
            for(int i = 0;i<fd_num;i++)
            {
                if (fdarray_[i] != defaultfd)
                    std::cout << "fd list: " << fdarray_[i] << " ";
            }
            std::cout << std::endl;
        }

        void HandlerEvent(fd_set& rfds)
        {
            // 这里目前只能是listensock!
            if(FD_ISSET(listensock_,&rfds))
            {
                //select告诉我们,listensock读事件就绪了!
                std::string ClientIp;
                uint16_t ClientPort = 0;
                int sock = Sock::Accept(listensock_, &ClientIp, &ClientPort); // 获取新连接
                //走到这里accept函数不会阻塞!
                if(sock == ACCEPT_ERR)return;

                ////////////////////////////////////////////////
                //将新的sock托管给select!
                //而托管给select的本质就是,将sock添加到fdarray_这数组中!
                //当下一次循环的时候,就会被托管进select里面!
                int i =0;
                for(i = 0;i<fd_num;i++)
                {
                    if(fdarray_[i] != defaultfd)
                        continue;
                    break;
                }
                if(i == fd_num)//这说明所有的数组元素都是合法的!
                {
                    LogMessage(WARNING,"server is full! please wait!");
                    close(sock);//关闭这个新的套接字
                }
                else 
                {
                    fdarray_[i] = sock;
                }
                Print();
                ////////////////////////////////////////////////
            }
        }
        void start()
        {
            for(;;)
            {
                fd_set readfds;
                FD_ZERO(&readfds);             // 初始化
                ////////////////////////////////////////////////
                int maxfd = fdarray_[0];
                for (int i = 0; i < fd_num; i++)
                {
                    if(fdarray_[i] == defaultfd)
                        continue;
                    if (fdarray_[i] > maxfd)//找到最大的fd
                        maxfd = fdarray_[i];

                    FD_SET(fdarray_[i], &readfds); // 将全部合法fd都全部添加都添加到集合里面!
                }
                ////////////////////////////////////////////////

                struct timeval timeout = {1, 0};
                int n = select(maxfd+1,&readfds,nullptr,nullptr,&timeout);

                switch(n)
                {
                    case 0://如果返回0说明超时了!
                        LogMessage(NORMAL,"timeout....");
                        break;
                    case -1:
                        LogMessage(WARNING,"select error,code: %d,err string: %s",errno,strerror(errno));
                        break;
                    default:
                        LogMessage(NORMAL,"get a new link ....");
                        HandlerEvent(readfds);
                        //说明事件就绪了!但是目前只有一个监听时间!
                        break;
                }
            }
        }
        ~SelectServer()
        {
            if(listensock_< 0) close(listensock_);
            if (fdarray_ != nullptr) delete[] fdarray_;
        }
    private:
        int port_;
        int listensock_;
        int *fdarray_;

    };
}

image-20231115174034616

==我们可以看到已经成功的把新的sock放入我们维护的数组中!我们只使用了单进程就可以接收多个的连接!==

==可是随着文件描述符越来越多!数组监管的文件描述符也会越来越多!可是我们事件的种类可不止有读事件!还有写事件和异常事件!==

void HandlerEvent(fd_set& rfds)
{
    //....
}

==对于handlerEvent这个函数的rfds参数我们,要要认识到,在fds里面不止有一个fd是就绪的!可能是有多个!==

==而我们现在的select只处理了读事件!——那么如果事件种类多起来!我们怎么知道那个文件描述符上有有种事件就绪了呢?==

#pragma once

#include"Sock.hpp"
#include<iostream>
#include<functional>
#include<string>



namespace select_ns
{
    class SelectServer
    {
        static const int default_port = 8080;
        static const int fd_num = sizeof(fd_set)*8;
        static const int defaultfd = -1;

        ////////////////////////////////////////////////////////////////////
        using func_t = std::function<std::string(const std::string&)>;
        ////////////////////////////////////////////////////////////////////
     public:
        SelectServer(func_t func,int port = default_port)
            : port_(port),listensock_(-1),fdarray_(nullptr),func_(func)
            {}

        void initServer()
        {
            listensock_ = Sock::Socket();//创建套接字
            Sock::Bind(listensock_,port_);//绑定套接字
            Sock::Listen(listensock_);    // 把套接字设置为监听状态!
            fdarray_ = new int[fd_num];
            for(int i = 0;i<fd_num;i++) fdarray_[i] = defaultfd;
            // 初始化该数组!
            fdarray_[0] =listensock_;
        }
        void Print()
        {
            for(int i = 0;i<fd_num;i++)
            {
                if (fdarray_[i] != defaultfd)
                    std::cout << "fd list: " << fdarray_[i] << " ";
            }
            std::cout << std::endl;
        }


        //////////////////////////////////////////////////////////////////////////////////
        void Accepter(int listensock)
        {
            // select告诉我们,listensock读事件就绪了!
            std::string ClientIp;
            uint16_t ClientPort = 0;
            int sock = Sock::Accept(listensock, &ClientIp, &ClientPort); // 获取新连接
            // 走到这里accept函数不会阻塞!
            if (sock == ACCEPT_ERR)
                return;

            // 将新的sock托管给select!
            // 而托管给select的本质就是,将sock添加到fdarray_这数组中!
            // 当下一次循环的时候,就会被托管进select里面!
            int i = 0;
            for (i = 0; i < fd_num; i++)
            {
                if (fdarray_[i] != defaultfd)
                    continue;
                break;
            }

            if (i == fd_num) // 这说明所有的数组元素都是合法的!
            {
                LogMessage(WARNING, "server is full! please wait!");
                close(sock); // 关闭这个新的套接字
            }
            else
            {
                fdarray_[i] = sock;
            }
            Print();
        }
        /////////////////////////////////////////////////////////////////////////////////////////


        ///////////////////////////////////////////////////////////////////////////////////////////
        void Recver(int sock,int pos)//pos参数是为了方便我们需要的时候删除掉这个sock
        {
            //1.读取
            char buffer[1024];
            ssize_t s = recv(sock,buffer,sizeof(buffer) -1,0);//这里再进行读取的时候就不会被阻塞了!
            //这样子的读写时有问题的!因为不能肯定能不能把缓冲区的数据都读取上来!
            //也不能保证读一半的时候该怎么办!序列化和反序列化该怎么处理!但是为了方便我演示我们就暂时怎么写!
            if(s > 0)
            {
                buffer[s] = 0;
                LogMessage(NORMAL,"Client# %s",buffer);
            }
            else if(s == 0)//对方关闭文件描述符!
            {
                close(sock);//我们也要关闭自己的这边的文件描述符!
                //然后让select不要再关心这个文件描述符了!
                fdarray_[pos] = defaultfd;
                LogMessage(NORMAL,"client quit! me too!");
                return;
            }
            else //读取失败
            {
                close(sock);//我们也要关闭自己的这边的文件描述符!
                //然后让select不要再关心这个文件描述符了!
                fdarray_[pos] = defaultfd;
                LogMessage(ERROR,"read error! errno: %s,error string: %s",errno,strerror(errno));
                return;
            }

            //2.处理request
            std::string response = func_(buffer);

            //返回response
            //write——我们这里打算把结果给用户返回!
            //但是我们如何保证写入的时候写事件就就绪了呢?——那么我们就要给select多加一个新的writefds
            //同时也要有一个新的数组来维护这些fd!但是因为这样子会让代码更加的复杂!我们这里就不进行实现!
            //所以我们就直接写回去了!暂时不考虑写回去的暂停问题!

            write(sock, response.c_str(), response.size());
        }
        //////////////////////////////////////////////////////////////////////////////////////////////


        void HandlerEvent(fd_set& rfds)
        {
            //我们无法知道那个文件描述符对应的事件是什么!
            //所以我们只能对所有的文件描述符进行遍历!
            for(int i = 0;i<fd_num;i++)
            {
                //过滤非法fd
                if(fdarray_[i]== defaultfd) continue;//非法fd不进行处理!

                //从这里开始就是正常的fd!但是正常的fd不一定就绪了!


                if (FD_ISSET(fdarray_[i], &rfds) && fdarray_[i] == listensock_)//listensock就是对应第0个下标!
                {
                    /////////////////////////////////////////////////////////////////////
                    std::cout << "this is accept"<<std::endl;
                    Accepter(listensock_);
                    //////////////////////////////////////////////////////////////////
                }
                else if(fdarray_[i] != defaultfd && FD_ISSET(fdarray_[i],&rfds))//走到这里就说明把listensock上的事件都处理完毕了!
                {
                    ////////////////////////////////////////////////////////////////////
                    //其他事件就是常规的IO事件!
                    std::cout << "this is Recver"<<std::endl;
                    Recver(fdarray_[i],i);
                    ////////////////////////////////////////////////////////////////////
                }
            }
        }

        void start()
        {

            for(;;)
            {

                fd_set readfds;
                FD_ZERO(&readfds);  // 初始化

                int maxfd = fdarray_[0];
                for (int i = 0; i < fd_num; i++)
                {
                    if(fdarray_[i] == defaultfd)
                        continue;
                    if (fdarray_[i] > maxfd)//找到最大的fd
                        maxfd = fdarray_[i];

                    FD_SET(fdarray_[i], &readfds); // 将全部合法fd都全部添加都添加到集合里面!
                }

                struct timeval timeout = {1, 0};
                //////////////////////////////////////////////////////////////////////////////////
                // int n = select(maxfd+1,&readfds,nullptr,nullptr,&timeout);
                int n = select(maxfd+1,&readfds,nullptr,nullptr,nullptr);//设置成阻塞方便演示
                ///////////////////////////////////////////////////////////////////////////////////

                switch(n)
                {
                    case 0://如果返回0说明超时了!
                        LogMessage(NORMAL,"timeout....");
                        break;
                    case -1:
                        LogMessage(WARNING,"select error,code: %d,err string: %s",errno,strerror(errno));
                        break;
                    default:
                        // LogMessage(NORMAL,"get a new link ....");
                        LogMessage(NORMAL,"have a event ready");
                        HandlerEvent(readfds);
                        //说明事件就绪了!但是目前只有一个监听时间!
                        break;
                }
            }

        }
        ~SelectServer()
        {
            if(listensock_< 0) close(listensock_);
            if (fdarray_ != nullptr) delete[] fdarray_;
        }

    private:
        int port_;
        int listensock_;
        int *fdarray_;
        func_t func_;//用来传入对请求的处理函数!

    };
}

image-20231115215336089

==这里select在获取到新链接后,就会继续阻塞!因为底层已经被拿完了!——然后同时监视着listensock和新sock两个连接!当我们发送消息的时候!新sock连接底层就会收到消息!select就会启动!提醒我们收到连接了!然后我们Recver函数中的recv函数就会拿起来底层的数据!然后通过处理函数!获得response!然后再将response发送回去!然后进入下一个循环!select继续阻塞!==

==而且无论接收多少个连接我们始终只有一个进程!==

如果我们想要处理写事件

void start()
{
    for(;;)
    {
        fd_set writefds;
        //.......
        switch(n)
        {
            case 0:
                LogMessage(NORMAL,"timeout....");
                break;
            case -1:
                LogMessage(WARNING,"select error,code: %d,err string: %s",errno,strerror(errno));
                break;
            default:
                LogMessage(NORMAL,"have a event ready");
                HandlerEvent(readfds);
                /////////////////////////////////////////////////////////////////////////////
                //如果以后我们想要处理写事件!我们只要多写一个函数即可处理!只不过要多一个数组保存写的文件描述符
                //然后select的参数中要传入writefds的位图!
                HandlerWriteEvent(wfds);//用来处理读事件的函数!
                /////////////////////////////////////////////////////////////////////////////
                break;
        }
    }

}

select服务器的缺点

1.可监控的文件描述符个数取决与sizeof(fd_set)的值.——也就是说select能同时等待的fd文件是有上限的!这个上限除非修改内核!否则无法解决!原因就是fd_set是一个位图结构!只要是位图结构就有上限!

2.将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd——即要借助第三方数组来维护合法的fd!

举报

相关推荐

0 条评论