【IO多路转接】IO 多路复用之 select:从接口解析到服务器实战 - 详解

news/2025/11/23 20:21:54/文章来源:https://www.cnblogs.com/yangykaifa/p/19261394

请添加图片描述


半桔:个人主页

  个人专栏: 《IO多路转接》《手撕面试算法》《C++从入门到入土》

当你一无所有,你没有什么可以失去。《泰坦尼克号》

文章目录

  • 前言
  • 一. 五种IO模型
  • 二. select实现多路转接
    • 2.1 select接口
    • 2.2 select服务器实现
      • 2.2.1 对网络套接字进行封装
      • 2.2.2 构建出服务器类
      • 2.2.3 进行初始化
      • 2.2.4 获取要进行等待的fd_set对象
      • 2.2.5 对读写事件就绪的文件进行处理
      • 2.2.6 服务器主循环
  • 三. select 的优缺点

前言

在网络编程领域,IO 模型是支撑高效通信的核心基础之一。当需要让单个进程或线程同时处理多个网络连接的 IO 事件时,“IO 多路复用(多路转接)” 技术成为了关键解法 —— 它能让程序通过少量进程 / 线程,高效监控并处理多个 IO 事件,极大提升系统对 IO 资源的利用效率。

select作为 IO 多路复用模型中经典且具有代表性的实现,是开发者接触 “多路转接” 的重要入门点。尽管随着技术演进,它逐渐显现出一些局限性,但深入理解select的工作机制、使用逻辑及其优缺点,不仅能帮助我们掌握 “单进程管理多连接” 的核心思路,更是学习更先进多路复用技术(如pollepoll)的重要前提。

本文将围绕 “select 实现多路转接” 展开,从select接口的基本定义入手,逐步讲解基于select的多路转接服务器实现(包含套接字封装、初始化流程、fd_set对象操作及服务器主循环设计等),最后剖析select自身的优势与不足。希望通过对这些内容的梳理,能让读者清晰把握select在多路 IO 转接中的核心作用,为后续 IO 模型学习与网络编程实践筑牢基础。

在介绍select这三种多路转接的IO模型之前,有必要先介绍以下5中IO模型分别是哪几种。

一. 五种IO模型

我们在操作系统中直接调用,read && write将数据读取上来,其本质就是将数据从用户层拷贝到操作系统中/从操作系统中拷贝到用户层——就是“拷贝”;

因此在进行拷贝之前,必须先判断条件是否成立,也就是读写事件是否就绪

我们通常定义高效IO指的是:单位时间内,IO过程中,等的比重越小,效率越高。

下面介绍五种IO模型:

  1. 阻塞性:直到 “等待数据就绪” 和 “数据拷贝” 两个阶段完全完成,IO 调用才返回;
  2. 非阻塞性:等待数据就绪阶段不阻塞(内核会立即返回结果),即若数据未就绪,内核会返回 EAGAINEWOULDBLOCK 错误;
  3. 信号驱动型:用一个线程监控多个 IO,避免进程在单个未就绪 IO 上阻塞
  4. 多路复用/多路转接型:让内核在 IO 数据就绪时主动发送SIGIO信号通知进程来拿取数据;
  5. 异步IO型:应用进程发起异步 IO 调用后,两个阶段(等待就绪、数据拷贝)均由内核完成,全程不阻塞进程。内核在完成所有操作后,通过 “信号” 或 “回调函数” 通知进程,进程直接使用已拷贝到用户缓冲区的数据。

下面介绍实现多路转接IO的3中方式。

二. select实现多路转接

关于select实现多路转接,此处将分为两部分进行介绍:

  1. 介绍select的接口;
  2. 使用select实现一个简单的ech服务器。

2.1 select接口

select可以一次等待多个文件,当有一个文件就绪了就返回,这样可以一次性等待多个文件,提高了等待的效率。

int select(int nfds , fd_set *readfds , fd_set *writefds , fd_set *expectfds , struct timeval *timeout*);

