高级IO—select
IO的概念
通常指数据在内部存储器和外部存储器或其他周边设备之间的输入和输出。输入是系统接收的信号或数据,输出则是从其发送的信号或数据。也可把输入输出认为是信息处理系统(例如计算器)与外部世界(人类或另一信息处理系统)之间的通信。
IO分为IO设备和IO接口
- IO设备
IO设备是硬件中由人使用并与计算机进行通信的设备。例如键盘或鼠标是计算机的输入设备,监视器和打印机是输出设备。计算机之间的通信设备进行的通常是运行输入输出操作。
- IO接口
I/O接口的功能是负责实现CPU通过系统总线把I/O电路和外围设备联系在一起。IO函数的底层是系统提供的系统调用,供用户通过调用来实现从用户态到内核态或内核态到用户态的数据拷贝。
实际上在网络通信中,调用write
并不是直接将数据写到网络中,而是将数据从应用层拷贝到传输层的发送缓冲区当中,然后由OS自主决定什么时候将数据向下交付,发送到网络中。同理调用read
并不是直接从网络中读取数据,而是将传输层的接收缓冲区的数据读到应用层中。这意味调用read
的时候,传输层的接收缓冲区并没有数据,那么read
函数就会阻塞住,直到缓冲区有数据,才能将数据读到应用层。
因此IO本质不仅仅只有读取/写入,还有等待资源就绪的过程,即等+拷贝。
提高IO的效率本质是每次IO中减少等待的时间,让IO过程尽可能都是拷贝。因此为了提高IO的效率,衍生出多种IO模型。
五种IO模型
阻塞IO
在内核将数据准备好之前,系统调用会一直等待,所有的套接字默认的是阻塞方式。
常见的阻塞IO模型
用户调用recvfrom函数,尝试读取数据,即调用系统调用,由用户态切换到内核态,由于数据没有准备好导致阻塞等待,数据准备好了立刻拷贝数据报并返回用户态。
代码以使用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;
}
非阻塞IO
如果内核还未将数据准备好, 系统调用不会阻塞等待,会直接返回, 并且返回EWOULDBLOCK错误码。
非阻塞IO往往需要程序员以循环的方式反复尝试读写文件描述符, 这个过程称为轮询。这意味着轮询的过程需要一直占用CPU资源,对CPU来说是较大的浪费,一 般只有特定场景下才使用。
常见的非阻塞IO模型
用户调用recvfrom函数,这次该函数是以非阻塞的方式进行调用,尝试读取数据,由用户态切换到内核态,由于数据没有准备好,直接返回
EWOULDBLOCK
。因此程序员需要以轮询的方式调用recvfrom
函数,数据准备好了立刻拷贝数据报并返回用户态。轮询的过程中一是需要占用CPU的资源,二是需要多次进行用户态与内核态之间的转换,资源浪费较为严重,该方式一般在特定场景才使用。
需要将文件描述符设置为非阻塞状态,那么读取该文件描述符就以非阻塞方式读取。
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;
}
- 非阻塞的返回值
对于非阻塞来说,底层没有数据直接返回,返回值为-1,但这并不是发生错误,原因由错误码来标记。错误码为EAGAIN
或EWOULDBLOCK
表示没有读取到数据。相同的还有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;
}
非阻塞没有读取到数据直接返回的错误码是11,EAGAIN
和EWOULDBLOCK
的错误码也是11。
信号驱动IO
内核将数据准备好的时候, 使用SIGIO
信号通知应用程序进行IO操作。
常见的信号驱动IO模型
先前建立好
SIGIO
信号处理程序,进程将等待资源就绪的过程托管给sigaction
函数,让该函数去等待数据,数据准备好后,以信号通知的方式返回,通知进程,此时进程直接调用recvfrom
函数,拷贝数据报并返回。
IO多路转接
IO多路转接能够同时等待多个文件描述符的就绪状态。
常见的IO多路转接模型
进程将等待资源的过程托管给
select
函数,让select
去等待数据,资源准备好后,select
函数返回可读条件,通知进程,此时进程直接调用recvfrom
函数,拷贝数据报并返回。这意味着可以让多个进程将等待资源的过程托管给同一个select
函数,哪个资源就绪,select
函数就通知相应的程序进行读取。
异步IO
由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。
常见异步IO模型
进程需要读取某种资源时,调用
aio_read
函数(系统调用),将IO(等+拷贝)的过程托管给OS,让OS负责等,数据准备好后,OS自动将数据拷贝到用户层的缓冲区,然后返回指定信号,通知进程来处理数据。进程并不参与IO的过程,只负责处理数据。
总结一下:
- 阻塞、非阻塞、信号驱动在IO的效率上并无差别,差别在于等待资源的过程。阻塞式在等的过程中不能做别的事,而非阻塞和信号驱动在等的过程中可以做其他事情。
- 阻塞、非阻塞、信号驱动、多路转接实际上都参与了IO的过程,即IO的等待过程和拷贝过程,参与了其中一个过程都算作是同步IO。
- 异步IO是将IO过程托管给OS,并没有参与IO的过程。
- 多路转接的高效在于可以同时等待多个文件描述符,即等待多个资源就绪,并行等待资源,减少了等待资源的过程。
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
结构的指针,用于设置超时时间。如果为NULL
,select
函数将一直阻塞,直到有文件描述符就绪。select
函数的返回值是就绪文件描述符的数量,如果返回值为-1,则表示出现错误。在这种情况下,可以使用perror
函数来输出错误信息。
说明一下:
-
由于文件描述符是OS中的文件描述符表的下标,该表是从小到大依次使用,因此所被占用的文件描述符是连续的,即nfds能涵盖所使用的文件描述符范围。如nfds=5,表示在0~4号文件描述符中查询。
-
readfds
,writefds
,exceptfds
和timeout
都是指针,即都是输入输出型参数。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
参数在传入时需要重置。 -
fd_set
实际上是一个位图结构。以readfd
为例,用户想要OS关心4,5号文件描述符的读时间,那么输入的位图结构是0011 0000
。
当关心时间内5号文件描述符就绪了,OS会对输入的位图进行改动,输出表示哪些文件描述符已经就绪。输出的位图结构是0010 0000
。
readfd
, writefd
,exceptfd
的结构都是位图,且是分别不同的位图,因此用户可以传入一个或多个位图让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;
}
说明一下:
-
通过select函数对_listensock进行读事件的关心,当连接到来时,select返回就绪的时间数。表示连接到来属于读事件。
-
连接没到来时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;
};
适用数组管理的原因在于:
- select的
readfd
,writefd
,exceptfd
参数是输入输出型参数,函数返回时会改变这三个位图,此时就需要通过数组去重置初始化这三个位图。 - 通过位图可以方便很方便的知道最大文件描述符数,前提是设置数组的默认sock。
- 根据数组内的默认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;
};
}
说明一下:
- 在
initServer
函数里,完成创建套接字,bind信息,将套接字设置为监听状态,并且初始化管理数组_fdarry
,并将监听套接字设置优先设置进数组的0号下标处,这不再改变。 - 在
Start
函数里,首先该函数是需要保证服务器的正常运行,因此是调用链是存在于死循环中。将管理数组内的sock设置进rfds
位图中,即告诉内核需要关心这些sock。接着调用select
函数进行等待就绪事件。等待到就绪事件后调用Handlerop
函数,对就绪事件进行处理。 - 由于该服务器目前只处理获取连接,接收客户端发送过来的数据并返回这两个业务。因此在
Handlerop
函数中,通过管理数组对已经返回的位图进行对比,判断出是listensock就绪还是通信的数据到来。若是获取到连接,则调用Accepter
函数将底层的连接提取到应用层。若是数据到来,则调用Recver
函数读取底层的数据,并进行处理。 - 在
Accpter
函数中,不仅将连接获取上来,还需要将获取到的连接添加到管理数组中,以便于在下次循环中告诉OS关心该新连接。 - 在
Recver
函数中,调用recv函数进行读取,通过仿函数对数据进行处理并写回到sock中。
梳理调用链
总结一下:
select可以同时等待多个文件描述符,提高了IO的效率。但是也存在以下缺陷:
- select能够等待的文件描述符是有上限的,在我这台云服务器中能够使用的fd一共有10002个(通过
ulimit -a
查询
而select使用的位图结构fd_set
所能管理的sock数为1024,这表明了select能够同时等待的文件描述符是具有上限的。除非更改内核的参数,否则不能解决。
- 由于
fd_set
位图是输入输出型参数,那么在传入传出时必然发送改变,因此我们需要通过第三方数组去管理合法的文件描述符。 select
函数的大部分参数都是输入输出型的,调用函数时,通过输入参数用户告诉内核信息,函数返回,通过输出参数内核告诉用户信息,即采用位图结构传递参数时,需要不断的进行用户到内核,内核到用户的状态切换,并且还进行了数据拷贝,造成了不少成本。- 由于使用的是位图结构传递参数,并且位图结构在输入输出时发生改变,导致我们需要遍历所有的文件文件描述符,这带来了一定的遍历成本。而
select
的第一个参数是最大fd+1,是用来确定遍历的范围。
基于以上select
函数的劣势,前人总结衍生出了更好的方案,如poll
,epoll
等等。