高级IO—select

news2024/11/17 23:33:40

高级IO—select


文章目录

  • 高级IO—select
      • IO的概念
    • 五种IO模型
      • 阻塞IO
      • 非阻塞IO
      • 信号驱动IO
      • IO多路转接
      • 异步IO
    • I/O多路转接之select

IO的概念

通常指数据在内部存储器和外部存储器或其他周边设备之间的输入和输出。输入是系统接收的信号或数据,输出则是从其发送的信号或数据。也可把输入输出认为是信息处理系统(例如计算器)与外部世界(人类或另一信息处理系统)之间的通信。

IO分为IO设备和IO接口

  • IO设备

IO设备是硬件中由人使用并与计算机进行通信的设备。例如键盘或鼠标是计算机的输入设备,监视器和打印机是输出设备。计算机之间的通信设备进行的通常是运行输入输出操作。

  • IO接口

I/O接口的功能是负责实现CPU通过系统总线把I/O电路和外围设备联系在一起。IO函数的底层是系统提供的系统调用,供用户通过调用来实现从用户态到内核态或内核态到用户态的数据拷贝。

image-20231120103359858

实际上在网络通信中,调用write并不是直接将数据写到网络中,而是将数据从应用层拷贝到传输层的发送缓冲区当中,然后由OS自主决定什么时候将数据向下交付,发送到网络中。同理调用read并不是直接从网络中读取数据,而是将传输层的接收缓冲区的数据读到应用层中。这意味调用read的时候,传输层的接收缓冲区并没有数据,那么read函数就会阻塞住,直到缓冲区有数据,才能将数据读到应用层。

因此IO本质不仅仅只有读取/写入,还有等待资源就绪的过程,即等+拷贝

提高IO的效率本质是每次IO中减少等待的时间,让IO过程尽可能都是拷贝。因此为了提高IO的效率,衍生出多种IO模型。

五种IO模型

阻塞IO

在内核将数据准备好之前,系统调用会一直等待,所有的套接字默认的是阻塞方式。

常见的阻塞IO模型

用户调用recvfrom函数,尝试读取数据,即调用系统调用,由用户态切换到内核态,由于数据没有准备好导致阻塞等待,数据准备好了立刻拷贝数据报并返回用户态。

image-20231120105340836

代码以使用read为例,读取文件描述符为0即stdin的数据,默认以阻塞式方式读取

#include"until.hpp"
#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
    char buffer[1024];
    while(true)
    {
        printf(">>>>");
        fflush(stdout);
        ssize_t i=read(0,buffer,sizeof(buffer)-1);
        if(i>0)
        {
            buffer[i-1]=0;
            cout<<"echo# "<<buffer<<endl;
        }else if(i==0)
        {
            cout<<"read end"<<endl;
            break;
        }else{
            //...
        }
    }
    return 0;
}

image-20231121171724379

非阻塞IO

如果内核还未将数据准备好, 系统调用不会阻塞等待,会直接返回, 并且返回EWOULDBLOCK错误码。

非阻塞IO往往需要程序员以循环的方式反复尝试读写文件描述符, 这个过程称为轮询。这意味着轮询的过程需要一直占用CPU资源,对CPU来说是较大的浪费,一 般只有特定场景下才使用。

常见的非阻塞IO模型

用户调用recvfrom函数,这次该函数是以非阻塞的方式进行调用,尝试读取数据,由用户态切换到内核态,由于数据没有准备好,直接返回EWOULDBLOCK。因此程序员需要以轮询的方式调用recvfrom函数,数据准备好了立刻拷贝数据报并返回用户态。轮询的过程中一是需要占用CPU的资源,二是需要多次进行用户态与内核态之间的转换,资源浪费较为严重,该方式一般在特定场景才使用。

image-20231120105902797

需要将文件描述符设置为非阻塞状态,那么读取该文件描述符就以非阻塞方式读取。

fcntl

用于控制文件描述符属性的系统调用,它可以用于执行各种操作,包括设置文件状态标志、获取文件状态标志、锁定文件等。

函数原型

#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* struct flock *flockptr */);
  • fd:表示要操作的文件描述符。
  • cmd:表示操作类型,可以是以下值之一:F_GETFL:获取文件状态标志,F_SETFL:设置文件状态标志,F_GETLK:获取文件锁定信息,F_SETLK:设置文件锁定等。
  • 使用不同的cmd,会有不同的返回值。使用F_GETFL时,返回值是文件状态标志flag。可以通过文件状态标志将文件设置为非阻塞状态。

until.hpp

