[Linux]IO多路转接(上)

1. IO 多路转接之select

1.1 select概述

select 是系统提供的一个多路转接接口,其核心工作在于等待。它能够让程序同时监视多个文件描述符上的事件是否就绪,只有当被监视的多个文件描述符中有一个或多个事件就绪时,select 才会成功返回,并将对应文件描述符的就绪事件告知调用者。

1.2 select函数

  1. 函数原型:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  2. 参数说明:
    • nfds:需要监视的文件描述符中,最大的文件描述符值 + 1。
    • readfds:输入输出型参数。调用时用户告知内核需监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已就绪。
    • writefds:输入输出型参数。调用时告知内核需监视哪些文件描述符的写事件是否就绪,返回时告知哪些文件描述符的写事件已就绪。
    • exceptfds:输入输出型参数。调用时告知内核需监视哪些文件描述符的异常事件是否就绪,返回时告知哪些文件描述符的异常事件已就绪。
    • timeout:输入输出型参数。调用时由用户设置 select 的等待时间,返回时表示 timeout 的剩余时间。其取值有以下几种情况:
      • NULL/nullptrselect 调用后进行阻塞等待,直至被监视的某个文件描述符上的某个事件就绪。
      • 0select 调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,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 读事件就绪条件
  1. 接收缓冲区字节数足够
    • socket 内核中接收缓冲区的字节数大于等于低水位标记 SO_RCVLOWAT 时,可以无阻塞地读取该文件描述符,且读取返回值大于0。
  2. 对端关闭连接
    • socket TCP 通信中,如果对端关闭连接,那么对该 socket 进行读操作时,会返回0。
  3. 监听socket有新连接请求
    • 对于监听的socket,当有新的连接请求到来时,该socket处于读就绪状态。
    • 这是服务器端socket常见的就绪情况,用于接受新的客户端连接。
  4. socket有未处理错误
    • 当socket上存在未处理的错误时,它也处于读就绪状态。
    • 这种情况需要及时处理错误,以确保socket的正常运行。
1.5.2 写事件就绪条件
  1. 发送缓冲区有足够空间
    • socket 内核中发送缓冲区的可用字节数大于等于低水位标记 SO_SNDLOWAT 时,可以无阻塞地进行写操作,且写操作返回值大于0。
  2. 写操作被关闭
    • socket 的写操作被关闭(例如通过 closeshutdown 函数)后,对这个写操作被关闭的 socket 进行写操作,会触发 SIGPIPE 信号。
  3. 非阻塞 connect 操作完成(成功或失败)
    • socket 使用非阻塞 connect 连接操作完成(无论是连接成功还是失败)后,该 socket 处于写就绪状态。
  4. socket 有未读取错误
    • socket 上存在未读取的错误时,它处于写就绪状态。
1.5.3 异常事件就绪
  1. 收到带外数据
    • 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个客户端,对于一些需要处理大量并发连接的场景,这个数量可能远远不够。

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

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

相关文章

推荐一款电脑清理和加速工具:Wise Care 365 Pro

Wise Care 365 Pro是一款可以清理注册表和磁盘垃圾文件&#xff0c;保护个人隐私记录&#xff0c;提高电脑使用安全的软件&#xff0c;是优化系统、提高Windows系统运行速度最好的选择!实时保护注册表不被其他程序未经许可地秘密修改。例如阻止程序更改您的浏览器主页&#xff…

Hook小程序

下载&#xff1a; https://github.com/JaveleyQAQ/WeChatOpenDevTools-Python 配置&#xff1a; pip install -r requirements 实现&#xff1a; 开启小程序开发者模式&#xff0c;类似浏览器F12 效果&#xff1a; 使用&#xff1a; 退出微信&#xff0c;进入安装的目录…

Mysql 8迁移到达梦DM8遇到的报错

在实战迁移时&#xff0c;遇到两个报错。 一、列[tag]长度超出定义 在mysql中&#xff0c;tag字段的长度是varchar(20)&#xff0c;在迁移到DM8后&#xff0c;这个长度不够用了。怎么解决&#xff1f; 在迁移过程中&#xff0c;“指定对象”时&#xff0c;选择转换。 在“列映…

PyQt5 在线环境搭建

1.记得升级pip&#xff0c;升级后就会一路畅通的安装&#xff0c;无论是在终端通过命令安装&#xff0c;还是在pycharm中&#xff0c;通过设置&#xff0c; python -m pip install --upgrade pip 升级后安装pyqt5,pyqt5-tools,pyqt5-stubs。 命令安装 pycharm->设置&…

第8章 利用CSS制作导航菜单

8.1 水平顶部导航栏 水平莱单导航栏是网站设计中应用范围最广的导航设计&#xff0c;一般放置在页面的顶部。水平 导航适用性强&#xff0c;几乎所有类型的网站都可以使用&#xff0c;设计难度较低。 如果导航过于普通&#xff0c;无法容纳复杂的信息结构&#xff0c;就需要在…

将数据上传至hdfs的两种方式:java代码上传、将数据放入kafka中,通过flume抽取

目录 1、 生成一条&#xff0c;使用 java 代码将数据放入hdfs上传。 2、 生成一条&#xff0c;编写kafka生产者&#xff0c;将数据放入kafka。kafka source-->flume -->hdfs sink 场景题&#xff1a; 使用 java 代码随机生成学生信息&#xff0c;学生的学号从 0001 开…

