高级IO—select

高级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等等。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LTD252次升级 | 独立商城促销功能大升级:新增折扣券、支持直接领取、自动送券 • 网站可设置横幅与弹窗广告

1、优惠券功能大升级&#xff1a;支持折扣券&#xff0c;支持商品页面领券支持自动送券&#xff1b; 2、网站版系统支持单独开通广告管理功能&#xff1b; 3、产品介绍页优化社交媒体分享&#xff1b; 4、已知问题优化与修复; 01 商城 商城优惠券功能升级。 本次升级中&#…

迎接“全全闪”时代 XSKY星辰天合发布星海架构和星飞产品

11 月 17 日消息&#xff0c;北京市星辰天合科技股份有限公司&#xff08;简称&#xff1a;XSKY星辰天合&#xff09;在北京首钢园举办了主题为“星星之火”的 XSKY 星海全闪架构暨星飞存储发布会。 &#xff08;图注&#xff1a;XSKY星辰天合 CEO 胥昕&#xff09; XSKY星辰天…

如何一键消除图片里的水印?图片去水印教程一看就会!

如何一键消除图片水印&#xff1f;在现今的数字时代&#xff0c;我们常常会遇到带有水印的图片&#xff0c;而传统的方法往往费时且复杂&#xff0c;让我们感到困扰。那么如何一键消除图片水印呢&#xff1f;今天&#xff0c;我们为您推荐一款非常实用的去水印软件&#xff0c;…

flutter编译和构建鸿蒙应用程序(windows环境)

flutter编译和构建鸿蒙应用程序&#xff08;windows环境&#xff09; 问题背景 针对 OpenHarmony 的 Flutter 版本已经开源&#xff0c;参考 https://gitee.com/openharmony-sig/flutter_flutter。 本文为实践该流程&#xff0c;实现flutter打包鸿蒙hap包的流程。目前流程已经…

一加7Pro为PixelExperience系统编译kernelsu

前言 之前写了一篇文章为一加七Pro(LineageOs17.1 4.14内核版本)编译KernelSu&#xff0c;最近想换个系统玩玩&#xff0c;因为我发现我自己编译的系统总是被某些APP风控了&#xff0c;比如淘宝有些活动就参与不了。 正好可以试试PixelExperience&#xff0c;顺便为PixelExpe…

【数据分享】2023年我国省市县三级的瞪羚企业数量(免费获取/Excel/Shp格式)

企业是经济活动的参与主体。一个城市的企业数量决定了这个城市的经济发展水平&#xff01;比如一个城市的金融企业较多&#xff0c;那这个城市的金融产业肯定比较发达&#xff1b;一个城市的制造业企业较多&#xff0c;那这个城市的制造业肯定比较发达。 之前我们给大家分享了…

为什么在Pycharm中使用Pandas画图,却不显示?

问题描述&#xff1a; 在 Pycharm 中使用 Pandas 的 plot() 方法画图&#xff0c;却不显示图像&#xff0c;源代码如下&#xff1a; import pandas as pd import numpy as np# 从文件中读取数据 starbucks pd.read_csv(./file_csv/directory.csv)# 按照国家分组&#xff0c;…

pycharm 创建的django目录和命令行创建的django再使用pycharm打开的目录对比截图 及相关

pytcharm创建django的项目 命令行创建的django 命令行创建项目时 不带路径时 (.venv) D:\gbCode>django-admin startproject gbCode 命令行创建项目时 带路径时 -- 所以如果有目录就指定路径好 (.venv) D:\gbCode>django-admin startproject gbCode d:\gbCode\

k8s集群资源监控工具metrics-server安装

1、下载镜像 docker pull swr.cn-east-2.myhuaweicloud.com/kuboard-dependency/metrics-server:v0.6.22、在任一一个主节点上创建角色&#xff0c;执行下面语句 kubectl create clusterrolebinding kube-proxy-cluster-admin --clusterrolecluster-admin --usersystem:kube-…

案例精选|聚铭网络流量智能分析审计系统加强南京市溧水区人社局信息安全防护能力