#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
void Setnonblock(int sock)
{
    int flag=fcntl(sock,F_GETFL,0);
    if(flag<0)
    {
        perror("fcntl");
        return;
    }
    fcntl(sock,F_SETFL,flag|O_NONBLOCK);//把文件描述符状态设置为非阻塞O_NONBLOCK
}
#include"until.hpp"
#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
    char buffer[1024];
    Setnonblock(0);
    while(true)
    {
        printf(">>>>");
        fflush(stdout);
        ssize_t i=read(0,buffer,sizeof(buffer)-1);
        if(i>0)
        {
            buffer[i-1]=0;
            cout<<"echo# "<<buffer<<endl;
        }else if(i==0)
        {
            cout<<"read end"<<endl;
            break;
        }else{
            //...
        }
        sleep(1);
    }
    return 0;
}

image-20231121173823759

  • 非阻塞的返回值

对于非阻塞来说,底层没有数据直接返回,返回值为-1,但这并不是发生错误,原因由错误码来标记。错误码为EAGAINEWOULDBLOCK表示没有读取到数据。相同的还有EINTER表示因为信号中断导致返回,需要继续读取。

#include"until.hpp"
using namespace std;


fd_set readset;
int main()
{
 setNonBlock(0);//将输入缓冲区的IO行为设置为非阻塞
    char buffer[1024];//设置缓冲区
    while(true)
    {
        ssize_t i= read(0,buffer,sizeof(buffer)-1);//从文件描述符为0(键盘)开始读,读到buffer缓冲区中
        if(i>0)
        {
            buffer[i-1]=0;
            cout<<"echo# "<<buffer<<endl;
        }else if(i==0)
        {
            cout<<"read end"<<endl;
            break;
        }else
        {
            cout<<"i: "<<i<<endl;
            cout<<"EAGAIN: "<<EAGAIN<<endl;
            cout<<"EWOULDBLOCK: "<<EWOULDBLOCK<<endl;

        }
        sleep(1);
    }

    return 0;
}

image-20231121205815467

非阻塞没有读取到数据直接返回的错误码是11,EAGAINEWOULDBLOCK的错误码也是11。

信号驱动IO

内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。

常见的信号驱动IO模型

先前建立好SIGIO信号处理程序,进程将等待资源就绪的过程托管给sigaction函数,让该函数去等待数据,数据准备好后,以信号通知的方式返回,通知进程,此时进程直接调用recvfrom函数,拷贝数据报并返回。

image-20231120110521585

IO多路转接

IO多路转接能够同时等待多个文件描述符的就绪状态。

常见的IO多路转接模型

进程将等待资源的过程托管给select函数,让select去等待数据,资源准备好后,select函数返回可读条件,通知进程,此时进程直接调用recvfrom函数,拷贝数据报并返回。这意味着可以让多个进程将等待资源的过程托管给同一个select函数,哪个资源就绪,select函数就通知相应的程序进行读取。

image-20231120114836319

异步IO

由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。

常见异步IO模型

进程需要读取某种资源时,调用aio_read函数(系统调用),将IO(等+拷贝)的过程托管给OS,让OS负责等,数据准备好后,OS自动将数据拷贝到用户层的缓冲区,然后返回指定信号,通知进程来处理数据。进程并不参与IO的过程,只负责处理数据。

image-20231120120525495

总结一下:

  1. 阻塞、非阻塞、信号驱动在IO的效率上并无差别,差别在于等待资源的过程。阻塞式在等的过程中不能做别的事,而非阻塞和信号驱动在等的过程中可以做其他事情。
  2. 阻塞、非阻塞、信号驱动、多路转接实际上都参与了IO的过程,即IO的等待过程和拷贝过程,参与了其中一个过程都算作是同步IO。
  3. 异步IO是将IO过程托管给OS,并没有参与IO的过程。
  4. 多路转接的高效在于可以同时等待多个文件描述符,即等待多个资源就绪,并行等待资源,减少了等待资源的过程。

I/O多路转接之select

select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。可以将多个文件描述符托管给select去等待,存在文件描述符就绪,select返回,通知程序调用读取调用对应的资源。

函数原型

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
  • nfds表示监视文件描述符的最大值加1。
  • readfds:指向一个fd_set结构的指针,包含要监视可读性的文件描述符。
  • writefds:指向一个fd_set结构的指针,包含要监视可写性的文件描述符。
  • exceptfds:指向一个fd_set结构的指针,包含要监视异常情况的文件描述符。
  • timeout:指向struct timeval结构的指针,用于设置超时时间。如果为NULLselect函数将一直阻塞,直到有文件描述符就绪。
  • select函数的返回值是就绪文件描述符的数量,如果返回值为-1,则表示出现错误。在这种情况下,可以使用perror函数来输出错误信息。

