当前位置: 首页 > news >正文

【Linux网络】深入解析I/O多路转接 - Select

📢博客主页:https://blog.csdn.net/2301_779549673
📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨

在这里插入图片描述

在这里插入图片描述

文章目录

  • 🏳️‍🌈一、什么是select
  • 🏳️‍🌈二、select 函数原型
  • 🏳️‍🌈三、测试 timeout
    • 3.1 SelectServer 类
      • 3.1.1 基本结构
      • 3.1.2 析构构造函数
      • 3.1.3 Loop()
      • 3.1.4 InitServer()
    • 3.2 主函数
    • 3.3 测试代码
  • 🏳️‍🌈四、Handler 处理函数 - 版本一
  • 🏳️‍🌈五、Handler 处理函数 - 版本二
    • 5.1 基本结构
    • 5.2 初始化函数
    • 5.3 Loop() 函数
    • 5.4 HandlerEvent(() 函数
    • 5.5 PrintDebug()
    • 5.6 测试
  • 🏳️‍🌈六、Handler 处理函数 - 版本三
  • 🏳️‍🌈七、select 的特点
  • 👥总结


11111111
11111111
11111111
11111111
**** 11111111

🏳️‍🌈一、什么是select

系统提供select 函数来实现多路复用输入/输出模型

  • select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

定位:只负责进行等,不进行拷贝!
作用:为了等待多个fd,等待fd上面的新事件就绪,通知程序员,事件已经就绪,可以进行IO拷贝了!

🏳️‍🌈二、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 结构

  • 描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件,发生则函数返回,返回值为 0。
struct timeval
{
#ifdef __USE_TIME_BITS64__time64_t tv_sec;		/* Seconds.  */__suseconds64_t tv_usec;	/* Microseconds.  */
#else__time_t tv_sec;		/* Seconds.  */__suseconds_t tv_usec;	/* Microseconds.  */
#endif
};

fd_set 结构

  • fds_bits 或 __fds_bits​:一个 __fd_mask 类型的数组,用于存储文件描述符的位掩码
  • __fd_mask​:通常是 unsigned long,表示一个位掩码单元。每个单元可存储 __NFDBITS 个文件描述符状态(如 64 位系统为 64 位)。
  • __FD_SETSIZE​:定义 fd_set 支持的最大文件描述符数量(默认通常为 1024)。
  • __NFDBITS​ :单个 __fd_mask 元素的位数(如 sizeof(__fd_mask) * 8)。
typedef struct {
#ifdef __USE_XOPEN__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
#else__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
#endif
} fd_set;

其实这个结构就是一个整数数组, 更严格的说, 是一个 “位图”。使用位图中对应的位来表示要监视的文件描述符
在这里插入图片描述
函数返回值

  • 执行成功返回文件描述词状态已改变的个数
  • 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回
  • 当有错误发生时则返回-1错误原因存于 errno,此时参数 readfds,writefds,exceptfds 和 timeout 的值变成不可预测。

错误值可能为:

  • EBADF 文件描述词为无效的或该文件已关闭
  • EINTR 此调用被信号所中断
  • EINVAL 参数 n 为负值。
  • ENOMEM 核心内存不足

🏳️‍🌈三、测试 timeout

前面 timeout 参数分析出三种情况,下面编写代码进行基本的测试!

3.1 SelectServer 类

SelectServer类 的成员需要用到 端口号套接字,成员函数暂时实现 InitServer()Loop(),此处的套接字使用前面封装的Socket类

3.1.1 基本结构

#pragma once#include <iostream>
#include "Socket.hpp"using namespace SocketModule;class SelectServer{public:SelectServer(uint16_t port);void InitServer();void Loop(); ~SelectServer();private:uint16_t _port;SockPtr _listensock;
};

3.1.2 析构构造函数

构造函数初始化端口号并根据端口号创建监听套接字对象析构函数暂时不做处理!

SelectServer(uint16_t port): _port(port), _listensock(std::make_shared<TcpSocket>()) {_listensock->BuildListenSocket(_port);
}
~SelectServer();

3.1.3 Loop()

