IO多路转接之select

news2024/11/23 16:27:49

本文分享的是IO多路转接中的select,其中包括select函数如何去使用,以及使用相关代码实现客户端向服务端发送消息的服务,从而更好地理解多路转接的select。

多路转接

多路转接是IO模型的一种,这种IO模型通过select函数进行IO等待,并且select函数能够同时等待多个文件描述符的就绪状态,单个文件描述符的等待与阻塞IO类似。

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:需要监视的最大的文件描述符值+1。

要解释readfds、writefds和exceptfds前,先解释它们的类型fd_set类型。

fd_set类型

fd_set是一个整数数组, 更严格的说, 是一个 "位图"。使用位图中对应的位来表示要监视的文件描述符。

在fd_set位图结构中,使用比特位的“位置”来表示某一个sock

而对于比特位的“内容”,首先我们需要知道的是,readfds、writefds和exceptfds三个参数都是输入输出型参数。

以readfds读为例:

用户在使用该参数进行输入时,实质上是用户告诉内核,内核你要帮我关心一下哪些文件描述符上的读事件就绪。

内核进行输出时,实质上是告诉用户,用户你所关心的那些文件描述符上的读事件已经就绪。

于是,对于比特位的“内容”,首先是输入时,是用户想要内核帮忙关心的文件描述符的合集。在输出时,是内核要告诉用户已经就绪的文件描述符的合集

比如,输入时,我们规定用户想要关心的文件描述,在位图结构中,其比特位的位置位1,3,5,于是在输入时,将其内容置为1。那么在输出时,假设这些文件描述符都已经就绪,输出回来时,这个合集中的1,3,5比特位的位置上的内容已经变成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的全部位。

②readfds、writefds和exceptfds三个参数:分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。

在解释参数timeout前,我们先来解释struct timeval结构。

timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。

 函数返回值:

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

错误值可能为:

EBADF 文件描述词为无效的或该文件已关闭。
EINTR 此调用被信号所中断。
EINVAL 参数n 为负值。
ENOMEM 核心内存不足。

③参数timeou:参数timeout为结构timeval,用来设置select()的等待时间。一般timeou参数的取值有三种:

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

理解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= 2, fd=1,则set变为0001,0011。

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