一字排开的社保综合服务窗口、实时滚动的数“智”人社大屏、便捷快速的社保卡自助服务机……每位到溧水市民中心人社大厅进行业务办理的市民对高效的社保服务经办效率赞叹不已。 党的二十大报告提出&#xff0c;健全覆盖全民、统筹城乡、公平统一、安全规范、可持续的多层次社…

怎么去掉视频水印?分享三种视频去水印技巧,轻松搞定!

怎么去掉视频水印&#xff1f;在分享视频时&#xff0c;我们常常会遇到因为水印而影响观感的问题&#xff0c;因此&#xff0c;掌握有效的去水印方法显得尤为重要&#xff0c;随着技术的进步&#xff0c;现在有多种方法可以帮助我们去除视频中的水印。 接下来分享三种简单且有效…

2023.11.23 云服务器实现 Spring Boot 项目文件上传并访问

环境介绍 云服务器&#xff1a;京东云云服务器系统&#xff1a; CentOS 7.9JDK 版本&#xff1a;1.8Spring Boot 版本&#xff1a;2.7.17 具体步骤 步骤一 首先我们得先创建一个 Spring Boot 项目 创建如下目录结构 关于如何创建一个 Spring Boot 项目 请点击下方链接详细了解 …

【公网远程手机Android服务器】安卓Termux搭建Web服务器

&#x1f3a5; 个人主页&#xff1a;深鱼~&#x1f525;收录专栏&#xff1a;cpolar&#x1f304;欢迎 &#x1f44d;点赞✍评论⭐收藏 目录 概述 1.搭建apache 2.安装cpolar内网穿透 3.公网访问配置 4.固定公网地址 5.添加站点 概述 Termux是一个Android终端仿真应用程…

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

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

分块矩阵知识点整理:

1.分块方法&#xff1a;横竖线不能拐弯&#xff0c;思想为将矩阵分块看作向量计算 2.标准型 不一定是方的 特殊性&#xff1a;经过分块后会出现单位矩阵和0矩阵 3.分块矩阵的运算: 1.加减乘的运算与向量运算相同 4.分块矩阵求转置&#xff1a; 1.将子块看作普通元素求转置 2…

深入理解数据结构:队列的实现及其应用场景

文章目录 &#x1f342;前言&#x1f342;队列的基本概念和特性&#x1f342;队列的实现方式️&#x1f331;顺序队列️&#x1f331;链式队列 &#x1f342;队列的基本操作及示例代码️&#x1f951;创建队列️&#x1f951;判空操作️&#x1f951;入队操作️&#x1f951;出…

java获取服务器配置文件的值

获取服务器配置文件的值 Value("${bpm.minio.bucketName:}")private String minioBarrel;

【python入门篇】函数(6)

这一节将详细介绍Python中函数的用法&#xff0c;包括函数的定义、调用、参数、返回值、作用域等。 函数的概述&#xff1a; Python函数是一种封装了特定任务的可重用代码块。通过将程序分解为更小、更具体的任务&#xff0c;函数提供了一种有效的方式来组织和管理代码&#xf…

Java系列之 String indexOf() 方法

我 | 在这里 &#x1f575;️ 读书 | 长沙 ⭐软件工程 ⭐ 本科 &#x1f3e0; 工作 | 广州 ⭐ Java 全栈开发&#xff08;软件工程师&#xff09; &#x1f383; 爱好 | 研究技术、旅游、阅读、运动、喜欢流行歌曲 &#x1f3f7;️ 标签 | 男 自律狂人 目标明确 责任心强 ✈️公…

App Inventor 2 数字转文本

App Inventor 2 是弱语言类型&#xff0c;文本和数字之间不用刻意去转换&#xff0c;之间赋值就可以了。 案例&#xff1a;数字转文本 App Inventor 2 是弱语言类型&#xff0c;同理数字也能直接赋值给文本变量&#xff1a; 更多请参考&#xff1a;App Inventor 2 文本代码块…