Linux:IO多路转接之select

文章目录

  • select
    • timeval结构体
    • fd_set
  • 优缺点分析
  • 完整代码

本节要介绍的主题是多路转接式IO

select

先说结论,这个select是做什么的呢?

select是负责在Linux系统中,让一个人可以有多个鱼竿,可以不停的进行轮询,只要有一个准备好了就可以进行等待,先看一下它的函数参数:

在这里插入图片描述
这个函数参数还是有点复杂的,下面对于这些函数的参数进行解析:

首先是第一个参数nfds,这个参数的值是最大的文件描述符的值加一,比如现在有1234,对于这四个文件描述符来说,要填写的第一个参数的值就是5

下面看一下返回值:

在这里插入图片描述
简单来说,对于返回值n来说,如果n是大于0的,表示的是有n个fd已经就绪了,如果n是等于0的,表示的是超时,虽然没有错误,但是也没有资源就绪,如果n是小于0的,表示的是出错了,比如可能文件描述符被关了等等

timeval结构体

下面的参数是这个timeval结构体:

在这里插入图片描述
对于这个结构体来说,首先有两个成员,一个代表的是秒,一个代表的是微妙,这个参数的主要目的是给select设置一个等待的方式,比如可以进行一些合适的设置,使得这个select可以在规律的周期性醒来,如果要是把这个参数设置为0,表示的就是立马返回,其实就是一个非阻塞,不过一般也不这么设置,不过是可以这样设置的

同时需要注意的是,对于select当中,这个参数是一个输入输出参数,它不仅是输入,而且还会输出,输出的信息是剩余的时间,比如输入的是五秒钟,但是经过2秒钟资源就已经就绪了,那么就会返回3秒钟,表示还剩下3秒钟

fd_set

下面要进入的是select当中最重要的一个模块,fd_set类型的参数,这个参数是一个内核的数据类型,其实就是所谓的位图,这个参数主要是设置要监听什么事件,正常来说我们比较关心的是这个文件描述符的读写事件

比如现在要设置文件描述符是012的这三个内容,我们要关心它的写事件,那么就可以把位图的信息从0000 0000设置为0000 0111,而其中比特位的位置,表示的是文件描述符的编号,而其中的比特位的内容,表示的是这个东西内核是否需要关心

这个参数也是一个输入输出型的参数,在进行输入的时候,用户告诉内核,我要关心的是一个或者多个fd,你来帮我进行检测一下上面的读时间,如果要是检测到了,你要告诉我,而进行输出的时候,是内核告诉用户,你让我关心的这些事件当中,已经有xxx已经就绪了,你来进行读取吧,这就是这个位图可以带给用户的信息

说白了,这个位图的意义就是来让用户和内核进行交互,来查看fd是否已经就绪的信息的,这就意味着在进行select的操作当中,是有很多的位图操作的

那么下面,就用代码来对于这些内容进行验证:

// socket.hpp
#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>
#include "Log.hpp"

enum
{
    SocketErr = 2,
    BindErr,
    ListenErr,
};

// TODO
const int backlog = 10;

class Sock
{
public:
    Sock()
    {
    }
    ~Sock()
    {
    }

public:
    void Socket()
    {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0)
        {
            lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
            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);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
            exit(BindErr);
        }
    }
    void Listen()
    {
        if (listen(sockfd_, backlog) < 0)
        {
            lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
            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)
        {
            lg(Warning, "accept error, %s: %d", strerror(errno), errno);
            return -1;
        }
        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);

        return newfd;
    }
    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);
        inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));

        int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
        if(n == -1) 
        {
            std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
            return false;
        }
        return true;
    }
    void Close()
    {
        close(sockfd_);
    }
    int Fd()
    {
        return sockfd_;
    }

private:
    int sockfd_;
};
// selectserver.hpp
#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"
using namespace std;

const uint16_t defaultport = 8888;

class selectserver
{
public:
    selectserver(uint16_t port = defaultport) : _port(port)
    {
    }
    ~selectserver()
    {
    }
    bool Init()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();