*(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。

*   注意:没有事件发生的fd=5被清空。

需要注意的是,因为select使用输入输出型参数标识不同的含义,因此每一此都会被清空,这意味着,每一次都需要对fd_set进行重新设置!并且,因为需要重新设置,我们需要通过第三方数组来对这些文件描述符进行保存!

代码简单实现多路转换

使用select实现一个简单服务器,客户端可以向服务端发送消息,服务端读取数据。

代码思路:代码分五步:

①创建监听套接字,端口号,绑定,进入监听状态一系列动作。进入监听状态后,不能马上进行accept,因为accept便是阻塞状态,监听套接字本身就可以看作是读事件就绪了。

②准备好一个数组,用于存放套接字。

③select等待前的准备:创建fd_ser类型的变量,并设置相关参数。

④使用select进行等待。在等待后,需要分情况,其返回值是如何。

⑤如果select成功返回读事件已经就绪的文件描述符个数,那么开始进行读取。当然,到达这一步,就证明现在的文件描述符是合法的,然而需要查看在数组中,哪些文件描述符是就绪的了。

找到已经就绪的文件描述符后,还不能马上进行读取,因为有可能该文件描述符是监听套接字,需要进行accept。

确定是用于通信的套接字后,就可以进行读取了。

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

//一、创建监听套接字,端口号,绑定,进入监听状态一系列动作!

//NUM为数组的大小,含义是能够包含NUM个fd,一个fd一个bit
#define NUM (sizeof(fd_set) * 8)//fd_set类型大小为128字节

int fd_array[NUM]; //内容>=0,合法的fd,如果是-1,该位置没有fd

static void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

//需要输入格式: ./select_server 8080
int main(int argc, char *argv[])
{
    //不符合格式
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    //符合格式
    uint16_t port = (uint16_t)atoi(argv[1]);//端口号
    int listen_sock = Sock::Socket();//创建监听套接字
    Sock::Bind(listen_sock, port);//绑定端口号
    Sock::Listen(listen_sock);//服务器进入监听状态


//二、准备好存放fd的数组

    //先将存放fd的数组,全部置为-1。-1表示不合法
    for (int i = 0; i < NUM; i++)
    {
        fd_array[i] = -1;
    }

    // 不会在这里进行accept,accept的本质叫做通过listen_sock获取新链接
    //accept是阻塞式等待
    //站在多路转接的视角,我们认为,链接到来,对于listen_sock,就是读事件就绪!!!
    //对于所有的服务器,最开始的时候,只有listen_sock


//三、select等待前的准备:创建fd_ser类型的变量,并设置相关参数

    //事件循环
    //创建fd_set结构的位图:使用位图中对应的位来表示要监视的文件描述符
    fd_set rfds;
    //将fd数组中的第一个元素,存放为监听套接字
    fd_array[0] = listen_sock;
    //进入循环
    for (;;)
    {
        //用来清除描述词组set的全部位:将位图全部置0,全部清除。
        FD_ZERO(&rfds);
        //创建最大的文件描述符,用于后续select中的第一个参数的设置
        int max_fd = fd_array[0];

        for (int i = 0; i < NUM; i++)
        {
            //不合法,继续
            if (fd_array[i] == -1)
                continue;
            //下面的都是合法的fd
            //FD_SET:用来设置描述词组set中相关fd的位
            FD_SET(fd_array[i], &rfds); //所有要关心读事件的fd,添加到rfds中
            if (max_fd < fd_array[i])
            {
                max_fd = fd_array[i]; //更新最大fd
            }
        }

        struct timeval timeout = {0, 0}; // 5s
        // 我们的服务器上的所有的fd(包括listen_sock),都要交给select进行检测!!
        // recv,read,write,send,accept : 只负责自己最核心的工作:真正的读写(listen_sock:accept)

//四、使用select进行等待

        int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr); //暂时阻塞
        switch (n)
        {
        case -1: //错误发生时则返回-1
            std::cerr << "select error" << std::endl;
            break;
        case 0:   //返回0代表在描述词状态改变前已超过timeout时间,没有返回
            std::cout << "select timeout" << std::endl;
            break;
        default:  //执行成功则返回文件描述词状态已改变的个数
            std::cout << "有fd对应的事件就绪啦!" << std::endl;

//五、成功返回个数,开始进行
        
        //5.1查看是否是就绪fd
            for (int i = 0; i < NUM; i++)
            {
                if (fd_array[i] == -1)
                    continue;
                //下面的fd都是合法的fd,合法的fd不一定是就绪的fd
                //FD_ISSET:用来测试描述词组set中相关fd 的位是否为真
                if (FD_ISSET(fd_array[i], &rfds))
                {
                    std::cout << "sock: " << fd_array[i] << " 上面有了读事件,可以读取了" << std::endl;
                    // 一定是读事件就绪了!!!
                    // 就绪的fd就在fd_array[i]保存!
                    // read, recv时,一定不会被阻塞!
                    // 读事件就绪,就一定是可以recv,read吗??不一定!!

                    //看看数组中的文件描述符,是属于监听套接字还是普通套接字。
                    //如果是监听套接字,那就需要accept
                    if (fd_array[i] == listen_sock)//
                    {
                        std::cout << "listen_sock: " << listen_sock << " 有了新的链接到来" << std::endl;
                        // accept
                        int sock = Sock::Accept(listen_sock);
                        if (sock >= 0)
                        {
                            std::cout << "listen_sock: " << listen_sock << " 获取新的链接成功" << std::endl;
                            // 获取成功
                            // recv,read了呢?绝对不能!
                            // 新链接到来,不意味着有数据到来!!直接读的话被阻塞!什么时候数据到来呢?不知道
                            // 可是,谁可以最清楚的知道那些fd,上面可以读取了?select!
                            // 无法直接将fd设置进select,但是,好在我们有fd_array[]!
                            int pos = 1;
                            for (; pos < NUM; pos++)
                            {
                                if (fd_array[pos] == -1)
                                    break;
                            }
                            // 1. 找到了一个位置没有被使用
                            if (pos < NUM)
                            {
                                std::cout << "新链接: " << sock << " 已经被添加到了数组[" << pos << "]的位置" << std::endl;
                                fd_array[pos] = sock;
                            }
                            else
                            {
                                // 2. 找完了所有的fd_array[],都没有找到没有被使用位置
                                // 说明服务器已经满载,没法处理新的请求了
                                std::cout << "服务器已经满载了,关闭新的链接" << std::endl;
                                close(sock);
                            }
                        }
                    }
                    else  //用于通信的套接字,可以读了
                    {
                        // 普通的sock,读事件就绪啦!
                        // 可以进行读取啦,recv,read
                        // 可是,本次读取就一定能读完吗?读完,就一定没有所谓的数据包粘包问题吗?
                        // 但是,我们今天没法解决!我们今天没有场景!仅仅用来测试
                        std::cout << "sock: " << fd_array[i] << " 上面有普通读取" << std::endl;
                        char recv_buffer[1024] = {0};
                        ssize_t s = recv(fd_array[i], recv_buffer, sizeof(recv_buffer) - 1, 0);
                        if (s > 0)
                        {
                            recv_buffer[s] = '\0';
                            std::cout << "client[ " << fd_array[i] << "]# " << recv_buffer << std::endl;
                        }
                        else if (s == 0)
                        {
                            std::cout << "sock: " << fd_array[i] << "关闭了, client退出啦!" << std::endl;
                            //对端关闭了链接
                            close(fd_array[i]);
                            std::cout << "已经在数组下标fd_array[" << i << "]"
                                      << "中,去掉了sock: " << fd_array[i] << std::endl;
                            fd_array[i] = -1;
                        }
                        else
                        {
                            //读取失败
                            close(fd_array[i]);
                            std::cout << "已经在数组下标fd_array[" << i << "]"
                                      << "中,去掉了sock: " << fd_array[i] << std::endl;
                            fd_array[i] = -1;
                        }
                    }
                }
            }
            break;
        }
    }

    return 0;
}

