高级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); // 获取sockif (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;elsebreak;}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管理的方式是传递整数给位图,因此该数组的类型也是整数intfunc_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.mzph.cn/news/166997.shtml

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

相关文章

Jmeter接口测试——使用教程(下)

前言 上一篇我给大家讲了jmeter的基本介绍跟参数化和jmeter脚本及jmeter断言&#xff0c;今天让我们继续往下看&#xff0c;学习一下jmeter新的知识点。 一、Jmeter关联 我们知道断言是从返回结果中检查有没有预期的值&#xff0c;现在有一个问题&#xff0c;有一个购买商品…

【学习笔记】GameFramework的非官方实例TowerDefense-GameFramework-Demo的流程

一、从游戏开始到打开一个Menu GameStart.unity GameEntry.Builtin.cs ProcedureComponent.cs GameStart.unity->GameFramework->Builtin->Procedure ProcedureLaunch.cs ProcedureSplash.cs ProcedurePreload.cs ProcedureLoadingScene.cs DataTables/Scene.txt Pro…

小学语文老师重点工作

小学语文老师是学生在语言学习过程中的关键引导者&#xff0c;他们的主要职责是帮助学生建立正确的语言基础&#xff0c;培养良好的阅读习惯&#xff0c;并提高学生的语文素养。以下是小学语文老师的一些重点工作。 一、教授语言知识 小学语文老师首要的任务是教授学生语言知识…

《DApp开发:开启全新数字时代篇章》

随着区块链技术的日益成熟&#xff0c;去中心化应用&#xff08;DApp&#xff09;逐渐成为数字世界的新焦点。在这个充满无限可能的全新领域&#xff0c;DApp开发为创新者们提供了开启数字时代新篇章的钥匙。 一、DApp&#xff1a;区块链创新成果 DApp是建立在区块链技术基础之…

C/C++ 开发SCM服务管理组件

SCM&#xff08;Service Control Manager&#xff09;服务管理器是 Windows 操作系统中的一个关键组件&#xff0c;负责管理系统服务的启动、停止和配置。服务是一种在后台运行的应用程序&#xff0c;可以在系统启动时自动启动&#xff0c;也可以由用户或其他应用程序手动启动。…

chromium通信系统-mojo系统(一)-ipcz系统基本概念

ipcz 是chromium的跨进程通信系统。z可能是代表zero&#xff0c;表示0拷贝通信。 chromium的文档是非常丰富的&#xff0c;关于ipcz最重要的一篇官方文档是IPCZ。 关于ipcz本篇文章主要的目的是通过源代码去分析它的实现。再进入分析前我们先对官方文档做一个总结&#xff0c;…

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…

Unity RenderFeature架构分析

自定义RenderFeature接口流程 URP内部ScriptableRenderPass分析 public、protected属性 renderPassEvent &#xff1a;渲染事件发生的时刻colorAttachments &#xff1a;渲染的颜色纹理列表 m_ColorAttachmentscolorAttachment &#xff1a;m_ColorAttachments[0];depthAttac…

【网络奇幻之旅】那年我与大数据的邂逅

&#x1f33a;个人主页&#xff1a;Dawn黎明开始 &#x1f380;系列专栏&#xff1a;网络奇幻之旅 ⭐每日一句&#xff1a;循梦而行&#xff0c;向阳而生 &#x1f4e2;欢迎大家&#xff1a;关注&#x1f50d;点赞&#x1f44d;评论&#x1f4dd;收藏⭐️ 文章目录 &#x1f4…

windows远程linux或远程虚拟机连接拒绝问题排查

当我们使用MobaXterm远程连接时&#xff0c;报错如下&#xff1a; 1.首先检查该ubuntu防火墙是否关闭&#xff0c;先将防火墙关闭。 1.检查防火墙状态 sudo ufw status 2.开启防火墙 sudo ufw enable 3.关闭防火墙 sudo ufw disable 2.关闭防火墙后&#xff0c;使用ping命令相…