        return true;
    }
    void Start()
    {
        int listensock = _listensock.Fd();
        for (;;)
        {
            fd_set rfds;
            FD_ZERO(&rfds);
            // 设置监听
            FD_SET(listensock, &rfds);
            struct timeval timeout = {1, 0};
            int n = select(5, &rfds, nullptr, nullptr, /*&timeout*/nullptr);
            switch (n)
            {
            case 0:
                cout << "timeout : " << timeout.tv_sec << "." << timeout.tv_usec << endl;
                break;
            case -1:
                cerr << "select error" << endl;
                break;
            default:
                cout << "get a new link" << endl;
                // 对select进行处理
                break;
            }
        }
    }

private:
    Sock _listensock;
    uint16_t _port;
};

对上述代码进行运行,进行链接后会发现,确实可以监听到效果

在这里插入图片描述
但是会非常快的打满整个屏幕,这告诉我们下面的结论

  1. 如果事件就绪了,但是上层不处理,select会一直通知用户
  2. select告诉就绪了,那么在接下来的一次读取的时候不会阻塞,因为事件已经就绪了

现在的这份代码注定是不完全的,起码对于建立的链接没有进行处理,所以下一步对于这样的链接要进行后续的处理,那现在的问题是,在进行处理的时候该如何进行处理?

由上面的结论可以看出,的确在select就绪的时候,说明下一次的读取是不会进行阻塞,可以直接进行读取的,因此在建立链接这件事上,是可以直接accept的,但是accept之后的内容呢?比如accept之后要进行接受数据,可以直接read吗?答案是不可以的,因为在建立链接之后用户未必会给你发消息,所以此时作为服务端要做的是要继续进行下一轮等待,再次进行read等待

所以等待也是要进行区分的,等的是accept还是read?所以在进行处理等到了的函数中,必然要对于等待的内容进行区分,如果等待的是accept,那么就建立链接,然后去等read,如果等待的是read,那么就可以直接去调用read了,所以下面继续对于这部分内容进行完善,我们要添加一个数组用来描述的建立的一个一个的文件描述符,位图的大小*8即可

void Dispatcher(fd_set &rfds)
{
    // 对于等待的信息进行循环等待
    for(int i = 0; i < fd_num_max; i++)
    {
        int fd = fd_array[i];
        // 如果这个fd没被使用过,就跳过它
        if(fd == defaultfd)
            continue;
        if(FD_ISSET(fd, &rfds))
        {
            // 如果是建立链接的select
            if(fd == _listensock.Fd())
                Accepter();
            // 如果是等待读取信息的select
            else
                Recver(fd, i);
        }
    }
}

如上所示的是一个基本的逻辑,对于要建立链接的select,就让他去建立链接,如果是要建立读取的select,就让他去执行读取的逻辑

那我们先处理建立链接的select:

