Linux高级IO-I/O多路转接之select

时间:2024-07-19 07:19:58

认识select

  1. 一定要以某种形式,一次等待多个fd (仅仅是等待,没有拷贝)
  2. 哪一个fd或者那些个fd就绪,用户需要知道的
  3. select 要等的的多个fd,一定有少量或者全部都是准备好的,一般是少量

在这里插入图片描述

select调用失败时,错误码可能被设置为:

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

关于fd_set结构体

typedef struct
  {
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
  } fd_set;

其实这个结构就是一个整数数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符
提供了一组操作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服务器
select服务器,使用时需要程序员自己维护一个第三方的数组,来进行已经获得的sock(文件描述符)进行管理。

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

class Sock
{
public:
    Sock(int ret = -1)
    :_sock(ret)
    {}

    void Socket()
    {
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if(-1 == _sock)
        {
            std::cout<< "socket error" << std::endl;
            exit(-1);
        }
    }

    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;//ip地址

        if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            std::cout<< "bind error" << std::endl;
            exit(-1);
        }
    }

    void Listen()
    {
        if(listen(_sock, 0) < 0)
        {
            std::cout<< "listen error" << std::endl;
            exit(-1);
        }
    }

    int Accept(std::string* clientIp, uint16_t* clientPort)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int sock = accept(_sock, (struct sockaddr*)&temp, &len);
        if(sock < 0)
        {

            std::cout<< "accept error" << std::endl;
            exit(-1);
        }
        else
        {
            *clientIp = inet_ntoa(temp.sin_addr);
            *clientPort = ntohs(temp.sin_port);
        }

        return sock;
    }
    // 输入: const &
    // 输出: *
    // 输入输出: &
    int Connect(std::string& serverIp, uint16_t& serverPort)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(serverPort);
        server.sin_addr.s_addr = inet_addr(serverIp.c_str());

        return connect(_sock, (struct sockaddr *)&server, sizeof(server));
    }
    int Fd()
    {
        return _sock;
    }
    void Close()
    {
        if (_sock != -1)
            close(_sock);
    }
    ~Sock()
    {}
private:
    int _sock;
};
//selectServer.hpp
#pragma once
#include <iostream>
#include <sys/select.h>
#include "Sock.hpp"

const static uint16_t gport = 8888;
typedef int type_t;
static const int defaultfd = -1;
class selectServer
{
    static const int N = sizeof(fd_set) *8 ;
public:
    selectServer(uint16_t port = gport)
    :_port(port)
    {}
    void InitServer()
    {
        _listenSock.Socket();
        _listenSock.Bind(_port);
        _listenSock.Listen();

        for(int i = 0; i < N; ++i)
        {
            fdarray[i] = defaultfd;
        }
    }
    void  Accepter()
    {
        std::cout<< "Accepter();" << std::endl;
        std::string clientip;
        uint16_t clientport;
        int sock = _listenSock.Accept(&clientip,&clientport);
        if(sock < 0)
        {
            return;
        }
        int pos = 1;
        for(; pos < N; ++pos)
        {
            if(fdarray[pos] == defaultfd)
            {
                fdarray[pos] = sock;
                break;
            }
        }
        if(pos >= N)
        {
            close(sock);
            std::cerr << "fd_set的位图已经超过了其大小"<<std::endl;
        }
        

    }
    void HandlerEvent(fd_set &rfds)
    {
        for(int i = 0; i < N; ++i)
        {
            if(fdarray[i] == defaultfd)
                continue;
            
            if((fdarray[i] == _listenSock.Fd()) && (FD_ISSET(fdarray[i], &rfds)))//将监听套接字文件准备就绪
            {
                
                Accepter();
            }
            else if((fdarray[i] != _listenSock.Fd()) && (FD_ISSET(fdarray[i], &rfds)))
            {
                int fd = fdarray[i];
                char buffer[1024];
                ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0);
                if (s > 0)
                {
                    buffer[s-1] = 0;
                    std::cout << "client# " << buffer << std::endl;

                    // 发送回去也要被select管理的,TODO
                    std::string echo = buffer;
                    echo += " [select server echo]";
                    send(fd, echo.c_str(), echo.size(), 0);
                }
                else
                {
                    if (s == 0)
                    {
                        close(fdarray[i]);
                        fdarray[i] = defaultfd;
                    }
                    else
                    {
                        std::cerr<<errno<< ":"<< strerror(errno) << std::endl;
                    }
                }
            }
        }
    }
    void Start()
    {
        // 1. 这里我们能够直接获取新的链接吗?
        // 2. 最开始的时候,我们的服务器是没有太多的sock的,甚至只有一个sock!listensock
        // 3. 在网络中, 新连接到来被当做 读事件就绪!
        fdarray[0] = _listenSock.Fd();

        while(true)
        {
            // 因为rfds是一个输入输出型参数,注定了每次都要对rfds进行重置,重置,必定要知道我历史上都有哪些fd?fdarray[]
            // 因为服务器在运行中,sockfd的值一直在动态变化,所以maxfd也一定在变化, maxfd是不是也要进行动态更新, fdarray[]
            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd = fdarray[0];
            for (int i = 0; i < N; ++i)
            {
                if(fdarray[i] == defaultfd)
                    continue;
                
                FD_SET(fdarray[i], &rfds);
                if(maxfd < fdarray[i])
                {
                    maxfd = fdarray[i];
                }  
            }
            int n = select(maxfd + 1,&rfds,nullptr,nullptr,nullptr);
            switch(n)
            {
            case 0:
                std::cout << "没有文件描述符,就绪" << std::endl;
                break;
            case -1:
                std::cerr<<errno<< ":"<< strerror(errno) << std::endl;
                break;
            default:
                std::cout << "有一个就绪事件发生了" << std::endl;
                HandlerEvent(rfds);
                DebugPrint();
                break;
            }
        }
    }

    void DebugPrint()
    {
        std::cout << "fdarray[]: ";
        for (int i = 0; i < N; i++)
        {
            if (fdarray[i] == defaultfd)
                continue;
            std::cout << fdarray[i] << " ";
        }
        std::cout << "\n";
        sleep(1);
    }
private:
    uint16_t _port;
    Sock _listenSock;
    type_t fdarray[N];
};
//test.cc
#include <iostream>
#include <memory>
#include "selectServer.hpp"

int main()
{
    std::unique_ptr<selectServer> svr(new selectServer());
    svr->InitServer();
    svr->Start();
    
    return 0;
}

select缺点

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