今天我们要介绍的是实现高效IO采用的方案IO多路转接中的select模型,关于select模型是如何实现高效IO,下面我们就一起来具体看看吧。
系统提供select函数来实现多路复用输入/输出模型.
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数介绍:
参数nfds是需要监视的最大的文件描述符值+1;
参数timeout为结构timeval,用来设置select()的等待时间
NULL:则表示select ()没有timeout, select将一 直被阻塞, 直到某个文件描述符上发生了事件;
timeout包含字段:
struct timeval timeout = {0, 0} ->表示非阻塞
struct timeval timeout = {5, 0} >5秒以内阻塞式,超过5秒,非阻塞返回一次
rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
fd_ set:位图结构,表示文件描述符的集合
rdset,wrset,exset都是输入输出型参数:
输入:表示用户告诉内核,哪些fd上的事件需要内核关心
比特位的位置,表示fd的数值,比特位的内容,表示是否需要关系
输出:内核告诉用户,关心的多个fd中,有哪些E经就绪了
比特位的位置,表示fd的数值,比特位的内容,表示哪些fd上面对应的事件已经就绪了
让用户和内核进行沟通
提供了一组操作fd_ set的接口,较方便的操作位图:
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
函数返回值:
执行成功则返回文件描述词状态已改变的个数
如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的
值变成不可预测。
说明:测试使用select,只监听读事件是否就绪,不关心写事件就绪和异常事件就绪,对收到的数据不进行判段是否读取完整,默认一次读取一个完整的报文。
逻辑流程图:
代码实现:
selectSever.hpp
#pragma once
#include
#include
#include"sock.hpp"
namespace ns
{
class selectServer
{
static const int defaultport = 8080;
static const int fdnum = sizeof(fd_set)*8;
static const int defaultfd = -1;
using func_t = std::function;
public:
selectServer(func_t f,int port = defaultport)
:fun(f),_port(port),_listensock(-1),fdarray(nullptr)
{}
void initServer() {
_listensock = Sock::Socket();
Sock::Bind(_listensock,_port);
Sock::Listen(_listensock);
fdarray = new int[fdnum];
for(int i = 0; i < fdnum; ++i) fdarray[i] = defaultfd;
fdarray[0] = _listensock;
}
void Accepter(int listensock) {
//走到这里,说明已经准备就绪,accept不会阻塞
std::string clientIp;
uint16_t clientport;
int sock = Sock::Accept(_listensock,&clientIp,&clientport);
if (sock < 0)
return;
logMessage(NORMAL, "accept success [%s:%d]", clientIp.c_str(), clientport);
// sock我们能直接recv/read 吗?不能,整个代码,只有select有资格检测事件是否就绪
// 将新的sock 托管给select!
// 将新的sock托管给select的本质,其实就是将sock,添加到fdarray数组中即可!
int i = 0;
for(;i < fdnum; ++i) {
if(fdarray[i] != defaultfd) continue;
else break;
}
if(i == fdnum) {
logMessage(WARNING, "server if full, please wait");
close(sock);
}
else fdarray[i] = sock;
logMessage(DEBUG, "Accepter out");
}
void Recver(int sock,int pos) {
logMessage(DEBUG, "in Recver");
//1.读取request
char buffer[1024];
ssize_t s = recv(sock,buffer,sizeof(buffer)-1,0);
if(s > 0) {
buffer[s] = 0;
logMessage(NORMAL,"client say# %s",buffer);
} else if (s == 0) {
close(sock);
fdarray[pos] = defaultfd;
logMessage(NORMAL,"client quit");
} else {
close(sock);
fdarray[pos] = defaultfd;
logMessage(ERROR,"client quit: %s",strerror(errno));
}
//处理request:
std::string response = fun(buffer);
write(sock,response.c_str(),response.size());
}
void HandlerEvent(fd_set& rfds) {
//HandlerEvent 不仅仅是只有一个fd是就绪的,可能有多个
for(int i = 0; i < fdnum; ++i) {
if(fdarray[i] == defaultfd) continue;
//正常的fd,但是不一定就绪了
if(FD_ISSET(fdarray[i],&rfds) && fdarray[i] == _listensock) Accepter(_listensock);
else if(FD_ISSET(fdarray[i],&rfds)) Recver(fdarray[i],i);
else {} //没有就绪
}
}
void start() {
for(;;) {
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = fdarray[0];
for(int i = 0; i < fdnum; ++i) {
if(fdarray[i] == defaultfd) continue;
//合法fd全部添加到读文件描述符集中:
FD_SET(fdarray[i],&rfds);
if(maxfd < fdarray[i]) maxfd = fdarray[i];
}
// struct timeval timeout = {0,0};
// 一般而言,要是用select,需要程序员自己维护一个保存所有合法fd的数组!
int n = select(maxfd+1,&rfds,nullptr,nullptr,nullptr);
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, "get a new link...");
HandlerEvent(rfds);
break;
}
}
}
~selectServer() {}
private:
int _port;
int _listensock;
int* fdarray;
func_t fun;
};
}
注:
a.不知道一次有多少文件描述符就绪,所以需要借助一个数组管理文件描述符,每次遍历寻找有哪些文件描述符处于就绪状态
b.因为输入输出型参数,第一次添加到读文件描述符集合中的事件没有就绪,内核就会清空对该文件描述符的关心,所以每次都要重新设置文件描述符
c.监控文件描述符大小受限fd_set,该数据类型大小为128byte,即1024个比特位,所以select最多可以监控1024文件描述符
1.select能同时等待的文件fd是有上限的
2.必须借助第三方数组维护fd
3.select的大部分参数是输入输出型的,调用select前,要重新设置所有的fd,调用之后,还要检查更新所有的fd,带来了遍历的成本
4.select为什么第一个参数是 最大fd+ 1呢?确定遍历范围
5.select采用位图,用户->内核 内核- >用户 进行数据拷贝,拷贝成本问题
select可以一次对多个文件描述符设置关心状态,当有事件就绪之后,通知上层处理事件,可以减少等的时间,进而提高IO效率,但是select同时存在许多的缺点,关于等待文件描述符有上限的问题和每次调用都要重新设置关心的fd的问题,会在下一篇poll模型的介绍中进行解决,欢迎大家继续来阅读,今天的内容就介绍这么多了,感谢大家的阅读,我们下次再见!