说明一下:

  1. 由于文件描述符是OS中的文件描述符表的下标,该表是从小到大依次使用,因此所被占用的文件描述符是连续的,即nfds能涵盖所使用的文件描述符范围。如nfds=5,表示在0~4号文件描述符中查询。

  2. readfdswritefdsexceptfdstimeout都是指针,即都是输入输出型参数。timeout所指向的结构是能够表示秒、微妙。

     struct timeval {
                   long    tv_sec;         /* seconds */
                   long    tv_usec;        /* microseconds */
               };
    
    struct timeval timeout ={0,0};//表示非阻塞。
    struct timeval timeout =nullptr;//表示阻塞。
    struct timeval timeout ={5,0};//表示5秒内是阻塞式,超过5秒,非阻塞返回一次。
    

    需要注意的是,timeout是输入输出型参数,例如timeout ={5,0},若在第3秒结束时sock就绪,此时timeout的返回值为等待的剩余时间,即返回值为2秒即timeout ={2,0}。若在5秒期间sock都没有就绪,那么返回值为0秒即timeout ={0,0},此时再次将该timeout参数传入就表示非阻塞等待。因此timeout参数在传入时需要重置。

  3. fd_set实际上是一个位图结构。以readfd为例,用户想要OS关心4,5号文件描述符的读时间,那么输入的位图结构是0011 0000

image-20231122154737459

当关心时间内5号文件描述符就绪了,OS会对输入的位图进行改动,输出表示哪些文件描述符已经就绪。输出的位图结构是0010 0000

image-20231122154905077

readfdwritefdexceptfd的结构都是位图,且是分别不同的位图,因此用户可以传入一个或多个位图让OS关心位图指定的文件描述符上的读事件,写事件,异常事件,OS通过该位图输出哪些事件已经就绪。

我们并不需要直接传入自己设置的位图结构,而是通过OS提供的接口对位图进行修改。 可以使用以下宏来操作fd_set

FD_ZERO(fd_set*set);将set中的所有位清零
FD_SET(int fd, fd_set *set);将set中的指定文件描述符位设置为1。传入fd,用位图来标定传入的fd是否需要关心。
FD_CLR(int fd, fd_set *set);将set中的指定文件描述符位清零。
FD_ISSET(int fd, fd_set *set);检查set中的指定文件描述符位是否被设置为1

通过一段server代码来应用select函数

select.hpp

#include<unistd.h>
#include"Sock.hpp"
using namespace std;
static const int defaultport=8081;
class SelectServer
{
public:

SelectServer(uint16_t port=defaultport):_port(port)
{}

void initserver()
{
_listensock=Sock::Socket();//创建套接字
Sock::Bind(_listensock,_port);//bind信息
Sock::Listen(_listensock);//把sock设置为监听状态

}

void start()
{
    for(;;)
    {
        fd_set rfd;
        FD_ZERO(&rfd);//清空位图
        FD_SET(_listensock,&rfd);//把listensock设置进位图,企图让OS对该sock关心
        struct timeval timeout={1,0};
        int n=select(_listensock+1,&rfd,nullptr,nullptr,&timeout);
        switch (n)
        {
        case 0:
        cout<<"timeout......"<<endl;
            break;
        case -1:
        cout<<"select err"<<endl;
        default:
        cout<<"get new link..."<<endl;
            break;
        }
        sleep(1);
    }
}

private:
uint16_t _port;
int _listensock;
};

main.cc

#include<iostream>
#include<unistd.h>
#include<memory>
#include"select.hpp"
using namespace std;

static void Usage(string proc)
{
    cerr<<"Usage:\n\t"<<proc<<" port "<<"\n\n";
}

string resp(const string& s)
{
    return s;
}

int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    unique_ptr<SelectServer> selsv(new SelectServer(atoi(argv[1])));
    selsv->initserver();
    selsv->start();
    
    return 0;
}

Sock.hpp

#pragma once

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

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR
};

class Sock
{
    const static int backlog=32;

    public:

    static int Socket()
    {
        int sock=socket(AF_INET,SOCK_STREAM,0);//创建套接字
        if(sock<0)//创建失败
        {
            logMessage(FATAL,"create sock error");
            exit(SOCKET_ERR);
        }
        //创建成功
        logMessage(NORMAL,"create sock success");
        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 local;
        memset(&local,0,sizeof(local));//将结构体清空
        local.sin_family=AF_INET;//添加协议
        local.sin_port=htons(port);//添加端口号
        local.sin_addr.s_addr=htons(INADDR_ANY);//不绑定指定IP,可以接收任意IP主机发送来的数据
        //将本地设置的信息绑定到网络协议栈
        if (bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
        {
            logMessage(FATAL,"bind socket error");
            exit(BIND_ERR);
        }
        logMessage(NORMAL,"bind socket success");
    }

    static void Listen(int sock)//将套接字设置为监听
    {
        if(listen(sock,0)<0)
        {
            logMessage(FATAL,"listen socket error");
            exit(LISTEN_ERR);
        }
        logMessage(NORMAL,"listen socket success");
    }

    static int Accpet(int listensock,string * clientip,uint16_t* clientport)
    {
        struct sockaddr_in cli;
        socklen_t len= sizeof(cli);
        int sock=accept(listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
            logMessage(FATAL,"accept error");//这里accept失败为什么不退出
        }else
        {
            logMessage(NORMAL,"accept a new link,get new sock : %d",sock);
            *clientip=inet_ntoa(cli.sin_addr);
            *clientport=ntohs(cli.sin_port);
        }
        return sock;
    }
};

log.hpp

#pragma once

#include <iostream>
#include <string>
#include<ctime>
#include <sys/types.h>
 #include <unistd.h>
 #include <stdio.h>
#include <stdarg.h>
using namespace std;
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

#define NUM 1024
#define LOG_STR "./logstr.txt"
#define LOG_ERR "./log.err"
const char* to_str(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,...)
{

char logprestr[NUM];
snprintf(logprestr,sizeof(logprestr),"[%s][%ld][%d]",to_str(level),(long int)time(nullptr),getpid());

char logeldstr[NUM];
va_list arg;
va_start(arg,format); 
vsnprintf(logeldstr,sizeof(logeldstr),format,arg);//arg是logmessage函数列表中的...

  cout<<logprestr<<logeldstr<<endl;

}

说明一下:

  1. 通过select函数对_listensock进行读事件的关心,当连接到来时,select返回就绪的时间数。表示连接到来属于读事件

  2. 连接没到来时select返回值为0,表示0个事件就绪。连接到来后返回值为1,表示已经有一个事件就绪。多次打印get new link...是因为底层的连接到来,上层并没有把连接取走,因此底层的就绪事件一直存在。

由于服务器在最初时只使用listensock拿取底层的连接,而后续需要等待多个文件描述符时,可以通过数组来管理fd_set位图的大小为128字节,那么该位图可以同时关心128*8—1024个sock的就绪事件,因此管理sock的数组大小也应该是1024。

select.hpp

#include <unistd.h>
#include "Sock.hpp"
using namespace std;
static const int defaultport = 8081;
static const int fdnum = sizeof(fd_set) * 8;
static const int defaultfd = -1;
class SelectServer
{
public:
    SelectServer(uint16_t port = defaultport) : _port(port), _listensock(-1), _fdarry(nullptr)
    {
    }

    void initserver()
    {
        _listensock = Sock::Socket();   // 创建套接字
        Sock::Bind(_listensock, _port); // bind信息
        Sock::Listen(_listensock);      // 把sock设置为监听状态
        // cout<<"fd_set size: "<<sizeof(fd_set)<<endl;
        _fdarry = new int[fdnum]; // 保存fd的数组
        for (int i = 0; i < fdnum; i++)
        {
            _fdarry[i] = defaultfd;
        }
        _fdarry[0] = _listensock;
    }

void Print()
{
    cout<<"fd list: ";
    for(int i=0;i<fdnum;i++)
    {
        if(_fdarry[i]!=defaultfd)
        cout<<_fdarry[i]<<" ";
    }
    cout<<endl;
}
    void handleract(fd_set&rfd)
    {
        if(FD_ISSET(_listensock,&rfd))
        {
            char buffer[1024];
        uint16_t clientport;
        string clientip;
        int sock = Sock::Accpet(_listensock, &clientip, &clientport); // 获取sock
        if (sock < 0)
        {
            cout << "Sock::Accept err " << endl;
            return;
        }
        cout << "get a new sock: " << sock << endl;
        int i=0;
        for(;i<fdnum;i++)
        {
            if(_fdarry[i]!=defaultfd)
            continue;
            else break;
        }
        if(i==fdnum)
        {
            cout<<"server is full,please wait"<<endl;
            close(sock);
        }
        _fdarry[i]=sock;
        FD_SET(_fdarry[i],&rfd);
        Print();
        }
        
    }

