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

【计算机网络】非阻塞IO——select实现多路转接

🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:计算机网络
🌹往期回顾🌹:【计算机网络】NAT、代理服务器、内网穿透、内网打洞、局域网中交换机
🔖流水不争,争的是滔滔不息


  • 一、非阻塞IO与多路转接
  • 二、select实现多路转接
    • select函数
    • 读就绪 写就绪
    • 非阻塞服务器 代码实现
      • 辅助数组
      • 启动方法
      • 事件派发
      • accept套接字 添加到辅助数组
      • 读数据
    • 源码:[非阻塞http服务](https://gitee.com/hanbuxuan/linux-warehouse/tree/master/Project/Network/Non-blocking%20IO/Select%20Server)
  • 三、select 的缺点

一、非阻塞IO与多路转接

非阻塞IO

IO分为阻塞IO和非阻塞IO,IO=拷贝+等的时间。非阻塞IO允许程序发起IO操作(网络读写等)后立即返回,不等待操作完成,通过回轮询或事件通知处理结果。阻塞IO会因为条件不满足卡住,直到条件就绪。非阻塞IO的关键是程序可以在IO操作未完成时继续执行其他任务。

多路转接

多路转接是实现非阻塞IO的一种高效的方式,通过监控多个IO描述符,通知程序哪些描述符已经就绪(可读、可写),让程序按需处理,避免阻塞。
路转接是一种技术,允许程序同时监控多个输入/输出(IO)事件(如网络套接字、文件描述符),当某些事件就绪(如数据可读、可写)时通知程序处理,而无需为每个IO操作分配单独的线程或阻塞等待。
实现多路转接可以用select、poll、epoll、等方式监控多个IO描述符的事件,当某个描述符就绪(网路),程序收到通知,处理对应的事件。

用多路转接,可以极大提高效率,在单个线程中处理多个并发IO操作,避免阻塞等待或者每个连接都创建线程。用多路转接实现非阻塞IO,程序无需等待某个IO操作完成,可以立即处理其他就绪的IO事件。

二、select实现多路转接

在这里插入图片描述
select负责一件事,就是等一次可以等待多个fd。等待多个fd,有任意一个或多个fd的事件就绪就会通知上层,告诉程序的调用发,哪些fd可以io了。select是等待多个fd的一种就绪事件通知机制。

select函数

#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

select的返回值:大于0是几就表示有几个fd就绪了。等于0超时了,在timeout内没有fd就绪。小于0,select报错。

参数

  1. nfds 是需要监视的最大的文件描述符值+1。

  2. 数 timeout 为结构 timeval, 用来设置 select()的等待时间。

  • NULL:表示select没有timeout,select一直被阻塞,直到某个文件描述符发生了事件。
  • 0:select 非阻塞,立即返回,检查当前是否有就绪事件。
  • 设置事件:timeout时间内,阻塞等待,超时就非阻塞返回。
  1. readfds、writefds、excepfds、分别是关系的读事件、写事件、异常事件。
  • 这三个参数的结构是一个整数数组,更严格来说是一个“位图”,使用位图中的位来表示监视的文件。
  • 这是三个参数,是输入输出型参数,比如说读事件,输入的时候是从用户态到内核态输出的时候是从内核态到用户态。输入的时候告诉内核,你要帮我关心哪些fd上的哪些事件,比特位的位置表示fd编号,比特位的内容,表示是否关心。返回的时候内核告诉用户,你让我关心的哪些fd上面的读事件已经就绪了,比特位的位置,表示fd的编号,比特位的内容,表示是否就绪。
  • 因为这个位图是输入输出型的,所以未来这个位图一定会被频繁变更。位图中有多少个比特位就决定了select最多关心多少个fd。

读就绪 写就绪

读就绪的含义
在 select 或 epoll 等多路转接机制中,“读就绪”表示文件描述符(如网络套接字、文件句柄)已准备好进行读取操作,即:
数据已到达,可以调用 read 或 recv 读取而不阻塞。
对于网络套接字,可能表示接收缓冲区有数据、连接建立完成、或连接关闭。

具体事件(以网络套接字为例):

  • 数据到达:远程端发送数据到套接字的接收缓冲区(如客户端收到服务器响应)。
  • 连接建立:监听套接字(server socket)收到新连接请求(如 accept 可立即执行)。
  • 连接关闭:对端关闭连接,recv 返回0(EOF,端点文件)。
  • 错误:套接字发生错误(如 ECONNRESET),可能通过读取检测。

写就绪的含义
定义:
在多路转接机制中,“写就绪”表示文件描述符(如网络套接字、文件句柄)已准备好进行写入操作,即:
数据可以写入而不阻塞,例如套接字的发送缓冲区有足够空间。
对于网络套接字,通常意味着可以调用 send 或 write 发送数据。

触发条件(以网络套接字为例):

  • 发送缓冲区有空间:套接字的发送缓冲区(TCP/UDP buffer)未满,可以写入数据。
  • 连接建立完成:客户端套接字完成连接(如 connect 后的TCP三次握手),可以发送数据。
  • 错误状态:套接字发生错误(如 ECONNREFUSED),可能通过写操作检测。

非阻塞服务器 代码实现

# pragma once#include "Common.hpp"
#include "Socket.hpp"
#include "Log.hpp"using namespace std;
using namespace LogModule;
using namespace SocketModule;class Selectserver
{
const static int size=8*sizeof(fd_set);
const static int defaultfd=-1;public:Selectserver(int port):_isrunning(false),_listensockfd(make_unique<TcpSocket>()){_listensockfd->BuildTcpSocketServer(port);for(int i=0;i<size;i++){_fd_array[i]=defaultfd;}_fd_array[0]=_listensockfd->FD();} void Start(){_isrunning=true;while(_isrunning){fd_set rfds; //定义fds集合FD_ZERO(&rfds);//清空集合int maxfd=defaultfd;for(int i=0;i<size;i++){if(_fd_array[i]==defaultfd)    //遍历辅助数组,跳过-1的continue;FD_SET(_fd_array[i],&rfds);    //每次进来都要重新设置rfdsif(maxfd<_fd_array[i])      //最大fd小于遍历到的fd更新{maxfd=_fd_array[i];} }PrintFd();int n=select(maxfd+1,&rfds,nullptr,nullptr,nullptr); //多路转接只关心读事件switch (n){case -1:LOG(LogLevel::ERROR)<<"select out";break;case 0:LOG(LogLevel::WARNING)<<"time out";default:LOG(LogLevel::INFO)<<"事件就绪";Dispatcher(rfds);   //派发break;}}}void Dispatcher(fd_set& rfds)   //事件派发{for(int i=0;i<size;i++){if(_fd_array[i]==defaultfd) continue;               //跳过没有fd的位置if(FD_ISSET(_fd_array[i],&rfds)){if(_fd_array[i]==_listensockfd->FD()) //是listen套接字{Accepter();}else        //accept套接字 干活{Recv(_fd_array[i],i);}}     }}void Accepter() //accept套接字{InetAddr client;int sockfd=_listensockfd->AcceptOrDie(&client);//accept了干活fdLOG(LogLevel::DEBUG)<<"accept a new client"<<client.StringAddr();int pos=0;for(;pos<size;pos++){if(_fd_array[pos]==defaultfd)   //遍历辅助数组,遇见-1跳出break;}if(pos==size){LOG(LogLevel::WARNING)<<"server full";close(sockfd);} else{_fd_array[pos]=sockfd;}}void Recv(int sockfd,int pos){char buffer[1024];ssize_t n=recv(sockfd,buffer,sizeof(buffer)-1,0);   //收信息if(n>0){buffer[n] = 0;cout << "client say@ "<< buffer <<endl;}else if(n==0)   //客户端退出{LOG(LogLevel::INFO)<<"client quit";_fd_array[pos]=defaultfd;close(sockfd);}else        //出现错误 异常{LOG(LogLevel::FATAL)<<"recv error";_fd_array[pos]=defaultfd;close(sockfd);}}void PrintFd(){cout << "_fd_array[]: ";for (int i = 0; i < size; i++){if (_fd_array[i] == defaultfd)continue;cout << _fd_array[i] << " ";}cout << "\r\n";}~Selectserver(){}
private:unique_ptr<Socket> _listensockfd;int _fd_array[size];    //辅助数组bool _isrunning;
};

辅助数组

读事件的集合是个位图结构,每次调用select都会修改之前的结构,是有破坏性的。

保留“原始的 fd 集合”,因为 select() 会修改你传进去的集合。select() 会清除掉那些没就绪的 fd!如果你不提前备份,就会把原来的 fd 集合搞没,下一轮 select 就找不到了。

比如说读事件,一开机读到的是listen套接字,下一次就是accept套接字,每次调用 select 时只保留最新的 connfd(accept 套接字),而忘了继续监听 listenfd,那它就“没了”——不会再监视新的连接了。


上面的代码是基于之前写的TCP阻塞的服务器改的非阻塞服务器,没写应用层代码。

    Selectserver(int port):_isrunning(false),_listensockfd(make_unique<TcpSocket>()){_listensockfd->BuildTcpSocketServer(port);for(int i=0;i<size;i++){_fd_array[i]=defaultfd;}_fd_array[0]=_listensockfd->FD();} 

构造服务器,创建辅助数组数组内数据全部设置为-1,把listen套接字放到这个辅助数组中。


启动方法

void Start(){_isrunning=true;while(_isrunning){fd_set rfds; //定义fds集合FD_ZERO(&rfds);//清空集合int maxfd=defaultfd;for(int i=0;i<size;i++){if(_fd_array[i]==defaultfd)    //遍历辅助数组,跳过-1的continue;FD_SET(_fd_array[i],&rfds);    //每次进来都要重新设置rfdsif(maxfd<_fd_array[i])      //最大fd小于遍历到的fd更新{maxfd=_fd_array[i];} }PrintFd();int n=select(maxfd+1,&rfds,nullptr,nullptr,nullptr); //多路转接只关心读事件switch (n){case -1:LOG(LogLevel::ERROR)<<"select out";break;case 0:LOG(LogLevel::WARNING)<<"time out";default:LOG(LogLevel::INFO)<<"事件就绪";Dispatcher(rfds);   //派发break;}}}

启动服务器,一直进行循环,定义fds集合清空集合定义最大的文件描述符。for循环遍历辅助数组,如果不是规定的套接字跳过。把辅助数组中的套接字重新设置进fds集合中,标记最大的文件描述符。
select多路转接(这里只关心读事件),对返回值进行判断,返回值大于0对事件进行派发(就是读事件中的文件描述符是listen套接字的去干listen套接字该干的事,是accept套接字去干accept套接字该干的事)


事件派发

void Dispatcher(fd_set& rfds)   //事件派发{for(int i=0;i<size;i++){if(_fd_array[i]==defaultfd) continue;               //跳过没有fd的位置if(FD_ISSET(_fd_array[i],&rfds)){if(_fd_array[i]==_listensockfd->FD()) //是listen套接字{Accepter();}else        //accept套接字 干活{Recv(_fd_array[i],i);}}     }}

事件派发,遍历辅助数组,跳过没有fd的位置,FD_ISSET检测文件描述符是否在fds中就绪,然后判断是listen套接字还是accept的套接字。


accept套接字 添加到辅助数组

void Accepter() //accept套接字{InetAddr client;int sockfd=_listensockfd->AcceptOrDie(&client);//accept了干活fdLOG(LogLevel::DEBUG)<<"accept a new client"<<client.StringAddr();int pos=0;for(;pos<size;pos++){if(_fd_array[pos]==defaultfd)   //遍历辅助数组,遇见-1跳出break;}if(pos==size){LOG(LogLevel::WARNING)<<"server full";close(sockfd);} else{_fd_array[pos]=sockfd;}}

有了accept的套接字要在辅助数组中填加accept的套接字。遍历辅助数组,遇见defaulted值就要把这个accept套接字添加进去。判断辅助数组是否满了。


读数据

void Recv(int sockfd,int pos){char buffer[1024];ssize_t n=recv(sockfd,buffer,sizeof(buffer)-1,0);   //收信息if(n>0){buffer[n] = 0;cout << "client say@ "<< buffer <<endl;}else if(n==0)   //客户端退出{LOG(LogLevel::INFO)<<"client quit";_fd_array[pos]=defaultfd;close(sockfd);}else        //出现错误 异常{LOG(LogLevel::FATAL)<<"recv error";_fd_array[pos]=defaultfd;close(sockfd);}}void PrintFd(){cout << "_fd_array[]: ";for (int i = 0; i < size; i++){if (_fd_array[i] == defaultfd)continue;cout << _fd_array[i] << " ";}cout << "\r\n";}~Selectserver(){}

到recv接收信息,已经是非阻塞的了,客户端退出要把辅助数组这个位置置为defaulted关闭文件描述符,出现异常也是上述操作。

在这里插入图片描述
在这里插入图片描述

源码:非阻塞http服务

三、select 的缺点

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

相关文章:

  • 量化面试绿皮书:5. 扑克牌游戏概率与期望值
  • Redis哨兵
  • 【C】-数据类型及存储空间长度
  • 使用python把json数据追加进文件,然后每次读取时,读取第一行并删除
  • 【时时三省】(C语言基础)局部变量和全局变量
  • 12.6Swing控件4 JSplitPane JTabbedPane
  • C++ if语句完全指南:从基础到工程实践
  • ​​TLV4062-Q1​​、TLV4082-Q1​​迟滞电压比较器应用笔记
  • 如何用 HTML 展示计算机代码
  • 11-Oracle 23ai Vector Embbeding和ONNX
  • 大模型编程助手-Cline
  • 深入Java8-日期时间API:TemporalAdjusters、TemporalAdjuster类
  • 第34次CCF-CSP认证真题解析(目标300分做法)
  • 数据结构与算法——并查集
  • Java并发编程实战 Day 11:并发设计模式
  • table表格合并,循环渲染样式
  • Web攻防-SQL注入二次攻击堆叠执行SQLMAPTamper编写指纹修改分析调试
  • NoSQL 之Redis哨兵
  • 【数据结构】图
  • C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)
  • php+mysql网站开发全程实例.pdf/想在百度做推广怎么做
  • 视频库网站建设/关键词挖掘工具免费
  • 凯里建设局网站/长沙百家号seo
  • 一家专门做代购的网站/百度推广登录入口下载
  • 区政府网站自查整改和制度建设/网络营销课程速成班
  • 学做预算网站/百度企业推广怎么收费