目录
I/O 多路转接之 select
初识 select
select 函数原型
#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解释:
- 参数 nfds 是需要监视的最大的文件描述符值+1;
- rdset,wrset,exset 分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
- 参数 timeout 为结构 timeval,用来设置 select()的等待时间
参数 timeout 取值:
- NULL:则表示 select()没有 timeout,select 将一直被阻塞,直到某个文件描述符上发生了事件;
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。相当于轮询多个fd~
- 特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回。例如设置(5,0)若是第3s有fd,则返回2代表剩余时间
fd_set 结构
fd_set:文件描述符集合的概念,也可以理解为是位图的标识。这样就意味着fd的存放就跟比特位上的位置与数据有关了。
把需要监视的fd设置进去(等待监视),然后它就会返回出已经就绪好的fd(钓鱼)。
- 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 的全部位
timeval 结构
函数返回值
- 执行成功则返回文件描述词状态已改变的个数 (检测多条鱼竿中钓鱼的数量)
- 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回
- 当有错误发生时则返回-1,错误原因存于 errno,此时参数 readfds,writefds, exceptfds 和 timeout 的值变成不可预测。
select 使用示例: 检测标准输入输出
模拟一下客户端输入数据,服务端输出数据,加入了select作为对整个过程fd文件状态的监视工程
基本框架
SelectServerMain.cc
#include "SelectServer.hpp"
#include <memory>
// ./selectserver port
int main(int argc,char *argv[])
{
if(argc!=2)
{
std::cout<<"Usage: "<<argv[0]<<" port" <<std::endl;
}
uint16_t port = std::stoi(argv[1]);
EnableScreen();
std::unique_ptr<SelectServer> svr = std::make_unique<SelectServer>(port);
svr->Loop();
return 0;
}
SelectServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include "Socket.hpp"
using namespace socket_ns;
class SelectServer
{
public:
SelectServer(uint16_t port)
: _port(port),
_listensock(std::make_unique<TcpSocket>())
{
InetAddr addr("0", _port);
_listensock->BuildListenSocket(addr);
}
void Loop()
{
while (true)
{
//_listensock等待客户端的连接请求,这就相当于对方给我发送数据,我们要触发IO(读取),所以要作为读事件处理
// 即:新连接的到来就意味着一次读事件,而这是select所在监视的~
fd_set rfds; // 设置读事件集合
FD_ZERO(&rfds); // 清空集合
FD_SET(_listensock->SockFd(), &rfds); // 把_listensock对应的fd加入读事件集合
struct timeval timeout = {1, 0};
// 关注rfds集合里面的fd,一旦有就绪状态就设置比特位,并且把关注fd状态改变的个数返回给n
int n = select(_listensock->SockFd() + 1, &rfds, nullptr, nullptr, &timeout);
// 根据fd个数进行判定
switch (n)
{
case 0:
//显示每一次等待的时间
LOG(INFO, "timeout, %d.%d\n", timeout.tv_sec, timeout.tv_usec);
break;
case -1:
//出现错误
LOG(ERROR, "select error...\n");
break;
default:
//表示有事件就绪
LOG(DEBUG, "Event Happen. n : %d\n", n);
HandlerEvent(rfds);
break;
}
}
}
~SelectServer()
{
}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensock;
};
我们设置好需要让内核关注的事件集合rfds后用select开始进行事件的监视~
ps:如果发现事件就绪了,但不去处理的话就会一直反复提醒“Event Happen"直到处理完毕。
所以我们再写一个事件处理函数~这里已经初见端倪了,rfds集合里面是已经就绪的事件,我们可以一次性处理这些事件,按照以前是需要多进程/线程的,现在用一个函数select就搞定了~
事件处理HandlerEvent
void HandlerEvent(fd_set &rfds)
{
InetAddr clientaddr;
//这里调用accpet并不会阻塞,因为由客户端发来的事件已经就绪
int sockfd = _listensock->Accepter(&clientaddr);
if(sockfd>=0)
{
LOG(DEBUG,"Get new link,sockfd: %d,client info %s: %d\n",sockfd,clientaddr.Ip().c_str(),clientaddr.Port());
}
}
我们在监视到客户端发来的连接请求事件后立马进行处理——服务端accept进行连接~
那么我们这个新的sockfd该如何管理呢?直接拿去用吗?recv(sockfd)/send(sockfd)——都不是,而是要托管给select~因为只有select知道读事件是否就绪,然后才会去合理使用系统调用~
我们把这个辅助数组当成rfds集合~
代码做出以下调整:
void HandlerEvent(fd_set &rfds)
{
for (int i = 0; i < N; i++)
{
InetAddr clientaddr;
// 这里调用accpet并不会阻塞,因为由客户端发来的事件已经就绪
int sockfd = _listensock->Accepter(&clientaddr);
if (sockfd >= 0)
{
LOG(DEBUG, "Get new link,sockfd: %d,client info %s: %d\n", sockfd, clientaddr.Ip().c_str(), clientaddr.Port());
}
// 将新的fd以加入集合的方式托管给select进行监视
int pos = 1;
// 寻找数组内空闲的位置
for (; pos < N; pos++)
{
if (_fd_array[pos] == defaultfd)
break;
}
// 若数组位置满了,则关闭当前的sockfd,因为无法处理事件
if (pos == N)
{
close(sockfd);
LOG(WARNING, "server is full\n");
return;
}
// 把sockfd加入到辅助数组中,后续再从辅助数组里拿出放进rfds
else
{
_fd_array[pos] = sockfd;
LOG(DEBUG, "%d add to select array!\n", sockfd);
}
}
}
因为select可以一次性监视多个fd是否就绪并同时进行处理,所以事件处理函数内不仅仅只是处理一次,而是保证能够全部处理完事件~因此加入了for循环~不然第一次也只是处理了Listen套接字而已。
但又引入了新的问题,除了listensock,我们还有其他的sockfd,也并不是全部的sockfd都是要走accept这一套的,所以我们还得进行区分~前者是为了获取新连接,而后者是为了读写~
HandlerEvent
void HandlerEvent(fd_set &rfds)
{
for (int i = 0; i < N; i++)
{
if (_fd_array[i] == defaultfd)
continue;
// 判断在辅助数组内的fd是否为rfds所关注的fd,若是则说明对方事件就绪,判断对方的事件处理类型
if (FD_ISSET(_fd_array[i], &rfds))
{
// 若关注fd为_listensock,则给其安排accept的业务
if (_fd_array[i] == _listensock->SockFd())
{
AcceptClient();
}
else
{
// 普通读写事件就绪,使用相应的系统调用~
ServiceIO(i);
}
}
}
}
AcceptClient
void AcceptClient()
{
InetAddr clientaddr;
// 这里调用accpet并不会阻塞,因为由客户端发来的事件已经就绪
int sockfd = _listensock->Accepter(&clientaddr);
if (sockfd >= 0)
{
LOG(DEBUG, "Get new link,sockfd: %d,client info %s: %d\n", sockfd, clientaddr.Ip().c_str(), clientaddr.Port());
}
// 将新的fd以加入集合的方式托管给select进行监视
int pos = 1;
// 寻找数组内空闲的位置
for (; pos < N; pos++)
{
if (_fd_array[pos] == defaultfd)
break;
}
// 若数组位置满了,则关闭当前的sockfd,因为无法处理事件
if (pos == N)
{
close(sockfd);
LOG(WARNING, "server is full\n");
return;
}
// 把sockfd加入到辅助数组中,后续再从辅助数组里拿出放进rfds
else
{
_fd_array[pos] = sockfd;
LOG(DEBUG, "%d add to select array!\n", sockfd);
}
}
ServiceIO
void ServiceIO(int pos)
{
char buffer[1024];
// 这里的读取一定不会阻塞,因为读事件已经就绪了,否则也不会来到这里了
ssize_t n = recv(_fd_array[pos], buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string echo_string = "[server echo]# ";
echo_string += buffer;
send(_fd_array[pos], echo_string.c_str(), echo_string.size(), 0);
}
// 下面普通事件处理完会被去除出辅助数组,但listensockfd永远不会被去除,因为它需要持续地去监听新的连接
else if (n == 0)
{
LOG(DEBUG, "%d is closed\n", _fd_array[pos]);
close(_fd_array[pos]);
_fd_array[pos] = defaultfd;
LOG(DEBUG, "curr fd_array[] fd list : %s\n", RfdsToString().c_str());
}
else
{
LOG(DEBUG, "%d recv error\n", _fd_array[pos]);
close(_fd_array[pos]);
_fd_array[pos] = defaultfd;
LOG(DEBUG, "curr fd_array[] fd list : %s\n", RfdsToString().c_str());
}
}
RfdsToString
std::string RfdsToString()
{
std::string fdstr;
for (int i = 0; i < N; i++)
{
if (_fd_array[i] == defaultfd)
continue;
fdstr += std::to_string(_fd_array[i]);
fdstr += " ";
}
return fdstr;
}
最后我们再来测试一下~
select优缺点
优点:显而易见,可以实现多线程/多进程的效果,同时处理多个请求事件~
缺点:也很明显,rfds是有大小限制的,并且由于输入输出混合型参数的特性,每次调用都需要重新做设定,然后就是在监视多个fd的时候,OS需要遍历所有的fd,至少一个准备就绪就立即返回~
全部代码