今天我们要介绍的是实现高效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
string(const std::string&)>; - 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模型的介绍中进行解决,欢迎大家继续来阅读,今天的内容就介绍这么多了,感谢大家的阅读,我们下次再见!