IO多路转接Select

一、I/O多路转接之select

1.1select介绍

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

(2)select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。

(3)程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。

1.2select函数原型及使用:

1.2.1参数解释:

(1)参数nfds是需要监视的最大的文件描述符值+1。

(2)rdset、wrset、exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合

及异常文件描述符的集合。

(3)参数timeout为结构timeval,用来设置selecto的等待时间。

1.2.2函数返回值:

(1)大于0,执行成功则返回文件描述词状态已改变的个数。

(2)等于0,如果返回0代表在描述词状态改变前已超过timeout时间,没有返回,超时代表没有文

件描述符就绪,也没有错误。

(3)小于0,当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测。

1.2.3timeval结构体:

1.2.3 fd_set结构

1.2.4 fd_set结构体位图的最大个数大小和错误码

1.3socket就绪条件

1.3.1读就绪

(1)sOcket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以

无阻塞的读该文件描述符,并且返回值大于0。

(2)socketTCP通信中,对端关闭连接,此时对该socket读,则返回0。

(3)监听的socket上有新的连接请求。

(4)socket上有未处理的错误。

1.3.2写就绪

(1)socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水

位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。

(2)socket的写操作被关闭(close或者shutdown),对一个写操作被关闭的socket进行写作,

会触发SIGPIPE信号。

(3)socket使用非阻塞connect连接成功或失败之后。

(4)socket上有未读取的错误。

二、select的使用------使用select实现服务器:

2.1SelectServer.hpp文件:

cpp 复制代码
#pragma once
#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Sock.hpp"

static const uint16_t defaultport = 8080;
static const int fd_num_max = sizeof(fd_set) * 8; // 获取fd_set中最大表示的文件描述符个数
int defaultfd = -1;

class SelectServer
{
public:
    SelectServer(uint16_t port = defaultport)
        : _port(port)
    {
        // 初始化辅助数组
        for (size_t i = 0; i < fd_num_max; i++)
        {
            _fd_array[i] = defaultfd;
        }
    }