【vue】echarts地图添加蒙版图片,多图层地图实现天气信息展示

实现原理&#xff1a;多层图层叠加实现复杂的信息展示。 <template><div class"wrapper"><el-drawertitle"天气信息":modal"iszz":visible.sync"weatherinfo":direction"direction"><drawer:labelnam…

100+SCI科研绘图系列教程(R和python)

科研绘图系列&#xff1a;箱线图加百分比点图展示组间差异-CSDN博客科研绘图系列&#xff1a;箱线图加蜜蜂图展示组间数据分布-CSDN博客科研绘图系列&#xff1a;小提琴图和双侧小提琴图展示组间差异-CSDN博客科研绘图系列&#xff1a;组间差异的STAMP图的ggplot2实现-CSDN博客…

QT鼠标事件

QT鼠标事件 1.概述 这篇文章介绍如何使用事件和获取事件的信号 2.创建项目 创建一个widget类型项目&#xff0c;在widget.ui文件中添加一个label控件 然后在项目名称上右键选择Add new... 添加文件&#xff0c;选择 C Class 自定义类名Mylabel&#xff0c;选择基类Base …

“双十一”电商狂欢进行时,在AI的加持下看网易云信IM、RTC如何助力商家!

作为一年一度的消费盛会&#xff0c;2024年“双十一”购物狂欢节早已拉开帷幕。蹲守直播间、在主播热情介绍中点开链接并加购&#xff0c;也已成为大多数人打开“双11”的重要方式。然而&#xff0c;在这火热的购物氛围背后&#xff0c;主播频频“翻车”、优质主播稀缺、客服响…

深入浅出rust内存对齐

在 Rust 中&#xff0c;内存对齐是一个重要的概念&#xff0c;它涉及到数据在内存中的存储方式&#xff0c;以及如何优化内存访问的效率。往往一门语言的内存布局以及对齐方式决定了一门语言的性能&#xff0c;因此学会并深入理解rust中内存布局会让我们写出高性能的rust代码&a…

题目练习之二叉树那些事儿(续集)

♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥ ♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥ ♥♥♥我们一起努力成为更好的自己~♥♥♥ ♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥ ♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥ ✨✨✨✨✨✨个人…

【STL栈和队列】:高效数据结构的应用秘籍

前言&#xff1a; C 标准模板库&#xff08;STL&#xff09;为我们提供了多种容器&#xff0c;其中 stack&#xff08;栈&#xff09;和 queue&#xff08;队列&#xff09;是非常常用的两种容器。 根据之前C语言实现的栈和队列&#xff0c;&#xff08;如有遗忘&#xff0c;…

Zabbix 7 最新版本安装 Rocky Linux 8

前言 本实验主要在Rocky Linux 中安装Zabbix&#xff0c;其他centos8、Debian、Ubuntu、Alma Linux都可以安装&#xff0c;就是在中间件有点不同。Nginx就要配置一下&#xff0c;官网给的教程也算是很规范的&#xff0c;就是在MySQL上要自己安装&#xff0c;他没有告诉我们&am…

git新手使用教程

git新手使用教程 一、安装和初始化配置2、新建仓库3.工作区域和文件状态4.添加和提交文件5 git reset回退版本6 使用git diff查看差异7 使用git rm删除文件8 .gitignore忽略文件9 注册GitHub账号10 SSH配置和克隆仓库11 关联本地仓库和远程仓库12 Gitee的使用 由B站视频教程整理…

【GPTs】Email Responder Pro:高效生成专业回复邮件

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | GPTs应用实例 文章目录 &#x1f4af;GPTs指令&#x1f4af;前言&#x1f4af;Email Responder Pro主要功能适用场景优点缺点 &#x1f4af;小结 &#x1f4af;GPTs指令 中文翻译&#xff1a; Email Craft 是一款专门用于…

2024下半年软考系统架构师案例分析题试题与答案--ROS机器人操作系统

一、知识点回顾 ROS(Robot Operating System)是一个用于编写机器人软件的框架。它提供了一系列的工具和库,帮助开发者创建复杂的、可以在多种操作系统上运行的机器人应用程序。 ROS的主要特点包括: 分布式计算能力:ROS提供了一种方式让多个计算机或设备协同工作,通过…

探索Copier:Python项目模板的革命者

文章目录 **探索Copier&#xff1a;Python项目模板的革命者**1. 背景介绍&#xff1a;为何Copier成为新宠&#xff1f;2. Copier是什么&#xff1f;3. 如何安装Copier&#xff1f;4. 简单库函数使用方法4.1 创建模板4.2 从Git URL创建项目4.3 使用快捷方式4.4 动态替换文本4.5 …

密码学知识点整理二:常见的加密算法

常用的加密算法包括对称加密算法、非对称加密算法和散列算法。 对称加密算法 AES&#xff1a;高级加密标准&#xff0c;是目前使用最广泛的对称加密算法之一&#xff0c;支持多种密钥长度&#xff08;128位、192位、256位&#xff09;&#xff0c;安全性高&#xff0c;加密效率…

大模型就业收入高吗?大模型入门到精通,收藏这篇就够了

目前&#xff0c;已经可以说人工智能&#xff08;AI&#xff09;是推动社会进步和产业升级的重要力量。 其中&#xff0c;AI大模型作为人工智能领域的核心技术之一&#xff0c;正引领着新一轮的技术革命。 2024年&#xff0c;AI大模型开发工程师无疑成为了IT行业中最炙手可热…