void Accepter()
{
    // 接收客户端的ip和端口号
    string clientip;
    uint16_t clientport = 0;
    int sock = _listensock.Accept(&clientip, &clientport);
    if(sock < 0)
        return;
    lg(Info, "accept new link, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);
    // 对于建立好的链接要去让它们进行等待select
    int pos = 1;
    // 建立链接要进行判断select还有没有空余位置,如果select都满了,那对于建立新的链接就无能为力了
    for(; pos < fd_num_max; pos++)
    {
        if(fd_array[pos] != defaultfd)
            continue;
        else
            break;
    }
    // 如果当前select已经满了,说明已经不能再建立新的链接了
    if(pos == fd_num_max)
    {
        lg(Warning, "server is full, close %d", sock);
        close(sock);
    }
    // 如果当前select没有满,那么就说明此时可以去进行等待了
    else
    {
        fd_array[pos] = sock;
    }
}

那如果当前识别到时要进行读取的select,说明接下来就可以直接进行读取了,不会进行阻塞了,下层已经把数据送上来了:

void Recver(int fd, int pos)
{
    char buffer[1024];
    // 此时可以直接进行读取,不会阻塞,因为已经是就绪了才会加到select当中的
    ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
    if (n > 0)
    {
        buffer[n] = 0;
        cout << "get message " << buffer << endl;
    }
    else if (n == 0)
    {
        // 如果是0,就说明客户端已经退出了,那么服务端也就不用维护这段链接了
        lg(Info, "client quit, server quit, close fd is %d", fd);
        close(fd);
        // 将对应的信息重新设置为-1,表示的是这个位置可以接收新的select了
        fd_array[pos] = defaultfd;
    }
    else
    {
        // 如果是这样,就是接收失败了,这里也把这个链接直接关掉就可以了
        lg(Warning, "read error, close fd is %d", fd);
        close(fd);
        // 将对应的信息重新设置为-1,表示的是这个位置可以接收新的select了
        fd_array[pos] = defaultfd;
    }
}

测试一下上面的代码

在这里插入图片描述
这样我们就完成了一个基本的select的多路转接

优缺点分析

优点

select有什么优点和缺点呢?对于select服务器来说,它的优点是比较明显的,因为它已经实现了一种多路转接的方案,在用单进程的方式实现了处理多个用户的请求,只要有内容就绪,那么就可以设置为就绪,用了一个辅助数组来标记到底有哪些数据已经就绪了

缺点

select的缺点也比较明显

  1. 等待的fd是有上限的,在我们当前这个版本来说,它能等待的最大值是1024,也就是说超过来了这个1024我们的处理方式是直接把链接的这个socket丢弃
  2. 输入输出型参数比较多,数据拷贝的频率比较高
  3. 输入输出型参数比较多,每次都要对关心的fd进行事件重置
  4. 在用户层来说,在使用第三方数组进行管理fd的时候,要进行很多次的遍历,在内核中检测fd的事件就绪的时候,也要进行遍历

完整代码

#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"
using namespace std;

const uint16_t defaultport = 8888;
const int fd_num_max = sizeof(fd_set) * 8;
int defaultfd = -1;

class selectserver
{
public:
    selectserver(uint16_t port = defaultport) : _port(port)
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            fd_array[i] = defaultfd;
        }
    }
    ~selectserver()
    {
    }
    bool Init()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();

        return true;
    }
    void Accepter()
    {
        // 接收客户端的ip和端口号
        string clientip;
        uint16_t clientport = 0;
        int sock = _listensock.Accept(&clientip, &clientport);
        if (sock < 0)
            return;
        lg(Info, "accept new link, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);
        // 对于建立好的链接要去让它们进行等待select
        int pos = 1;
        // 建立链接要进行判断select还有没有空余位置,如果select都满了,那对于建立新的链接就无能为力了
        for (; pos < fd_num_max; pos++)
        {
            if (fd_array[pos] != defaultfd)
                continue;
            else
                break;
        }
        // 如果当前select已经满了,说明已经不能再建立新的链接了
        if (pos == fd_num_max)
        {
            lg(Warning, "server is full, close %d", sock);
            close(sock);
        }
        // 如果当前select没有满,那么就说明此时可以去进行等待了
        else
        {
            fd_array[pos] = sock;
        }
    }
    void Recver(int fd, int pos)
    {
        char buffer[1024];
        // 此时可以直接进行读取,不会阻塞,因为已经是就绪了才会加到select当中的
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = 0;
            cout << "get message " << buffer << endl;
        }
        else if (n == 0)
        {
            // 如果是0,就说明客户端已经退出了,那么服务端也就不用维护这段链接了
            lg(Info, "client quit, server quit, close fd is %d", fd);
            close(fd);
            // 将对应的信息重新设置为-1,表示的是这个位置可以接收新的select了
            fd_array[pos] = defaultfd;
        }
        else
        {
            // 如果是这样,就是接收失败了,这里也把这个链接直接关掉就可以了
            lg(Warning, "read error, close fd is %d", fd);
            close(fd);
            // 将对应的信息重新设置为-1,表示的是这个位置可以接收新的select了
            fd_array[pos] = defaultfd;
        }
    }
    void Dispatcher(fd_set &rfds)
    {
        // 对于等待的信息进行循环等待
        for (int i = 0; i < fd_num_max; i++)
        {
            int fd = fd_array[i];
            // 如果这个fd没被使用过,就跳过它
            if (fd == defaultfd)
                continue;
            if (FD_ISSET(fd, &rfds))
            {
                // 如果是建立链接的select
                if (fd == _listensock.Fd())
                    Accepter();
                // 如果是等待读取信息的select
                else
                    Recver(fd, i);
            }
        }
    }

    void Start()
    {
        int listensock = _listensock.Fd();
        fd_array[0] = listensock;
        for (;;)
        {
            fd_set rfds;
            FD_ZERO(&rfds);
            // 设置监听
            int maxfd = fd_array[0];
            // 循环判断有哪些需要被监听
            for (int i = 0; i < fd_num_max; i++)
            {
                if (fd_array[i] == defaultfd)
                    continue;
                FD_SET(fd_array[i], &rfds);
                if (maxfd < fd_array[i])
                {
                    maxfd = fd_array[i];
                    lg(Info, "max fd update, max fd is: %d", maxfd);
                }
            }
            struct timeval timeout = {0, 0};
            int n = select(5, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
            switch (n)
            {
            case 0:
                cout << "timeout : " << timeout.tv_sec << "." << timeout.tv_usec << endl;
                break;
            case -1:
                cerr << "select error" << endl;
                break;
            default:
                cout << "get a new link" << endl;
                // 对select进行处理
                Dispatcher(rfds);
                break;
            }
        }
    }

private:
    Sock _listensock;
    uint16_t _port;
    int fd_array[fd_num_max];
};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C语言基础】:文件操作详解(前篇:准备知识)