    bool Init()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();
        return true;
    }

    void PrintFd()
    {
        std::cout << "online fd list:";
        for (int i = 0; i < fd_num_max; i++)
        {
            if (_fd_array[i] == defaultfd)
            {
                continue;
            }
            else
            {
                std::cout << _fd_array[i] << "->";
            }
        }
        std::cout << std::endl;
    }

    void Accepter()
    {
        // 连接事件就绪
        uint16_t clientport;
        std::string clientip;
        // 在此处不会在阻塞了,因为selec已经告诉我事件已经就绪
        int sock = _listensock.Accept(&clientip, &clientport);
        if (sock < 0)
        {
            return;
        }
        else
        {
            lg.logmessage(Info, "accept success! %s, %d", clientip.c_str(), clientport);
            // 这里不能直接读,因为读写事件不一定就绪,所以要直接把这里获取的sock交给select

            // 这里吧sock交给辅助数组就可以把sock交给select来监听了
            int pos = 1;
            for (; pos < fd_num_max; pos++)
            {
                if (_fd_array[pos] != defaultfd)
                {
                    continue;
                }
                else
                {
                    break;
                }
            }
            // 这里跳出循环有两种情况,一是找到了没有设置的数组下标,二是数组满了
            if (pos == fd_num_max)
            {
                // 整个数组已经满载了
                lg.logmessage(Warning, "sorry, server is full, sockfd[%d] will close", sock);
                close(sock);
                return;
            }
            else
            {
                // 找到未被设置的下标了
                _fd_array[pos] = sock;
                PrintFd();
            }
        }
    }

    void Recvr(int fd, int pos)
    {
        // 其他的文件描述符就绪,进行读取操作
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "get a message:" << buffer << std::endl;
        }
        else if (n == 0)
        {
            lg.logmessage(Info, "client quit, close fd is:%d", fd);
            _fd_array[pos] = defaultfd; // 从select中移除了文件描述符
            close(fd);
        }
        else
        {
            lg.logmessage(Warning, "read error, fd is:%d", fd);
            _fd_array[pos] = defaultfd; // 从select中移除了文件描述符
            close(fd);
        }
    }

    void Dispacter(fd_set &rfds)
    {
        // 便利所有的文件描述符数组,因为我不知道哪一个就绪了
        for (int i = 0; i < fd_num_max; i++)
        {
            int fd = _fd_array[i];
            if (fd == defaultfd)
            {
                // 不是合法的文件描述符
                continue;
            }

            // 判断listensock是否在rfds中,判断连接是否就绪
            if (FD_ISSET(fd, &rfds))
            {
                if (_listensock.Fd() == fd)
                {
                    // 连接管理器
                    Accepter();
                }
                else
                {
                    Recvr(fd, i);
                }
            }
        }
    }

    void Start()
    {
        int listensock = _listensock.Fd();
        _fd_array[0] = listensock;
        for (;;)
        {
            // 这里不能直接accept,这里必须要检测并获取listensock上面的时间,新链接到来,等价于读事件的就绪
            fd_set rfds;

            // 创建的位图可能有乱码,所以进行清空处理
            FD_ZERO(&rfds);

            // 找最大的文件描述符
            int maxfd = _fd_array[0];

            // 便利辅助数组,将合法的文件描述符添加进rfds
            for (int i = 0; i < fd_num_max; i++)
            {
                if (_fd_array[i] == defaultfd)
                {
                    continue;
                }
                else
                {
                    // 将合法文件描述符设置进位图
                    FD_SET(_fd_array[i], &rfds);
                    if (maxfd < _fd_array[i])
                    {
                        maxfd = _fd_array[i];
                        lg.logmessage(Info, "maxfd update, maxfd is %d", maxfd);
                    }
                }
            }

            // 每次重新设置每个5秒返回一次(要针对周期的重复设置)
            struct timeval timeout = {5, 0};

            // 如果时间就绪了,上层不对数据进行处理,select就会一直处理
            // select告诉就绪了,接下来的一次读取,读取fd的时候,不会发生阻塞
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
            switch (n)
            {
            case 0:
                std::cout << "time out, timeout:" << timeout.tv_sec << "." << timeout.tv_usec << std::endl;
                break;
            case -1:
                std::cout << "select fail" << std::endl;
            default:
                // 有事件就绪
                std::cout << "get a link" << std::endl;
                // 处理事件
                Dispacter(rfds);
                break;
            }
        }
    }

    ~SelectServer()
    {
        _listensock.Close();
    }

private:
    Sock _listensock;
    uint16_t _port;
    int _fd_array[fd_num_max]; // 使用辅助数组进行select和文件描述符之间的传递
};

2.2 Sock.hpp文件

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Log.hpp"

const int backlog = 10;
extern log lg;

enum
{
    SocketError = 2,
    BindError,
    ListenError
};

class Sock
{
public:
    Sock()
    : _sockfd(-1)
    {
    }

    ~Sock()
    {
        if (_sockfd >= 0)
        {
            close(_sockfd);
        }
    }

    // 创建套接字
    void Socket()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            lg.logmessage(Fatal, "socket fail, %s, %d", errno, strerror(errno));
            exit(SocketError);
        }
        int opt = -1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }

    // 创建一个绑定接口
    void Bind(const 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.logmessage(Fatal, "bind fail, %s, %d", errno, strerror(errno));
            exit(BindError);
        }
    }

    void Listen()
    {
        if (listen(_sockfd, backlog) < 0)
        {
            lg.logmessage(Fatal, "listen fail, %s, %d", errno, strerror(errno));
            exit(ListenError);
        }
    }

    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.logmessage(Warning, "accept fail, %s, %d", errno, strerror(errno));
            return -1;
        }

        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        // 获取远端主机的信息
        *clientport = ntohs(peer.sin_port);
        *clientip = ipstr;

        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 << std::endl;
            return false;
        }
        return true;
    }

    void Close()
    {
        close(_sockfd);
    }

    int Fd()
    {
        return _sockfd;
    }

