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

news2025/6/7 15:11:14

🔥个人主页🔥:孤寂大仙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);    //每次进来都要重新设置rfds
                if(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了干活fd
        LOG(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);    //每次进来都要重新设置rfds
                if(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了干活fd
        LOG(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 支持的文件描述符数量太小。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2403041.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode--23.合并k个升序链表

解题思路&#xff1a; 1.获取信息&#xff1a; 给出了多个升序链表&#xff0c;要求合并成一个升序链表&#xff0c;返回首元结点 2.分析题目&#xff1a; 外面在21题的时候&#xff0c;讲了怎样合并两个升序链表为一个升序链表&#xff0c;不了解的&#xff0c;建议去看一下21…

【推荐算法】NeuralCF:深度学习重构协同过滤的革命性突破

NeuralCF&#xff1a;深度学习重构协同过滤的革命性突破 一、算法背景知识&#xff1a;协同过滤的演进与局限1.1 协同过滤的发展历程1.2 传统矩阵分解的缺陷 二、算法理论/结构&#xff1a;NeuralCF架构设计2.1 基础NeuralCF结构2.2 双塔模型进阶结构2.3 模型实现流程对比 三、…

负载均衡相关基本概念

负载均衡在系统架构设计中至关重要&#xff0c;其核心目标是合理分配负载&#xff0c;提升系统整体性能和可靠性。本文简要介绍了负载均衡的基本概念&#xff0c;包括四层和七层负载均衡、负载均衡的使用场景和实现方式、负载均衡的常用算法以及一些配置相关知识。 1、负载均衡…

集成电路设计:从概念到实现的完整解析优雅草卓伊凡

集成电路设计&#xff1a;从概念到实现的完整解析优雅草卓伊凡 一、集成电路设计&#xff1a;芯片制造的”灵魂蓝图” 1.1 什么是集成电路设计&#xff1f; 集成电路&#xff08;IC&#xff09;设计是指通过电子设计自动化&#xff08;EDA&#xff09;工具&#xff0c;将数百…

动态规划之网格图模型(二)

文章目录 动态规划之网格图模型&#xff08;二&#xff09;LeetCode 931. 下降路径最小和思路Golang 代码 LeetCode 2684. 矩阵中移动的最大次数思路Golang 代码 LeetCode 2304. 网格中的最小路径代价思路Golang 代码 LeetCode 1289. 下降路径最小和 II思路Golang 代码 LeetCod…

robot_lab——rsl_rl的train.py整体逻辑

文章目录 Go2机器人训练流程详细分析概述1. 训练启动流程1.1 命令行参数解析RSL-RL相关参数组Isaac Sim应用启动参数组 1.2 RL配置1.3 Isaac Sim启动 2. 环境配置加载2.1 Hydra配置系统 3. 环境创建与初始化3.1 Gym环境创建3.2 Manager系统初始化3.2.1 ObservationManager3.2.2…

.NET 原生驾驭 AI 新基建实战系列(三):Chroma ── 轻松构建智能应用的向量数据库

在人工智能AI和机器学习ML迅猛发展的今天&#xff0c;数据的存储和检索需求发生了巨大变化。传统的数据库擅长处理结构化数据&#xff0c;但在面对高维向量数据时往往力不从心。向量数据库作为一种新兴技术&#xff0c;专为AI应用设计&#xff0c;能够高效地存储和查询高维向量…

8.RV1126-OPENCV 视频中添加LOGO

一.视频中添加 LOGO 图像大体流程 首先初始化VI,VENC模块并使能&#xff0c;然后创建两个线程&#xff1a;1.把LOGO灰度化&#xff0c;然后获取VI原始数据&#xff0c;其次把VI数据Mat化并创建一个感兴趣区域&#xff0c;最后把LOGO放感兴趣区域里并把数据发送给VENC。2.专门获…

API管理是什么?API自动化测试怎么搭建?

目录 一、API管理是什么 &#xff08;一&#xff09;API管理的定义 &#xff08;二&#xff09;API管理的重要性 二、API管理的主要内容 &#xff08;一&#xff09;API设计 1. 遵循标准规范 2. 考虑可扩展性 3. 保证接口的易用性 &#xff08;二&#xff09;API开发 …

GIC v3 v4 虚拟化架构

ARMV8-A架构中包含了对虚拟化的支持。为了与架构保持匹配&#xff0c;GICV3也对虚拟化做了支持。新增了以下特性&#xff1a; 对CPU interface的硬件虚拟化虚拟中断maintenance 中断&#xff1a;用于通知监管程序&#xff08;例如hypervisor&#xff09;一些特定的虚拟机事件 …

2025远离Deno和Fresh

原创作者&#xff1a;庄晓立&#xff08;LIIGO&#xff09; 原创时间&#xff1a;2025年6月6日 原创链接&#xff1a;https://blog.csdn.net/liigo/article/details/148479884 版权所有&#xff0c;转载请注明出处&#xff01; 相识 Deno&#xff0c;是Nodejs原开发者Ryan Da…

Flask+LayUI开发手记(七):头像的上传及突破static目录限制

看了看&#xff0c;上篇开发手记是去年8月份写的&#xff0c;到现在差2个月整一年了。停更这么长时间&#xff0c;第一个原因是中间帮朋友忙一个活&#xff0c;那个技术架构是用springboot的&#xff0c;虽然前端也用layUI&#xff0c;但和Flask-python完全不搭界&#xff0c;所…

MiniExcel模板填充Excel导出

目录 1.官方文档 2. 把要导出的数据new一个匿名对象 3.导出 4.注意事项 5.模板制作 6.结果 1.官方文档 https://gitee.com/dotnetchina/MiniExcel/#%E6%A8%A1%E6%9D%BF%E5%A1%AB%E5%85%85-excel // 1. By POCO var value new {Name "Jack",CreateDate n…

MCP协议重构AI Agent生态:万能插槽如何终结工具孤岛?

前言 在人工智能技术快速发展的2025年&#xff0c;MCP(Model Context Protocol&#xff0c;模型上下文协议)正逐渐成为AI Agent生态系统的关键基础设施。这一由Anthropic主导的开放协议&#xff0c;旨在解决AI模型与外部工具和数据源之间的连接难题&#xff0c;被业界形象地称…

阿里云事件总线 EventBridge 正式商业化,构建智能化时代的企业级云上事件枢纽

作者&#xff1a;肯梦、稚柳 产品演进历程&#xff1a;在技术浪潮中的成长之路 早在 2018 年&#xff0c;Gartner 评估报告便将事件驱动模型&#xff08;Event-Driven Model&#xff09;列为十大战略技术趋势之一&#xff0c;指出事件驱动架构&#xff08;EDA&#xff0c;Eve…

CentOS8.3+Kubernetes1.32.5+Docker28.2.2高可用集群二进制部署

一、准备工作 1.1 主机列表 HostnameHost IPDocker IPRolek8s31.vm.com192.168.26.3110.26.31.1/24master&worker、etcd、dockerk8s32.vm.com192.168.26.3210.26.32.1/24master&worker、etcd、dockerk8s33.vm.com192.168.26.3310.26.33.1/24master&worker、etcd、…

学习日记-day23-6.6

完成目标&#xff1a; 知识点&#xff1a; 1.IO流_转换流使用 ## 转换流_InputStreamReader1.字节流读取中文在编码一致的情况,也不要边读边看,因为如果字节读不准,读不全,输出的内容有可能会出现乱码 2.所以,我们学了字符流,字符流读取文本文档中的内容如果编码一致,就不会出…

Pytorch安装后 如何快速查看经典的网络模型.py文件(例如Alexnet,VGG)(已解决)

当你用conda 安装好虚拟环境后&#xff0c; 找到你的Anaconda 的安装位置。 我的在D盘下&#xff1b; 然后 从Anaconda3文件夹开始&#xff1a;一级一级的查看&#xff0c;一直到models Anaconda3\envs\openmmlab\Lib\site-packages\torchvision\models 在models下面&#x…

有人-无人(人机)交互记忆、共享心智模型与AI准确率的边际提升

有人-无人&#xff08;人机&#xff09;交互记忆、共享心智模型与AI准确率的边际提升是人工智能发展中相互关联且各有侧重的三个方面。人机交互记忆通过记录和理解用户与机器之间的交互历史&#xff0c;增强机器对用户需求的个性化响应能力&#xff0c;从而提升用户体验和协作效…

【OpenGL学习】(五)自定义着色器类

文章目录 【OpenGL学习】&#xff08;五&#xff09;自定义着色器类着色器类插值着色统一着色 【OpenGL学习】&#xff08;五&#xff09;自定义着色器类 项目结构&#xff1a; 着色器类 // shader_s.h #ifndef SHADER_H #define SHADER_H#include <glad/glad.h>#inc…