[Linux]IO多路转接(上)
1. IO 多路转接之select
1.1 select概述
select
是系统提供的一个多路转接接口,其核心工作在于等待。它能够让程序同时监视多个文件描述符上的事件是否就绪,只有当被监视的多个文件描述符中有一个或多个事件就绪时,select
才会成功返回,并将对应文件描述符的就绪事件告知调用者。
1.2 select函数
- 函数原型:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 参数说明:
nfds
:需要监视的文件描述符中,最大的文件描述符值 + 1。readfds
:输入输出型参数。调用时用户告知内核需监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已就绪。writefds
:输入输出型参数。调用时告知内核需监视哪些文件描述符的写事件是否就绪,返回时告知哪些文件描述符的写事件已就绪。exceptfds
:输入输出型参数。调用时告知内核需监视哪些文件描述符的异常事件是否就绪,返回时告知哪些文件描述符的异常事件已就绪。timeout
:输入输出型参数。调用时由用户设置select
的等待时间,返回时表示timeout
的剩余时间。其取值有以下几种情况:
NULL/nullptr
:select
调用后进行阻塞等待,直至被监视的某个文件描述符上的某个事件就绪。0
:select
调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select
检测后都会立即返回。- 特定的时间值:
select
调用后在指定时间内进行阻塞等待,若被监视的文件描述符上一直无事件就绪,则在该时间后select
进行超时返回。
- 返回值说明:
- 若函数调用成功,则返回有事件就绪的文件描述符个数。
- 若
timeout
时间耗尽,则返回0。- 若函数调用失败,则返回 -1,同时错误码会被设置,可能的错误码有:
EBADF
:文件描述符为无效的或该文件已关闭。EINTR
:此调用被信号所中断。EINVAL
:参数nfds
为负值。ENOMEM
:核心内存不足。
1.3 fd_set结构
fd_set
结构与 sigset_t
结构类似,本质是一个位图,通过位图中对应的位来表示要监视的文件描述符。在调用 select
函数之前,需用 fd_set
结构定义出对应的文件描述符集,然后将需监视的文件描述符添加到该集合中。
/* fd_set for select and pselect. */
typedef struct
{/* XPG4.2 requires this member name. Otherwise avoid the namefrom the global namespace. */#ifdef _USE_XOPEN__fd_mask fds_bits[__FD_SETSIZE / __NDBITS];#define _FDS_BITS(set) ((set)->fds_bits)#else__fd_mask _fds_bits[__FD_SETSIZE / __NDBITS];#define _FDS_BITS(set) ((set)->_fds_bits)#endif
} fd_set;
typedef long int _fd_mask;
这个添加过程虽本质是位操作,但系统提供了一组专门接口来操作 fd_set
类型的位图,如下:
void FD_CLR (int fd, fd_set *set); // 用来清除描述词组 set 中相关 fd 的位
int FD_ISSET (int fd, fd_set *set); // 用来测试描述词组 set 中相关 fd 的位是否为真
void FD_SET (int fd, fd_set *set); // 用来设置描述词组 set 中相关 fd 的位
void FD_ZERO (fd_set *set); // 用来清除描述词组 set 的全部位
1.4 timeval结构
传入 select
函数的最后一个参数 timeout
,是一个指向 timeval
结构的指针。timeval
结构用于描述一段时间长度,该结构包含两个成员,其中 tv_sec
表示秒,tv_usec
表示微秒。
struct timeval {__kernel_time_t tv_sec; /* seconds */__kernel_suseconds_t tv_usec; /* microseconds */
};
总的来说,select
机制为程序同时处理多个文件描述符的事件就绪情况提供了一种有效的方式,通过合理设置其参数及利用相关结构的操作接口,能较好地实现对多个文件描述符的监控与处理,不过在使用过程中也需要注意处理可能出现的各种返回情况及错误码。
1.5 socket 就绪条件
1.5.1 读事件就绪条件
- 接收缓冲区字节数足够
- 当
socket
内核中接收缓冲区的字节数大于等于低水位标记SO_RCVLOWAT
时,可以无阻塞地读取该文件描述符,且读取返回值大于0。- 对端关闭连接
- 在
socket TCP
通信中,如果对端关闭连接,那么对该socket
进行读操作时,会返回0。- 监听socket有新连接请求
- 对于监听的socket,当有新的连接请求到来时,该socket处于读就绪状态。
- 这是服务器端socket常见的就绪情况,用于接受新的客户端连接。
- socket有未处理错误
- 当socket上存在未处理的错误时,它也处于读就绪状态。
- 这种情况需要及时处理错误,以确保socket的正常运行。
1.5.2 写事件就绪条件
- 发送缓冲区有足够空间
- 当
socket
内核中发送缓冲区的可用字节数大于等于低水位标记SO_SNDLOWAT
时,可以无阻塞地进行写操作,且写操作返回值大于0。- 写操作被关闭
- 当
socket
的写操作被关闭(例如通过close
或shutdown
函数)后,对这个写操作被关闭的socket
进行写操作,会触发SIGPIPE
信号。- 非阻塞
connect
操作完成(成功或失败)
- 当
socket
使用非阻塞connect
连接操作完成(无论是连接成功还是失败)后,该socket
处于写就绪状态。socket
有未读取错误
- 当
socket
上存在未读取的错误时,它处于写就绪状态。
1.5.3 异常事件就绪
- 收到带外数据
- 当
socket
收到带外数据时,处于异常就绪状态。带外数据与TCP的紧急模式相关,通过TCP报头中的URG
标志位和16位紧急指针搭配使用来发送和接收带外数据。
2. 服务端代码
然后我们就可以编写一个基于 select
多路转接的 TCP 服务端:
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>// 定义可能出现的错误码
enum
{SocketErr = 1,BindErr,ListenErr
};// 定义最大连接数
const int backlog = 10;class Sock
{
public:Sock() {}public:// 创建套接字void Socket(){// 使用IPv4协议族,流式套接字(TCP)创建套接字_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){// 如果创建套接字失败,输出错误信息并退出程序std::cerr << "socket error..." << std::endl;exit(SocketErr);}int opt = 1;// 设置套接字选项,允许地址重用setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));}// 绑定套接字到指定端口void Bind(uint16_t port){struct sockaddr_in local;// 初始化结构体memset(&local, 0, sizeof(local));local.sin_family = AF_INET;// 将端口转换为网络字节序local.sin_port = htons(port);// 绑定任意本地IP地址local.sin_addr.s_addr = INADDR_ANY;if (bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){// 如果绑定失败,输出错误信息并退出程序std::cerr << "bind error..." << std::endl;exit(BindErr);}}// 监听套接字void Listen(){if (listen(_sockfd, backlog) < 0){// 如果监听失败,输出错误信息并退出程序std::cerr << "listen error..." << std::endl;exit(ListenErr);}}// 接受客户端连接int Accept(std::string *clientip, uint16_t *clientport){struct sockaddr_in peer;socklen_t len = sizeof(peer);// 接受客户端连接,返回新的套接字描述符int newfd = accept(_sockfd, (struct sockaddr *)&peer, &len);if (newfd < 0){std::cout << "accept error..." << std::endl;return -1;}char ipstr[64];// 将网络字节序的IP地址转换为点分十进制字符串inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));*clientip = ipstr;// 将网络字节序的端口转换为主机字节序*clientport = ntohs(peer.sin_port);return newfd;}// 连接到指定IP和端口bool Connect(const std::string &ip, const uint16_t &port){struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(port);// 将点分十进制IP字符串转换为网络字节序inet_pton(AF_INET, ip.c_str(), &peer.sin_addr);int n = connect(_sockfd, (const struct sockaddr *)&peer, sizeof(peer));if (n == -1){// 如果连接失败,输出错误信息并返回falsestd::cerr << "connect to " << ip << ":" << port << "error" << std::endl;return false;}return true;}// 关闭套接字void Close(){close(_sockfd);}// 获取套接字描述符int Fd(){return _sockfd;}private:int _sockfd;
};
#pragma once
#include "Sock.hpp"
#include <sys/select.h>// 定义默认文件描述符值
#define DFL_FD -1
// 定义文件描述符数组的大小
#define NUM 128class SelectServer
{
public:// 构造函数,初始化服务器监听端口SelectServer(int port): _port(port){}// 初始化服务器相关设置,包括创建、绑定和监听套接字void InitSelectServer(){// 创建套接字_listensock.Socket();// 将套接字绑定到指定端口_listensock.Bind(_port);// 开始监听套接字_listensock.Listen();}// 运行服务器,处理客户端连接和数据读取等操作void Run(){fd_set readfds;int fd_array[NUM];// 初始化文件描述符数组,将所有元素设为默认值for (int i = 0; i < NUM; i++){fd_array[i] = DFL_FD;}// 将监听套接字的文件描述符放入数组的第一个位置fd_array[0] = _listensock.Fd();while (true){// 清空读文件描述符集合FD_ZERO(&readfds);int maxfd = DFL_FD;// 遍历文件描述符数组,将有效的文件描述符添加到读文件描述符集合中,并更新最大文件描述符值for (int i = 0; i < NUM; i++){if (fd_array[i] == DFL_FD)continue;FD_SET(fd_array[i], &readfds);if (fd_array[i] > maxfd){maxfd = fd_array[i];}}// 调用select函数等待事件发生// struct timeval timeout = {2, 0};switch (select(maxfd + 1, &readfds, nullptr, nullptr, nullptr)){case 0:// 如果select返回0,表示超时// std::cout << "time out..." << std::endl;break;case -1:// 如果select返回 -1,表示发生错误,输出错误信息std::cerr << "select error" << std::endl;break;default:// 如果select正常返回,调用HandlerEvent处理就绪事件HandlerEvent(readfds, fd_array, NUM);break;}}}// 析构函数,关闭监听套接字~SelectServer(){if (_listensock.Fd() >= 0){_listensock.Close();}}private:// 处理就绪事件的函数void HandlerEvent(const fd_set &readfds, int fd_array[], int num){for (int i = 0; i < num; i++){if (fd_array[i] == DFL_FD)continue;// 如果是监听套接字且有可读事件,表示有新的客户端连接if (fd_array[i] == _listensock.Fd() && FD_ISSET(fd_array[i], &readfds)){struct sockaddr_in peer;socklen_t len = sizeof(peer);memset(&peer, 0, len);std::string clientip;uint16_t clientport;// 接受新的客户端连接,获取客户端的套接字描述符、IP地址和端口号int sock = _listensock.Accept(&clientip, &clientport);std::cout << "get a new link[" << clientip << ":" << clientport << "]" << std::endl;// 将新连接的套接字描述符放入文件描述符数组中,如果数组已满则关闭该套接字并输出提示信息if (!SetFdArray(fd_array, num, sock)){close(sock);std::cout << "select server is full,close fd:" << sock << std::endl;}}// 如果不是监听套接字且有可读事件,表示有数据可读,进行数据读取和处理else if (FD_ISSET(fd_array[i], &readfds)){char buffer[1024];ssize_t n = read(fd_array[i], buffer, sizeof(buffer) - 1);if (n > 0){// 如果读取到数据,添加字符串结束符并输出数据内容buffer[n] = 0;std::cout << "echo# " << buffer << std::endl;}else if (n == 0){// 如果读取到的字节数为0,表示客户端已断开连接,关闭对应的套接字并将数组元素设为默认值std::cout << "client quit..." << std::endl;close(fd_array[i]);fd_array[i] = DFL_FD;}else{// 如果读取发生错误,输出错误信息,关闭对应的套接字并将数组元素设为默认值std::cerr << "read error" << std::endl;close(fd_array[i]);fd_array[i] = DFL_FD;}}}}// 将新的套接字描述符放入文件描述符数组中的函数bool SetFdArray(int fd_array[], int num, int fd){for (int i = 0; i < num; i++){if (fd_array[i] == DFL_FD){fd_array[i] = fd;return true;}}return false;}private:Sock _listensock;int _port;
};
服务器当前调用select
函数时将timeout
参数设置为nullptr
,这使得select
函数调用后会进入阻塞等待状态。
起初,服务器第一次调用select
函数时,仅让其监视监听套接字的读事件。如此一来,在服务器运行后,若没有客户端发送连接请求,监听套接字的读事件就不会变为就绪状态,那么服务器就会一直在这第一次调用的select
函数中持续阻塞等待下去。
当我们利用telnet
工具向该select
服务器发起连接请求时,情况就会发生变化。此时,select
函数能够立刻检测到监听套接字的读事件已经就绪,进而select
函数会成功返回。并且,执行相应的事件处理。
3. select 的缺陷
虽然 select
可以实现多路转接,提升 IO 效率。但是我们在实际应用中,很少会用到 select
,因为:
- 每次调用
select
时,都需要手动设置fd集合,从接口使用的便捷性角度来看,这种操作方式较为繁琐,给开发者带来了不便。- 每次调用
select
,都要把fd
集合从用户态拷贝到内核态。当需要监控的文件描述符数量很多时,这种数据拷贝操作所产生的开销会变得很大,影响系统性能。- 每次调用
select
,内核都需要遍历传递进来的所有fd
。同样,在fd
数量众多的情况下,这个遍历过程所消耗的系统资源也会很大,进一步降低系统的运行效率。
并且 select
可监控的文件描述描述符数量取决于 fd_set
类型的比特位个数。一般情况下 select
可监控的文件描述符个数通常为1024个。这在实际应用中是一个较大的局限,例如在实现 select
服务器时,除去一个监听套接字,最多只能连接1023个客户端,对于一些需要处理大量并发连接的场景,这个数量可能远远不够。
相关文章:

