引言
linux中创建的多路转接的方式有三种,select ,poll ,epoll;多路转接的核心思想是一次等待多个文件描述符,也就是socket;内核对多路转接的大量fd的管理使用的是红黑树,epoll 内核用红黑树管理所有监听文件描述符,依靠平衡特性实现快速增删查 ;网卡数据触发硬件中断后,内核通过红黑树精准定位对应 fd,放入就绪队列,最后唤醒用户进程处理事件。 内核用 epoll 监听哪些套接字就绪,拿到就绪事件后,扔给线程池去读写业务数据,监听靠内核,干活靠线程池。
1.select理解

select 对文件描述符的管理使用的是位图;

这三个参数的作用是将fd可以放置到对应的关心集中,表示希望OS对这个fd的读还是写还是异常进行关心,当然可以一次三个都关系,也可以只关心写事件
select系统调用函数除了第一个参数是输入性参数,其他参数都是输入输出形参数;只有托管给select的文件描述符,它才会关系,即在就绪的时候给我们返回,如果没有托管给select,就算这个文件描述符就绪了,select也不会关心的;

2.select的使用场景
适用于一些小型的嵌入式设备或者开源库中,因为它能够管理的fd的数量是有上限的,大小取决于位图的大小;
3.在实际代码中理解select的作用
这是让select帮listen_socket关心是否就绪:
cpp
void Start()
{
fd_set rfds; //定义读文件描述符集合
_isrunning=true;
while(_isrunning)
{
//清空rfds;
FD_ZERO(&rfds);
//将监听套接字加入到rfds集合中
FD_SET(_listen_socket->GetFd(),&rfds);
struct timeval timeout={10,0}; //设置超时时间为1秒, 1秒以内正常等待,超时之后直接返回
//我们不能让accept来阻塞检测新连接的到来,而应该让select来负责就绪事件的检测
int n = select(_listen_socket->GetFd()+1,&rfds,nullptr,nullptr,&timeout);
//rfds = 我想让内核帮我监听读事件的所有 fd 清单
//监听 fd 必须放进去,因为它的 "读事件" = 新客户端连接
//这个时候传进来的文件描述符集rfds是用户告诉操作系统让操作系统关心这个文件描述符集上的哪些事件发生了变化
switch(n)
{
case 0:
//没有就绪事件
std::cout<<"time out "<<std::endl;
break;
case -1:
//出错
std::cout<<"select error"<<std::endl;
break;
default:
//os告诉用户已经有哪些文件描述符集上的fd事件发生了改变
std::cout<<"已经有读事件准备就绪....."<<timeout.tv_sec<<timeout.tv_usec<<std::endl;
//处理对应的socket
HandleEvent(rfds);
break;
}
}
_isrunning =false;
}
void HandleEvent(fd_set &rfds)
{
//判断listensocket是否在rfds中
if(FD_ISSET(_listen_socket->GetFd(),&rfds))
{
InetAddr client;
int newsockfd=_listen_socket->Accepter(&client); //这里的accepter一定不会被阻塞了,这个时候执行的动作就是IO当中的拷贝
if(newsockfd < 0) return;
{
//处理客户端连接
std::cout<<"new client connect from "<<client.Ip()<<":"<<client.Port()<<std::endl;
}
}
}
我们要知道前面的代码只是做了让select帮我们判断_listen_socket读事件是否就绪了,也就是监听套接字中的全队列中里面是否有数据了,判断就绪的时候我们使用accept从全队列中获取新连接并创建新的socket返回,而这个新的socket可以直接进行recv吗?并不可以,前面判断是否就绪是给listen_socket判断的,并不是给这个新创建的socket判断的,所以想要让这个socket不关心等待是否就绪事件,就得把它也放到rfds中让select关心;
因为select每次调用结束之后都会对对应的位图做清空,所以也就要求我们每次调用select之前都应该重新设置要被OS关系的fd;
下面的代码我们会将新的文件描述符放置到rfds
cpp
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <cstdint>
#include <sys/select.h>
#include "log.hpp"
#include "Socket.hpp"
#include "InetAddr.hpp"
using namespace socketModule;
using namespace LogModule;
#define NUM sizeof(fd_set) * 8
const int gdefaultfd = -1;
class SelectServer
{
public:
SelectServer(uint16_t port)
: _port(port), _listen_socket(std::make_shared<TcpSocket>())
{
}
void Init()
{
_listen_socket->BuildTcpSocket(_port);
_array_fd[0] = _listen_socket->GetFd();
for (int i = 1; i < NUM; i++)
{
_array_fd[i] = gdefaultfd;
}
}
void Loop()
{
fd_set rfds; // 定义读文件描述符集合
_isrunning = true;
while (_isrunning)
{
// 清空rfds;
FD_ZERO(&rfds);
struct timeval timeout = {10, 0}; // 设置超时时间为1秒, 1秒以内正常等待,超时之后直接返回
// 我们不能让accept来阻塞检测新连接的到来,而应该让select来负责就绪事件的检测
// 将监听套接字加入到rfds集合中
FD_SET(_listen_socket->GetFd(), &rfds);
int maxfd = gdefaultfd;
for (int i = 0; i < NUM; i++)
{
if (_array_fd[i] == gdefaultfd)
{
continue;
}
FD_SET(_array_fd[i], &rfds);
// 更新出最大值 -> select 的第一个参数 nfds,规定是:你要监听的所有文件描述符中,最大值 + 1
if (maxfd < _array_fd[i])
{
maxfd = _array_fd[i];
}
}
int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
// rfds = 我想让内核帮我监听读事件的所有 fd 清单
// 监听 fd 必须放进去,因为它的 "读事件" = 新客户端连接
// 这个时候传进来的文件描述符集rfds是用户告诉操作系统让操作系统关心这个文件描述符集上的哪些事件发生了变化
switch (n)
{
case 0:
// 没有就绪事件
std::cout << "time out " << std::endl;
break;
case -1:
// 出错
std::cout << "select error" << std::endl;
break;
default:
// os告诉用户已经有哪些文件描述符集上的fd事件发生了改变
std::cout << "已经有读事件准备就绪....." << timeout.tv_sec << timeout.tv_usec << std::endl;
// 处理对应的socket
Dispatcher(rfds); //事件派发
break;
}
}
_isrunning = false;
}
void Accepter()
{
InetAddr client;
int newsockfd = _listen_socket->Accepter(&client); // 这里的accepter一定不会被阻塞了,这个时候执行的动作就是IO当中的拷贝
if (newsockfd < 0)
return;
else
{
// 处理客户端连接
std::cout << "new client connect from " << client.Ip() << ":" << client.Port() << std::endl;
int pos = -1;
for (int j = 0; j < NUM; j++) // 我们现在要将新的fd放置到数组中,所以需要找到一个空位;
{
if (_array_fd[j] == gdefaultfd)
{
pos = j;
break;
}
}
if (pos == -1)
{
LOG(loglevel::ERROR) << "服务器已经满了....";
close(newsockfd);
}
if (pos < NUM)
{
_array_fd[pos] = newsockfd;
std::cout << "new client socket fd is" << newsockfd << ",pos=" << pos << std::endl;
}
}
}
void Recvr(int i)
{
// 处理客户端数据接收
char buffer[1024] = {0};
int n = ::recv(_array_fd[i], buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
std::cout << "client# " << buffer << std::endl;
// 把读到的信息再回显回去
std::string str = "echo#";
str += buffer;
::send(_array_fd[i], str.c_str(), str.size(), 0);
}
else if (n == 0)
{
// 说明客户端已经退出了,呢我们这个关联客户端的fd也应该关闭
close(_array_fd[i]);
_array_fd[i] = gdefaultfd;
LOG(loglevel::ERROR) << "客户端已经退出了....";
}
else
{
close(_array_fd[i]);
_array_fd[i] = gdefaultfd;
LOG(loglevel::ERROR) << "读取出现异常......";
}
}
void Dispatcher(fd_set &rfds)
{
// 判断listensocket是否在rfds中,并将新的fd放置在rfds中
for (int i = 0; i < NUM; i++)
{
if (_array_fd[i] == gdefaultfd)
{
continue;
}
if (_array_fd[i] == _listen_socket->GetFd())
{
if (FD_ISSET(_listen_socket->GetFd(), &rfds))
{
Accepter();
}
}
else
{
// 进行数据读取
if (FD_ISSET(_array_fd[i], &rfds))
{
Recvr(i);
}
}
}
}
~SelectServer() {}
private:
uint16_t _port;
std::shared_ptr<Socket> _listen_socket;
bool _isrunning;
int _array_fd[NUM];
};
4.select的优缺点

select可以关心的文件描述符的数量太少并不是因为一个进程可以打开的文件描述符太少决定的,而是它自己的设计决定的,而进程的文件描述符个数其实是可以根据情况的不同进行动态扩容的;