该就接口就是select的等待接口:

  1. 参数一nfds:标识等待的文件描述符中最大的 + 1;

fd_set是内核提供的一种数据结构,其本质是一张位图,记录着要关心的文件描述符。
2. 参数二readfds:是一个结构体,记录要关心读事件就绪的文件描述符;
3. 参数三writefds:记录要关心写事件就绪的文件描述符;
4. 参数四expectfds:记录要关心异常事件的文件描述符;

struct timeval也是内核提供的一种结构体,用于记录select要进行等待的事件:

struct timeval {
time_t      tv_sec;     /* seconds */
suseconds_t tv_usec;    /* microseconds */
};

当时间到达/有文件读写时间就绪就会进行返回。

  1. 参数五timeval:标识select等待的时间,如果等待时间到了,还没有一个文件读写时间就绪select也会进行返回,传nullptr标识阻塞式的等待;
  2. 返回值:一个整形,标识就绪的文件描述符的个数。

上面的fd_set是操作系统提供给我们的数据结构,我们不能直接对该数据结构进行操作,而应该使用操作系统提供的接口来进行操作:

  • void FD_ZERO(fd_set *set):将位图全部请零,用于初始化;
  • void FD_SET(int fd , fd_set *set):将fd文件描述符添加到位图中;
  • void FD_CLR(int fd , fd_set *set):将fd文件描述符从位图中移除;
  • void FD_ISSET(int fd , fd_set *set):检查fd文件描述符是否在位图中。

如果有文件描述符就绪,操作系统怎么告诉我们是那些文件就绪了???

为了让操作系统能够通知我们,select接口的后4个参数被设计为输入输出型参数

  1. readfds输出来告诉,那些文件描述符的读事件已经就绪;
  2. writefdsexpectfds也一样;
  3. timeval告诉我们,距离规定的返回时间还剩余多久。

select使用的是内核提供的现成的数据结构fd_set,因此这也就意味着其可以监视的文件描述符的数量是有限的,可以通过sizeof(fd_set)*8来计算出来。

2.2 select服务器实现

为了方便理解,我们实现一个简单的服务器,将用户发送过来的数据在前面添加一个server got a message后直接进行返回。

2.2.1 对网络套接字进行封装

首先我们先对网络套接字的接口进行封装:创建套接字,绑定,监听;关于这方面的知识可以查看之前的TCP相关内容,此时就直接贴实现方法:

const std::string defaultip_ = "0.0.0.0";
enum SockErr
{
SOCKET_Err,
BIND_Err,
};
class Sock
{
public:
Sock(uint16_t port)
: port_(port),
listensockfd_(-1)
{
}
void Socket()
{
listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (listensockfd_ < 0)
{
Log(Fatal) << "socket fail";
exit(SOCKET_Err);
}
Log(Info) << "socket sucess";
}
void Bind()
{
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port_);
inet_pton(AF_INET, defaultip_.c_str(), &server.sin_addr);
if (bind(listensockfd_, (struct sockaddr *)&server, sizeof(server)) < 0)
{
Log(Fatal) << "bind fail";
exit(BIND_Err);
}
Log(Info) << "bind sucess";
}
void Listen()
{
if (listen(listensockfd_, 10) < 0)
{
Log(Warning) << "listen fail";
}
Log(Info) << "listen sucess";
}
int Accept()
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int fd = accept(listensockfd_ , (sockaddr*)&client , &len);
if(fd < 0)
{
Log(Warning) << "accept fail";
}
return fd;
}
int Accept(std::string& ip , uint16_t& port)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int fd = accept(listensockfd_ , (sockaddr*)&client , &len);
if(fd < 0)
{
Log(Warning) << "accept fail";
}
port = ntohs(client.sin_port);
char bufferip[64];
inet_ntop(AF_INET , &client.sin_addr , bufferip , sizeof(bufferip) - 1);
ip = bufferip;
return fd;
}
int Get_fd()
{
return listensockfd_;
}
~Sock()
{
close(listensockfd_);
}
private:
uint16_t port_;
int listensockfd_;
};```

下面就来实现selectserver服务器:

2.2.2 构建出服务器类

首先就是构造出Selectserver类来对服务器进行管理:

  1. 首先需要一个Sock对象,进行TCP通信;
  2. 接着我们需要使用一个容器来存储所有要进行等待读写事件就绪的容器,此处为了简单我们直接使用一个数组来实现,该数组的大小就是fd_set能够等待的文件个数;
  3. 此处我们假设TCP接收到的就是完整报文,因此就不设置writefds的位图了,理论上是要进行设置的,大家可以自行实现以下;
const int fds_num_max = sizeof(fd_set) * 8;
const int defaultfd = -1;
class Selectserver
{
public:
Selectserver(uint16_t port)
: _sock_ptr(new Sock(port))
{
for (int i = 0; i < fds_num_max; i++)
{
_fds_array[i] = defaultfd;
}
}
private:
std::shared_ptr<Sock> _sock_ptr;int _fds_array[fds_num_max]; // 该数组用来存储select要进行等待的文件描述符,初始值为-1};

下一步就是进行初始化:

2.2.3 进行初始化

初始化一共就分为4个步骤:

  1. 创建套接字;
  2. 进行绑定;
  3. 设置监听模式;
  4. 将套接字添加到_fd_array数组中。

对于前三个步骤在前面我们已经进行封装过来,因此,此处可以直接进行调用。

  • 对于第四个步骤来说:我们在与客户端建立连接的时候,不知道什么时候客户端来进行连接,因此也需要进行等待,而这一等待工作本质上是在等待Sock指向的套接字文件,因此也应该使用select进行等待。

以下是具体实现:

void AddToArray(int fd)
{
int pos = 0;
for(; pos < fds_num_max && _fds_array[pos] != defaultfd ; pos++)
;
if(pos == fds_num_max)
{
// select已经到达监听极限了,不能再添加要进行监听的文件了
// 1. 关闭文件
// 2. 打印日志
close(fd);
Log(Warning) << "select is full";
}
else
{
// 1. 有位置直接进行添加
_fds_array[pos] = fd;
Log(Info) << "add a new fd : " << fd;
}
}
void Init()
{
// 1. 创建套接字
// 2. 绑定
// 3. 设置监听
// 4. 将套接字描述符加入到_fds_array数组中
_sock_ptr->Socket();
_sock_ptr->Bind();
_sock_ptr->Listen();
AddToArray(_sock_ptr->Get_fd());
}

2.2.4 获取要进行等待的fd_set对象

我们此处设计的select接口并不考虑writefdsexpectfds,因此我们只需要实现初始化传入的readfds接口即可,我们需要有一个已经设置好了的fd_set,以及一个其中最大的文件描述符,因此此处使用一个pair作为返回值。

std::pair<fd_set , int> Get_readfds(){// 1. 对位图进行初始化// 2. 循环遍历_fds_array数组,将要进行等待的文件描述符添加到位图中int max_num = 0;fd_set readfds;FD_ZERO(&readfds);for (int i = 0; i < fds_num_max; i++){if (_fds_array[i] == -1)continue;FD_SET(_fds_array[i], &readfds);max_num = std::max(max_num , _fds_array[i]);}return std::make_pair(readfds , max_num);}

2.2.5 对读写事件就绪的文件进行处理

select等待后,存在文件描述符就绪,就需要将这些文件描述符对应的数据拿上来。
而文件描述符又分为两种:

  1. 是Sock套接字文件描述符,要将已经建立好连接的文件描述符拿上来;
  2. 普通文件描述符,直接将输入缓冲区中的数据拿上来。
// 是套接字就绪
void Sockfd_Ready()
{
// 1. 将套接字中建立好的连接拿上来
// 2. 将拿上来的文件描述符加入到_fds_array中,等到客户端发送消息过来
int fd = _sock_ptr->Accept();
AddToArray(fd);
}
// fd表示文件描述符 , i表示在数组中的位置
void Normalfd_Ready(int fd , int i)
{
// 1. 读取文件描述符中的数据
// 2. 将数据简单处理后,进行返回(此处假设TCP接收的报文是完整的)
char inbuffer[1024];
int n = read(fd , inbuffer , sizeof(inbuffer) - 1);
if(n > 0)
{
inbuffer[n] = 0;
std::string ret = "server got a message : ";
ret += inbuffer;
write(fd , ret.c_str() , ret.size());
}
else  if(n == 0)
{
// 对方已经关闭文件
// 1. 将在_fds_array中的对应位置设为-1表示已经被移除了,不需要再进行等待
// 2. 关闭文件描述符
_fds_array[i] = defaultfd;
close(fd);
}
else
{
// 出错了
Log(Error) << "read fail";
}
}

2.2.6 服务器主循环

  1. 进行select等待;
  2. 有文件描述符就绪,识别对应的文件描述符,将任务进行派发,看交给哪一个函数进行完成。
void Dispather(fd_set* fdreads)
{
int listensock = _sock_ptr->Get_fd();
for(int i = 0 ; i < fds_num_max ; i++)
{
if(_fds_array[i] == defaultfd || !FD_ISSET(_fds_array[i] , fdreads)) continue;
if(_fds_array[i] == listensock)
{
Sockfd_Ready();
}
else
{
Normalfd_Ready(_fds_array[i] , i);
}
}
}
void Run()
{
while (true)
{
auto [fdreads , max_num] = Get_readfds();
int n = select(max_num + 1 , &fdreads , nullptr , nullptr , nullptr);
if(n > 0)
{
// 有事件就绪, 进行任务的派发
Dispather(&fdreads);
}
else if(n == 0)
{
Log(Info) << "no file";
}
else
{
Log(Error) << "select fail";
}
}
}

以上就是整个selectserver类的实现了。

三. select 的优缺点

优点:

  1. 所有的等待交给select来做,只要有读事件就绪就通知上层来将数据取走;
  2. 多路转接,在单进程的情况下能够处理多个用户的请求;

缺点:

  1. 使用的是内核提供的数据结构fd_set,等待的文件描述符的数量是有限的;
  2. 输入输出型参数使用起来麻烦,并且每次进行select的时候都要进行重新设置;
  3. 要将fd_set从用户层拷贝到内核中,又要拷贝回来,拷贝数据频繁;
  4. 使用第三方数组对用户的fd进行管理,用户称需要进行多次遍历,内核在进行检测的时候也要进行多次遍历。

后续文章中我们将讲解select的替代方案:pollepoll.

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

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

相关文章

java sql注入的危害有哪些

Java SQL注入是一种常见的网络攻击手段,其危害主要包括以下几个方面:数据泄露:攻击者可以通过SQL注入攻击获取数据库中的敏感信息,如用户账号、密码、电话号码等,从而导致数据泄露。 数据篡改:攻击者可以通过SQL…

单片机控制继电器及其原理

工作原理当继电器线圈通电后,线圈中的铁芯产生强大的电磁力,吸动衔铁带动簧片,使触点1、2断开,1、3接通。当线圈断电后,弹簧使簧片复位,使触点1、2接通,1、3断开。我们只要把需要控制的电路接在触点1、2间(1、…

【Linux】 层层递进,抽丝剥茧:调度队列、命令行参数、环境变量 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

字符串大小写转换

函数和Ascii码都要会 Ascii码强制转换 #include<bits/stdc++.h> using namespace std; int main() {string s;while(getline(cin,s)){for(int i=0;i<s.size();i++){if(s[i]>=a&&s[i]<=z) cout&…

vitepress如何支持vue2组件

VitePress 默认不支持 Vue 2 组件,因为:✅ VitePress 是基于 Vue 3 + Vite 构建的静态站点生成器; ❌ 它的运行时环境是 Vue 3,无法直接渲染 Vue 2 组件(Vue 2 和 Vue 3 的 API、响应式系统、虚拟 DOM 结构都不兼…

2025.11.23

今天继续学习vue,然后开始细节微调界面

java linux环境变量

在 Linux 环境中,Java 的运行依赖于 JAVA_HOME 环境变量,它指向 Java 开发工具包(JDK)的安装目录。以下是关于 Java 环境变量的详细说明和相关命令:一、设置 Java 环境变量1. 设置 JAVA_HOME(推荐)export JAVA_…

java linux服务器

您提到的“Java Linux服务器”可能是指在Linux服务器上部署和运行Java应用。以下是一些常见的Java相关Linux命令和Shell脚本,帮助您管理Java应用、部署、监控和日志等:一、Java环境配置1. 安装JDK(Java Development…

贪心做题记录-2

贪心做题记录-2 CF1477D. Nezzar and Hidden Permutations 若一个点的度数为 \(n-1\),则其在 \(p,q\) 中的位置定然相同,因此可以先将这个点删去,缩减为子问题,直到剩下 \(n\) 个点且所有点的度数都小于 \(n-1\)。…

机器人世界杯物流联赛技术解析

本文深入探讨机器人世界杯物流联赛的技术细节,包括自主移动机器人导航、多机器人协作规划、实时执行监控与重规划系统,以及智能工厂环境下的生产物流挑战,展现了现代机器人系统的复杂技术架构。机器人世界杯物流联赛…

fcitx5要一统江湖了

为了面向未来,又装上fcitx5细细地品了一下。转换正确率很高。 于是apt source fcitx5-pinyin看了下,发现两个2012年的文件:1.4M 2012年12月 2日 py_stroke.mb359K 2012年11月25日 py_table.mb用这两个文件就能达到如…

2025 年上海金蝶软件定制开发代理商推荐榜出炉

🥇 推荐一:上海宝蝶信息技术有限公司 (金蝶铂金级定制开发专家)推荐指数:★★★★★ | 口碑评分:10分 (一)上海宝蝶信息科技有限公司品牌介绍上海宝蝶信息技术有限公司成立于 2016 年,是金蝶集团在上海地区授权…

【开发者导航】全自动 AI 视频创作与发布工具:LuoGen-agent - 教程

【开发者导航】全自动 AI 视频创作与发布工具:LuoGen-agent - 教程2025-11-23 19:54 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !…

2025-09-10-Wed-T-AI基础知识

1 2 MCP What is the Model Context Protocol (MCP)? - Model Context Protocol 2.1 MCP能干什么 举例: 开发部署 开发者通过自然语言指令”部署新版本到测试环境“,触发MCP链式调用GitLab API(代码合并),Jenkin…

20251123-Port3 攻击事件分析:签名验证绕过+跨链增发代币

20251123,攻击者利用 CATERC20 合约中签名验证过程中没有检查非零地址签名的漏洞,调用 registerChain 函数设置了其他链的代币合约。使得跨链代币增发,抛售代币进行获利。 Hacker:https://bscscan.com/txs?a=0xb1…

JAX 核心特性详解:纯函数、JIT 编译、自动微分等十大必知概念

JAX 是 Google 和 NVIDIA 联合开发的高性能数值计算库,这两年 JAX 生态快速发展,周边工具链也日益完善了。如果你用过 NumPy 或 PyTorch,但还没接触过 JAX,这篇文章能帮助你快速上手。 围绕 JAX 已经涌现出一批好用…

java linux 进程

在 Linux 系统中,Java 进程的管理通常涉及以下几个方面:一、Java 进程的查看1. 查看 Java 进程(使用 ps 或 top)ps -ef | grep java这个命令会列出所有运行中的 Java 进程,包括进程 ID(PID)、用户名、是否为 Ja…

截图工具

faststone capture工具可以进行滚动截屏使用教程https://www.faststonecapture.cn/article/1004.html