[Linux]IO多路转接(上)
1. IO 多路转接之select 1.1 select概述 select 是系统提供的一个多路转接接口,其核心工作在于等待。它能够让程序同时监视多个文件描述符上的事件是否就绪,只有当被监视的多个文件描述符中有一个或多个事件就绪时,select 才会成功返回&…...

基于Java的药店管理系统
药店管理系统 一:基本介绍开发环境管理员功能模块图系统功能部分数据库表设计 二:部分系统页面展示登录界面管理员管理进货信息界面管理员管理药品信息界面管理员管理员工界面管理员管理供应商信息界面管理员管理销售信息界面员工对信息进行管理员工对销…...

LaTeX之四:如何兼容中文(上手中文简历和中文论文)、在win/mac上安装新字体。
改成中文版 如果你已经修改了.cls文件和主文档,但编译后的PDF仍然显示英文版本,可能有以下几个原因: 编译器问题:确保你使用的是XeLaTeX或LuaLaTeX进行编译,因为它们对Unicode和中文支持更好。你可以在你的LaTeX编辑器…...

Unity自动LOD工具AutoLOD Mesh Decimator的使用
最近在研究大批量物体生成,由于我们没有专业美术,在模型减面工作上没有人手,所以准备用插件来实现LOD功能,所以找到了AutoLOD Mesh Decimator这个插件。 1,导入插件后,我们拿个实验的僵尸狗来做实验。 空…...

