【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.mfbz.cn/a/174.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MongoDB的下载安装与启动

MongoDB的下载安装与启动&#xff0c; 一、MongoDB下载安装 1. 官网下载 打开官网&#xff1a;https://www.mongodb.com/try/download/community选择&#xff1a; 版本&#xff08;Version&#xff09;&#xff1a;选最新版或者根据需要选旧版。平台&#xff08;OS&#xff0…

Docker 容器虚拟化技术和自动化部署

Docker 容器虚拟化技术和自动化部署 一、Docker 核心组件1.1 Docker 引擎1.2 Docker 镜像1.3 Docker 容器1.4 Docker 仓库 二、Docker 环境安装清华镜像安装 三、Docker 基本操作3.1 镜像管理3.1.1 查看本地镜像 docker images3.1.2 添加镜像标签 docker tag3.1.3 查看镜像信息…

MaxScript二维图形布尔(并)运算

以下脚本演示了在 3ds Max 中使用 MaxScript 进行二维图形的布尔并运算&#xff08;Union&#xff09;。该脚本创建两个圆形&#xff08;obj_a 和 obj_b&#xff09;&#xff0c;并通过 ShapeBooleanObject 将它们合并为一个新的样条线对象 obj_boolean。运行此脚本后&#xff…

【Linux实践系列】:进程间通信:万字详解命名管道实现通信

&#x1f525; 本文专栏&#xff1a;Linux Linux实践项目 &#x1f338;作者主页&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客励志语录&#xff1a; 与其等待完美的风&#xff0c;不如学会在逆风中调整帆的角度——所有伟大航程都始于此刻出发的勇气 ★★★ 本文前置知…

【数据结构】二叉搜索树

数据结构系列五&#xff1a;Map与Set(三) 二叉搜索树 一、搜索原理&#xff08;有序维护&#xff09; 1.插入 2.删除 2.1单子树节点 2.2双子树节点 二、搜索性能 1.排成完全二叉树 2.排成链表 二叉搜索树 每个节点都经过 整体搜索比较排 维护组成的有序树是二叉排序树…

【无标题】

一、Matplotlib 基础认知 功能特性&#xff1a;是 Python 强大的绘图库&#xff0c;能将数据以多样化的图表形式呈现&#xff0c;涵盖静态、动态和交互式图表&#xff0c;支持多种输出格式&#xff0c;满足不同场景下的数据可视化需求。 二Matplotlib Pyplott 函数绘图技巧&a…

wangEditor 移除表情包菜单项的配置步骤‌

wangEditor 移除表情包菜单项的配置步骤‌ 1. 确认表情包菜单项的 Key‌‌2. 配置 excludeKeys 排除表情包‌‌3. 验证配置生效‌注意事项‌ 1. 确认表情包菜单项的 Key‌ ‌默认 Key 为 emotion‌&#xff1a;根据工具栏默认配置&#xff0c;表情包菜单项的 Key 为 emotion。…

正则表达式详解

文章目录 1. 正则表达式基础1.1 什么是正则表达式1.2 为什么需要学习正则表达式1.3 Java中的正则表达式支持2. 正则表达式语法2.1 基本匹配2.2 元字符2.2.1 常用元字符2.2.2 转义字符2.2.3 字符类2.2.4 预定义字符类2.2.5 量词2.3 贪婪与非贪婪匹配2.4 分组与捕获2.4.1 命名分组…

MLLM之Bench:LEGO-Puzzles的简介、安装和使用方法、案例应用之详细攻略

MLLM之Bench&#xff1a;LEGO-Puzzles的简介、安装和使用方法、案例应用之详细攻略 目录 LEGO-Puzzles的简介 1、LEGO-Puzzles的特点 LEGO-Puzzles的安装和使用方法 1、安装 步骤 0&#xff1a;安装 VLMEvalKit 步骤 1&#xff1a;设置 API 密钥&#xff08;可选&#xf…

Java大厂面试突击:从Spring Boot自动配置到Kafka分区策略实战解析

第一轮核心知识 面试官&#xff1a;请解释Spring Boot中自动配置的工作原理并演示如何自定义一个ConfigurationProperties组件&#xff1f; xbhog&#xff1a;自动配置通过EnableAutoConfiguration注解触发&#xff0c;结合当前环境判断&#xff08;如是否检测到MyBatis依赖&…

STM32 定时器TIM

定时器基础知识 定时器就是用来定时的机器&#xff0c;是存在于STM32单片机中的一个外设。STM32总共有8个定时器&#xff0c;分别是2个高级定时器(TIM1、TIM8)&#xff0c;4个通用定时器(TIM2、TIM3、TIM4、TIM5)和2个基本定时器(TIM6、TIM7)&#xff0c;如下图所示: STM32F1…

NEPCON China 2025 | 具身智能时代来临,灵途科技助力人形机器人“感知升级”

4月22日至24日&#xff0c;生产设备暨微电子工业展&#xff08;NEPCON China 2025&#xff09;在上海如期开展。本届展会重磅推出“人形机器人拆解展区”&#xff0c;汇聚35家具身智能产业链领军企业&#xff0c;围绕机械结构、传感器布局、驱动系统与AI算法的落地应用&#xf…