建设景区网站推文,中国林业工程建设网站,郑州小型网站制作公司,网络科技公司名称文章目录 1、思路2、select接口3、实现1、准备工作2、实现等待多个fd3、辨别连接和简单处理读事件4、简单处理写、读事件 4、特点 1、思路
select就是多路转接IO。select能以某种形式#xff0c;等待多个文件描述符#xff0c;只要有哪个fd有数据就可以读取并全部返回。就绪… 文章目录 1、思路2、select接口3、实现1、准备工作2、实现等待多个fd3、辨别连接和简单处理读事件4、简单处理写、读事件 4、特点 1、思路
select就是多路转接IO。select能以某种形式等待多个文件描述符只要有哪个fd有数据就可以读取并全部返回。就绪的fd要让用户知道。select等待的多个fd中一定有少量或者全部都准备好了数据。
2、select接口 nfds输入型参数表示select等待的多个fd中fd对应的数 1
剩下四个参数都是输入输出型参数意思是用户传入参数操作系统通过这些参数把结果交给用户。
除去timeout中间三个是同一个类型的分别对应读、写、异常只是用处不同但思路相同。timeout的类型是一个结构体表示select应该以什么方式来轮询检测一共有3种值。NULL/nullptr表示阻塞等待等待多个fd的时候就是把文件结构体的指针放到阻塞队列中如果没有就绪的就不返回{00}表示非阻塞等待也就是等待多个文件描述符时如果没有就绪的就等待0s后出错返回{nm}表示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 rfds0110 111表明文件描述符为4和7的不管看fd为012356的文件如果只有3号fd就绪了内核传回来的rfds则是0000 1000用户拿到rfds后扫描位图哪个位置为1就说明是哪个文件就绪了。
fd_set是OS提供的固定大小的数据类型比特位数有上限所以select能管理的fd也有上限。查看多大
#include iostream
#include sys/select.hint main()
{fd_set fd;std::cout sizeof(fd) std::endl;return 0;
}字节数就是sizeof(fd) * 8。
3、实现
1、准备工作
makefile
selectserver:main.ccg -o $ $^ -stdc11
.PHONY:clean
clean:rm -f selectservermain.cc
#include SelectServer.hpp
#include memoryint main()
{//fd_set fd;//std::cout sizeof(fd) std::endl;std::unique_ptrSelectServer svr(new SelectServer(3389));svr-InitServer();svr-Start();return 0;
}SelectServer.hpp
#pragma once
#include iostream
#include string
#include sys/select.hclass 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.hppstatic 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.hconst 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.hppclass 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.hppclass 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 (0X11)
#define EXCEPT_EVENT (0X12)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
结束。