文章目录 一、什么是文件以及文件的分类1.1 程序文件1.2 数据文件1.3 文件名 二、文本文件和二进制文件2.1 数据在文件中的存储 三、文件的打开和关闭3.1 流和标准流3.1.1 流3.1.2 标准流 3.3 文件指针3.5 文件的打开和关闭 一、什么是文件以及文件的分类 文件是指存储在计算机…

【SCI绘图】【曲线图系列1 python】绘制扫描点平滑曲线图

SCI&#xff0c;CCF&#xff0c;EI及核心期刊绘图宝典&#xff0c;爆款持续更新&#xff0c;助力科研&#xff01; 本期分享&#xff1a; 【SCI绘图】【曲线图1 python】绘制扫描点平滑曲线图 1.环境准备 python 3 import numpy as np import pandas as pd import proplot …

代码随想第31天 | 122.买卖股票的最佳时机II 、 55. 跳跃游戏 、 45.跳跃游戏II

一、前言 参考文献&#xff1a;代码随想录 今天主要还是贪心&#xff0c;但是是比较难想到的题目&#xff0c;不管那么多吧&#xff0c;直接做吧&#xff01; 二、买卖股票的最佳时机|| 1、思路&#xff1a; 这个思路我确实想不出来&#xff0c;但是这个思路又异常的简单&…

MySQL复制拓扑1

文章目录 主要内容一.安装MySQL服务器1.MySQL 安装程序和其它文件保存在下发的 mysql8-files.iso 镜像文件中&#xff0c;可以使用虚拟光驱来提取到 Linux 文件系统。代码如下&#xff08;示例&#xff09;: 2.将 MySQL8.0 程序解压到 /opt 目录&#xff0c;再创建到 MySQL 默认…

c# wpf LiveCharts 绑定 简单试验

1.概要 c# wpf LiveCharts 绑定 简单试验 2.代码 <Window x:Class"WpfApp3.Window2"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d"http://schem…

嵌入式驱动学习第六周——内核函数调用(堆栈打印)

前言 在内核中&#xff0c;函数调用堆栈非常重要&#xff0c;因为它可以帮助开发人员理解代码是如何执行的&#xff0c;从而进行调试、性能优化或问题排查。堆栈可以显示当前执行的函数以及导致该函数调用的先前函数&#xff0c;从而形成一个函数调用链。本篇博客就介绍堆栈打印…

编程新手必看,学习python中字符串数据类型内容(8)

