【Linux网络】深入解析I/O多路转接 - Select

📢博客主页:https://blog.csdn.net/2301_779549673

📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson

📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!

📢本文由 JohnKi 原创,首发于 CSDN🙉

📢未来很长,值得我们全力奔赴更美好的生活✨

文章目录

  • 🏳️‍🌈一、什么是select
  • [🏳️‍🌈二、select 函数原型](#🏳️‍🌈二、select 函数原型)
  • [🏳️‍🌈三、测试 timeout](#🏳️‍🌈三、测试 timeout)
    • [3.1 SelectServer 类](#3.1 SelectServer 类)
      • [3.1.1 基本结构](#3.1.1 基本结构)
      • [3.1.2 析构构造函数](#3.1.2 析构构造函数)
      • [3.1.3 Loop()](#3.1.3 Loop())
      • [3.1.4 InitServer()](#3.1.4 InitServer())
    • [3.2 主函数](#3.2 主函数)
    • [3.3 测试代码](#3.3 测试代码)
  • [🏳️‍🌈四、Handler 处理函数 - 版本一](#🏳️‍🌈四、Handler 处理函数 - 版本一)
  • [🏳️‍🌈五、Handler 处理函数 - 版本二](#🏳️‍🌈五、Handler 处理函数 - 版本二)
    • [5.1 基本结构](#5.1 基本结构)
    • [5.2 初始化函数](#5.2 初始化函数)
    • [5.3 Loop() 函数](#5.3 Loop() 函数)
    • [5.4 HandlerEvent(() 函数](#5.4 HandlerEvent(() 函数)
    • [5.5 PrintDebug()](#5.5 PrintDebug())
    • [5.6 测试](#5.6 测试)
  • [🏳️‍🌈六、Handler 处理函数 - 版本三](#🏳️‍🌈六、Handler 处理函数 - 版本三)
  • [🏳️‍🌈七、select 的特点](#🏳️‍🌈七、select 的特点)
  • 👥总结

11111111
11111111
11111111
11111111
**** 11111111

🏳️‍🌈一、什么是select

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

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

定位 :只负责进行等,不进行拷贝!
作用:为了等待多个fd,等待fd上面的新事件就绪,通知程序员,事件已经就绪,可以进行IO拷贝了!

🏳️‍🌈二、select 函数原型

select 的函数原型如下:

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

参数

  • nfds:这是一个整数值,指定要监控的文件描述符集合中最大文件描述符的值加1。这是因为文件描述符是从0开始编号的,所以nfds实际上是文件描述符集合中最大索引值加1。
  • readfds:指向一个 fd_set 结构体的指针,该结构体包含了所有需要监控是否有数据可读的文件描述符。如果不需要监控读事件,可以传递 NULL。
  • writefds:指向一个 fd_set 结构体的指针,该结构体包含了所有需要监控是否有数据可写的文件描述符。如果不需要监控写事件,可以传递 NULL。
  • exceptfds:指向一个 fd_set 结构体的指针,该结构体包含了所有需要监控是否出现异常条件的文件描述符。如果不需要监控异常事件,可以传递 NULL。
  • timeout:指向一个 timeval 结构体的指针,用来设置 select()的等待时间

参数 timeout 取值:

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

timeval 结构

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

    struct timeval
    {
    #ifdef __USE_TIME_BITS64
    __time64_t tv_sec; /* Seconds. /
    __suseconds64_t tv_usec; /
    Microseconds. /
    #else
    __time_t tv_sec; /
    Seconds. /
    __suseconds_t tv_usec; /
    Microseconds. */
    #endif
    };

fd_set 结构

  • fds_bits 或 __fds_bits :一个 __fd_mask 类型的数组,用于存储文件描述符的位掩码

  • __fd_mask :通常是 unsigned long,表示一个位掩码单元。每个单元可存储 __NFDBITS 个文件描述符状态(如 64 位系统为 64 位)。

  • __FD_SETSIZE :定义 fd_set 支持的最大文件描述符数量(默认通常为 1024)。

  • __NFDBITS :单个 __fd_mask 元素的位数(如 sizeof(__fd_mask) * 8)。

    typedef struct {
    #ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
    #else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
    #endif
    } fd_set;

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

函数返回值

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

错误值可能为:

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

🏳️‍🌈三、测试 timeout

前面 timeout 参数分析出三种情况,下面编写代码进行基本的测试!

3.1 SelectServer 类

SelectServer类 的成员需要用到 端口号套接字 ,成员函数暂时实现 InitServer()Loop(),此处的套接字使用前面封装的Socket类

3.1.1 基本结构

复制代码
#pragma once

#include <iostream>
#include "Socket.hpp"

using namespace SocketModule;

class SelectServer{

    public:
        SelectServer(uint16_t port);
        void InitServer();
        void Loop(); 
        ~SelectServer();
 
    private:
        uint16_t _port;
        SockPtr _listensock;
};

3.1.2 析构构造函数

构造函数 初始化端口号并根据端口号创建监听套接字对象析构函数暂时不做处理!

复制代码
SelectServer(uint16_t port)
    : _port(port), _listensock(std::make_shared<TcpSocket>()) {
    _listensock->BuildListenSocket(_port);
}
~SelectServer();

3.1.3 Loop()

Loop()函数此处主要用来测试timeout,也是后序使用的轮询函数!

复制代码
void Loop() {
    while (true) {
        // 临时
        fd_set rfds; // 清除 rfds 中相关的fd的位
        FD_ZERO(&rfds);
        FD_SET(_listensock->Sockfd(), &rfds);

        struct timeval timeout = {3, 0};
        int n =
            ::select(_listensock->Sockfd() + 1, &rfds, NULL, NULL, &timeout);
        switch (n) {
        case 0:
            LOG(LogLevel::DEBUG) << "time out " << timeout.tv_sec << "s";
            break;
        case -1:
            LOG(LogLevel::ERROR) << "select error";
            break;
        default:
            LOG(LogLevel::INFO) << "haved event ready, " << n;
            break;
        }
    }
}

3.1.4 InitServer()

InitServer()函数暂时不用填写代码,保证主函数把代码跑过即可

3.2 主函数

输入端口号运行即可

复制代码
int main(int argc, char* argv[]){
    if(!argc != 2){
        std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl; 
    }
    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<SelectServer> svr = std::make_unique<SelectServer>(port);
    svr->InitServer();
    svr->Loop();

    return 0;
}

3.3 测试代码

根据左边的日志,我们会发现平均每 3 s会弹出一次超时

我们修改一下监听的情况,每3s 监听一次,并且超时为 30s

复制代码
LOG(LogLevel::INFO) << "haved event ready, " << n;
LOG(LogLevel::DEBUG) << "time out " << timeout.tv_sec << "." << timeout.tv_usec
                     << "s";
sleep(3);

我们使用 telnet 模拟访问服务端, 每 3s 弹出一次套接字已就绪的字样

🏳️‍🌈四、Handler 处理函数 - 版本一

timeout 参数测试成功之后,需要正式进入事件处理select() 函数的返回值不是0或者1就表示事件已经就绪,此处需要处理任务!

我们这里不进行计时即 select最后一个参数设为 NULL

复制代码
void Loop() {
    while (true) {
        fd_set rfds; // 清除 rfds 中相关的fd的位
        FD_ZERO(&rfds);
        FD_SET(_listensock->Sockfd(), &rfds);

        int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, nullptr);
        switch (n) {
        // case 0: 因为不会超时所有case 0 的情况不存在
        case -1:
            LOG(LogLevel::ERROR) << "select error";
            break;
        default:
            LOG(LogLevel::INFO) << "haved event ready, " << n;

            break;
        }
    }
}

HandlerEvent() 版本一进行正式的任务处理,如果fd在读文件描述符集合中则获取链接并且获取链接成功,打印调试日志,否则直接返回!

复制代码
void HandlerEvent(fd_set& rfds) {
    if (FD_ISSET(_listensock->Sockfd(), &rfds)) {
        // 连接事件就绪,等价于读事件就绪
        InetAddr addr;
        int sockfd = _listensock->Accepter(&addr);
        if (sockfd > 0) {
            LOG(LogLevel::DEBUG)
                << "get a new connection from " << addr.AddrStr().c_str()
                << ", sockfd : " << sockfd;
        } else
            return;
    }
}

这里还需要更改一下 socket.hpp 的 Accepter 函数,因为我们返回的是一个 int 类型

复制代码
int Accepter(InetAddr* cli) override {
    struct sockaddr_in client;
    socklen_t clientlen = sizeof(client);
    // accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
    // 返回一个新的套接字,该套接字与调用进程间接地建立了连接。
    int sockfd = ::accept(_sockfd, CONV(&client), &clientlen);
    if (sockfd < 0) {
        LOG(LogLevel::ERROR) << "accept socket error";
        return -1;
    }
    *cli = InetAddr(client);
    LOG(LogLevel::DEBUG) << "get a new connection from "
                         << cli->AddrStr().c_str() << ", sockfd : " << sockfd;

    return sockfd;
}

🏳️‍🌈五、Handler 处理函数 - 版本二

在轮询的过程中,可能会有fd是合法的,但是没有就绪 ,而这次执行完之后,读文件描述符集合会清空,可能会出现问题,因此需要增加一个数组(数组成员个数为fd_set集合的位数),来保存合法的fd!

5.1 基本结构

  • 我们需要添加一个能够存储文件描述符的数组

  • 同时要设置最大监听数量,以及默认描述符

    class SelectServer {
    const static int gnum = sizeof(fd_set) * 8;
    const static int gdefaultfd = -1;

    private:
    uint16_t _port;
    SockPtr _listensock;

    复制代码
      // select要正常工作,需要借助一个辅助数组,来保存所有合法fd
      int fd_array[gnum];

    };

5.2 初始化函数

  • 这里需要初始化文件描述符数值中的所有文件描述符,并且要设置监听套接字

    void InitServer() {
    for (int i = 0; i < gnum; ++i) {
    fd_array[i] = gdefaultfd;
    }
    fd_array[0] = _listensock->Sockfd();
    }

5.3 Loop() 函数

Loop()函数主要分以下三步:

  1. 文件描述符初始化

  2. 合法的fd添加到rfds集合中

    2.1. 更新出最大的fd的值

  3. 检查读条件是否就绪

    void Loop() {
    while (true) {
    // 1. 文件描述符初始化
    fd_set rfds; // 清除 rfds 中相关的fd的位
    FD_ZERO(&rfds);
    int max_fd = gdefaultfd;

    复制代码
         // 2. 合法的 fd 添加到 rfds 集合中
         for (int i = 0; i < gnum; ++i) {
             if (fd_array[i] == gdefaultfd)
                 continue;
             FD_SET(fd_array[i], &rfds);
             if (fd_array[i] > max_fd)
                 max_fd = fd_array[i];
         }
    
         struct timeval timeout = {30, 0};
    
         // 3. 检查都条件是否就绪
         int n = ::select(max_fd + 1, &rfds, nullptr, nullptr, &timeout);
         switch (n) {
         case 0:
             LOG(LogLevel::DEBUG) << "time out " << timeout.tv_sec << "."
                                  << timeout.tv_usec << "s";
             break;
         case -1:
             LOG(LogLevel::ERROR) << "select error";
             break;
         default:
             // 如果事件就绪,但是不做处理,select 就会一直通知,直到处理
             LOG(LogLevel::DEBUG) << "time remain " << timeout.tv_sec << "."
                                  << timeout.tv_usec << "s";
             LOG(LogLevel::INFO) << "haved event ready, " << n;
             HandlerEvent(rfds);
             sleep(3);
             break;
         }
     }

    }

5.4 HandlerEvent(() 函数

在执行HandlerEvent()函数之前,赋值数组中一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd,此处主要分以下两步:

  • 1、判断fd是否合法
  • 2、判断fd是否就绪
    • 2.1、就绪是listensockfd
      • 2.1.1、获取链接
        2.1.2、获取链接成功将新的fd添加到数组中
        2.1.3、数组满了,不能添加,需关闭sockfd
    • 2.2、就绪是normal sockfd
      • 2.2.1、直接读取fd中内容
复制代码
void HandlerEvent(fd_set& rfds) {
    // version - 0
    // if(FD_ISSET(_listensock->Sockfd(), &rfds)){
    //     // 连接事件就绪,等价于读事件就绪
    //     InetAddr addr;
    //     int sockfd = _listensock->Accepter(&addr);
    //     if(sockfd > 0){
    //         LOG(LogLevel::DEBUG) << "get a new connection from " <<
    //         addr.AddrStr().c_str() << ", sockfd : " << sockfd;
    //     } else return;
    // }

    // version - 1
    for (int i = 0; i < gnum; ++i) {
        // 1. 判断 fd 是否合法
        if (fd_array[i] == gdefaultfd)
            continue;

        // 2. 判断 fd 是否就绪
        if (FD_ISSET(fd_array[i], &rfds)) {
            // 判断是 listensocket
            if (_listensock->Sockfd() == fd_array[i]) {
                InetAddr client;
                int sockfd = _listensock->Accepter(&client);
                if (sockfd > 0) {
                    LOG(LogLevel::INFO)
                        << "get a new connection from "
                        << client.AddrStr().c_str() << ", sockfd : " << sockfd;

                    // 将获取成功的新的 fd 添加到 fd_array 中
                    bool flag = false;
                    for (int pos = 1; pos < gnum; ++pos) {
                        if (fd_array[pos] == gdefaultfd) {
                            flag = true;
                            fd_array[pos] = sockfd;
                            LOG(LogLevel::DEBUG)
                                << "add new sockfd " << sockfd
                                << " to fd_array[" << pos << "]";
                            break;
                        }
                        if (!flag) {
                            LOG(LogLevel::ERROR)
                                << "fd_array is full, can't add new sockfd "
                                << sockfd;
                            ::close(sockfd);
                        }
                    }
                }
            }
            // 判断是其他 socket
            else {
                // 正常读写
            }
        }
    }
}

5.5 PrintDebug()

PrintDebug() 遍历辅助数组,将合法的文件描述符打印出来!

复制代码
void PrintDebug() {
    std::cout << "fd list: ";
    for (int i = 0; i < gnum; ++i) {
        if (fd_array[i] == gdefaultfd)
            continue;
        std::cout << fd_array[i] << " ";
    }
    std::cout << std::endl;
}

5.6 测试

🏳️‍🌈六、Handler 处理函数 - 版本三

前面两个版本已经完成对监听套接字和普通套接字的测试,但是结构看起来还是没有那么清晰,这个版本使用函数进行进一步封装!

复制代码
void HandlerEvent(fd_set& rfds) {
    // version - 0
    // if(FD_ISSET(_listensock->Sockfd(), &rfds)){
    //     // 连接事件就绪,等价于读事件就绪
    //     InetAddr addr;
    //     int sockfd = _listensock->Accepter(&addr);
    //     if(sockfd > 0){
    //         LOG(LogLevel::DEBUG) << "get a new connection from " <<
    //         addr.AddrStr().c_str() << ", sockfd : " << sockfd;
    //     } else return;
    // }

    // version - 1
    for (int i = 0; i < gnum; ++i) {
        // 1. 判断 fd 是否合法
        if (fd_array[i] == gdefaultfd)
            continue;

        // 2. 判断 fd 是否就绪
        if (FD_ISSET(fd_array[i], &rfds)) {
            // 判断是 listensocket
            if (_listensock->Sockfd() == fd_array[i]) {
                HandlerNewConnection();
            }
            // 判断是其他 socket
            else {
                // 正常读写
                HandlerIO(i);
            }
        }
    }
}

void HandlerNewConnection() {
    InetAddr client;
    int sockfd = _listensock->Accepter(&client);
    if (sockfd > 0) {
        LOG(LogLevel::INFO)
            << "get a new connection from " << client.AddrStr().c_str()
            << ", sockfd : " << sockfd;

        // 将获取成功的新的 fd 添加到 fd_array 中
        bool flag = false;
        for (int pos = 1; pos < gnum; ++pos) {
            if (fd_array[pos] == gdefaultfd) {
                flag = true;
                fd_array[pos] = sockfd;
                LOG(LogLevel::DEBUG) << "add new sockfd " << sockfd
                                     << " to fd_array[" << pos << "]";
                break;
            }
        }
        if (!flag) {
            LOG(LogLevel::ERROR)
                << "fd_array is full, can't add new sockfd " << sockfd;
            ::close(sockfd);
        }
    }
}

void HandlerIO(int i) {
    char buffer[1024];
    ssize_t n = ::recv(fd_array[i], buffer, sizeof(buffer) - 1, 0);
    if (n > 0) {
        buffer[n] = 0;
        std::cout << "client say# " << buffer << std::endl;
        std::string content = "<html><body><h1>hello linux</h1></body></html>";
        std::string echo_str = "HTTP/1.0 200 OK\r\n";
        echo_str += "Content-Type: text/html\r\n";
        echo_str +=
            "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";
        echo_str += content;
        ::send(fd_array[i], echo_str.c_str(), echo_str.size(), 0);
    } else if (n == 0) { // 客户端关闭连接
        LOG(LogLevel::INFO)
            << "client closed connection, sockfd: " << fd_array[i];
        ::close(fd_array[i]);
        fd_array[i] = gdefaultfd; // 清理数组中的fd
    } else {                      // recv 错误(如连接重置)
        LOG(LogLevel::ERROR) << "recv error, sockfd: " << fd_array[i];
        ::close(fd_array[i]);
        fd_array[i] = gdefaultfd;
    }
}

🏳️‍🌈七、select 的特点

优点

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

缺点

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

👥总结

本篇博文对 【Linux网络】深入解析I/O多路转接 - Select 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

相关推荐
@t.t.1 分钟前
Docker容器资源控制--CGroup
linux·运维·docker·容器·云计算
像风一样自由20204 分钟前
浏览器自动化工具:Selenium 和 Playwright
运维·selenium·自动化
林九生7 分钟前
【运维】Ubuntu apt 更新失败?Temporary failure resolving ‘cn.archive.ubuntu.com‘ 问题
运维·ubuntu·postgresql
不想当程序猿_15 分钟前
Centos 7系统 宝塔部署Tomcat项目(保姆级教程)
linux·redis·centos·tomcat·minio·宝塔
吴声子夜歌16 分钟前
Linux运维——Vim基础
linux·运维·vim
照海19Gin1 小时前
HCIA-Datacom 高阶:VLAN、VLANIF 与静态路由综合实验
网络·智能路由器
Zz_waiting.1 小时前
网络原理 - 11(HTTP/HTTPS - 2 - 请求)
运维·网络·网络协议·mysql·http·https·抓包
davenian1 小时前
< 自用文 rclone > 在 Ubuntu 24 访问 Google Drive 网络内容
linux·ubuntu·rclone
单车少年ing2 小时前
linux两个特殊的宏 _RET_IP_ 和_THIS_IP_ 实现
linux·arm