封装套接字相关接口:

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;

class Sock
{
public:
    static int Socket()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock < 0)
        {
            cerr << "socket error" << endl;
            exit(2);
        }
        int opt = 1;
        setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        return sock;
    }

    static void Bind(int sock, uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            cerr << "bind error!" << endl;
            exit(3);
        }
    }

    static void Listen(int sock)
    {
        if (listen(sock, 5) < 0)
        {
            cerr << "listen error !" << endl;
            exit(4);
        }
    }

    static int Accept(int sock)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = accept(sock, (struct sockaddr *)&peer, &len);
        if(fd >= 0){
            return fd;
        }
        return -1;
    }

    static void Connect(int sock, std::string ip, uint16_t port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));

        server.sin_family = AF_INET;
        server.sin_port = htons(port);
        server.sin_addr.s_addr = inet_addr(ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
        {
            cout << "Connect Success!" << endl;
        }
        else
        {
            cout << "Connect Failed!" << endl;
            exit(5);
        }
    }
};

select的特点

可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)= 512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是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的第一个参数。

select缺点

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

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

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

相关文章

AI浪潮再掀低代码开发热,快来了解最新趋势!

在近些年的发展中&#xff0c;人工智能 (AI) 已融入我们社会和生活的方方面面。从聊天机器人和虚拟助手到自动化工业机械和自动驾驶汽车&#xff0c;我们已经越来越离不开AI技术了&#xff0c;哪怕是我们的日常生活中也充满了它的影子&#xff0c;我们很难忽视它的影响。 AI时代…

小程序、网页跳转App的原理

从不同的渠道&#xff0c;如小程序、二维码、网页等&#xff0c;直接跳转到App内对应的页面&#xff0c;并传递相关的参数信息&#xff0c;已经由拥有深度链接技术的SDK实现了&#xff0c;App只需接入这类SDK即可获得多样化跳转的功能。本文将详细介绍多样化跳转的原理。 多样…

Unity | HDRP高清渲染管线学习笔记:基本操作

目录 一、场景整体环境光强度 1.HDRI Sky 2.Shadows 二、屏幕后处理效果(Post Processing) 1.Exposure 2.Post-processing/Tonemapping 三、抗锯齿 四、添加光源 1.Light Explorer窗口 2.光照探针组 3.反射探针 4.烘焙光照贴图 本文主要是了解HDRP基本操作&#xf…

高完整性系统:Hoare Logic

目录 1. 霍尔逻辑&#xff08;Proving Programs Correct&#xff09; 1.1 警告&#xff08;Caveats&#xff09; 1.2 误解&#xff08;Misconception&#xff09; 1.3 编程语言&#xff08;Programming Language&#xff09; 1.4 程序&#xff08;Programs&#xff09; 1…

Html源代码加密?

什么是Html源代码加密&#xff1f; 使用JavaScript加密转化技术将Html变为密文&#xff0c;以此保护html源代码&#xff0c;这便是Html源码加密。 同时&#xff0c;这种加密技术还可实现网页反调试、防复制、链接加密等功能。 应用场景 什么情况下需要Html源代码加密&#x…

clipboard复制粘题问题

clipboard复制粘贴问题 简单的clipboard用法引入clipboard使用方法 通过监听获取剪切板数据自定义获取clipboard剪切板值 记录下项目中使用clipboard复制粘题问题 简单的clipboard用法 引入clipboard npm install clipboard --save官网地址:传送门 使用方法 通过监听获取剪切…

基于neo4图数据库的简易对话系统

文章目录 一、环境二、思路第一步&#xff1a;输入问句第二步&#xff1a;针对问句进行分析&#xff0c;包括意图识别和实体识别第三步&#xff1a;问句转化第四步&#xff1a;问题回答的模板设计 三、代码解读1. 项目结构2. 数据说明3. 主文件kbqa_test.py解读4. entity_extra…

【第三方库】PHP实现创建PDF文件和编辑PDF文件

