1.select的核心定位

2.认识select函数
三个参数

其他参数
select是来"等"的,等 读事件就绪、写事件就绪,所以剩余的参数就是这,它们是分开的

3.demo,select server服务器


最开始只有listensocketfd,接下来有客户端新连接到来,fd 就绪,调用HanderEvent()去处理新连接

接下来,我们和对方建立好连接了,但我们不敢直接 read读取,客户端可能此时没有 发来请求!
fd 不一定 读事件就绪!而且我们今天是单进程,一旦 阻塞了,直接挂起该进程!

每次select返回时,内核都会修改 我们传入的,要select关心的 位图,下次select调用时,我们当然还要它关心这些事件,所以我们得记录--select返回之后,哪些fd还需要被添加到 位图里,让select关心!
所以select ,需要借助辅助数组,在下一次select 之前,把所有需要关心的fd都加回去!

但是这里有一个问题,我们把普通fd也添加进了select,那下次有fd就绪,不一定是listenfd就绪,那就不能直接调用HanderEvent(),然后去accept!

一旦有fd就绪,select就会立即返回,立即修改位图,所以,判断该fd是否就绪,只要判断,该fd 是否在位图里就行!IS_ISSET

我们代码甚至只是单进程,却能同时处理多个IO请求!
4.select多路转接的特点
- 将fd加入到select的同时,还要在使用一个fd_array来保存 我们 想让select监控的fd,因为每次调用select,select一旦返回有fd就绪,就会修改我们设置的位图。
- 检测的fd 有上限,我这边1024
5.select的缺点
- select底层,检测多个文件描述符是否就绪,采用的是遍历for,当前进程文件描述符表,这就是为什么 select第一个参数,是当前需要监测的 文件描述符的最大值!开销大!【)
- select的位图时 输入输出型参数,每次调用select,都需要手动重置select,添加我们要关心的fd!
- select能同时关心的fd 数量比较少,虽然当前进程能打开的文件描述符也有上限=w=
为了解决这些缺点,我们要学习 poll、epoll多路转接
代码:
cpp
#include <iostream>
#include <unistd.h>
#include "Socket.hpp"
#include "Log.hpp"
using namespace SocketModule;
using namespace LogModule;
class SelectServer
{
const static int size = sizeof(fd_set) * 8;
const static int defaultfd = -1;
public:
SelectServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false)
{
_listensock->BulidListenSocket(port); // 得到监听套接字
for (int i = 0; i < size; i++) // 初始化辅助数组
{
_fd_array[i] = defaultfd;
}
_fd_array[0] = _listensock->Fd(); // listenfd直接添加进辅助数组
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
// 每次select返回后,需要重新添加 我们要关心的历史的fd!
// 辅助数组,保存所有需要关心的fd
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(_listensock->Fd(), &rfds);
int maxfd = defaultfd;
for (int i = 0; i < size; i++)
{
if (_fd_array[i] == defaultfd)
continue;
FD_SET(_fd_array[i], &rfds); // 在这添加
if (maxfd < _fd_array[i])
maxfd = _fd_array[i];
}
// struct timeval timeout = {2, 0};
// 最大fd,一定变化,每次select之前,都要对rfds重置
PrintFd();
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case -1: // select 出错了
LOG(LogLevel::ERROR) << "select error";
break;
case 0: // 超时了
LOG(LogLevel::INFO) << "select time out";
break;
default: // 返回值>0,有事件就绪了
// 此时有事件就绪,不一定是有新连接到来
// 还有可能是普通fd就绪,客户端给服务器发数据了!
LOG(LogLevel::DEBUG) << "有事件就绪了";
Dispatcher(rfds); // 事件派发器
break;
}
}
_isrunning = false;
}
void PrintFd()
{
std::cout << "fd_array[]: ";
for (int i = 0; i < size; i++)
{
if (_fd_array[i] == defaultfd)
continue;
std::cout << _fd_array[i] << " ";
}
std::cout << "\r\n";
}
// 有新连接到来,或者 客户端发数据了!
// 事件派发器!
void Dispatcher(fd_set &rfds)
{
// 如果是listenfd就绪
for (int i = 0; i < size; i++)
{
if (_fd_array[i] == defaultfd)
continue;
// fd合法,但不一定就绪,我怎么直到这个fd是就绪的
// 一旦有fd就绪,select就会立即返回,修改位图
// 只需要判断当前fd是否在 rfd位图里!
if (FD_ISSET(_fd_array[i], &rfds))
{
// 该fd 读就绪
if (_fd_array[i] == _listensock->Fd())
{
// 1.listenfd就绪,新连接到来
Accepter();
}
else
{
// 2.普通fd就绪,客户端发消息了!
Recver(_fd_array[i], i);
}
}
}
}
// 连接管理器!
void Accepter()
{
InetAddr client;
int fd = _listensock->Accept(&client); // 这回accept不会阻塞,select已经解决了等 的问题
if (fd >= 0)
{
LOG(LogLevel::INFO) << "get a new link success,sockfd:"
<< fd << ",client is" << client.StringAddr();
// 不能直接read,如何托管给select?
// 添加到辅助数组里
int pos = 0;
for (pos = 0; pos < size; pos++)
{
if (_fd_array[pos] == defaultfd)
break;
}
if (pos == size)
{
LOG(LogLevel::WARNING) << "select 满了";
close(fd);
}
else
_fd_array[pos] = fd;
}
}
// IO处理器
void Recver(int fd, int pos)
{
char buffer[1024];
// 普通 fd就绪,直接 读取,不会阻塞
ssize_t n = recv(fd, &buffer, sizeof(buffer) - 1, 0); // bug
if (n > 0)
{
std::cout << "client say@ " << buffer << std::endl;
}
else if (n == 0) // 客户端不写了
{
LOG(LogLevel::INFO) << "client quit";
_fd_array[pos] = defaultfd; // 1.从辅助数组中删除
close(fd); // 2.关闭fd
}
else // 读取出错
{
LOG(LogLevel::ERROR) << "recv error";
_fd_array[pos] = defaultfd; // 1.从辅助数组中删除
close(fd); // 2.关闭fd
}
}
void Stop()
{
_isrunning = false;
}
~SelectServer()
{
}
private:
std::unique_ptr<Socket> _listensock;
bool _isrunning;
int _fd_array[size];
};
完。