赞
踩
select就是多路转接IO。select能以某种形式,等待多个文件描述符,只要有哪个fd有数据就可以读取并全部返回。就绪的fd,要让用户知道。select等待的多个fd中,一定有少量或者全部都准备好了数据。

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。
makefile
selectserver:main.cc
g++ -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 once
enum
{
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年开始算的,所以+1900 return 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];//用来显示日志等级,时间,pid std::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 }
#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); // 先清空,即使是新的rfds FD_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就是当前可用的fd logMessage(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; //合法fd FD_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就是当前可用的fd logMessage(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并管理好它们了。
能够处理很多个连接,也都能把它们一次次的设置进集合里,但我们需要有条件地接收,要有目的地去接收。
先改一下形式
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就是当前可用的fd logMessage(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; } 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); else logMessage(Warning, "recv error, client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[i], defaultfd); // 清空当前这个连接,关闭fd close(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); } } }
为了方便,先建立一个结构体
#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; // 合法fd if(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就是当前可用的fd logMessage(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); else logMessage(Warning, "recv error, client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[index], defaultfd); // 清空当前这个连接,关闭fd close(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 {} } }
可监控,能关心的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代码的基础上进行更改。
结束。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。