高级IO -- 多路转接之 select

news2024/11/23 19:17:15

高级IO – 多路转接之 select

文章目录

  • 高级IO -- 多路转接之 select
    • 初识`select`
    • select 函数原型
      • 关于`fd_set`结构
    • select使用示例
      • 编写Sock
      • 编写selectService
      • 测试
    • 理解select执行过程
    • socket就绪条件
      • 读就绪
      • 写就绪
    • select的特点
    • select的缺点
    • select的缺点

初识select

系统提供select函数来实现多路复用 输入/输出 模型

  • select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的
  • 程序会停在 select这里等待,直到被监视的文件描述符有一个或多个发生了状态变化。

select 函数原型

select 的函数原型如下:

#include <sys/select.h> 
int select(int nfds, fd_set *readfds, fd_set *writefds,
			fd_set *exceptfds, struct timeval *timeout);

参数解释:

nfds:

select 是用来等待多个文件描述符变化的,多个文件描述符在底层是用数组来保存的。

因此select要有权力知道当前文件描述符的,因此第一个参数为最大文件描述符+1。

为什么是maxfd+1

​ 因为多个文件描述符是通过数组传来的,而文件描述符和数组的下标是一一对应的,都是从0开始的,因此我们需要让select知道当前管理的文件描述符的范围,因此传入最大的文件描述符+1。这里传入+1,是在循环的时候保证左闭右开罢了。

readfds/writefds/exceptfds :

这三个参数分别对应需要检测的可读文件描述符的集合,可写文件描述符的集合以及异常文件描述符的集合。

在此使用readfds举例:

readfds是一个输入输出型参数:

  • 输入:用户告诉操作系统,你要帮我关心一下所设置的多个文件描述中读事件是否就绪,如果读事件就绪就立马告诉我。
  • 输出:内核告诉用户,用户曾经让我关心的文件描述符有哪些已经就绪了

关于fd_set结构

fd_set:其实这个结构就是一个整数数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符.

  • 比特位的位置代表fd的编号
  • 比特位的内容代表 ‘是否’ 的概念,其中 1表示是,0表示否。

提供了一组操作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的全部位

那么这里就会存在一个问题:输入和输出用的是同一张位图吗?

答案:是一张位图,只不过需要重新添加,再次设置。因为同一张位图是覆盖式的,每次重新使用都需要重新设置 [在下面select的特点中有提到]

timeout: 的结构为 timeval,是用来设置select()的等待时间的

  • NULL:则表示select()没有timeoutselect将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

函数返回值:

  • 执行成功则返回文件描述词状态已经改变的个数。
  • 返回0代表在描述词状态改变前已超过timeout时间,没有返回。
  • 当有错误发生时返回-1。错误原因存于 errno,此时参数readfdswritefds, exceptfdstimeout的值变成不可预测。

select使用示例

编写Sock

基本的tcp套接字代码,这里做了封装,不再赘述。

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cerrno>
#include <cassert>

class Sock
{
public:

    static const int gbacklog = 20;

    static int Socket()
    {
        int listenSock = socket(PF_INET,SOCK_STREAM,0);
        if(listenSock < 0){
            exit(1);
        }
        int opt = 1;
        setsockopt(listenSock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));
        return listenSock;
    }

    static void Bind(int socket,uint16_t port)
    {
        struct sockaddr_in local;//用户栈
        memset(&local,0,sizeof(local));
        local.sin_family = PF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        
        if(bind(socket,(const struct sockaddr *)&local,sizeof(local))<0)
        {
            exit(2);
        }
    }

    static void Listen(int socket)
    {
        if(listen(socket,gbacklog) < 0)
        {
            exit(3);
        }
    }

    static int Accept(int socket,std::string * clientip,uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        
        int serviceSock = accept(socket,(struct sockaddr*)&peer,&len);
        if(serviceSock <0){
            return -1;//获取链接失败
        }
        if(clientport) *clientport = ntohs(peer.sin_port);
        if(clientip) *clientip = inet_ntoa(peer.sin_addr);
        return serviceSock;
    }
};

编写selectService

#include <iostream>
#include <sys/select.h>
#include "Sock.hpp"

int fdsArray[sizeof(fd_set) * 8] = {0}; // 保存历史上所有的合法fd
int gnum = sizeof(fdsArray) / sizeof(fdsArray[0]);

#define DFL -1