private:
    int _sockfd;
};

2.3 main.cc文件

cpp 复制代码
#include "SelectServer.hpp"
#include <memory>

int main()
{
    //std::cout << "fd_set bit max:" << sizeof(fd_set)* 8 << std::endl;

    std::unique_ptr<SelectServer> svr(new SelectServer());
    svr->Init();
    svr->Start();

    return 0;
}

2.4 Log.hpp文件

cpp 复制代码
#pragma once

#include <iostream>
#include <stdarg.h>
#include <time.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include <fcntl.h>
#include<string.h>

#define SIZE 1024

// 设置日志等级
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define logFile "log.txt"

class log
{
public:
    log()
    {
        PrintMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        PrintMethod = method;
    }

    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        default:
            return "None";
        }
    }

    void logmessage(int level, const char *format, ...) // 后面的省略号表示可变参数
    {

        char leftbuffer[SIZE];

        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);

        snprintf(leftbuffer, sizeof(leftbuffer), "[%s],[%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        char rightbuffer[SIZE];

        va_list s;
        va_start(s, format);
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        char logtxt[SIZE * 3];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

        // printf("%d-%d-%d %d:%d:%d\n",ctime->tm_year + 1900, ctime->tm_mon, ctime->tm_mday, ctime->tm_hour,ctime->tm_min,ctime->tm_sec);

        //printf("%s", logtxt);
        PrintLog(level,logtxt);
        // 格式:默认部分+自定义部分(可变参数部分)
    }

    void PrintLog(int level, const std::string& logtxt)
    {
        switch(PrintMethod)
        {
            case Screen:
                std::cout << logtxt << std::endl;
            break;
            case Onefile:
                printOneFile(logFile, logtxt);
            break;
            case Classfile:
                printClassFile(level, logtxt);
            break;
            default:
            break;
        }
    }
    void printOneFile(const std::string &logname, const std::string &logtxt)
    { 
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);
        if(fd < 0)
        {
            return  ;
        }
        write(fd,logtxt.c_str(),logtxt.size());
        close(fd);

    }
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = logFile;
        filename += ".";
        filename += levelToString(level);
        printOneFile(filename, logtxt);
    }
    ~log()
    {
    }
private:
    int PrintMethod;
    std::string path;
};


log lg;

三、Select的缺点

从上面我们可以看出select的缺点:

(1)每次调用select,都需要手动重新设置fd集合,从接口使用角度来说也非常不便。

(2)由于select的输入输出型参数比较多,每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。

(3)每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd也很大。

(4)select支持的文件描述符数量太小,也就是等待的fd有上限。

相关推荐
csjane10799 小时前
Redis 配置文件
数据库·redis·缓存
樱桃花下的小猫9 小时前
Rust 服务器倍率参数配置指南
服务器·云鸢互联·零门槛一键搭建·新手友好无技术门槛要求·腐蚀rust服务器一键开服·腐蚀rust·腐蚀rust低延迟稳定服务器
深藏bIue9 小时前
MySQL切换服务器数据迁移记录
服务器·mysql·oracle
_codemonster9 小时前
系统分析师案例刷题(八)数据库
数据库
Yushan Bai9 小时前
ORACLE SQL Performance Analyzer (SPA) 测试流程
数据库·sql
云智慧AIOps社区9 小时前
轻帆云ITSM|制造业智能化转型,从流程重构看 IT 服务管理发展新趋势
运维·自动化·aiops·智能运维·itsm平台·it服务管理系统
Data_Journal9 小时前
什么是数据采购,它究竟如何运作?
大数据·开发语言·数据库·人工智能·python
闵孚龙9 小时前
Claude Code 技能系统全解析:AI Agent 自定义能力、SKILL.md、MCP 扩展、上下文预算与企业级自动化落地
运维·人工智能·自动化
沪漂阿龙9 小时前
Docker 面试题详解:容器、镜像、Dockerfile、网络、Volume、Compose、安全与生产实践一次讲透
网络·安全·docker