【Linux网络编程】第二十弹---熟练I/O多路转接:深入解析select机制与实战
I/O 多路转接之 select:初识 select,select 函数原型,测试timeout,处理事件(三个版本),补充select的理解,select 的特点,select 缺点;完整代码:SelectServer.hpp,Main.cc~~~
✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】
目录
1、I/O 多路转接之 select
1.1、初识 select
系统提供 select 函数来实现多路复用输入/输出模型.
- select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
- 程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
定位:只负责进行等,不进行拷贝!
作用:为了等待多个fd,等待fd上面的新事件就绪,通知程序员,事件已经就绪,可以进行IO拷贝了!
1.2、select 函数原型
select 的函数原型如下:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
参数:
-
nfds
:这是一个整数值,指定要监控的文件描述符集合中最大文件描述符的值加1。这是因为文件描述符是从0开始编号的,所以nfds
实际上是文件描述符集合中最大索引值加1。 -
readfds
:指向一个fd_set
结构体的指针,该结构体包含了所有需要监控是否有数据可读的文件描述符。如果不需要监控读事件,可以传递NULL
。 -
writefds
:指向一个fd_set
结构体的指针,该结构体包含了所有需要监控是否有数据可写的文件描述符。如果不需要监控写事件,可以传递NULL
。 -
exceptfds
:指向一个fd_set
结构体的指针,该结构体包含了所有需要监控是否出现异常条件的文件描述符。如果不需要监控异常事件,可以传递NULL
。 -
timeout
:指向一个timeval
结构体的指针,用来设置 select()的等待时间。
参数 timeout 取值:
- nullptr:则表示 select()没有 timeout,select 将一直被阻塞,直到某个文件描述符上发生了事件;
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
- 特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回。
timeval结构
timeval 结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件
发生则函数返回,返回值为 0。
关于 fd_set 结构
测试代码:
#include <iostream>
#include <sys/select.h>
int main()
{
// fd_set;
std::cout << "fd_set位数容量: " << sizeof(fd_set) * 8 << std::endl;
return 0;
}
其实这个结构就是一个整数数组, 更严格的说, 是一个 "位图". 使用位图中对应的位来表示要监视的文件描述符.
提供了一组操作 fd_set 的接口, 来比较方便的操作位图.
// 用来清除描述词组 set 中相关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 的全部位
void FD_ZERO(fd_set *set);
函数返回值
- 执行成功则返回文件描述词状态已改变的个数
- 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回
- 当有错误发生时则返回-1,错误原因存于 errno,此时参数 readfds,writefds,exceptfds 和 timeout 的值变成不可预测。
错误值可能为:
- EBADF 文件描述词为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数 n 为负值。
- ENOMEM 核心内存不足
1.3、测试timeout
前面timeout参数分析出三种情况,下面编写代码进行基本的测试!
1.3.1、主函数
同前面一样,我们先写主函数再依次实现主函数中需要使用到的类型和函数,调用该可执行程序使用 文件名 + 端口号!
// ./select_server 8888
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
EnableScreen(); // 开启日志
std::unique_ptr<SelectServer> svr = std::make_unique<SelectServer>(port);
svr->InitServer();
svr->Loop();
return 0;
}
1.3.2、SelectServer类
SelectServer类 的成员需要用到端口号 和 套接字,成员函数暂时实现 InitServer() 和 Loop(),此处的套接字使用前面封装的Socket类,即需要使用到Socket.hpp,Log.hpp,LockGuard.hpp,InetAddr.hpp这四个文件!
基本结构
SelectServer类基本结构包括成员变量端口号和套接字,成员函数包括构造,析构,初始化和轮询函数!
class SelectServer
{
public:
SelectServer(uint16_t port);
void InitServer();
void Loop();
~SelectServer();
private:
uint16_t _port;
std::unique_ptr<Socket> _listensock;
};
构造析构函数
构造函数初始化端口号并根据端口号创建监听套接字对象,析构函数暂时不做处理!
SelectServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>())
{
_listensock->BuildListenSocket(_port);
}
~SelectServer()
{}
Loop()
Loop()函数此处主要用来测试timeout,也是后序使用的轮询函数!
void Loop()
{
while (true)
{
// 临时
fd_set rfds;
FD_ZERO(&rfds); // 清除 rfds 中相关fd的位
FD_SET(_listensock->Sockfd(), &rfds); // 将fd设置进rfds中
struct timeval timeout = {3, 0}; // 每隔3秒timeout一次
// 不能直接获取连接,listensock && accept 我们把他也看做IO类的函数,只关心新链接到来,等价于读事情就绪!
// _listensock->Accepter();
// int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, &timeout); // 临时(第一个参数),只关心读
int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case 0:
LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
break;
case -1:
LOG(ERROR, "select error\n");
break;
default:
LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
break;
}
}
}
InitServer()
InitServer()函数暂时不用填写代码,保证主函数把代码跑过即可!
运行结果
timeout参数填写实际数值时!
timeout参数填写nullptr时!
等待时间设置为30秒,并3秒执行一次任务!
1.4、处理事件
timeout参数测试成功之后,需要正式进入事件处理,select()函数的返回值不是0或者1就表示事件已经就绪,此处需要处理任务!
1.4.1、版本一
Loop()
Loop()进行轮询判断,如果事件就绪,就调用处理任务的函数!
void Loop()
{
while (true)
{
// 临时
fd_set rfds;
FD_ZERO(&rfds); // 清除 rfds 中相关fd的位
FD_SET(_listensock->Sockfd(), &rfds); // 将fd设置进rfds中
// 不能直接获取连接,listensock && accept 我们把他也看做IO类的函数,只关心新链接到来,等价于读事情就绪!
// _listensock->Accepter();
int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case 0:
LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
break;
case -1:
LOG(ERROR, "select error\n");
break;
default:
// 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
HandlerEvent(rfds);
break;
}
}
}
HandlerEvent()
HandlerEvent()版本一进行正式的任务处理,如果fd在读文件描述符集合中则获取链接并且获取链接成功,打印调试日志,否则直接返回!
void HandlerEvent(fd_set &rfds)
{
// 判断fd是否在rfds集合中
if(FD_ISSET(_listensock->Sockfd(), &rfds))
{
// 链接事件就绪,等价于读事件就绪
InetAddr addr;
int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
if(sockfd > 0)
{
LOG(DEBUG,"get a new link,client info %s:%d\n",addr.Ip().c_str(),addr.Port());
// 已经获得了一个新的sockfd
// 接下来我们可以读取?绝对不能读,条件不一定满足
// 谁最清楚底层fd的数据是否就绪了呢?通过select
// 想办法把新的fd添加给select,由select统一进行监管
// select 为什么等待的fd越来越多呢? 因为要把新的fd添加给select
}
else
{
return;
}
}
}
注意:此处为了更加原生,需要修改Socket类的Accepter()函数的返回值!
Socket类和TcpSocket类
class Socket
{
public:
virtual int Accepter(InetAddr *cliaddr) = 0; // 获取连接
}
class TcpSocket : public Socket
{
public:
// 获取连接
int Accepter(InetAddr *cliaddr) override
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 获取新连接
int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);
// 获取失败继续获取
if (sockfd < 0)
{
LOG(WARNING, "sccept reeor\n");
// return nullptr;
return -1;
}
*cliaddr = InetAddr(client);
LOG(INFO, "get a new link,client info: %s,sockfd:%d\n", cliaddr->AddrStr().c_str(), _sockfd);
// return std::make_shared<TcpSocket>(sockfd); // C++14
return sockfd;
}
}
运行结果
1.4.2、版本二
在轮询的过程中,可能会有fd是合法的,但是没有就绪,而这次执行完之后,读文件描述符集合会清空,可能会出现问题,因此需要增加一个数组(数组成员个数为fd_set集合的位数),来保存合法的fd!
SelectServer类结构
class SelectServer
{
const static int gnum = sizeof(fd_set) * 8;
const static int gdefaultfd = -1;
private:
uint16_t _port;
std::unique_ptr<Socket> _listensock;
// select要正常工作,需要借助一个辅助数组,来保存所有合法fd
int fd_array[gnum];
};
InitServer()
InitServer()函数将数组初始化为默认fd,并将listensockfd添加到数组的第一个元素!
void InitServer()
{
for (int i = 0; i < gnum; i++)
{
fd_array[i] = gdefaultfd;
}
fd_array[0] = _listensock->Sockfd(); // 默认直接添加sockfd到数组中
}
Loop()
Loop()函数主要分以下三步:
1、文件描述符初始化
2、合法的fd添加到rfds集合中 2.1、更新出最大的fd的值
3、检查读条件是否就绪
void Loop()
{
while (true)
{
// 1.文件描述符初始化
fd_set rfds;
FD_ZERO(&rfds); // 清除 rfds 中相关fd的位
int max_fd = gdefaultfd;
// 2.合法的fd添加到rfds集合中
for (int i = 0; i < gnum; i++)
{
if (fd_array[i] == gdefaultfd)
continue;
FD_SET(fd_array[i], &rfds); // 将fd设置进rfds中
// 2.1 更新出最大的fd的值
if (max_fd < fd_array[i])
{
max_fd = fd_array[i];
}
}
struct timeval timeout = {30, 0}; // 等待30秒
// 3.检查读条件是否就绪
int n = ::select(max_fd + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case 0:
LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
break;
case -1:
LOG(ERROR, "select error\n");
break;
default:
// 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
HandlerEvent(rfds);
PrintDebug();
sleep(1);
break;
}
}
}
HandlerEvent()
在执行HandlerEvent()函数之前,赋值数组中一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd,此处主要分以下两步:
- 1、判断fd是否合法
- 2、判断fd是否就绪
- 2.1、就绪是listensockfd
- 2.1.1、获取链接
- 2.1.2、获取链接成功将新的fd添加到数组中
- 2.1.3、数组满了,不能添加,需关闭sockfd
- 2.2、就绪是normal sockfd
- 2.2.1、直接读取fd中内容
就绪是listensockfd
// 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
void HandlerEvent(fd_set &rfds)
{
for (int i = 0; i < gnum; i++)
{
// 1.判断fd是否合法
if (fd_array[i] == gdefaultfd)
continue;
// 2.判断fd是否就绪
// fd一定是合法的fd
// 合法的fd不一定就绪,判断fd是否就绪?
if (FD_ISSET(fd_array[i], &rfds))
{
// 读事件就绪
// 2.1 listensockfd 2.2 normal sockfd
if (_listensock->Sockfd() == fd_array[i])
{
// listensockfd
// 链接事件就绪,等价于读事件就绪
InetAddr addr;
int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
if (sockfd > 0)
{
LOG(DEBUG, "get a new link,client info %s:%d\n", addr.Ip().c_str(), addr.Port());
// 已经获得了一个新的sockfd
// 接下来我们可以读取?绝对不能读,条件不一定满足
// 谁最清楚底层fd的数据是否就绪了呢?通过select
// 想办法把新的fd添加给select,由select统一进行监管--怎么做到?
// select 为什么等待的fd越来越多呢? 因为要把新的fd添加给select
// 只要将新的fd添加到fd_array中即可!
bool flag = false;
for (int pos = 1; pos < gnum; pos++)
{
if (fd_array[pos] == gdefaultfd)
{
flag = true;
fd_array[pos] = sockfd; // 把新的fd放入数组中
LOG(INFO,"add %d to fd_array success \n",sockfd);
break;
}
}
// 数组满了
if (!flag)
{
LOG(WARNING, "Server Is Full\n");
::close(sockfd);
}
}
}
else
{
// normal sockfd,正常的读写
}
}
}
}
PrintDebug()
PrintDebug()遍历辅助数组,将合法的文件描述符打印出来!
void PrintDebug()
{
std::cout << "fd list: ";
for(int i = 0; i < gnum; i++)
{
if(fd_array[i] == gdefaultfd) continue;
std::cout << fd_array[i] << " ";
}
std::cout << "\n";
}
测试listenfd
就绪是normal sockfd
// 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
void HandlerEvent(fd_set &rfds)
{
for (int i = 0; i < gnum; i++)
{
// 1.判断fd是否合法
if (fd_array[i] == gdefaultfd)
continue;
// 2.判断fd是否就绪
// fd一定是合法的fd
// 合法的fd不一定就绪,判断fd是否就绪?
if (FD_ISSET(fd_array[i], &rfds))
{
// 读事件就绪
// 2.1 listensockfd 2.2 normal sockfd
if (_listensock->Sockfd() == fd_array[i])
{
// listensockfd
// ...
}
else
{
// normal sockfd,正常的读写
char buffer[1024];
ssize_t n = ::recv(fd_array[i],buffer,sizeof(buffer) - 1,0); // 这里读取会阻塞?不会,因为读事件就绪了
if(n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string echo_str = "[server echo info]";
echo_str += buffer;
// 读写是空的 写就绪: 发送缓冲区有没有空间
::send(fd_array[i],echo_str.c_str(),echo_str.size(),0); // 可以直接发,临时方案
}
else if(n == 0)
{
LOG(INFO,"client quit...\n");
// 关闭fd
::close(fd_array[i]);
// select 不再关心这个fd了
fd_array[i] = gdefaultfd;
}
else
{
LOG(ERROR,"recv error\n");
// 关闭fd
::close(fd_array[i]);
// select 不再关心这个fd了
fd_array[i] = gdefaultfd;
}
}
}
}
}
上面的代码确实能够测试普通sockfd,但是浏览器不能显示内容,可以在发送给客户端时,增加一点显示效果!
// normal sockfd,正常的读写
char buffer[1024];
ssize_t n = ::recv(fd_array[i],buffer,sizeof(buffer) - 1,0); // 这里读取会阻塞?不会,因为读事件就绪了
if(n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
// 测试二
std::string content = "<html><body><h1>hello linux</h1></body></html>";
std::string echo_str = "HTTP/1.0 200 OK\r\n";
echo_str += "Content-Type: text/html\r\n";
echo_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";
echo_str += content;
::send(fd_array[i],echo_str.c_str(),echo_str.size(),0);
}
注意:此处的读写消息是不太严谨的,根据我们前面学过的协议,我们读取的内容是包含报头和有效数据的,但是此处重点是为了测试select,协议不是这里的重点!
1.4.3、版本三
前面两个版本已经完成对监听套接字和普通套接字的测试,但是结构看起来还是没有那么清晰,这个版本使用函数进行进一步封装!
HandlerEvent()
// 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
void HandlerEvent(fd_set &rfds)
{
// 事件派发
for (int i = 0; i < gnum; i++)
{
// 1.判断fd是否合法
if (fd_array[i] == gdefaultfd)
continue;
// 2.判断fd是否就绪
// fd一定是合法的fd
// 合法的fd不一定就绪,判断fd是否就绪?
if (FD_ISSET(fd_array[i], &rfds))
{
// 读事件就绪
// 2.1 listensockfd 2.2 normal sockfd
if (_listensock->Sockfd() == fd_array[i])
{
// listensockfd
// 链接事件就绪,等价于读事件就绪
Accepter();
}
else
{
// normal sockfd,正常的读写
HandlerIO(i);
}
}
}
}
Accepter()
// 处理新链接
void Accepter()
{
InetAddr addr;
int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
if (sockfd > 0)
{
LOG(DEBUG, "get a new link,client info %s:%d\n", addr.Ip().c_str(), addr.Port());
// 已经获得了一个新的sockfd
// 接下来我们可以读取?绝对不能读,条件不一定满足
// 谁最清楚底层fd的数据是否就绪了呢?通过select
// 想办法把新的fd添加给select,由select统一进行监管--怎么做到?
// select 为什么等待的fd越来越多呢? 因为要把新的fd添加给select
// 只要将新的fd添加到fd_array中即可!
bool flag = false;
for (int pos = 1; pos < gnum; pos++)
{
if (fd_array[pos] == gdefaultfd)
{
flag = true;
fd_array[pos] = sockfd; // 把新的fd放入数组中
LOG(INFO, "add %d to fd_array success \n", sockfd);
break;
}
}
// 数组满了
if (!flag)
{
LOG(WARNING, "Server Is Full\n");
::close(sockfd);
}
}
}
HandlerIO()
// 处理普通fd就绪
void HandlerIO(int i)
{
char buffer[1024];
ssize_t n = ::recv(fd_array[i], buffer, sizeof(buffer) - 1, 0); // 这里读取会阻塞?不会,因为读事件就绪了
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
// 测试一
// std::string echo_str = "[server echo info]";
// echo_str += buffer;
// 读写是空的 写就绪: 发送缓冲区有没有空间
// ::send(fd_array[i],echo_str.c_str(),echo_str.size(),0); // 可以直接发,临时方案
// 测试二
std::string content = "<html><body><h1>hello linux</h1></body></html>";
std::string echo_str = "HTTP/1.0 200 OK\r\n";
echo_str += "Content-Type: text/html\r\n";
echo_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";
echo_str += content;
::send(fd_array[i], echo_str.c_str(), echo_str.size(), 0);
}
else if (n == 0)
{
LOG(INFO, "client quit...\n");
// 关闭fd
::close(fd_array[i]);
// select 不再关心这个fd了
fd_array[i] = gdefaultfd;
}
else
{
LOG(ERROR, "recv error\n");
// 关闭fd
::close(fd_array[i]);
// select 不再关心这个fd了
fd_array[i] = gdefaultfd;
}
}
1.5、补充select的理解
1、select要正常工作,需要借助一个辅助数组,来保存所有合法fd!
2、每次使用都要重置!
3、就绪了,循环检测处理所有事件!
1.6、select 的特点
- 可监控的文件描述符个数取决于 sizeof(fd_set)的值. 博主这边服务器上sizeof(fd_set)=128,每 bit 表示一个文件描述符,则博主服务器上支持的最大文件描述符是 128*8=1024.
- 将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select监控集中的 fd,
- 一是用于在 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET 判断。
- 二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始select 前都要重新从 array 取得 fd 逐一加入(FD_ZERO 最先),扫描 array 的同时取得 fd 最大值 maxfd,用于 select 的第一个参数。
1.7、select 缺点
- 每次调用 select, 都需要手动设置 fd 集合, 从接口使用角度来说也非常不便.
- 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大(这个开销是无法避免的)
- 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
- select 支持的文件描述符数量太小.
1.8、完整代码
1.8.1、SelectServer.hpp
#pragma once
#include <iostream>
#include <sys/select.h>
#include "Socket.hpp"
#include "InetAddr.hpp"
using namespace socket_ns;
class SelectServer
{
const static int gnum = sizeof(fd_set) * 8;
const static int gdefaultfd = -1;
public:
SelectServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>())
{
_listensock->BuildListenSocket(_port);
}
// 版本一
// void InitServer()
// {}
// 版本一
// void HandlerEvent(fd_set &rfds)
// {
// // 判断fd是否在rfds集合中
// if(FD_ISSET(_listensock->Sockfd(), &rfds))
// {
// // 链接事件就绪,等价于读事件就绪
// InetAddr addr;
// int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
// if(sockfd > 0)
// {
// LOG(DEBUG,"get a new link,client info %s:%d\n",addr.Ip().c_str(),addr.Port());
// // 已经获得了一个新的sockfd
// // 接下来我们可以读取?绝对不能读,条件不一定满足
// // 谁最清楚底层fd的数据是否就绪了呢?通过select
// // 想办法把新的fd添加给select,由select统一进行监管
// // select 为什么等待的fd越来越多呢? 因为要把新的fd添加给select
// }
// else
// {
// return;
// }
// }
// }
// 版本一
// void Loop()
// {
// while (true)
// {
// // 临时
// fd_set rfds;
// FD_ZERO(&rfds); // 清除 rfds 中相关fd的位
// FD_SET(_listensock->Sockfd(), &rfds); // 将fd设置进rfds中
// struct timeval timeout = {3, 0}; // 每隔3秒timeout一次
// // 不能直接获取连接,listensock && accept 我们把他也看做IO类的函数,只关心新链接到来,等价于读事情就绪!
// // _listensock->Accepter();
// // int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, &timeout); // 临时(第一个参数),只关心读
// int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, nullptr);
// switch (n)
// {
// case 0:
// LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
// break;
// case -1:
// LOG(ERROR, "select error\n");
// break;
// default:
// // LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
// // 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
// LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
// HandlerEvent(rfds);
// // sleep(3);
// break;
// }
// }
// }
void InitServer()
{
for (int i = 0; i < gnum; i++)
{
fd_array[i] = gdefaultfd;
}
fd_array[0] = _listensock->Sockfd(); // 默认直接添加sockfd到数组中
}
// 处理新链接
void Accepter()
{
InetAddr addr;
int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
if (sockfd > 0)
{
LOG(DEBUG, "get a new link,client info %s:%d\n", addr.Ip().c_str(), addr.Port());
// 已经获得了一个新的sockfd
// 接下来我们可以读取?绝对不能读,条件不一定满足
// 谁最清楚底层fd的数据是否就绪了呢?通过select
// 想办法把新的fd添加给select,由select统一进行监管--怎么做到?
// select 为什么等待的fd越来越多呢? 因为要把新的fd添加给select
// 只要将新的fd添加到fd_array中即可!
bool flag = false;
for (int pos = 1; pos < gnum; pos++)
{
if (fd_array[pos] == gdefaultfd)
{
flag = true;
fd_array[pos] = sockfd; // 把新的fd放入数组中
LOG(INFO, "add %d to fd_array success \n", sockfd);
break;
}
}
// 数组满了
if (!flag)
{
LOG(WARNING, "Server Is Full\n");
::close(sockfd);
}
}
}
// 处理普通fd就绪
void HandlerIO(int i)
{
char buffer[1024];
ssize_t n = ::recv(fd_array[i], buffer, sizeof(buffer) - 1, 0); // 这里读取会阻塞?不会,因为读事件就绪了
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
// 测试一
// std::string echo_str = "[server echo info]";
// echo_str += buffer;
// 读写是空的 写就绪: 发送缓冲区有没有空间
// ::send(fd_array[i],echo_str.c_str(),echo_str.size(),0); // 可以直接发,临时方案
// 测试二
std::string content = "<html><body><h1>hello linux</h1></body></html>";
std::string echo_str = "HTTP/1.0 200 OK\r\n";
echo_str += "Content-Type: text/html\r\n";
echo_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";
echo_str += content;
::send(fd_array[i], echo_str.c_str(), echo_str.size(), 0);
}
else if (n == 0)
{
LOG(INFO, "client quit...\n");
// 关闭fd
::close(fd_array[i]);
// select 不再关心这个fd了
fd_array[i] = gdefaultfd;
}
else
{
LOG(ERROR, "recv error\n");
// 关闭fd
::close(fd_array[i]);
// select 不再关心这个fd了
fd_array[i] = gdefaultfd;
}
}
// 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
void HandlerEvent(fd_set &rfds)
{
// 事件派发
for (int i = 0; i < gnum; i++)
{
// 1.判断fd是否合法
if (fd_array[i] == gdefaultfd)
continue;
// 2.判断fd是否就绪
// fd一定是合法的fd
// 合法的fd不一定就绪,判断fd是否就绪?
if (FD_ISSET(fd_array[i], &rfds))
{
// 读事件就绪
// 2.1 listensockfd 2.2 normal sockfd
if (_listensock->Sockfd() == fd_array[i])
{
// listensockfd
// 链接事件就绪,等价于读事件就绪
Accepter();
}
else
{
// normal sockfd,正常的读写
HandlerIO(i);
}
}
}
}
void Loop()
{
while (true)
{
// 1.文件描述符初始化
fd_set rfds;
FD_ZERO(&rfds); // 清除 rfds 中相关fd的位
int max_fd = gdefaultfd;
// 2.合法的fd添加到rfds集合中
for (int i = 0; i < gnum; i++)
{
if (fd_array[i] == gdefaultfd)
continue;
FD_SET(fd_array[i], &rfds); // 将fd设置进rfds中
// 2.1 更新出最大的fd的值
if (max_fd < fd_array[i])
{
max_fd = fd_array[i];
}
}
struct timeval timeout = {30, 0}; // 等待30秒
// 3.检查读条件是否就绪
int n = ::select(max_fd + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case 0:
LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
break;
case -1:
LOG(ERROR, "select error\n");
break;
default:
// 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
HandlerEvent(rfds);
PrintDebug();
// sleep(1);
break;
}
}
}
void PrintDebug()
{
std::cout << "fd list: ";
for (int i = 0; i < gnum; i++)
{
if (fd_array[i] == gdefaultfd)
continue;
std::cout << fd_array[i] << " ";
}
std::cout << "\n";
}
~SelectServer()
{
}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensock;
// select要正常工作,需要借助一个辅助数组,来保存所有合法fd
int fd_array[gnum];
};
1.8.2、Main.cc
#include <iostream>
#include <memory>
#include "SelectServer.hpp"
// ./select_server 8888
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
EnableScreen(); // 开启日志
std::unique_ptr<SelectServer> svr = std::make_unique<SelectServer>(port);
svr->InitServer();
svr->Loop();
return 0;
}
更多推荐
所有评论(0)