文章目录
- 1、思路
- 2、select接口
- 3、实现
- 1、准备工作
- 2、实现等待多个fd
- 3、辨别连接和简单处理读事件
- 4、简单处理写、读事件
- 4、特点
1、思路
select就是多路转接IO。select能以某种形式,等待多个文件描述符,只要有哪个fd有数据就可以读取并全部返回。就绪的fd,要让用户知道。select等待的多个fd中,一定有少量或者全部都准备好了数据。
2、select接口
nfds输入型参数,表示select等待的多个fd中,fd对应的数 + 1
剩下四个参数都是输入输出型参数,意思是用户传入参数,操作系统通过这些参数把结果交给用户。
除去timeout,中间三个是同一个类型的,分别对应读、写、异常,只是用处不同,但思路相同。timeout的类型是一个结构体,表示select应该以什么方式来轮询检测,一共有3种值。NULL/nullptr表示阻塞等待,等待多个fd的时候就是把文件结构体的指针放到阻塞队列中,如果没有就绪的就不返回;{0,0}表示非阻塞等待,也就是等待多个文件描述符时,如果没有就绪的,就等待0s后出错返回;{n,m}表示n秒以内阻塞等待,超过这个时间就timeout一次,也就是非阻塞一次,也就是出错返回,如果n秒内得到了数据并返回,那么timeout此时就表示剩余时间。
返回值是一个int值,大于0表示有几个fd是就绪的,为0表示超时出错返回了,小于0表示等待失败。
readfd参数的类型是fd_set类型,是一个位图结构
用位图结构来表示多个fd,进行用户和内核之间的信息的互相传递。对于位图结构的操作只能用系统给定的接口。
FD_CLR把指定的描述符从指定的集合中清除;FD_ISSET查看指定fd是否在集合中;FD_SET添加fd到集合中;FD_ZERO将集合清空。
通过这个位图结构,用户告诉内核,用户关心哪些fd对应的文件有数据,内核要告诉用户,哪些fd的读事件就绪了。
假设用户这样设置,fd_set rfds:0110 111,表明文件描述符为4和7的不管,看fd为012356的文件;如果只有3号fd就绪了,内核传回来的rfds则是0000 1000;用户拿到rfds后,扫描位图,哪个位置为1就说明是哪个文件就绪了。
fd_set是OS提供的固定大小的数据类型,比特位数有上限,所以select能管理的fd也有上限。查看多大
#include <iostream>
#include <sys/select.h>int main()
{fd_set fd;std::cout << sizeof(fd) << std::endl;return 0;
}
字节数就是sizeof(fd) * 8。
3、实现
1、准备工作
makefile
selectserver:main.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f selectserver
main.cc
#include "SelectServer.hpp"
#include <memory>int main()
{//fd_set fd;//std::cout << sizeof(fd) << std::endl;std::unique_ptr<SelectServer> svr(new SelectServer(3389));svr->InitServer();svr->Start();return 0;
}
SelectServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/select.h>class SelectServer
{
public:SelectServer(uint16_t port): port_(port){}void InitServer(){}void Start(){}~SelectServer(){}
private:uint16_t port_;
};
引入之前的Sock.hpp和log.hpp和err.hpp
Sock.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include "err.hpp"
#include "log.hpp"static const int gbacklog = 32;
static const int defaultfd = -1;class Sock
{
public:Sock(): _sock(defaultfd){}void Socket(){_sock= socket(AF_INET, SOCK_STREAM, 0);if(_sock < 0){logMessage(Fatal, "socket error, code: %d, errstring: %s", errno, strerror(errno));exit(SOCKET_ERR);}//设置地址是可复用的int opt = 1;setsockopt(_sock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));}void Bind(const uint16_t& 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 = INADDR_ANY;if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));exit(BIND_ERR);}}void Listen(){if(listen(_sock, gbacklog) < 0)//第二个参数维护了一个队列,发送了连接请求但是服务端没有处理的客户端,服务端开始accept后,就会出现另一个队列,就是服务端接受了请求但还没被accept的客户端{logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));exit(LISTEN_ERR);}}int Accept(std::string* clientip, uint16_t* clientport){struct sockaddr_in temp;socklen_t len = sizeof(temp);int sock = accept(_sock, (struct sockaddr*)&temp, &len);if(sock < 0){logMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));}else{*clientip = inet_ntoa(temp.sin_addr);//这个函数就可以从结构体中拿出ip地址,转换好后返回*clientport = ntohs(temp.sin_port);}return sock;}int Connect(const std::string& serverip, const uint16_t& serverport)//让别的客户端来连接服务端{struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());return connect(_sock, (struct sockaddr*)&server, sizeof(server));//先不打印消息}int Fd(){return _sock;}void Close(){if(_sock != defaultfd) close(_sock);}~Sock(){}
private:int _sock;
};
err.hpp
#pragma onceenum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,SETSID_ERR,OPEN_ERR
};
log.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdarg>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>const std::string filename0 = "log/tcpserver.log.Debug";
const std::string filename1 = "log/tcpserver.log.Info";
const std::string filename2 = "log/tcpserver.log.Warning";
const std::string filename3 = "log/tcpserver.log.Error";
const std::string filename4 = "log/tcpserver.log.Fatal";
const std::string filename5 = "log/tcpserver.log.Unknown";enum
{Debug = 0,//调试信息Info,//正常信息Warning,//告警,不影响运行Error,//一般错误Fatal,//严重错误Unknown
};static std::string toLevelString(int level, std::string& filename)
{switch(level){case Debug:filename = filename0;return "Debug";case Info:filename = filename1;return "Info";case Warning:filename = filename2;return "Warning";case Error:filename = filename3;return "Error";case Fatal:filename = filename4;return "Fatal";default:filename = filename5;return "Unknown";}
}static std::string getTime()
{time_t curr = time(nullptr);//拿到当前时间struct tm *tmp = localtime(&curr);//这个结构体有对于时间单位的int变量char buffer[128];snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_mday, \tmp->tm_hour, tmp->tm_min, tmp->tm_sec);//这些tm_的变量就是结构体中自带的,tm_year是从1900年开始算的,所以+1900return buffer;
}//日志格式: 日志等级 时间 pid 消息体
//logMessage(DEBUG, "hello: %d, %s", 12, s.c_str()); 12以%d形式打印, s.c_str()以%s形式打印
void logMessage(int level, const char* format, ...)//...就是可变参数,format是输出格式
{//写入到两个缓冲区中char logLeft[1024];//用来显示日志等级,时间,pidstd::string filename;std::string level_string = toLevelString(level, filename);std::string curr_time = getTime();snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", level_string.c_str(), curr_time.c_str(), getpid());char logRight[1024];//用来显示消息体va_list p;va_start(p, format);//直接用这个接口来对format进行操作,提取信息vsnprintf(logRight, sizeof(logRight), format, p);va_end(p);//打印printf("%s%s\n", logLeft, logRight);//format是一个字符串,里面有格式,比如%d, %c,通过这个就可以用arg来提取参数//保存到文件中FILE* fp = fopen(filename.c_str(), "a");if(fp == nullptr) return ;fprintf(fp, "%s%s\n", logLeft, logRight);fflush(fp);fclose(fp);//va_list p;//char*//下面是三个宏函数//int a = va_arg(p, int);//根据类型提取参数//va_start(p, format);//让p指向可变参数部分的起始地址//va_end(p);//把p置为空, p = NULL
}
2、实现等待多个fd
#pragma once
#include <iostream>
#include <string>
#include <sys/select.h>
#include "Sock.hpp"
#include "log.hpp"
#include "err.hpp"class SelectServer
{
public:SelectServer(uint16_t port): port_(port){}void InitServer(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();}void Start(){listensock_.Accept();}~SelectServer(){}
private:uint16_t port_;Sock listensock_;
};
最一开始,服务器只有一个socket,就是上面这些代码创建出来的。如果没创建,服务器根本就没有socket,那么这时候Accept也没有检测到链接,只能阻塞着。所以不能直接Accept,要先有链接。在网络中,新链接当作读事件来就绪。我们先添加套接字到select中再处理。
#pragma once
#include <iostream>
#include <string>
#include <sys/select.h>
#include <cstring>
#include "Sock.hpp"
#include "log.hpp"
#include "err.hpp"class SelectServer
{
public:SelectServer(uint16_t port): port_(port){}void InitServer(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();}void Start(){fd_set rfds;FD_ZERO(&rfds); // 先清空,即使是新的rfdsFD_SET(listensock_.Fd(), &rfds);while (true){struct timeval timeout = {2, 0};//timeout是输出型参数,每一次都需要重新设置,不然后续都会变成0而直接打印第一条语句int n = select(listensock_.Fd() + 1, &rfds, nullptr, nullptr, &timeout);switch (n){case 0:logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));break;case -1:logMessage(Warning, "%d: %s", errno, strerror(errno));break;default:logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了break;}}}~SelectServer(){listensock_.Close();}
private:uint16_t port_;Sock listensock_;
};
事件现在已经可以检测到是否就绪了。
void HandlerEvent(fd_set& rfds){if(FD_ISSET(listensock_.Fd(), &rfds)){std::cout << "有一个新连接到了" << std::endl;//进行Accept}}default:logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了HandlerEvent(rfds);//rfds已经设置好了break;
然后完善处理
void HandlerEvent(fd_set& rfds){if(FD_ISSET(listensock_.Fd(), &rfds)){std::cout << "有一个新连接到了" << std::endl;//进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了std::string clientip;uint16_t clientport;int sock = listensock_.Accept(&clientip, &clientport);//获取连接if(sock < 0) return ;//到这里已得到了新连接对应的fd,但不能直接读取数据//将sock添加到select的rfds中,让select来管理//打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fdlogMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);//select对fd要有持续监听能力,不能有一个就绪其它全部重置}}
获取连接代表新增了一个fd,可以用套接字来进行IO服务,但还不知道数据是否就绪,只知道有连接。客户端发送请求,和我们的服务端建立连接后,客户端可以不发数据,这时调用read接口就会阻塞在这里。
获得的sock不能直接添加到rfds,以后越来越多的sock都要处理,时间耗费长,且有个前提,应当处理用户关心的fd。无脑设置进去,当我们关心的fd就绪后,其它都会清0,只有那个fd的位置会是1,所以也无用,所以select对fd要有持续监听能力,不能有一个就绪其它全部清零。
这里的思路其实也不难,要持续监听,我们就维护一个数组保护所有获得连接的sock就行。
const static int gport = 8888;
typedef int type_t;class SelectServer
{static const int N = (sizeof(fd_set) * 8);
public:SelectServer(uint16_t port = gport) : port_(port){}
//...
private:uint16_t port_;Sock listensock_;type_t fdarray_[N];
初始化
static const int defaultfd = -1;//在之前的Sock.hpp中就已经有了这个全局变量,所以SelectServer.hpp中可以不设置这个,不过Sock.hpp中不要加static,这样两个文件都可以用void InitServer(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();for(int i = 0; i < N; i++) fdarray_[i] = defaultfd;}
Start
void Start(){fdarray_[0] = listensock_.Fd();while (true){struct timeval timeout = {2, 0};//rfds是输入输出参数,所以rfds每次都要重置//服务器在运行中,套接字对应的fd的值一直在动态变化,所以select中第一个参数的值也得变化,否则无法照顾到其它fd//所以rfds相关操作要在循环里做fd_set rfds;FD_ZERO(&rfds);int maxfd = fdarray_[0];//用max_fd来表示fd中的最大值,看select接口那里的说明for(int i = 0; i < N; i++){if(fdarray_[i] == defaultfd) continue;//合法fdFD_SET(fdarray_[i], &rfds);if(maxfd < fdarray_[i]) maxfd = fdarray_[i];}int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);switch (n){case 0:logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));break;case -1:logMessage(Warning, "%d: %s", errno, strerror(errno));break;default:logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了HandlerEvent(rfds);//rfds已经设置好了break;}}}
接下来就把sock放到fdarray_中。
void HandlerEvent(fd_set& rfds){if(FD_ISSET(listensock_.Fd(), &rfds)){std::cout << "有一个新连接到了" << std::endl;//进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了std::string clientip;uint16_t clientport;int sock = listensock_.Accept(&clientip, &clientport);//获取连接if(sock < 0) return ;//到这里已得到了新连接对应的fd,但不能直接读取数据//将sock添加到select的rfds中,让select来管理//打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fdlogMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);//select对fd要有持续监听能力,不能有一个就绪其它全部重置//让select来管理,把sock添加到fdarray_[]中int pos = 1;for( ; pos < N; pos++){if(fdarray_[pos] == defaultfd) break;}if(pos >= N){close(sock);//说明不在工作范围内,数组已经满了,那就关闭这个连接logMessage(Warning, "sockfd array[] full");}else fdarray_[pos] = sock;}//...default:logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了HandlerEvent(rfds);//rfds已经设置好了DebugPrint();break;}}}void DebugPrint(){std::cout << "fdarray[]: ";for(int i = 0; i < N; ++i){if(fdarray_[i] == defaultfd) continue;std::cout << fdarray_[i] << " ";}std::cout << "\n";}
以上的代码就已经能够做到等到多个fd并管理好它们了。
3、辨别连接和简单处理读事件
能够处理很多个连接,也都能把它们一次次的设置进集合里,但我们需要有条件地接收,要有目的地去接收。
先改一下形式
void Accepter(){std::cout << "有一个新连接到了" << std::endl;//进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了std::string clientip;uint16_t clientport;int sock = listensock_.Accept(&clientip, &clientport);//获取连接if(sock < 0) return ;//到这里已得到了新连接对应的fd,但不能直接读取数据//将sock添加到select的rfds中,让select来管理//打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fdlogMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);//select对fd要有持续监听能力,不能有一个就绪其它全部重置// 让select来管理,把sock添加到fdarray_[]中int pos = 1;for (; pos < N; pos++){if (fdarray_[pos] == defaultfd)break;}if (pos >= N){close(sock); // 说明不在工作范围内,数组已经满了,那就关闭这个连接logMessage(Warning, "sockfd array[] full");}elsefdarray_[pos] = sock;}void HandlerEvent(fd_set &rfds){if (FD_ISSET(listensock_.Fd(), &rfds)){Accepter();}}
HandlerEvent除了放入连接,还得处理一下连接。
void Recver(int index){int fd = fdarray_[index];// 用户不关心的fd就绪了char buffer[1024];// 读取不会被阻塞,因为经过上面的筛选,select已经确定这个fd是已经有数据了的// 不过只有这一次不会阻塞,之后不确定,因为一次不保证数据全部读完ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s - 1] = 0;std::cout << "client# " << buffer << std::endl;// 写一下读了再发回去,发回去也需要select管理,因为不知道自己发送条件是否就绪,比如自己的发送缓冲区是否满std::string echo = buffer;echo += " [select server echo]";send(fd, echo.c_str(), echo.size(), 0); // 先用阻塞式}else // 读完或者出错{if (s == 0)logMessage(Info, "client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[i], defaultfd);elselogMessage(Warning, "recv error, client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[i], defaultfd);// 清空当前这个连接,关闭fdclose(fdarray_[index]);fdarray_[index] = defaultfd;}}void HandlerEvent(fd_set &rfds){//这个函数并不知道rfds的哪些fd就绪了,所以得循环检测for(int i = 0; i < N; i++){if(fdarray_[i] == defaultfd) continue;//就绪的fd属于用户关心的,且它存在与rfds集合中if ((fdarray_[i] == listensock_.Fd()) && FD_ISSET(listensock_.Fd(), &rfds)){Accepter();}else if((fdarray_[i] != listensock_.Fd()) &&FD_ISSET(fdarray_[i], &rfds)){Recver(i);}}}
4、简单处理写、读事件
为了方便,先建立一个结构体
#define READ_EVENT (0X1)
#define WRITE_EVENT (0X1<<1)
#define EXCEPT_EVENT (0X1<<2)typedef struct FdEvent
{int fd;uint8_t event;std::string clientip;uint16_t clientport;
}type_t;static const int defaultevent = 0;
这样fdarray_这个数组就变成结构体类型的数组了。代码很多地方也要改一下,比如原本fdarray_[i]改成fdarray_[i].fd。
void InitServer(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();for (int i = 0; i < N; i++)//初始化一下{fdarray_[i].fd = defaultfd;fdarray_[i].event = defaultevent;fdarray_[i].clientport = 0;}}void Start(){fdarray_[0].fd = listensock_.Fd();fdarray_[0].event = READ_EVENT;while (true){struct timeval timeout = {2, 0};// rfds是输入输出参数,所以rfds每次都要重置// 服务器在运行中,套接字对应的fd的值一直在动态变化,所以select中第一个参数的值也得变化,否则无法照顾到其它fd// 所以rfds相关操作要在循环里做fd_set rfds;fd_set wfds;FD_ZERO(&rfds);FD_ZERO(&wfds);int maxfd = fdarray_[0].fd; // 用max_fd来表示fd中的最大值,看select接口那里的说明for (int i = 0; i < N; i++){if (fdarray_[i].fd == defaultfd)continue;// 合法fdif(fdarray_[i].event & READ_EVENT) FD_SET(fdarray_[i].fd, &rfds);if(fdarray_[i].event & WRITE_EVENT) FD_SET(fdarray_[i].fd, &wfds);if (maxfd < fdarray_[i].fd)maxfd = fdarray_[i].fd;}int n = select(maxfd + 1, &rfds, &wfds, nullptr, &timeout);switch (n){case 0:logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));break;case -1:logMessage(Warning, "%d: %s", errno, strerror(errno));break;default:logMessage(Debug, "有一个就绪事件发生了"); // 这时候就说明至少有一个fd就绪了HandlerEvent(rfds, wfds); DebugPrint();break;}}}void DebugPrint(){std::cout << "fdarray[]: ";for (int i = 0; i < N; ++i){if (fdarray_[i].fd == defaultfd)continue;std::cout << fdarray_[i].fd << " ";}std::cout << "\n";}
上面加入写事件,虽然我们还是关心读事件,但是写事件也要管理。以及处理用的函数部分
void Accepter(){std::cout << "有一个新连接到了" << std::endl;//进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了std::string clientip;uint16_t clientport;int sock = listensock_.Accept(&clientip, &clientport);//获取连接if(sock < 0) return ;//到这里已得到了新连接对应的fd,但不能直接读取数据//将sock添加到select的rfds中,让select来管理//打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fdlogMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);//select对fd要有持续监听能力,不能有一个就绪其它全部重置// 让select来管理,把sock添加到fdarray_[]中int pos = 1;for (; pos < N; pos++){if (fdarray_[pos].fd == defaultfd)break;}if (pos >= N){close(sock); // 说明不在工作范围内,数组已经满了,那就关闭这个连接logMessage(Warning, "sockfd array[] full");}else{fdarray_[pos].fd = sock;//fdarray_[pos].event = (READ_EVENT | WRITE_EVENT);//读写都关心,如果只关心一个,那就写一个fdarray_[pos].event = READ_EVENT;fdarray_[pos].clientip = clientip;fdarray_[pos].clientport = clientport ;}}void Recver(int index){int fd = fdarray_[index].fd;// 用户不关心的fd就绪了char buffer[1024];// 读取不会被阻塞,因为经过上面的筛选,select已经确定这个fd是已经有数据了的// 不过只有这一次不会阻塞,之后不确定,因为一次不保证数据全部读完ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s - 1] = 0;std::cout << fdarray_[index].clientip << ":" << fdarray_[index].clientport << "# " <<buffer << std::endl;// 写一下读了再发回去,发回去也需要select管理,因为不知道自己发送条件是否就绪,比如自己的发送缓冲区是否满std::string echo = buffer;echo += " [select server echo]";send(fd, echo.c_str(), echo.size(), 0); // 先用阻塞式}else // 读完或者出错{if (s == 0)logMessage(Info, "client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[index], defaultfd);elselogMessage(Warning, "recv error, client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[index], defaultfd);// 清空当前这个连接,关闭fdclose(fdarray_[index].fd);fdarray_[index].fd = defaultfd;fdarray_[index].event = defaultevent;fdarray_[index].clientip.resize(0);fdarray_[index].clientport = 0;}}void HandlerEvent(fd_set &rfds, fd_set &wfds){//这个函数并不知道rfds的哪些fd就绪了,所以得循环检测for (int i = 0; i < N; i++){if (fdarray_[i].fd == defaultfd) continue;// 关心读事件且读事件已经就绪if ((fdarray_[i].event & READ_EVENT) && FD_ISSET(fdarray_[i].fd, &rfds)){if (fdarray_[i].fd == listensock_.Fd()){Accepter();}else if (fdarray_[i].fd != listensock_.Fd()){Recver(i);}else {}}// 关心写事件且读事件已经就绪else if ((fdarray_[i].event & WRITE_EVENT) && FD_ISSET(fdarray_[i].fd, &wfds)){}else {}}}
4、特点
可监控,能关心的fd个数有上限;将fd加入select监控集的同时,还需要使用一个数组结构array保存放到select监控集中的fd,用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断,以及select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。fd_set的大小可以调整,不过涉及到内核。
select也有缺点。每次调用select,都需要手动设置fd集合来告知系统用户关心哪些fd,不方便,且要把fd集合从用户态拷贝到内核态,每次都这样,开销不算小。以及在代码中可以发现,操作系统底层不知道fd对应的文件里是否有数据,它就得遍历用户关心的全部fd,而用户层也要遍历,系统在遍历时,如果没有fd就绪且是非阻塞,就直接返回了,如果有timeout,那就过了时间还没有就绪再返回,如果是阻塞的,那么没有就绪的就挂起全部进程,直到有就绪的,就再遍历一次找到这个就绪的,除此之外,我们的代码中也遍历了好多次。select只是相对于之前的IO方式高效,但也并不是好方案。
select能等待的fd数量太少。存储文件描述符的一般是数组,也就是数组下标表示fd,数组有上限,如果做成服务器,这个数组会调最大,所以有上限是必然的。一个进程能等的fd数量有限,但select能等的更少,所以select等的少是select设计时的限制,和进程无关。select上限就不高。
下一篇写poll型服务器,会在select代码的基础上进行更改。
本篇gitee
结束。