    void start()
    {
        for (;;)
        {
            fd_set rfd;
            FD_ZERO(&rfd); // 清空位图
            int maxfd = _fdarry[0];
            int i = 0;
            for (; i < fdnum; i++)
            {
                if(_fdarry[i]==defaultfd)
                continue;

                FD_SET(_fdarry[i],&rfd);
                maxfd=maxfd>_fdarry[i]?maxfd:_fdarry[i];//更新最大fd数
            }

            // struct timeval timeout={1,0};
            // int n=select(_listensock+1,&rfd,nullptr,nullptr,&timeout);
            int n = select(maxfd + 1, &rfd, nullptr, nullptr, nullptr); // 阻塞式
            switch (n)
            {
            case 0:
                cout << "timeout......" << endl;
                break;
            case -1:
                cout << "select err" << endl;
            default:
                cout << "get new link..." << endl;
                handleract(rfd);
                break;
            }
            sleep(1);
        }
    }

private:
    uint16_t _port;
    int _listensock;
    int *_fdarry;
};

适用数组管理的原因在于:

  1. select的readfdwritefdexceptfd参数是输入输出型参数,函数返回时会改变这三个位图,此时就需要通过数组去重置初始化这三个位图。
  2. 通过位图可以方便很方便的知道最大文件描述符数,前提是设置数组的默认sock。
  3. 根据数组内的默认sock和已经保存的sock,很方便的赋值给fd_set位图参数。

现结合管理数组和select函数写一个能够接收client端发送来的信息,并且能够返回的服务器

main.cc

#include<iostream>
#include<functional>
#include<vector>
#include<memory>
#include"err.hpp"
#include"selectserver.hpp"
using namespace std;
using namespace Select_sv;

static void Usage(string proc)
{
    cerr<<"Usage:\n\t"<<proc<<" port "<<"\n\n";
}

string resp(const string& s)
{
    return s;
}

int main(int argc,char* argv[])
{
    unique_ptr<SelectServer> selsv(new SelectServer(resp));
    selsv->initServer();
    selsv->Start();
    
    return 0;
}

selectserver.hpp

#pragma once

#include <iostream>
#include <sys/select.h>
#include <string>
#include <functional>
#include "Sock.hpp"

using namespace std;

namespace Select_sv
{
    static const int defaultport = 8080;         // 默认端口号
    static const int fdnum = sizeof(fd_set) * 8; // 可使用的套接字数量
    static const int defaultfd = -1;             // 默认套接字标志
    using func_t = function<string(const string &)>;
    class SelectServer
    {
    public:
        SelectServer(func_t f, int port = defaultport) : _func(f), _port(port), _listensock(-1), _fdarray(nullptr)
        {
        }
        void initServer()
        {
            // 获取套接字
            _listensock = Sock::Socket();
            cout << "Sock success" << endl;
            // 绑定网络信息
            Sock::Bind(_listensock, _port);
            cout << "Bind success" << endl;
            // 把套接字设置为监听状态
            Sock::Listen(_listensock);
            cout << "Listen success" << endl;
            // 给每一个套接字都设置一个数组,保存套接字的设置情况
            cout << "fd_set size: " << sizeof(fd_set) << endl;
            _fdarray = new int[fdnum];
            for (int i = 0; i < fdnum; i++)
                _fdarray[i] = defaultfd; // 将每个套接字状态都设置为默认(未使用状态)

            _fdarray[0] = _listensock; // 第一个设置的套接字是通信套接字,供accept函数使用-建立连接
        //    cout << "initServer" << endl;
        }
        void Print()
        {
            cout << "now using socket: ";
            for (int i = 0; i < fdnum; i++)
            {
                if (_fdarray[i] != defaultfd)
                    cout << _fdarray[i] << " "; // 将设置进数组内的套接字进行打印
            }
            cout << endl;
        }
        void Accpter(int lsock)
        {
       //     logMessage(DEBUG, "Accepter begin");
            string clientip;
            uint16_t clientport = 0;
            int sock = Sock::Accpet(lsock, &clientip, &clientport); // 若成功返回,返回一个用于通信的套接字
            if (sock < 0)
                return;
            logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);

            int i = 0;
            for (; i < fdnum; i++)
            {
                if (_fdarray[i] != defaultfd)
                    continue;
                else
                    break;
            }
            if (i == fdnum) // 遍历完全部socket发现没用可使用的套接字
            {
                logMessage(WARNING, "server is full,please wait");
                close(sock); // 关闭用于通信的套接字,重新建立连接
               // _fdarray[i] = defaultfd;不需要去除,规定数组的0号下标对应的位置是专门用来拿连接的
            }
            else
            {
                _fdarray[i] = sock; // 把用于通信的套接字给select监管,让它等待
            }
            Print();
       //     logMessage(DEBUG, "Accepter end");
        }