1、 Python3 字符串 字符串是 Python 中最常用的数据类型。我们可以使用引号( ’ 或 " )来创建字符串。 创建字符串很简单&#xff0c;只要为变量分配一个值即可。例如&#xff1a; var1 Hello World! var2 "Runoob"Python 访问字符串中的值 Python 不支持单…

虚拟主机、VPS主机和云服务器的区别

对于每个建站新手来说&#xff0c;首先要解决的就是服务器购买的问题&#xff0c;目前市面有很多类型的服务器&#xff0c;常见的有&#xff1a;阿里云、腾讯云、Vultr云服务器&#xff0c;也有RackNerd、Cloudways等提供的VPS&#xff0c;还有SiteGround、ChemiCloud 、 Hosti…

如何保护大模型API安全

大模型的崛起正在改变着我们对机器学习和人工智能的理解&#xff0c;它们不仅提供了令人惊叹的预测和分析能力&#xff0c;还在各行各业的应用中发挥着重要作用。通过提供 API&#xff0c;用户无需了解底层实现细节&#xff0c;使大型模型能够更好地与用户和应用程序进行交互&a…

C++面向对象程序设计 - 对象指针和this指针

在C学习中&#xff0c;指针是一个用于指向另一个变量的地址的变量。理解指针有一定难度&#xff0c;但是理解它的工作原理后&#xff0c;会发现它们是非常强大和有用的工具。指针可以用来指向一般的变量&#xff0c;也可以指向对象。 一、指向对象的指针 在建立对象时&#xf…

C++——栈和队列容器

前言&#xff1a;这篇文章我们将栈和队列两个容器放在一起进行分享&#xff0c;因为这两个要分享的知识较少&#xff0c;而且两者在结构上有很多相似之处&#xff0c;比如栈只能在栈顶操作&#xff0c;队列只能在队头和队尾操作。 不同于前边所分享的三种容器&#xff0c;这篇…

文件的随机读写--fseek,ftell,拷贝文件

想要查看fseek&#xff0c;ftell&#xff0c;函数&#xff0c;请登录这个网站&#xff1a; cplusplus.com - The C Resources Networkhttps://legacy.cplusplus.com/ 还有一个函数没有写出来&#xff0c;是rewind&#xff0c;它是&#xff1a;让⽂件指针的位置回到⽂件的起始位…

[WIP]Sora相关工作汇总VQGAN、MAGVIT、VideoPoet

视觉任务相对语言任务种类较多(detection, grounding, etc.)、粒度不同 (object-level, patch-level, pixel-level, etc.)&#xff0c;且部分任务差异较大&#xff0c;利用Tokenizer核心则为如何把其他模态映射到language space&#xff0c;并能让语言模型更好理解不同的视觉任…

图片总丢?为何不自己搭建一个图床服务

图片总丢?为何不自己搭建一个图床服务 经常写博客或者Markdown文章的同学都知道,图片资源总莫名其妙丢了,我们或者每次把图片随着md文件移过来换过去,或者找一个提供图床服务的产品,又或者扔到自己的服务器,然后将资源目录发布出来。 但是,这些方法总归存在一些问题,…

【数据结构与算法】力扣 206. 反转链表

题目描述 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a; head [1,2,3,4,5] 输出&#xff1a; [5,4,3,2,1]示例 2&#xff1a; 输入&#xff1a; head [1,2] 输出&#xff1a; [2,1]示例 3&#…

浏览器中的桌面环境daedalOS

什么是 daedalOS &#xff1f; daedalOS 是一款 Web 桌面操作系统环境&#xff0c;但采用了与 GNOME 和 KDE 等传统桌面环境不同的方法。daedalOS 使用 JavaScript 和 TypeScript 编写&#xff0c;能够运行 dos 程序和 16/32 位 windows 程序。daedalOS 创建了一个基于网络的桌…

深入理解计算机系统 家庭作业 2.90

查一下书本的82页图2-36的表就行了 float u2f(unsigned u) {return *(float *) &u; }float fpwr2(int x) {unsigned exp, frac;unsigned u;// 小于最小的非规格化数if (x < -149) {exp 0;frac 0;}// 非规格化数else if (x < -126) {exp 0;frac 1 << (x 1…

LabVIEW深度学习

目录 一、配置环境1.1、显卡选择1.2、下载显卡驱动1.3、下载并安装Anaconda1.4、配置Anaconda软件包下载服务器1.5、配置虚拟环境tf_gpu1.6、安装vscode1.7、安装tensorflow1.8、下载安装Git1.9、安装TensorFlow Object Detection API框架1.10、安装依赖的python软件包1.11、配…

【数据结构与算法】:直接插入排序和希尔排序

1. 排序的概念及其意义 1.1 排序的概念 所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 1.2 排序的稳定性 假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&#xff0c;若…

08 Python进阶:XML 解析

什么是 XML&#xff1f; XML&#xff08;可扩展标记语言&#xff0c;Extensible Markup Language&#xff09;是一种用于表示和传输数据的标记语言。它被设计用来以一种结构化的形式描述文档的内容&#xff0c;并且具有良好的跨平台和跨语言的特性。XML使用标签来定义数据的结构…