using namespace std;

static void showArray(int arr[], int num)
{
    cout << " 当前合法sock list# ";
    for (int i = 0; i < num; i++)
    {
        if (arr[i] == DFL)
            continue;
        else
            cout << arr[i] << " ";
    }
    cout << endl;
}

static void HandlerEvent(int listensock, fd_set &readfds)
{
    for (int i = 0; i < gnum; i++)
    {
        if (fdsArray[i] == DFL)
            continue;
        if (i == 0 && fdsArray[i] == listensock)
        {
            if (FD_ISSET(listensock, &readfds))
            {
                cout << "已经有一个新连接到来了,需要进行获取了" << endl;
                string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(listensock, &clientip, &clientport);
                if (sock < 0)
                    return;
                cout << "获取新连接成功: " << clientip << " : " << clientport << " | sock: " << sock << endl;

                // read/write -- 不能 因为read不知道底层数据是否就绪 select知道
                // 需要将fd托管给select
                int i = 0;
                for (; i < gnum; i++)
                {
                    if (fdsArray[i] == DFL)
                        break;
                }
                if (i == gnum)
                {
                    cerr << "我的服务器已经到达最大上限了,无法承载更多同时保持的链接了" << endl;
                    close(sock);
                }
                else
                {
                    fdsArray[i] = sock;        // 将sock添加到select中,进行进一步的监听就绪事件了
                    showArray(fdsArray, gnum); // 打印
                }
            }
        }
        else
        {
            // 处理普通sock的IO事件
            if (FD_ISSET(fdsArray[i], &readfds))
            {
                char buffer[1024];
                ssize_t s = recv(fdsArray[i], buffer, sizeof(buffer), 0); // 不会阻塞
                if (s > 0)
                {
                    buffer[s] = 0;
                    cout << "client[ " << fdsArray[i] << "]# " << buffer << endl;
                }
                else if (s == 0)
                {
                    cout << " client[ " << fdsArray[i] << "] quit,server close " << fdsArray[i] << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DFL;
                    showArray(fdsArray, gnum);
                }
                else
                {
                    cout << " client[ " << fdsArray[i] << "] error,server close " << fdsArray[i] << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DFL;
                    showArray(fdsArray, gnum);
                }
            }
        }
    }
}

static void usage(std::string process)
{
    cerr << "\nUsage: " << process << " port\n"
         << endl;
}

// ./SelectServer 8080
// 只关心读事件
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    int listensock = Sock::Socket();
    Sock::Bind(listensock, atoi(argv[1]));
    Sock::Listen(listensock);

    // 初始化全为-1 表示均可用
    for (int i = 0; i < gnum; i++)
    {
        fdsArray[i] = DFL;
    }
    fdsArray[0] = listensock;
    while (true)
    {
        // 在每次进行select的时候进行参数的重新设定
        int maxFd = DFL;
        fd_set readfds;
        FD_ZERO(&readfds);
        for (int i = 0; i < gnum; i++)
        {
            if (fdsArray[i] == DFL)
                continue;                  // 1.过滤掉不合法的fd
            FD_SET(fdsArray[i], &readfds); // 2.添加所有的合法fd到readfds中,方便select统一进行就绪监听
            if (maxFd < fdsArray[i])
            {
                maxFd = fdsArray[i]; // 3.更新maxFd
            }
        }

        struct timeval timeout = {100, 0};
        int n = select(maxFd + 1, &readfds, nullptr, nullptr, &timeout);
        switch (n)
        {
        case 0:
            cout << "time out .... " << (unsigned long)time(nullptr) << endl;
            break;
        case -1:
            cerr << errno << " : " << strerror(errno) << endl;
            break;
        default:
            HandlerEvent(listensock, readfds);
            break;
        }
    }

    return 0;
}

测试

测试

理解select执行过程

理解select模型的关键在于理解fd_set,为方便说明,取fd_set长度为1字节,fd_set中的每一个bit可以对应一个文件描述符fd,则1字节长的fd_set最大可以对应8个fd

(1)执行fd_set set; FD_ZERO(&set);则set用位图表示是0000,0000