        void Recver(int sock, int pos)
        {
        //    logMessage(DEBUG, "Recver begin");
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
            if (s > 0)
            {
                buffer[s] = 0;
                cout << "client# " << buffer << endl;
            }
            else if (s == 0)
            {
                close(sock);               // 关闭该套接字,关闭通信通道
                _fdarray[pos] = defaultfd; // 将数组中的该套接字清除
                logMessage(NORMAL, "client quit");
                return;
            }
            else
            {
                close(sock);
                _fdarray[pos] = defaultfd; // 将数组中的该套接字清除
                logMessage(ERROR, "recv error");
                return;
            }
            // 将客户端发来的数据原样写回去
            string resp = _func(buffer);

            write(sock, resp.c_str(), resp.size()); // 写回去
       //     logMessage(DEBUG, "Recever end");
        }
        void Handlerop(fd_set &rfds)
        {
            for (int i = 0; i < fdnum; i++)
            {
                if (_fdarray[i] == defaultfd)
                    continue;

                if (FD_ISSET(_fdarray[i], &rfds) && (_fdarray[i] == _listensock))
                // 此时i对应的数组位置是拿到连接的文件描述符,意味着在底层连接已经拿到,等待上层提取
                {
                    Accpter(_listensock);
                }
                else if (FD_ISSET(_fdarray[i], &rfds)) // 此时存在数组内的对应套接字都是底层读资源就绪
                {
                    Recver(_fdarray[i], i);
                }
                else
                {
                }
            }
        }
        void Start()
        {
            // 将数组管理的套接字设置进fd_set类型的结构内
            for (;;)
            {
                fd_set rfds;    // 当前程序只关心读事件
                FD_ZERO(&rfds); // 对该结构(位图)清空
                int maxfd = _fdarray[0];
                for (int i = 0; i < fdnum; i++)
                {
                    if (_fdarray[i] == defaultfd)
                        continue;
                    FD_SET(_fdarray[i], &rfds); // 将需要使用的套接字设置进读事件结构中
                    //若此时已经将连接拿到上层,因此select管理连接对应的sock就不会就绪,而可以只管理通信资源是否就绪
                    if (maxfd < _fdarray[i])
                        maxfd = _fdarray[i]; // 更新最大文件描述符
            //        cout << "listensock set to _fdarray success" << endl;
                }

                // 把读事件交给select监管
                cout << "will select " << endl;
                int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr); // 阻塞式监管
                cout << "select end" << endl;
                switch (n)
                {
                case 0:
                    logMessage(NORMAL, "timeout..."); // 监管时间内没用套接字就绪,即超时返回
                    break;
                case -1:
                    logMessage(WARNING, "select error,error:%d, error string: ", errno, strerror(errno));
                    break;
                default:
                    logMessage(NORMAL, "get a new link..."); // 拿到新连接,即拿到通信的连接,客户端主动断开连接后,为何后续循环select都是拿到连接?
                    Handlerop(rfds);
                    break;
                }
            }
        }

        ~SelectServer()
        {
            if (_listensock < 0) // 为什么是小于0?
                close(_listensock);
            if (_fdarray)
                delete[] _fdarray;
        }

    private:
        int _port;
        int _listensock;
        int *_fdarray; // 记录需要交给select管理的套接字,每个套接字交给select管理的方式是传递整数给位图,因此该数组的类型也是整数int
        func_t _func;
    };
}

image-20231124155440498

说明一下:

  1. initServer函数里,完成创建套接字,bind信息,将套接字设置为监听状态,并且初始化管理数组_fdarry,并将监听套接字设置优先设置进数组的0号下标处,这不再改变。
  2. Start函数里,首先该函数是需要保证服务器的正常运行,因此是调用链是存在于死循环中。将管理数组内的sock设置进rfds位图中,即告诉内核需要关心这些sock。接着调用select函数进行等待就绪事件。等待到就绪事件后调用Handlerop函数,对就绪事件进行处理。
  3. 由于该服务器目前只处理获取连接,接收客户端发送过来的数据并返回这两个业务。因此在Handlerop函数中,通过管理数组对已经返回的位图进行对比,判断出是listensock就绪还是通信的数据到来。若是获取到连接,则调用Accepter函数将底层的连接提取到应用层。若是数据到来,则调用Recver函数读取底层的数据,并进行处理。
  4. Accpter函数中,不仅将连接获取上来,还需要将获取到的连接添加到管理数组中,以便于在下次循环中告诉OS关心该新连接。
  5. Recver函数中,调用recv函数进行读取,通过仿函数对数据进行处理并写回到sock中。

梳理调用链

image-20231123211007403

