
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- 前言
- [1 ~> 多路转接基础概念](#1 ~> 多路转接基础概念)
-
- [1.1 IO 模型原理拆分](#1.1 IO 模型原理拆分)
- [1.2 多路转接(多路复用)定义](#1.2 多路转接(多路复用)定义)
- [1.3 多路转接select概念相关知识图谱](#1.3 多路转接select概念相关知识图谱)
- [2 ~> select 函数核心详解](#2 ~> select 函数核心详解)
-
- [2.1 select 整体设计思路](#2.1 select 整体设计思路)
- [2.2 函数原型与依赖头文件](#2.2 函数原型与依赖头文件)
- [2.3 函数参数全面解析](#2.3 函数参数全面解析)
-
- [2.3.1 nfds 参数](#2.3.1 nfds 参数)
- [2.3.2 fd_set 与三类事件文件描述符集](#2.3.2 fd_set 与三类事件文件描述符集)
- [2.3.3 timeout 时间结构体参数](#2.3.3 timeout 时间结构体参数)
- [2.3.4 函数返回值规则](#2.3.4 函数返回值规则)
- [2.3.5 select函数参数知识图谱](#2.3.5 select函数参数知识图谱)
- [2.4 fd_set 配套操作宏](#2.4 fd_set 配套操作宏)
- [3 ~> select 三种等待模式](#3 ~> select 三种等待模式)
-
- [3.1 永久阻塞模式](#3.1 永久阻塞模式)
- [3.2 限时阻塞模式](#3.2 限时阻塞模式)
- [3.3 非阻塞轮询模式](#3.3 非阻塞轮询模式)
- [4 ~> select 代码实战:Echo 服务端开发](#4 ~> select 代码实战:Echo 服务端开发)
- [5 ~> select 核心特性与关键问题](#5 ~> select 核心特性与关键问题)
-
- [5.1 文件描述符数量上限](#5.1 文件描述符数量上限)
- [5.2 辅助数组的作用](#5.2 辅助数组的作用)
- [5.3 accept 阻塞问题解答](#5.3 accept 阻塞问题解答)
- [5.4 重复置位的必要性](#5.4 重复置位的必要性)
- [6 ~> 延伸知识点](#6 ~> 延伸知识点)
- [7 ~> 代码](#7 ~> 代码)
-
- [7.1 代码演示](#7.1 代码演示)
- [7.2 代码编译运行](#7.2 代码编译运行)
- [8 ~> select的优缺点](#8 ~> select的优缺点)
- [9 ~> select的原理](#9 ~> select的原理)
- 总结
- 结尾

前言
思维导图

导入语
在传统阻塞式 IO 模型中,read、write、accept 这类函数会将等待事件就绪 和数据内核态与用户态拷贝 两个步骤合并执行,当程序需要同时监听多个文件描述符(套接字)时,单线程阻塞模型会出现严重的性能问题,无法同时响应多个 IO 事件。而 ** 多路转接(多路复用)** 技术正是为了解决该问题而生,它将 IO 的两个步骤拆分,由专门的函数负责监听多个文件描述符是否事件就绪,数据拷贝工作依旧由传统 IO 函数完成。本文以 Linux 下经典的 select 多路转接函数为核心,从原理、函数参数、使用模式、代码实战、底层特性等维度完整讲解,结合服务端开发案例落地知识点,梳理 select 的完整使用逻辑与底层原理,帮助理解多路复用 IO 模型的设计思想。
1 ~> 多路转接基础概念

1.1 IO 模型原理拆分
所有 IO 操作本质都分为两个核心步骤:第一步是等待读写 / 连接条件就绪 ,第二步是内核与用户空间的数据拷贝 。传统的 read、write 函数会将这两个步骤封装在同一个函数中,调用函数后线程会先阻塞等待事件就绪,再执行数据拷贝。多路转接技术的核心就是对这两个步骤进行解耦,由 select、poll、epoll 这类专用函数单独承担等待文件描述符事件就绪 的工作,当检测到事件就绪后,再调用 read、accept 等函数完成数据拷贝,以此实现单线程监听多个文件描述符。
可以用钓鱼的例子直观理解:一名钓鱼者手持多根鱼竿(对应多个文件描述符),传统 IO 是一根鱼竿盯到底,鱼上钩(事件就绪)后再处理;多路转接则是统一巡视所有鱼竿,只要任意一根鱼竿有鱼上钩,就立刻进行处理,实现多鱼竿并行监听。
1.2 多路转接(多路复用)定义
多路转接本质是就绪事件的通知机制 ,核心工作只有一个:等待。它可以同时监听多个文件描述符(fd),持续检测每个 fd 是否满足就绪条件,就绪条件分为两类,一是读就绪 (缓冲区有数据可读),二是写就绪(缓冲区有空间可写),同时还支持监听异常事件。当任意一个被监听的文件描述符满足就绪条件时,函数会立即返回并告知程序具体是哪些 fd 就绪,程序再针对性执行后续 IO 操作。
不同操作系统平台提供了不同的多路转接实现方案,Linux 平台主流使用 select、poll、epoll,Windows 平台则有独立的实现接口,接口语法和底层逻辑存在差异,但核心思想均为监听多 fd 事件就绪。
1.3 多路转接select概念相关知识图谱

2 ~> select 函数核心详解
2.1 select 整体设计思路
select 是 Linux 下最早的多路转接函数,属于事件驱动型编程接口,整体学习与使用分为三个核心环节:第一是了解函数基本定义与作用,第二是深度拆解函数所有参数,理解其监听多个文件描述符的底层逻辑,第三是结合服务端代码实战,掌握 select 的编程规范与使用特点。select 的核心能力就是在一个函数内部完成对多个文件描述符的统一等待,区分不同类型的就绪事件。
2.2 函数原型与依赖头文件
使用 select 必须引入对应的系统头文件,完整函数原型如下:
c
#include <sys/select.h>
typedef /* ... */ fd_set;
int select(int nfds, fd_set *_Nullable restrict readfds, fd_set *_Nullable restrict writefds, fd_set *_Nullable restrict exceptfds, struct timeval *_Nullable restrict timeout);
函数返回值为整型,用于标识监听结果,五个入参各司其职,是 select 学习的重点内容。
2.3 函数参数全面解析
2.3.1 nfds 参数
该参数取值规则为所有被监听文件描述符中的最大值 + 1 ,和被监听文件描述符的总数量无关。例如仅监听文件描述符 10,那么 nfds 就需要设置为 11;若同时监听 3、5、7 三个 fd,最大值为 7,nfds 则设置为 8。select 依靠该参数确定需要遍历检测的文件描述符范围。
2.3.2 fd_set 与三类事件文件描述符集
readfds、writefds、exceptfds 三个参数类型均为 fd_set(文件描述符集),分别对应读事件集 、写事件集 、异常事件集,三者逻辑完全一致,仅监听的事件类型不同。
fd_set 底层采用位图结构 实现,和信号集的设计思路相似,系统内部通过数组拼接成位图,每一个比特位对应一个文件描述符编号:比特位的位置代表 fd 编号,比特位的值(0/1)代表是否需要监听该 fd。这三个参数属于输入输出型参数 ,存在两次数据交互:调用 select 时(输入阶段),用户通过位图告诉内核需要监听哪些 fd 的对应事件;select 返回时(输出阶段),内核会修改位图,仅将已就绪的 fd 对应比特位置为 1,其余比特位清零,以此告知用户哪些 fd 触发了事件。
基于该特性衍生出三个实际使用问题:第一,若同一个 fd 需要同时监听读、写事件,可将该 fd 同时添加到读集合和写集合中;第二,工程最佳实践为分阶段监听,优先监听读事件,读事件处理完成后再监听写事件,减少资源消耗;第三,内核只会反馈用户主动添加监听的 fd 状态,未加入集合的 fd 即便客观上事件就绪,也不会被标记。
2.3.3 timeout 时间结构体参数
timeout 是 struct timeval 类型的指针,用于设置 select 的等待超时时间,同时也是输入输出型参数,等待过程中会实时更新剩余等待时长。该结构体定义在 <sys/time.h> 头文件中,结构如下:
c
struct timeval {
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微秒
};
配合 timeout 的不同赋值,select 分为三种等待模式,同时搭配 gettimeofday 函数可以获取系统时间戳,辅助时间逻辑调试。
2.3.4 函数返回值规则
select 的整型返回值分为三种情况:返回值 大于 0 ,代表有对应数量的文件描述符事件就绪;返回值 小于 0 ,代表函数调用出错,监听失败;返回值 等于 0,代表等待超时,没有任何文件描述符事件就绪。
2.3.5 select函数参数知识图谱

2.4 fd_set 配套操作宏
由于 fd_set 是位图结构,直接进行位运算会降低代码跨平台兼容性,Linux 系统提供了一组标准宏函数专门操作文件描述符集,所有操作 fd_set 的场景都必须使用这组宏:
FD_ZERO(fd_set *set):清空整个文件描述符集,将所有比特位置 0,每次循环监听前都需要执行该操作重置位图;FD_SET(int fd, fd_set *set):将指定文件描述符 fd 对应的比特位置 1,代表开始监听该 fd;FD_CLR(int fd, fd_set *set):将指定文件描述符 fd 对应的比特位置 0,代表取消监听该 fd;FD_ISSET(int fd, fd_set *set):检测指定 fd 在集合中对应的比特位是否为 1,用于判断 fd 是否事件就绪。
3 ~> select 三种等待模式
3.1 永久阻塞模式
将 timeout 参数设置为 NULL,即为永久阻塞模式。此时 select 会一直阻塞线程,直到任意一个被监听的文件描述符事件就绪才会返回,无超时机制。该模式适用于需要持续监听、不允许轮询的服务端场景。
3.2 限时阻塞模式
给 timeval 结构体设置固定时长,例如 struct timeval timeout = {5, 0},代表阻塞等待 5 秒。在等待时间内如果有事件就绪,select 会立即返回,同时 timeout 会更新为剩余等待时间;如果超过设定时长依旧无事件就绪,函数返回 0 表示超时。该模式兼顾监听与定时任务,是服务端常用模式。
3.3 非阻塞轮询模式
将 timeval 结构体设置为 {0, 0}(0 秒 0 微秒),即为非阻塞模式。调用 select 后不会阻塞线程,会立刻检测所有监听的 fd:有事件就绪则返回就绪数量,无事件则直接返回 0。该模式会持续循环轮询 fd,CPU 占用率极高,一般不建议长时间使用。
4 ~> select 代码实战:Echo 服务端开发
4.1 项目目录与基础环境
本次基于 C++ 实现 select 版 Echo 服务器,项目目录包含 SelectServer.hpp(服务端核心类)、Main.cc(程序入口)、Makefile(编译脚本),同时复用已封装的套接字工具类 Socket.hpp、地址类 InetAddr.hpp、日志类 Logger.hpp,编译采用 C++17 标准。
4.2 基础类与成员变量设计
服务端核心类 SelectServer 封装端口、监听套接字、文件描述符辅助数组等成员,首先定义宏计算 fd_set 最大支持的 fd 数量,默认无效 fd 标记为 -1:
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <memory>
#include <sys/select.h>
#include "Socket.hpp"
#include "Logger.hpp"
#define NUM (sizeof(fd_set) * 8)
const int gdefaultfd = -1;
using namespace LogModule;
class SelectServer
{
public:
SelectServer(uint16_t port);
~SelectServer() = default;
void Start(); // 服务启动,select 主循环
void HandlerEvent();// 事件处理函数
private:
uint16_t _port;
std::unique_ptr<Socket> _listensockfd;
int array_fds[NUM]; // 辅助数组,托管所有待监听 fd
};
构造函数负责初始化监听套接字,完成套接字创建、地址绑定、端口监听、地址复用配置,同时将辅助数组全部初始化为 -1(标记为无效 fd):
cpp
SelectServer::SelectServer(uint16_t port)
:_port(port), _listensockfd(std::make_unique<TcpSocket>())
{
_listensockfd->BuildSocketMethod(_port);
// 初始化辅助数组
for(int i = 0; i < NUM; i++)
{
array_fds[i] = gdefaultfd;
}
// 将监听套接字加入辅助数组
array_fds[0] = _listensockfd->Socketfd();
}
套接字底层开启地址复用(SO_REUSEADDR、SO_REUSEPORT),避免端口绑定失败,BuildSocketMethod 内部依次执行创建套接字、绑定地址、开始监听三个操作。
4.3 select 主循环逻辑(Start 函数)
Start 函数是服务端核心循环,循环内每次都需要重置 fd 位图 、遍历辅助数组添加待监听 fd、计算最大 fd、调用 select 监听、根据返回值分支处理:
cpp
void SelectServer::Start()
{
fd_set rfds;
while(true)
{
// 1. 清空位图,重置文件描述符集
FD_ZERO(&rfds);
int max_fd = gdefaultfd;
// 2. 遍历辅助数组,将有效 fd 加入监听集合
for(int i = 0; i < NUM; i++)
{
if(array_fds[i] == gdefaultfd)
continue;
FD_SET(array_fds[i], &rfds);
// 更新最大文件描述符
if(max_fd < array_fds[i])
{
max_fd = array_fds[i];
}
}
// 3. 调用 select,永久阻塞模式
int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr);
// 4. 根据返回值分支处理
switch(n)
{
case 0:
LOG(LogLevel::DEBUG) << "select 等待超时";
break;
case -1:
LOG(LogLevel::DEBUG) << "select 调用出错";
break;
default:
LOG(LogLevel::DEBUG) << "检测到事件就绪,就绪数量:" << n;
HandlerEvent();
break;
}
}
}
由于 fd_set 是输入输出型参数,select 返回后位图会被内核修改,因此每次循环必须执行 FD_ZERO 清空位图,并重新添加所有待监听 fd ,这是 select 编程的强制规范。
4.4 事件处理函数(HandlerEvent)
监听套接字的读就绪事件等价于有新客户端连接到来 ,因此事件处理函数中调用 accept 获取新连接,select 已经保证 fd 就绪,此时 accept 不会阻塞。获取到新的客户端 fd 后,将其存入辅助数组,实现新 fd 托管给 select 监听:
cpp
void SelectServer::HandlerEvent()
{
InetAddr clientaddr;
// 获取新连接,不会阻塞
int new_fd = _listensockfd->Accepter(&clientaddr);
if(new_fd < 0)
return;
LOG(LogLevel::INFO) << "成功获取新连接,客户端 fd:" << new_fd;
// 将新 fd 存入辅助数组,完成托管
int pos = 0;
for(; pos < NUM; pos++)
{
if(array_fds[pos] == gdefaultfd)
break;
}
// 数组已满,超出 select 最大监听数量,关闭新连接
if(pos == NUM)
{
close(new_fd);
LOG(LogLevel::DEBUG) << "文件描述符数量达到上限,关闭新连接";
}
else
{
array_fds[pos] = new_fd;
}
}
4.5 程序入口 <Main.cc>
入口函数创建服务端对象,指定监听端口并启动服务:
cpp
#include "SelectServer.hpp"
#include <memory>
const uint16_t gport = 8080;
int main()
{
std::unique_ptr<SelectServer> select_svr = std::make_unique<SelectServer>(gport);
select_svr->Start();
return 0;
}
4.6 代码相关知识图谱

5 ~> select 核心特性与关键问题
5.1 文件描述符数量上限
fd_set 位图的大小固定,通过 sizeof(fd_set) * 8 可以计算出系统默认最大支持监听 1024 个文件描述符 ,这是 select 最核心的短板,无法支持高并发场景下大量客户端连接。因此代码中必须借助辅助数组管理 fd,同时做数组满溢判断。

5.2 辅助数组的作用
select 无法主动保存所有待监听的 fd,而 fd_set 每次循环都会被重置,因此必须自定义全局辅助数组 ,长期存储所有需要监听的有效 fd。每次循环遍历该数组,将有效 fd 重新添加到 fd_set 中,实现多 fd 持续监听。
5.3 accept 阻塞问题解答
当 select 检测到监听套接字读就绪后,再调用 accept 不会发生阻塞 。原因是 select 已经完成了 "等待新连接" 的步骤,内核的连接队列中已经存在有效连接,accept 仅执行 "获取连接" 的操作,和 IO 两步拆分的设计思想完全契合。同理,客户端 fd 执行 recv 前,也必须由 select 检测读就绪,避免阻塞。
5.4 重复置位的必要性
fd_set 作为输入输出型参数,select 返回后会清空未就绪 fd 的比特位,因此每一轮循环都必须调用 FD_ZERO 清空集合,再通过 FD_SET 重新添加所有待监听 fd,否则下一轮监听会丢失部分文件描述符。
6 ~> 延伸知识点
传统阻塞 IO 函数 read、accept 逻辑高度相似,都分为 "等待就绪" 和 "数据 / 连接获取" 两步,多路转接的本质就是把 "等待" 动作统一交给 select 处理。在图形框架 QT 中,信号与槽的事件机制底层同样基于 IO 事件模型,槽函数本质就是事件触发后的回调函数,和 select 检测到事件后执行 HandlerEvent 的逻辑一致,所有事件驱动框架的底层思想都源于此类多路复用模型。
7 ~> 代码

代码目录结构:

7.1 代码演示
InetAddr.hpp
这里的代码前面有,就不再赘言和展示了。
Logger.hpp
这里的代码前面有,就不再赘言和展示了。
Main.cc
cpp
#include "SelectServer.hpp"
#include <memory>
const uint16_t gport = 8080;
int main(int argc, char *argv[])
{
std::unique_ptr<SelectServer> select_svr = std::make_unique<SelectServer>(gport);
select_svr->Start();
return 0;
}
Makefile
bash
select_server:Main.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f select_server;
Mutex.hpp
这里的代码前面有,就不再赘言和展示了。
SelectServer.hpp
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <memory>
#include <sys/select.h>
#include "Socket.hpp"
#include "Logger.hpp"
#define NUM (sizeof(fd_set)* 8)
const int gdefaultfd = -1;
using namespace LogModule;
class SelectServer
{
public:
SelectServer(uint16_t port)
: _port(port),
_listensockfd(std::make_unique<TcpSocket>())
{
// 创建出套接字
_listensockfd->BuildSocketMethod(_port);
for(int i = 0;i < NUM;i++)
array_fds[i] = gdefaultfd;
}
void HanderEvent()
{
InetAddr clientaddr;
int fd = _listensockfd->Accepter(&clientaddr);
LOG(LogLevel::INFO) << "get a new link: " << fd;
// 你得到了一个新的连接,这个连接怎么处理??
// recv(fd)?? 等 + 拷贝, 不能!!
// fd -> 托管给select-> 只有select具有"等"的能力!-> 如何托管?? -> 只要把fd添加的辅助数组即可!
// 现在我想知道,如何把新的文件描述符托管给select?
if(fd >= 0)
{
int pos = 0;
for(;pos <= NUM;pos++)
{
if(array_fds[pos] == gdefaultfd)
break;
}
if(pos == NUM)
{
close(fd);
}
else
{
array_fds[pos] = fd;
}
}
}
void Start()
{
fd_set rfds; // read fd set:读文件描述符集
int max_fd = gdefaultfd;
while(true)
{
// // 服务器需要启动起来
// InetAddr client;
// auto sockfd = _listensockfd->Accepter(&client);
// rfds参数重置 --> 在循环里面,每次都会select
FD_ZERO(&rfds);
for(int i = 0;i < NUM;i++)
{
if(array_fds[i] == gdefaultfd)
continue;
FD_SET(array_fds[i],&rfds);
if(max_fd < array_fds[i])
{
max_fd = array_fds[i];
}
}
// FD_SET(_listensockfd->Socketfd(),&rfds); // 不是_listensockfd了
// // 把timeout设置成nullptr了,没有新连接到来会永久堵塞
// struct timeval timeout = {2,0}; // s ms
// // 一直轮询
// struct timeval timeout = {0,0}; // s ms
// // 每隔5秒timeout一次
// struct timeval timeout = {5,0}; // s ms
// 这个timeout我们不关心,后面会设置成nullptr
// int n = select(_listensockfd->Socketfd() + 1,&rfds,nullptr,nullptr,&timeout);
// 把timeout设置成nullptr
// int n = select(_listensockfd->Socketfd() + 1,&rfds,nullptr,nullptr,nullptr);
// 设置成max_fd
int n = select(max_fd + 1,&rfds,nullptr,nullptr,nullptr);
// n:返回值
switch(n)
{
case 0:
// LOG(LogLevel::DEBUG) << "time out...";
// LOG(LogLevel::DEBUG) << "time out..." << "剩余时间: " << timeout.tv_sec << "." << timeout.tv_usec;
LOG(LogLevel::DEBUG) << "time out...";
break;
case -1:
LOG(LogLevel::DEBUG) << "select error";
break;
default:
// LOG(LogLevel::DEBUG) << "事件就绪...";
LOG(LogLevel::DEBUG) << "事件就绪...: n :" << n;
// 设置一个函数来设置已经发生的事件
HanderEvent();
// 为了防止刷屏
sleep(1);
break;
}
}
}
~SelectServer()
{}
private:
uint16_t _port;
// 创建一个基类套接字
std::unique_ptr<Socket> _listensockfd;
// // 读文件描述符集
// fd_set rfds; // read fd set:读文件描述符集
// select服务器是要借助一个辅助数组的!
int array_fds[NUM];
};
Socket.hpp
这里的代码虽然前面有,但是有改动,大家注意观察。
cpp
#ifndef __SOCKRT_HPP
#define __SOCKRT_HPP
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <memory>
#include "InetAddr.hpp"
#include "Logger.hpp"
static const int gbacklog = 16;
using namespace LogModule;
enum
{
SOCKET_ERR = 1,
BIND_ERR,
LISTEN_ERR
};
// 基类
// 把socket创建过层,模版化,方法化 -- 模版方法模式
class Socket
{
public:
virtual ~Socket() {}
virtual void CreateSocketOrDie() = 0;
virtual void BindSocketOrDie(uint16_t port) = 0;
virtual void ListenSocketOrDie() = 0;
// virtual std::shared_ptr<Socket> Accepter(InetAddr *clientaddr) = 0;
// 基类方法那里改成int
virtual int Accepter(InetAddr *clientaddr) = 0;
virtual void ConnectOrDie(const std::string &serverip, uint16_t serverport) = 0;
virtual int Socketfd() = 0;
virtual void Close() = 0;
virtual int Recv(std::string *outstr) = 0;
virtual int Send(const std::string &outstr) = 0;
public:
void BuildSocketMethod(uint16_t port)
{
CreateSocketOrDie();
BindSocketOrDie(port);
ListenSocketOrDie();
}
void BuildClientSocketMethod(const std::string &serverip, uint16_t serverport)
{
CreateSocketOrDie();
ConnectOrDie(serverip, serverport);
}
};
class TcpSocket : public Socket
{
public:
TcpSocket() : _sockfd(-1)
{
}
TcpSocket(int sockfd) : _sockfd(sockfd)
{
}
void CreateSocketOrDie() override
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
int opt = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
LOG(LogLevel::INFO) << "create socket success";
}
void BindSocketOrDie(uint16_t port) override
{
InetAddr local(port);
int n = bind(_sockfd, local.Addr(), local.AddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success";
}
void ListenSocketOrDie() override
{
int n = listen(_sockfd, gbacklog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen socket success";
}
// std::shared_ptr<Socket> Accepter(InetAddr *clientaddr) override
// {
// struct sockaddr_in peer;
// socklen_t len = sizeof(peer);
// int sockfd = accept(_sockfd, CONV(&peer), &len);
// if (sockfd < 0)
// {
// return nullptr;
// }
// *clientaddr = peer;
// return std::make_unique<TcpSocket>(sockfd);
// }
// 获取新连接改一下
int Accepter(InetAddr *clientaddr) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_sockfd, CONV(&peer), &len);
if (sockfd < 0)
{
return -1;
}
*clientaddr = peer;
return sockfd;
}
int Socketfd() override
{
return _sockfd;
}
void Close() override
{
if (_sockfd >= 0)
{
close(_sockfd);
_sockfd = -1;
}
}
int Recv(std::string *outstr) override // 读写的依旧是字符串
{
char buffer[1024];
ssize_t n = recv(_sockfd, buffer, sizeof(buffer) - 1, 0); // bug
if (n > 0)
{
buffer[n] = 0;
*outstr += buffer; // +=的本质是拼接,入队列,outstr当做一个字节流队列!
return n;
}
else if (n == 0)
{
return 0;
}
else
{
return -1;
}
}
int Send(const std::string &outstr) override
{
return send(_sockfd, outstr.c_str(), outstr.size(), 0);
}
void ConnectOrDie(const std::string &serverip, uint16_t serverport) override
{
InetAddr serveraddr(serverport, serverip);
int n = connect(_sockfd, serveraddr.Addr(), serveraddr.AddrLen());
if (n != 0)
{
LOG(LogLevel::FATAL) << "connect " << serveraddr.StringAddress() << " failed";
return;
}
LOG(LogLevel::INFO) << "connect " << serveraddr.StringAddress() << " success";
}
private:
int _sockfd;
};
7.2 代码编译运行


8 ~> select的优缺点

9 ~> select的原理

总结
本文完整梳理了 Linux select 多路转接技术的全部知识点,从底层原理、函数接口、使用模式到工程代码实战形成完整逻辑链条,核心重点汇总如下:
- IO 拆分核心思想 :将 IO 分为「等待事件就绪」和「数据拷贝」两步,
select专门负责等待,传统 IO 函数负责拷贝,以此实现单线程监听多文件描述符。 - select 函数核心 :五个参数中
nfds取最大 fd+1,三类fd_set是位图结构且为输入输出型参数,timeout决定阻塞模式;配套四个宏函数是操作位图的标准方式,不可直接位运算。 - 三种等待模式 :
timeout=NULL永久阻塞、固定时间为限时阻塞、{0,0}非阻塞轮询,不同模式适配不同业务场景。 - 编程规范要点 :每次循环必须
FD_ZERO重置位图并重新添加 fd;必须借助辅助数组长期托管所有待监听 fd;select检测就绪后再调用accept/read,彻底规避阻塞问题。 - select 固有缺陷 :受
fd_set位图大小限制,默认最大仅支持 1024 个文件描述符,并发能力有限,这也是后续poll、epoll诞生的主要原因。 - 代码逻辑闭环:监听套接字初始化 → 辅助数组托管 fd → 循环重置位图 + 调用 select → 事件就绪后处理连接 / 数据 → 新 fd 加入辅助数组实现持续监听,整套流程是 Linux 多路复用服务端的经典写法。
select 是学习多路复用 IO 模型的基础,理解其位图原理、参数特性与代码逻辑 ,能够为后续学习 poll、epoll 以及各类事件驱动框架打下坚实基础。
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!"
"技术之路难免有困惑,但同行的人会让前进更有方向。" |
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