(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

(3)若再加入fd=2fd=1,则set变为0001,0011

(4)执行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

socket就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件 描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;

写就绪

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

select的特点

  • 可监控的文件描述符个数取决与sizeof(fd_set)的值. 每bit表示一个文件描述符,假如一个服务器上sizeof(fd_set) = 512 则该服务器上支持的最大文件描述符是512*8=4096.
  • fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd
    • 一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
    • 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

备注: fd_set的大小可以调整,可能涉及到重新编译内核…

select的缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小

select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

备注: fd_set的大小可以调整,可能涉及到重新编译内核…

select的缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小

(本篇完)

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

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

相关文章

2023同为测试你的薪资为何比别人低?

软件测试这个行业我见过工作5年依然是初级水平的功能测试&#xff0c;拿着不到1W的薪资&#xff0c;也见过1年时间达到高级自动化水平的大牛&#xff01;拿着20K的薪资。 影响你薪资的有两点&#xff01; 一、你的技术水平高低直接决定你薪资的多少&#xff0c;工作时间长短并…

3D引擎和渲染

常见的三维引擎 三维引擎按平台可分为客户端三维引擎&#xff0c;Web端三维引擎&#xff1b;按用途可分为游戏三维引擎、CAD/CAM/CAE三维引擎&#xff1b; 游戏引擎常见的有UE4、Unity3D&#xff1b;CAD工程用的三维引擎又分商用的还有开源的&#xff0c;商用的比较著名的有A…

华为OD-2023B卷-太阳能板最大面积(java)

2.华为OD-2023B卷 -太阳能板最大面积(回到目录) 太阳能板最大面积 知识点分治 时间限制:1s 空间限制:32MB 限定语言:不限 难度:★★ 中规中矩的双指针题目。暴力for循环过不了全部样例。 题目描述: 给航天器一侧加装长方形或正方形的太阳能板(图中的红色斜线区域),需…

uniapp请求封装入门级实战记录

简单记录一下uniapp请求进行封装的相关内容,方便以后查看! 1.目录结构 2.封装文件 2.1 全局公共方法封装 2.2 请求方法封装 2.3 项目请求路径封装 2.4 常用变量封装 2.5 页面使用 1.目录结构 项目根目录下创建common文件夹,包含内容: …

springBoot源码分析如何加载配置文件

前言&#xff1a;springBoot的版本是 2.2.4.RELEASE 一、入口 /*** Run the Spring application, creating and refreshing a new* {link ApplicationContext}.* param args the application arguments (usually passed from a Java main method)* return a running {link A…

DM5加密

MD5加密 概述 MD5&#xff08;Message Digest Algorithm 5&#xff09;是一种常用的哈希函数&#xff0c;能够将任意长度的消息压缩成一个128位的哈希值。它由Ronald Rivest在1991年设计&#xff0c;其设计目标是用来取代较早的MD4算法。 MD5算法通过多次处理分组数据来生成…

2023年5月青少年机器人技术等级考试理论综合试卷(三级)

青少年机器人技术等级考试理论综合试卷&#xff08;三级&#xff09;2023.06 分数&#xff1a; 100 题数&#xff1a; 30 一、 单选题(共 20 题&#xff0c; 共 80 分) 1.如图所示电路&#xff0c; 下列说法正确的是&#xff1f; &#xff08; &#xff09; A.电路中电阻 R3 和…

Kafka有几种消费者分区分配策略?

Range范围分配策略 Range范围分配策略是Kafka默认的分配策略&#xff0c;它可以确保每个消费者消费的分区数量是均衡的。 注意&#xff1a;Rangle范围分配策略是针对每个Topic的。 配置 配置消费者的partition.assignment.strategy为org.apache.kafka.clients.consumer.Ran…

JMeter如何从数据库中获取数据并作为变量使用?

目录 前言 1、JMeter连接MySQL数据库 2、线程组下新建一个 JDBC Connection Configuration 配置元件 3、实现数据库的查询-单值引用 4、实现数据库的查询-多值引用 总结&#xff1a; 前言 JMeter如何从数据库中获取数据并作为变量使用&#xff1f;这在我们使用JMeter做接…

Selenium Python教程第5章

5. 等待页面加载完成(Waits) 现在的大多数的Web应用程序是使用AJAX技术。当一个页面被加载到浏览器时&#xff0c;该页面内的元素可以在不同的时间点被加载。这使得定位元素变得困难。如果元素不再页面之中&#xff0c;会抛出 ElementNotVisibleException 异常。 使用 waits功能…

蓝桥杯单片机定时器不够用?PCA大力助你测距超声波!

在国赛的练习中遇到了定时器不够用的问题&#xff0c;也在网上有查阅到许多蓝桥杯单片机的用PCA定时器测距超声波的例子&#xff0c;但在移植实践运用了几个人的代码后总是各种各样的的问题不好用&#xff0c;因此深感有必要自己好好研究下&#xff0c;终于在一番摸爬中写出了用…

Shapr3d建模制图软件大学生教育优惠免费1年申请教程

前言介绍 shapr3d是iOS系统上一款专业的3D建模App&#xff0c;目前已经有window版本&#xff0c;主要搭配iPad Pro与Apple Pencil使用&#xff0c;它的特点是易学、方便与专业。我们可以使用它可以在几分钟内就可以把草图上的想法变成设计图。 从工程项目到珠宝设计&#xff…

ADAudit Plus:保护企业信息安全的强大内部审计解决方案

内部安全审计在现代企业中扮演着至关重要的角色。它是确保组织网络和系统安全的关键步骤&#xff0c;帮助企业发现和解决潜在的安全风险和漏洞。在这个信息技术高度发达的时代&#xff0c;保护企业的敏感数据和防范内部和外部威胁变得尤为重要。 内部安全审计 ADAudit Plus是一…

Windows本地账号数据迁移工具,可迁移本地账号数据到域账号,包括配置文件,桌面文件,浏览器收藏,聊天记录等。

Transwiz可以很容易地将您的个人数据和设置转移到新计算机: 用于备份和恢复用户配置文件的简单向导界面 下载链接 链接:https://pan.baidu.com/s/1LWmplUgHYg9ut3QLMnFslg?pwd=ogpx 提取码:ogpx 以下为Transwiz工具的使用教程实录: 创建一台虚拟机,我在上面简单模拟…

基于STL的 演讲比赛流程管理系统

目录 一、演讲比赛程序要求 1、比赛规则 2、程序功能 二、创建管理类 三、菜单功能 四、退出功能 五、演讲比赛功能 1、功能分析 2、创建选手类 3、进行比赛 4、保存分数 六、查看比赛记录 1、读取记录分数 2、查看记录功能 3、bug解决 七、清空功能 一、演讲比赛…

【Linux】信号(上)

文章目录 &#x1f4d5; 信号入门生活角度的信号技术角度的信号 &#x1f4d5; 信号产生认识 signal 函数键盘产生信号通过系统调用产生信号软件条件产生信号硬件异常产生信号 &#x1f4d5; 核心转储&#x1f4d5; 信号保存信号集函数 &#x1f4d5; 信号处理用户态与内核态处…

如何通过桥接模式重构代码?

文章目录 什么是桥接模式&#xff1f;UML结构图通用代码实现适用场景案例场景分析⽤⼀坨坨代码实现桥接模式重构代码代码实现⽀付类型桥接抽象类⽀付类型的实现定义⽀付模式接⼝测试 总结 同类的业务、同样的功能&#xff0c;怎么就你能写出来那么多if else。 很多时候你写出来…

【Web服务器】Tomcat的部署

文章目录 前言一、Tomcat 的概念1. Tomcat 核心组件1.1 什么是 servlet1.2 什么是 JSP 2. Tomcat 功能组件结构2.1 Container 结构分析 3. Tomcat 请求过程4. 配置文件4.1 安装目录4.2 conf 子目录 二、Tomcat 服务部署1. 下载并安装 JDK1.1 关闭防火墙&#xff0c;将安装 Tomc…

Milvus Lite 已交卷!轻量版 Milvus,主打就是一个轻便、无负担

想要体验世界上最快的向量数据库&#xff1f;缺少专业的工程师团队作为支撑&#xff1f;Milvus 安装环境受限&#xff1f; 别担心&#xff0c;轻量版 Milvus 来啦&#xff01; 在正式介绍 Milvus Lite 之前&#xff0c;先简单回顾一下 Milvus。Milvus 是一款开源的向量数据库&a…

logstash启动时默认连接本机节点elasticsearch问题

背景 今天在排查处理一个logstash读取kafka数据写入到hdfs的问题时候&#xff0c;发现在启动日志中多了个 logstash.outputs.elasticsearch 连接的地址是localhost:9200 部分日志如下&#xff1a; 排查过程说明 1、首先确认 logstash 启动的配置文件中的 output 配置&#x…