目录 引入Setasign/fpdf、Setasign/fpdi 解决写入中文时乱码问题 1.下载并放置中文语言包&#xff08;他人封装&#xff09;&#xff1a;https://github.com/DCgithub21/cd_FPDF 2.编写并运行生成字体文件的程序文件&#xff08;addFont.php&#xff09; 中文字体举例&…

【数据结构】第七周

稀疏矩阵快速转置 【问题描述】 稀疏矩阵的存储不宜用二维数组存储每个元素&#xff0c;那样的话会浪费很多的存储空间。所以可以使用一个一维数组存储其中的非零元素。这个一维数组的元素类型是一个三元组&#xff0c;由非零元素在该稀疏矩阵中的位置&#xff08;行号…

xxl-job的部署及springboot集成使用

介绍 XXL-Job是一个分布式任务调度平台&#xff0c;可进行任务调度、管理和监控&#xff0c;并提供任务分片、失败重试、动态分配等功能。它是一个开源项目&#xff0c;基于Spring Boot和Quartz开发&#xff0c;支持常见的任务调度场景。 XXL-Job的使用相对简单&#xff0c;只…

自学网络安全最细规划(建议收藏)

01 什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防两面…

微信小程序后台:解决微信扫普通链接地址无法跳转到体验版微信的问题,配置普通链接二维码规则解释和理解

微信小程序后台&#xff1a;解决微信扫普通链接地址无法跳转到体验版微信的问题&#xff0c;配置普通链接二维码规则解释和理解 一、现象与原因 最近突然发现微信管理平台中&#xff0c;设置好的普通二维码连接跳转到体验版小程序的功能&#xff0c;没有区分体验版和生产版&a…

条件变量基本使用

一、条件变量 应用场景&#xff1a;生产者消费者问题&#xff0c;是线程同步的一种手段。 必要性&#xff1a;为了实现等待某个资源&#xff0c;让线程休眠。提高运行效率 int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); int pthr…

手把手教你做独立t检验

一、案例介绍 为研究国产四类新药阿卡波糖胶囊的降血糖效果&#xff0c;某医院用40名2型糖尿病患者进行同期随机对照试验。研究者将这些患者随机等分到试验组&#xff08;用阿卡波糖胶囊&#xff09;和对照组&#xff08;用拜唐苹胶囊&#xff09;&#xff0c;分别测得试验开始…

如何使用宝塔面板搭建网站(Linux服务器配置篇)

搭建网站我们需要&#xff1a; 必须是Linux服务器&#xff08;最低要求配置1核1G当然再低些也能运行但是不建议&#xff09;自己的域名&#xff08;可以去阿里云或者腾讯云了解&#xff09;PHP项目 此处展示的是华为云服务器&#xff08;各个服务器的购买和使用差别不大&#…

“以API接口快速获得aliexpress速卖通商品详情-返回值说明

为了方便商家获取速卖通上的商品信息&#xff0c;速卖通提供了API接口来获取商品数据。本文将介绍如何通过API接口获取速卖通商品数据。 一、申请API接口权限 在使用API接口前&#xff0c;首先需要在速卖通官网注册账号并通过实名认证。然后&#xff0c;在个人资料页面找到开…

Java 的多线程浅析

前言 Java 的多线程在当今业界已经成为了一个非常重要的技能。无论您是新手还是经验丰富的程序员&#xff0c;精通 Java 的多线程编程都是至关重要的。因为多线程可以帮助您实现更快的应用程序、更高效的性能以及更出色的用户体验。在这篇文章中&#xff0c;我们将介绍有关 Ja…

【资料分享】PLC中输入输出端子

PLC输入输出分为高速和低速&#xff0c;一般来说不会超出&#xff0c;隔离器MOS的设计。其中具体采用光耦隔离还是数字隔离器隔离&#xff0c;其隔离器件会限制其输入输出的速率&#xff1b;PLC的源型和漏型就取决于最后末端所接的MOS管是如何布置的。 MOS管的源极和漏极 MOS…

Java 注解配合Spring AOP 导入Excel文件

Java 注解配合Spring AOP 导入Excel文件 这个就是把上一篇&#xff0c;封装了一层&#xff1b;根据注解中配置的变量名和方法名&#xff0c;通过JoinPoint获取到对应的对象和方法 注解 import static java.lang.annotation.ElementType.METHOD; import static java.lang.ann…

02 Redis经典五种数据类型介绍及落地运用

命令大全9大类型 String(字符类型)Hash(散列类型)List(列表类型)Set(集合类型)SortedSet(有序集合类型&#xff0c;简称zset)Bitmap(位图)HyperLogLog(统计)GEO(地理)Stream&#xff08;了解&#xff09; string 常用命令 最常用 set key valueget key 同时设置/获取多个键…