image-20231123211700037

image-20231123212652194

image-20231123212844031

总结一下:

select可以同时等待多个文件描述符,提高了IO的效率。但是也存在以下缺陷:

  1. select能够等待的文件描述符是有上限的,在我这台云服务器中能够使用的fd一共有10002个(通过ulimit -a查询

image-20231124153505075

而select使用的位图结构fd_set所能管理的sock数为1024,这表明了select能够同时等待的文件描述符是具有上限的。除非更改内核的参数,否则不能解决。

  1. 由于fd_set位图是输入输出型参数,那么在传入传出时必然发送改变,因此我们需要通过第三方数组去管理合法的文件描述符。
  2. select函数的大部分参数都是输入输出型的,调用函数时,通过输入参数用户告诉内核信息,函数返回,通过输出参数内核告诉用户信息,即采用位图结构传递参数时,需要不断的进行用户到内核,内核到用户的状态切换,并且还进行了数据拷贝,造成了不少成本。
  3. 由于使用的是位图结构传递参数,并且位图结构在输入输出时发生改变,导致我们需要遍历所有的文件文件描述符,这带来了一定的遍历成本。而select的第一个参数是最大fd+1,是用来确定遍历的范围。

基于以上select函数的劣势,前人总结衍生出了更好的方案,如pollepoll等等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1247127.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python中range函数的用法

range() 是Python的一个内置函数。语法格式为&#xff1a;range(start, stop, step) start是初始值&#xff0c;stop是最终值&#xff0c;step是步长。range()函数仅适用于整数&#xff0c;所有参数都必须是整数。步长值可以为正数或负数&#xff0c;不得为零。使用range函数时…

Mintegral业务再创新高,汇量科技基建成果已然显现?

游戏出海经过几年的跨越式发展&#xff0c;红海特征日益明显。 《2023中国游戏App出海驱动力报告》显示&#xff0c;2022年1月至2023年6月&#xff0c;我国约17000款游戏类App出海&#xff0c;安装总量超过240亿次。与此同时&#xff0c;Android和iOS侧的安装量在2022年三季度…

ONNX实践系列-修改yolov5-seg的proto分支输出shape

一、目标 本文主要介绍要将原始yolov5分割的输出掩膜从[b,c,h,.w]修改为[b, h, w, c] 原来的: 目标的: 代码如下: Descripttion: version: @Company: WT-XM Author: yang jinyi Date: 2023-09-08 11:26:28 LastEditors: yang jinyi LastEditTime: 2023-09-08 11:48:01 …

【深度学习】因果推断与机器学习

2023年初是人工智能爆发的里程碑式的重要阶段&#xff0c;以OpenAI研发的GPT为代表的大模型大行其道&#xff0c;NLP领域的ChatGPT模型火爆一时&#xff0c;引发了全民热议。而最新更新的GPT-4更是实现了大型多模态模型的飞跃式提升&#xff0c;它能够同时接受图像和文本的输入…

2023 Unite 大会关于“Muse“ AI 大模型训练

Unity Muse 借助强大的 AI 能力帮助你探索、构思和迭代&#xff0c;其中包括纹理和精灵两项功能&#xff0c;可将自然语言和视觉输入转化为可用资产。 将 AI 引入 Unity Editor 中的 Muse 提供了更快将想法转化为实物的选项。您可以调整并使用文本提示、图案、颜色和草图&…

全面预算管理,帮助企业财务团队冲破市场挑战

在实现企业财务发展的必经之路上&#xff0c;大多数财务专业人士会通过实施全面预算管理策略&#xff0c;为部门乃至整个组织建立一个用于数据管理和预测分析的财务模型&#xff0c;旨在影响和监控业务决策和变化趋势。全面预算管理通常包括历史数据分析和关于未来走向更详细的…

加入华为云鲲鹏极简开发创造营,激活创造力,探索无限可能!

数字经济时代&#xff0c;速度、效率、质量安全已成为各行业告诉拓新发展的关键&#xff0c;华为云不断打磨敏捷安全开发的软件平台&#xff0c;为更高效率的生产力变革积蓄能量。 在刚刚过去不久的2023华为全联接大会上&#xff0c;华为最新发布了华为云CodeArts与鲲鹏DevKit…

鸿蒙开发之android开发人员指南《基础知识》

基于华为鸿蒙未来可能不再兼容android应用&#xff0c;推出鸿蒙开发系列文档&#xff0c;帮助android开发人员快速上手鸿蒙应用开发。 1. 鸿蒙使用什么基础语言开发&#xff1f; ArkTS是鸿蒙生态的应用开发语言。它在保持TypeScript&#xff08;简称TS&#xff09;基本语法风…

别再为仓库库存损耗头疼了!掌握6S必备装备管理轻松降低损失!

仓库库存损耗是很多装配制造企业头疼的问题&#xff0c;不仅会导致企业资产流失&#xff0c;还可能会对企业的生产和运营产生严重影响&#xff0c;因此&#xff0c;企业应认真分析导致损耗的原因&#xff0c;并有针对性地采取策略&#xff0c;降低库存损耗&#xff0c;提高企业…

JS 中的随机数方法 Math.random()

有时候项目中遇到一个功能需要随机返回多条不重复的数据&#xff0c;也可以是拿了就用&#xff0c;下次再需要时已经忘记如何使用了。 js中的生成随机数操作是基于 Math 方法下的 random() 方法 Math.random() &#xff1a; 随机获取范围内的一个数 &#xff08; 精确到小数点…

Mac系统-tomcat部署war包指定jdk版本步骤

背景: 因本机jdk安装了好几个版本, 有 1.8、 11 、17 , 现在想要启动tomcat的时候指定11版本 步骤如下: 1、 在官网下载了“apache-tomcat-9.0.83.tar.gz” 这个包. 官网地址: Apache Tomcat - Apache Tomcat 9 Software Downloads 2、解压缩, 得到tomcat目录: /Users/..…

三菱PLC定时中断应用编程(计数器+比较器)

三菱PLC如何开启定时中断可以查看下面文章链接: PLC定时中断程序应用注意事项(西门子三菱信捷)_plc设置断点之后会怎样_RXXW_Dor的博客-CSDN博客文章浏览阅读2.5k次,点赞5次,收藏6次。首先我们了解下什么是中断。中断(打断的意思),在PLC执行当前程序时,由于系统出现了…

Maxwell安装部署消费到kafka集群

1.上传安装包到linux系统上面 2.解压安装包到安装目录下&#xff0c;并且重命名 [rootVM-4-10-centos package]# tar -zxvf maxwell-1.29.2.tar.gz -C /opt/software/3.配置mysql 增加以下配置 #数据库id server-id 1 #启动binlog&#xff0c;该参数的值会作为binlog的文件…

【JavaEE】认识多线程

作者主页&#xff1a;paper jie_博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文录入于《vaEE》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精力)打造&am…

葡萄采收时节分类,其中大有学问

葡萄在酿造葡萄酒的时候有一个关键的因素那就是葡萄的采收期&#xff0c;符合采收时节的葡萄大部分属于成熟好的葡萄&#xff0c;那么&#xff0c;云仓酒庄的品牌雷盛红酒分享这样的葡萄酿出来的酒自然优质。正是因为采收时分不同&#xff0c;根据采摘的不同&#xff0c;给不同…

Elasticsearch知识

目录 Elasticsearch逻辑设计和物理设计 逻辑设计物理设计Elasticsearch原理 倒排索引文档的分析过程保存文档搜索文档写数据的底层原理 数据刷新&#xff08;fresh&#xff09;事务日志的写入ES在大数据量下的性能优化 文件系统缓存优化数据预热文档&#xff08;Document&…

Java计算两个时间的相差年,日,小时,分,秒

主函数 public static int dateDiff(char flag, Calendar calSrc, Calendar calDes) {long millisDiff getMillis(calSrc) - getMillis(calDes);if (flag y) {return (calSrc.get(Calendar.YEAR) - calDes.get(Calendar.YEAR));}if (flag d) {return (int) (millisDiff / D…

FANUC机器人到达某个点位时,为什么不显示@符号?

FANUC机器人到达某个点位时,为什么不显示@符号? 该功能由变量$MNDSP_POSCF = 0(不显示)/1(显示)/2(光标移动该行显示) 控制,该变量设置为不同的值,则启用对应的功能。 如下图所示,为该变量设置不同的值时的对比, 其他常用的系统变量可参考以下内容: 在R寄存器指定速度…

NX二次开发UF_CSYS_map_point 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_CSYS_map_point Defined in: uf_csys.h int UF_CSYS_map_point(int input_csys, double input_point [ 3 ] , int output_csys, double output_point [ 3 ] ) overview 概述 Ma…

小白一文搞懂正则表达式

大学的时候学过&#xff0c;没搞明白&#xff0c;工作的时候学了几次&#xff0c;还是懵逼的状态&#xff0c;写篇入门文字记录下如何快速搞懂正则表达式 首先写几个常用的正则表达式&#xff1a; 1.匹配手机号 1[34578]\d{9} 2.QQ号&#xff0c;第一位不能是0&#xff0c;5位…