【数据结构/C++】栈和队列_顺序栈

#include<iostream> using namespace std; #define MaxSize 10 // 1. 顺序栈 typedef int ElemType; struct Stack {ElemType data[MaxSize];int top; } SqStack; // 初始化栈 void init(Stack &s) {// 初始化栈顶指针s.top -1; } // 入栈 bool push(Stack &s, …

什么是工业物联网(IOT)?这样的IOT平台你需要吗?——青创智通

物联网(IOT)是指在互联网上为传输和共享数据而嵌入传感器和软件的互联设备的广泛性网络。这允许将从物理对象收集的信息(数据)存储在专用服务器或云中。通过分析这些积累的信息&#xff0c;通过提供最优的设备控制和方法&#xff0c;可以实现一个更安全、更方便的社会。在智能家…

【完美解决】 Python pyecharts Map 地图数据不显示

目录 项目场景问题描述原因分析解决方案完整代码 项目场景 Python数据可视化&#xff0c;使用 Pyecharts.charts 模块中的Map&#xff0c;并导入数据来构建全国疫情热力地图 B站 黑马程序员 Python课程【P106 第一阶段 - 第十一章 - 02全国疫情地图构建】 问题描述 本人在学习…

vue+face-api.js实现前端人脸识别功能

近期做了一个前端vue实现人脸识别的功能&#xff0c;主要功能逻辑包含&#xff1a;人脸识别&#xff0c;人脸验证&#xff0c;唤起摄像头视频流之后从三个事件&#xff08;用户点头、摇头、眨眼睛&#xff09;中随机选中两个事件&#xff0c;待两个事件通过判断后人脸静止不动3…

基于Java+Vue+uniapp微信小程序微信阅读网站平台设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

使用端口扫描工具解决开放端口威胁并增强安全性

从暴露网络漏洞到成为入侵者的通道&#xff0c;开放端口可能会带来多种风险向量&#xff0c;威胁到网络的机密性、完整性和可用性。因此&#xff0c;最佳做法是关闭打开的端口&#xff0c;为了应对开放端口带来的风险&#xff0c;网络管理员依靠端口扫描工具来识别、检查、分析…

ubuntu下配置qtcreator交叉编译环境

文章目录 安装交叉编译工具安装qt creator开发环境配置交叉编译示例demo参考 安装交叉编译工具 安装qt creator开发环境 1 官网 2 填写信息 3 下载 默认没有出现Qt5.15版本 WISONIC\80081001ub16-1001:~$ /opt/Qt/Tools/QtCreator/bin/qtcreator /opt/Qt/Tools/QtCreat…

【PDF.js】2023 最新 PDF.js 在 Vue3 中的使用

因为自己写业务要定制各种 pdf 预览情况&#xff08;可能&#xff09;&#xff0c;所以采用了 pdf.js 而不是各种第三方封装库&#xff0c;主要还是为了更好的自由度。 一、PDF.js 介绍 官方地址 中文文档 PDF.js 是一个使用 HTML5 构建的便携式文档格式查看器。 pdf.js 是社区…

ThreeJs实现简单的动画

上一节实现可用鼠标控制相机的方式实现动态效果&#xff0c;但很多时候是需要场景自己产恒动态效果&#xff0c;而不是通过鼠标拖动&#xff0c;此时引入一个requestAnimationFrame方法&#xff0c;它实际上是通过定时任务的方式&#xff0c;每隔一点时间改变场景中内容后重新渲…

Ant Design Vue 树形表格计算盈收金额

树形表格计算 一、盈收金额计算1、根据需要输入的子级位置&#xff0c;修改数据2、获取兄弟节点数据&#xff0c;并计算兄弟节点的金额合计3、金额合计&#xff0c;遍历给所有的父级 一、盈收金额计算 1、根据需要输入的子级位置&#xff0c;修改数据 2、获取兄弟节点数据&am…