Loop()函数此处主要用来测试timeout,也是后序使用的轮询函数!

void Loop() {while (true) {// 临时fd_set rfds; // 清除 rfds 中相关的fd的位FD_ZERO(&rfds);FD_SET(_listensock->Sockfd(), &rfds);struct timeval timeout = {3, 0};int n =::select(_listensock->Sockfd() + 1, &rfds, NULL, NULL, &timeout);switch (n) {case 0:LOG(LogLevel::DEBUG) << "time out " << timeout.tv_sec << "s";break;case -1:LOG(LogLevel::ERROR) << "select error";break;default:LOG(LogLevel::INFO) << "haved event ready, " << n;break;}}
}

3.1.4 InitServer()

InitServer()函数暂时不用填写代码,保证主函数把代码跑过即可

3.2 主函数

输入端口号运行即可

int main(int argc, char* argv[]){if(!argc != 2){std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl; }uint16_t port = std::stoi(argv[1]);std::unique_ptr<SelectServer> svr = std::make_unique<SelectServer>(port);svr->InitServer();svr->Loop();return 0;
}

3.3 测试代码

根据左边的日志,我们会发现平均每 3 s会弹出一次超时

在这里插入图片描述

我们修改一下监听的情况,每3s 监听一次,并且超时为 30s

LOG(LogLevel::INFO) << "haved event ready, " << n;
LOG(LogLevel::DEBUG) << "time out " << timeout.tv_sec << "." << timeout.tv_usec<< "s";
sleep(3);

我们使用 telnet 模拟访问服务端, 每 3s 弹出一次套接字已就绪的字样

在这里插入图片描述

🏳️‍🌈四、Handler 处理函数 - 版本一

timeout 参数测试成功之后,需要正式进入事件处理select() 函数的返回值不是0或者1就表示事件已经就绪,此处需要处理任务!

我们这里不进行计时即 select最后一个参数设为 NULL

void Loop() {while (true) {fd_set rfds; // 清除 rfds 中相关的fd的位FD_ZERO(&rfds);FD_SET(_listensock->Sockfd(), &rfds);int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, nullptr);switch (n) {// case 0: 因为不会超时所有case 0 的情况不存在case -1:LOG(LogLevel::ERROR) << "select error";break;default:LOG(LogLevel::INFO) << "haved event ready, " << n;break;}}
}

HandlerEvent() 版本一进行正式的任务处理,如果fd在读文件描述符集合中则获取链接并且获取链接成功,打印调试日志,否则直接返回!

void HandlerEvent(fd_set& rfds) {if (FD_ISSET(_listensock->Sockfd(), &rfds)) {// 连接事件就绪,等价于读事件就绪InetAddr addr;int sockfd = _listensock->Accepter(&addr);if (sockfd > 0) {LOG(LogLevel::DEBUG)<< "get a new connection from " << addr.AddrStr().c_str()<< ", sockfd : " << sockfd;} elsereturn;}
}

这里还需要更改一下 socket.hpp 的 Accepter 函数,因为我们返回的是一个 int 类型

int Accepter(InetAddr* cli) override {struct sockaddr_in client;socklen_t clientlen = sizeof(client);// accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)// 返回一个新的套接字,该套接字与调用进程间接地建立了连接。int sockfd = ::accept(_sockfd, CONV(&client), &clientlen);if (sockfd < 0) {LOG(LogLevel::ERROR) << "accept socket error";return -1;}*cli = InetAddr(client);LOG(LogLevel::DEBUG) << "get a new connection from "<< cli->AddrStr().c_str() << ", sockfd : " << sockfd;return sockfd;
}

在这里插入图片描述

🏳️‍🌈五、Handler 处理函数 - 版本二

在轮询的过程中,可能会有fd是合法的,但是没有就绪,而这次执行完之后,读文件描述符集合会清空,可能会出现问题,因此需要增加一个数组(数组成员个数为fd_set集合的位数),来保存合法的fd!

5.1 基本结构

  • 我们需要添加一个能够存储文件描述符的数组
  • 同时要设置最大监听数量,以及默认描述符
class SelectServer {const static int gnum = sizeof(fd_set) * 8;const static int gdefaultfd = -1;private:uint16_t _port;SockPtr _listensock;// select要正常工作,需要借助一个辅助数组,来保存所有合法fdint fd_array[gnum];
};
—

5.2 初始化函数

  • 这里需要初始化文件描述符数值中的所有文件描述符,并且要设置监听套接字
void InitServer() {for (int i = 0; i < gnum; ++i) {fd_array[i] = gdefaultfd;}fd_array[0] = _listensock->Sockfd();
}

5.3 Loop() 函数

Loop()函数主要分以下三步:

  1. 文件描述符初始化

  2. 合法的fd添加到rfds集合中
    2.1. 更新出最大的fd的值

  3. 检查读条件是否就绪

void Loop() {while (true) {// 1. 文件描述符初始化fd_set rfds; // 清除 rfds 中相关的fd的位FD_ZERO(&rfds);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);if (fd_array[i] > max_fd)max_fd = fd_array[i];}struct timeval timeout = {30, 0};// 3. 检查都条件是否就绪int n = ::select(max_fd + 1, &rfds, nullptr, nullptr, &timeout);switch (n) {case 0:LOG(LogLevel::DEBUG) << "time out " << timeout.tv_sec << "."<< timeout.tv_usec << "s";break;case -1:LOG(LogLevel::ERROR) << "select error";break;default:// 如果事件就绪,但是不做处理,select 就会一直通知,直到处理LOG(LogLevel::DEBUG) << "time remain " << timeout.tv_sec << "."<< timeout.tv_usec << "s";LOG(LogLevel::INFO) << "haved event ready, " << n;HandlerEvent(rfds);sleep(3);break;}}
}

5.4 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中内容
void HandlerEvent(fd_set& rfds) {// version - 0// if(FD_ISSET(_listensock->Sockfd(), &rfds)){//     // 连接事件就绪,等价于读事件就绪//     InetAddr addr;//     int sockfd = _listensock->Accepter(&addr);//     if(sockfd > 0){//         LOG(LogLevel::DEBUG) << "get a new connection from " <<//         addr.AddrStr().c_str() << ", sockfd : " << sockfd;//     } else return;// }// version - 1for (int i = 0; i < gnum; ++i) {// 1. 判断 fd 是否合法if (fd_array[i] == gdefaultfd)continue;// 2. 判断 fd 是否就绪if (FD_ISSET(fd_array[i], &rfds)) {// 判断是 listensocketif (_listensock->Sockfd() == fd_array[i]) {InetAddr client;int sockfd = _listensock->Accepter(&client);if (sockfd > 0) {LOG(LogLevel::INFO)<< "get a new connection from "<< client.AddrStr().c_str() << ", sockfd : " << sockfd;// 将获取成功的新的 fd 添加到 fd_array 中bool flag = false;for (int pos = 1; pos < gnum; ++pos) {if (fd_array[pos] == gdefaultfd) {flag = true;fd_array[pos] = sockfd;LOG(LogLevel::DEBUG)<< "add new sockfd " << sockfd<< " to fd_array[" << pos << "]";break;}if (!flag) {LOG(LogLevel::ERROR)<< "fd_array is full, can't add new sockfd "<< sockfd;::close(sockfd);}}}}// 判断是其他 socketelse {// 正常读写}}}
}

5.5 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 << std::endl;
}

5.6 测试

在这里插入图片描述

🏳️‍🌈六、Handler 处理函数 - 版本三

前面两个版本已经完成对监听套接字和普通套接字的测试,但是结构看起来还是没有那么清晰,这个版本使用函数进行进一步封装!

void HandlerEvent(fd_set& rfds) {// version - 0// if(FD_ISSET(_listensock->Sockfd(), &rfds)){//     // 连接事件就绪,等价于读事件就绪//     InetAddr addr;//     int sockfd = _listensock->Accepter(&addr);//     if(sockfd > 0){//         LOG(LogLevel::DEBUG) << "get a new connection from " <<//         addr.AddrStr().c_str() << ", sockfd : " << sockfd;//     } else return;// }// version - 1for (int i = 0; i < gnum; ++i) {// 1. 判断 fd 是否合法if (fd_array[i] == gdefaultfd)continue;// 2. 判断 fd 是否就绪if (FD_ISSET(fd_array[i], &rfds)) {// 判断是 listensocketif (_listensock->Sockfd() == fd_array[i]) {HandlerNewConnection();}// 判断是其他 socketelse {// 正常读写HandlerIO(i);}}}
}
void HandlerNewConnection() {InetAddr client;int sockfd = _listensock->Accepter(&client);if (sockfd > 0) {LOG(LogLevel::INFO)<< "get a new connection from " << client.AddrStr().c_str()<< ", sockfd : " << sockfd;// 将获取成功的新的 fd 添加到 fd_array 中bool flag = false;for (int pos = 1; pos < gnum; ++pos) {if (fd_array[pos] == gdefaultfd) {flag = true;fd_array[pos] = sockfd;LOG(LogLevel::DEBUG) << "add new sockfd " << sockfd<< " to fd_array[" << pos << "]";break;}}if (!flag) {LOG(LogLevel::ERROR)<< "fd_array is full, can't add new sockfd " << sockfd;::close(sockfd);}}
}
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 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(LogLevel::INFO)<< "client closed connection, sockfd: " << fd_array[i];::close(fd_array[i]);fd_array[i] = gdefaultfd; // 清理数组中的fd} else {                      // recv 错误(如连接重置)LOG(LogLevel::ERROR) << "recv error, sockfd: " << fd_array[i];::close(fd_array[i]);fd_array[i] = gdefaultfd;}
}

在这里插入图片描述

🏳️‍🌈七、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 的第一个参数。

缺点

  • 每次调用 select, 都需要手动设置 fd 集合, 从接口使用角度来说也非常不便.
  • 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大(这个开销是无法避免的)
  • 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
  • select 支持的文件描述符数量太小.

👥总结

本篇博文对 【Linux网络】深入解析I/O多路转接 - Select 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

http://www.xdnf.cn/news/205327.html

相关文章:

  • 探索PyTorch中的空间与通道双重注意力机制:实现concise的scSE模块
  • HotSpot的算法细节
  • 数据库原理及应用mysql版陈业斌实验三
  • IOS 国际化词条 Python3 脚本
  • tarjan缩点+强联通分量
  • 【无报错,亲测有效】如何在Windows和Linux系统中查看MySQL版本
  • 0429/AIGC model mark Blog
  • Ansible安装配置
  • Open WebUI 设置通过硅基流动访问 DeepSeek v3 教程​
  • Hadoop 和 Spark 生态系统中的核心组件
  • AIGC(生成式AI)技术全景图:从文本到图像的革命
  • 技术白皮书:Oracle GoldenGate 优势
  • [特殊字符]OCR,给交通领域开了“外挂”?
  • Kivy使用uniad原生sdk 1,构建项目与选型
  • IDEA新版本Local Changes
  • Android 实现一个隐私弹窗
  • GitHub Actions 自动化部署 Azure Container App 全流程指南
  • 257. 二叉树的所有路径
  • 【Linux】Linux内核模块开发
  • 深入蜂窝物联网 第四章 Cat-1 与 5G RedCap:带宽、低时延与未来趋势
  • redis 有序集合zrange和zrangebyscore的区别
  • Android ndk 编译opencv后部分接口std::__ndk1与项目std::__1不匹配
  • 【LeetCode 热题 100】矩阵置零 / 螺旋矩阵 / 旋转图像 / 搜索二维矩阵 II
  • 【Vagrant+VirtualBox创建自动化虚拟环境】Ansible测试Playbook
  • springboot 框架把 resources下的zip压缩包, springboot 项目启动后解压到项目根目录工具类
  • DeepSeek主动学习系统:低质量数据炼金术的工程化实践
  • runpod team 怎么设置自己的ssh key呢?
  • LLamaFactory如何在Windows系统下部署安装训练(保姆级教程)
  • 松下机器人快速入门指南(2025年更新版)
  • Kotlin-高阶函数,Lambda表达式,内联函数