Flutter:使用Future发送网络请求
pubspec.yaml配置http的SDK cupertino_icons: ^1.0.8 http: ^1.2.2请求数据的格式转换 // Map 转 json final chat {name: 张三,message: 吃饭了吗, }; final chatJson json.encode(chat); print(chatJson);// json转Map final newChat json.decode(chatJson); print(newCha…...

4000字浅谈Java网络编程
什么是网络编程? 可以让设备中的程序与网络上的其他设备中的程序进行数据交互的技术(实现网络通信)。 基本的通信架构 基本的通信架构有两种形式:CS架构(Client客户端/Server服务端)、BS架构(…...

立体工业相机提升工业自动化中的立体深度感知
深度感知对仓库机器人应用至关重要,尤其是在自主导航、物品拾取与放置、库存管理等方面。 通过将深度感知与各种类型的3D数据(如体积数据、点云、纹理等)相结合,仓库机器人可以在错综复杂环境中实现自主导航,物品检测…...

大模型基础BERT——Transformers的双向编码器表示
大模型基础BERT——Transformers的双向编码器表示 整体概况 BERT:用于语言理解的深度双向Transform的预训练 论文题目:BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding Bidirectional Encoder Representations from…...

怎么禁止Ubuntu自动更新升级
怎么禁止Ubuntu自动更新升级 笔者在做MIT 6.S081的时候发现他给我的qemu自动更新了又卡住了,故关闭了自动更新 文章目录 怎么禁止Ubuntu自动更新升级一、图形化修改二、基于命令行修改配置文件的方法 一、图形化修改 1.打开设置->软件和更新->更新 2.选择自…...

【SpringBoot】20 同步调用、异步调用、异步回调
Git仓库 https://gitee.com/Lin_DH/system 介绍 同步调用:指程序在执行时,调用方需要等待函数调用返回结果后,才能继续执行下一步操作,是一种阻塞式调用。 异步调用:指程序在执行时,调用方在调用函数后立…...

【Excel】数据透视表分析方法大全
数据透视表的最常用的功能是分类汇总,其实它还有很强大的数据分析功能。在数据透视表右键菜单的值显示方式中,可以看到有14个很实用的分析选项。 1、总计的百分比 作用:透视表中每一个数字(包括汇总行、总计行)占右…...

深度学习在边缘检测中的应用及代码分析
摘要: 本文深入探讨了深度学习在边缘检测领域的应用。首先介绍了边缘检测的基本概念和传统方法的局限性,然后详细阐述了基于深度学习的边缘检测模型,包括其网络结构、训练方法和优势。文中分析了不同的深度学习架构在边缘检测中的性能表现&am…...

k8s 1.28.2 集群部署 docker registry 接入 MinIO 存储
文章目录 [toc]docker registry 部署生成 htpasswd 文件生成 secret 文件 生成 registry 配置文件创建 service创建 statefulset创建 ingress验证 docker registry docker registry 监控docker registry ui docker registry dockerfile docker registry 配置文件 S3 storage dr…...

常用的生物医药专利查询数据库及网站(很全!)
生物医药专利信息检索是药物研发前期不可或缺的一步,通过对国内外生物医药专利网站信息查询,可详细了解其专利技术,进而有效降低药物研发过程中的风险。 目前主要使用的生物医药专利查询网站分为两大类,一个是免费生物医药专利查询…...

「QT」几何数据类 之 QPolygon 多边形类
✨博客主页何曾参静谧的博客📌文章专栏「QT」QT5程序设计📚全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasolid…...

写给初学者的React Native 全栈开发实战班
React Native 全栈开发实战班 亲爱的同学们: 很高兴在这里与大家相聚!我是你们的讲师,将带领大家一起踏上 React Native 移动开发的学习之旅。 为什么选择 React Native? 在这个移动互联网时代,App 开发工程师已经…...

工作和学习遇到的技术问题
写在前面 记录工作和学习遇到的技术问题,以求再次遇到可以快速解决。 1:Ubuntu TSL换源报错:Err:1 http://mirrors.aliyun.com/ubuntu focal InRelease 执行如下操作(已经操作的则忽略),首先在文件/etc/apt/sources…...

如何解决JAVA程序通过obloader并发导数导致系统夯住的问题 | OceanBase 运维实践
案例背景 某保险机构客户的数据中台,自系统上线后不久,会定期的用 obload 工具从上游业务系统导入数据至OceanBase数据库。但,不久便遇到了应用服务器的 Memory 与 CPU 资源占用持续攀升,最终导致系统夯住而不可用的异常。 memo…...
Git零基础到入门
一、开始工作区 clone: 克隆一个仓库到新的目录。 git clone https://github.com/username/repository.git init: 创建一个新的空 Git 仓库或重新初始化现有的仓库,新建git项目。 //创建项目两种方式 //一、本地项目自己创建项目,先创建好工作文件夹,通…...
HTTP 1.0、HTTP 1.1 和 HTTP 2.0 区别
HTTP 1.0、HTTP 1.1 和 HTTP 2.0 是超文本传输协议(HTTP)不同版本的规范,各自进行了多项更新和改进: 1. HTTP/1.0 单一请求-响应:每次请求都需要建立一个新的 TCP 连接,完成后立即断开。无状态连接&#…...

Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...

label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

聊聊 Pulsar:Producer 源码解析
一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台,以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中,Producer(生产者) 是连接客户端应用与消息队列的第一步。生产者…...

srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...

【Oracle】分区表
个人主页:Guiat 归属专栏:Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...
大数据学习(132)-HIve数据分析
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言Ǵ…...

企业如何增强终端安全?
在数字化转型加速的今天,企业的业务运行越来越依赖于终端设备。从员工的笔记本电脑、智能手机,到工厂里的物联网设备、智能传感器,这些终端构成了企业与外部世界连接的 “神经末梢”。然而,随着远程办公的常态化和设备接入的爆炸式…...

JVM虚拟机:内存结构、垃圾回收、性能优化
1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...
IP如何挑?2025年海外专线IP如何购买?
你花了时间和预算买了IP,结果IP质量不佳,项目效率低下不说,还可能带来莫名的网络问题,是不是太闹心了?尤其是在面对海外专线IP时,到底怎么才能买到适合自己的呢?所以,挑IP绝对是个技…...