【Linux】IO多路转接——select接口

news2024/11/26 14:38:21

目录

I/O多路转接之select

select初识

select函数

socket就绪条件

select基本工作流程

select服务器

select的优点

select的缺点

select的适用场景


I/O多路转接之select

select初识

select是系统提供的一个多路转接接口。

  • select系统调用可以让我们的程序同时监视多个文件描述符的上的事件是否就绪。
  • select的核心工作就是等,当监视的多个文件描述符中有一个或多个事件就绪时,select才会成功返回并将对应文件描述符的就绪事件告知调用者。

select函数

select函数

select函数的函数原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

参数说明:

  • nfds:需要监视的文件描述符中,最大的文件描述符值+1。
  • readfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已经就绪。
  • writefds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已经就绪。
  • exceptfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已经就绪。
  • timeout:输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间。

参数timeout的取值:

  • NULL/nullptr:select调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
  • 0:selec调用后t进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。
  • 特定的时间值:select调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后select进行超时返回。

返回值说明:

  • 如果函数调用成功,则返回有事件就绪的文件描述符个数
  • 如果timeout时间耗尽,则返回0。
  • 如果函数调用失败,则返回-1,同时错误码会被设置。

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

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

fd_set结构

fd_set结构与sigset_t结构类似,fd_set本质也是一个位图,用位图中对应的位来表示要监视的文件描述符。

调用select函数之前就需要用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的全部位

 timeval结构

传入select函数的最后一个参数timeout,就是一个指向timeval结构的指针,timeval结构用于描述一段时间长度,该结构当中包含两个成员,其中tv_sec表示的是秒,tv_usec表示的是微秒。

socket就绪条件

读就绪

  • socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读取该文件描述符,并且返回值大于0。
  • socket TCP通信中,对端关闭连接,此时对该socket读,则返回0。
  • 监听的socket上有新的连接请求。
  • socket上有未处理的错误。

写就绪

  • socket内核中,发送缓冲区中的可用字节数,大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。
  • socket的写操作被关闭(close或者shutdown),对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号。
  • socket使用非阻塞connect连接成功或失败之后。
  • socket上有未读取的错误。

异常就绪

  • socket上收到带外数据。

注:带外数据和TCP的紧急模式相关,TCP报头当中的URG标志位和16位紧急指针搭配使用,就能够发送/接收带外数据。

select基本工作流程

如果我们要实现一个简单的select服务器,该服务器要做的就是读取客户端发来的数据并进行打印,那么这个select服务器的工作流程应该是这样的:

  • 先初始化服务器,完成套接字的创建、绑定和监听。
  • 定义一个fd_array数组用于保存监听套接字和已经与客户端建立连接的套接字,刚开始就将监听套接字添加到fd_array数组当中。
  • 然后服务器开始循环调用select函数,检测读事件是否就绪,如果就绪则执行对应的操作。
  • 每次调用select函数之前,都需要定义一个读文件描述符集readfds,并将fd_array当中的文件描述符依次设置进readfds当中,表示让select帮我们监视这些文件描述符的读事件是否就绪。
  • 当select检测到数据就绪时会将读事件就绪的文件描述符设置进readfds当中,此时我们就能够得知哪些文件描述符的读事件就绪了,并且对这些描述文件描述进行对应的操作。
  • 如果读事件就绪的是监听套接字,则调用accept函数从底层全连接队列获取已经建立好的连接,并将该连接对应的套接字添加到fd_array数组当中。
  • 如果读事件就绪的是与客户端建立连接的套接字,则调用read函数读取客户端发来的数据并进行打印输出。
  • 当然,服务器与客户端建立连接的套接字读事件就绪,也可能是因为客户端将连接关闭了,此时服务器应该调用close关闭该套接字,并将该套接字从fd_array数组当中清除,因为下一次不需要再监视该文件描述符的读事件了。

说明一下:

  • 因为传入select函数的readfds、writefds和exceptfds都是输入输出型参数,当select函数返回时,这些参数当中的值已经被修改了,因此,每次调用select函数时都需要对其进行重新设置,timeout也是类似的道理。
  • 因为每次调用select函数之前都需要对readfds进行重新设置,所以需要定义一个fd_array数组保存与客户端已经建立的若干连接喊监听套接字,实际fd_array,数组当中的文件描述符就是需要让select监视读事件的文件描述符。
  • 我们的select服务器只是读取客户端发来的数据,因此只需要让select帮我们监视特定文件描述符的读事件,如果要同时让select帮我们监视特定文件描述符的读事件和写事件,则需要分别定义readfds和writefds,并定义两个数组分别保存需要被监视读事件和写事件的文件描述符,便于每次调用select函数前对readfds和writefds进行重新设置。
  • 服务器刚开始运行时,fd_array数组只有监听套接字,因此select第一次调用时只需要监视监听套接字的读事件是否就绪,但每次调用accept获取到新连接后,都会将新连接对应的套接字添加到fd_array当中,因此后续select调用时就需要监视监听套接字和若干个连接套接字的读事件是否就绪。
  • 由于调用select时还需要传入被监视的文件描述符中最大文件描述符值+1,因此每次在遍历fd_array和readfds进行重新设置时,还需要记录最大文件描述符值

这其中还有很多细节,下面我们就来实现这样一个select服务器。

select服务器

编写思路

Socket类

首先我们可以编写一个Socket类,对套接字相关的接口进行一定程度的封装。

代码如下:

#pragma once
#include "Err.hpp"
#include "Log.hpp"

#include <string>

#include <cstdlib>
#include <cstring>

#include <sys/socket.h>
#include <sys/fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>

const static int defaultfd = -1;
const static int gbacklog = 5;

class Sock
{
public:
    Sock() : sockfd_(defaultfd)
    {
    }

    void Socket() // 创建套接字
    {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0)
        {
            logMessage(Fatal, "sockfd create error:%s,[code:%d]", strerror(errno), errno);
            exit(SOCKET_ERR);
        }
        logMessage(Info, "socket create sucessfully");
        // 设置地址是复用的,即端口号是复用的
        int opt = 1;
        int ret = setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        if (ret < 0)
        {
            logMessage(Error, "setsockopt error:%s,[code:%d]", strerror(errno), errno);
        }
        logMessage(Info, "setsockopt sucessfully");
    }

    void Bind(uint16_t port)
    {
        struct sockaddr_in loacal;
        memset(&loacal, 0, sizeof(loacal));
        loacal.sin_family = AF_INET;
        loacal.sin_port = htons(port);
        loacal.sin_addr.s_addr = INADDR_ANY;
        int ret = bind(sockfd_, reinterpret_cast<const struct sockaddr *>(&loacal), sizeof(loacal));
        if (ret < 0)
        {
            logMessage(Fatal, "sockfd bind error:%s,[code:%d]", strerror(errno), errno);
            exit(BIND_ERR);
        }
        logMessage(Info, "sock bind sucessfully");
    }

    void Listen()
    {
        int ret = listen(sockfd_, gbacklog);
        if (ret < 0)
        {
            logMessage(Fatal, "sockfd listen error:%s,[code:%d]", strerror(errno), errno);
            exit(LISTEN_ERR);
        }
        logMessage(Info, "sock listen sucessfully");
    }

    int Accept(std::string *clinetip, uint16_t *clientport)
    {
        struct sockaddr_in client;
        memset(&client, 0, sizeof(client));
        socklen_t len = sizeof(client);
        int sockfd = accept(sockfd_, reinterpret_cast<struct sockaddr *>(&client), &len);
        if (sockfd < 0)
        {
            logMessage(Warning, "sockfd accept error:%s,[code:%d]", strerror(errno), errno);
            return -1;
        }
        *clinetip = inet_ntoa(client.sin_addr);
        *clientport = ntohs(client.sin_port);
        logMessage(Info, "sock accept sucessfully,clientip:%s, clientport:%d", clinetip->c_str(), clientport);
        return sockfd;
    }

    void Connect(const std::string &serverip, const 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());
        int ret = connect(sockfd_, (struct sockaddr *)&server, sizeof(server));
        if (ret < 0)
        {
            logMessage(Fatal, "sockfd connect error:%s,[code:%d]", strerror(errno), errno);
            exit(CONNECT_ERR);
        }
        logMessage(Info, "sock connect sucessfully");
    }

    void Close()
    {
        if (sockfd_ != defaultfd)
        {
            close(sockfd_);
        }
    }

    int Fd()
    {
        return sockfd_;
    }

    ~Sock()
    {
    }

private:
    int sockfd_;
};

SelectServer类

现在编写SelectServer类,因为我当前使用的是云服务器,所以编写的select服务器在绑定时不需要显示绑定IP地址,直接将IP地址设置为INADDR_ANY就行了,所以类当中只包含监听套接字和端口号两个成员变量。

  • 在构造SelectServer对象时,需要指明select服务器的端口号,当然也可以在初始化select服务器的时候指明。
  • 在初始化select服务器的时候调用Socket类当中的函数,依次进行套接字的创建、绑定和监听即可。
  • 在析构函数中可以选择调用close函数将监听套接字进行关闭,但实际也可以不进行该动作,因为服务器运行后一般是不退出的。

代码如下:

#pragma once
#include "Sock.hpp"
#include "Err.hpp"
#include "Log.hpp"
#include<iostream>
#include <string>
#include <functional>
#include <cstring>
#include <cstdlib>
#include <sys/select.h>

const static int N = sizeof(fd_set) * 8; // select可以管理文件描述符为fd_set类型比特位
const static uint16_t defaultport = 8888;
class SelectServer
{
public:
    SelectServer(int port = defaultport)
        : port_(defaultport)
    {
    }
    void InitServer()
    {
        listensockfd_.Socket();
        listensockfd_.Bind(port_);
        listensockfd_.Listen();

        for (int i = 0; i < N; ++i)
        {
            fd_arry[i] = defaultfd;
        }

        fd_arry[0] = listensockfd_.Fd(); // 默认0号下标存储的是listen套接字
    }
    void Start()
    {
        // 1. 这里我们能够直接获取新的链接吗?
        // 2. 最开始的时候,我们的服务器是没有太多的sock的,甚至只有一个sock!listensock
        // 3. 在网络中, 新连接到来被当做 读事件就绪!
        // listensock_.Accept(); 不能!
        while (true)
        {
            // 1.设置我们需要关心的文件描述符,用户告诉内核
            fd_set rfds;
            FD_ZERO(&rfds);
            int maxsockfd = listensockfd_.Fd();
            for (int i = 0; i < N; ++i)
            {
                if (fd_arry[i] == defaultfd)
                    continue;
                FD_SET(fd_arry[i], &rfds);
                if (fd_arry[i] > maxsockfd)
                    maxsockfd = fd_arry[i];
            }
            // 2. 调用select接口
            int n = select(maxsockfd + 1, &rfds, nullptr, nullptr, nullptr);
            if (n > 0)
            {
                // 有事件就绪了,开始处理
                logMessage(Info, "有事件就绪了,开始处理相关事件");
                HandlerEvent(rfds);
            }
            else if (n == 0)
            {
                logMessage(Info, "select timeout ...");
            }
            else
            {
                logMessage(Warning, "select error:%s [code:%d]", strerror(errno), errno);
            }
        }
    }
    void HandlerEvent(fd_set &rfds)
    {
        for (int i = 0; i < N; ++i)
        {
            int fd = fd_arry[i];
            if (fd == defaultfd)
                continue;
            if ((FD_ISSET(fd, &rfds)) && (fd == listensockfd_.Fd())) // listen套接字有读事件发生了
            {
                Accepter();
            }
            if ((FD_ISSET(fd, &rfds)) && (fd != listensockfd_.Fd())) // 其它的套接字有读事件发生了
            {
                ServerIO(i);
            }
        }
    }
    void Accepter()
    {
        // 1.接受客户
        std::string clientip;
        uint16_t clientport;
        int sockfd = listensockfd_.Accept(&clientip, &clientport);
        if (sockfd < 0)
            return;
        // 2. 存储sockfd到fd_array数组中
        int index = 0;
        while (index < N)
        {
            if (fd_arry[index] == defaultfd)
                break;
            ++index;
        }
        if (index < N)
        {
            fd_arry[index] = sockfd;
            logMessage(Info, "存储%d套接字成功!", sockfd);
        }
        else
        {
            close(sockfd);
            logMessage(Warning, "存储%d失败,fd_arry[] is full", sockfd);
        }
    }
    void ServerIO(int i)
    {
        int fd=fd_arry[i];
        char buffer[1024];
        int n=recv(fd,buffer,sizeof(buffer)-1,0);
        if(n>0)
        {
            buffer[n]=0;
            std::cout<<"client#"<<buffer<<std::endl;

            std::string echo=buffer;
            echo+="[server echo client]";
            send(fd,echo.c_str(),echo.size(),0);//TODO
        }
        else
        {
            logMessage(Info,"clinet:%d quit...",fd);
            close(fd);
            fd_arry[i]=defaultfd;
        }
    }
    ~SelectServer() 
    {
        listensockfd_.Close();
    }

private:
    uint16_t port_;
    Sock listensockfd_;
    int fd_arry[N]; // 存储socket套接字
};

运行服务器

服务器初始化完毕后就应该周期性的执行某种动作了,而select服务器要做的就是不断调用select函数,当事件就绪时对应执行某种动作即可。

  • 首先,在select服务器开始死循环调用select函数之前,需要先定义一个fd_array数组,先把数组中所有的位置初始化为无效,并将监听套接字添加到数组当中,fd_array数组当中保存的就是需要被select监视事件是否就绪的文件描述符。
  • 此后,select服务器就不断调用select函数监视读事件是否就绪,每次调用select函数之前都需要重新设置readfds,具体设置过程就时遍历fd_array数组,将fd_array数组当中的文件描述符添加到readfds当中,并同时记录最大的文件描述符值maxsockfd,因为后续调用select函数时需要将maxsockfd+1作为第一个参数传入。
  • 当select函数返回后,如果返回值为0,则说明timeout时间耗尽,此时直接准备下一次select调用即可。如果select的返回值为-1,则说明select调用失败,此时也让服务器准备下一次select调用,但实际应该进一步判断错误码,根据错误来判断是否应该继续调用select函数
  • 如果select的返回值大于0,则说明select函数调用成功,此时已经有文件描述符的读事件就绪,接下来就应该对就绪事件进行处理。

说明一下: 为了测试timeout不同取值时的不同效果,当有事件就绪时这里先只打印一句提示语句。

timeout测试

在运行服务器时需要先实例化一个SelectServer类对象,对select服务器进行初始化后就可以调用Start成员函数运行服务器了。

由于当前服务器调用select函数时直接将timeout设置为了nullptr,因此select函数调用后会进行阻塞等待。而服务器在第一次调用select函数时只让select监视监听套接字的读事件,所以运行服务器后如果没有客户端发来连接请求,那么读事件就不会就绪,而服务器则会一直在第一次调用的select函数中进行阻塞等待。

当我们借助telnet工具向select服务器发起连接请求后,select函数就会立马检测到监听套接字的读事件就绪,此时select函数便会成功返回,并将我们设置的提示语句进行打印输出,因为当前程序并没有对就绪事件进行处理,此后每次select函数一调用就会检测到读事件就绪并成功返回,因此会看到屏幕不断打印输出提示语句。

如果服务器在调用select函数时将timeout的值设置为0,那么select函数调用后就会进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。

此时如果select监视的文件描述符上有事件就绪,那么select函数的返回值就是大于0的,如果select监视的文件描述符上没有事件就绪,那么select的返回值就是等于0的。

运行服务器后如果没有客户端发来连接请求,那么select服务器就会一直调用select函数进行轮询检测,但每次检测时读事件都不就绪,因此每次select函数的返回值都是0,因此就会不断打印“timeout…”提示语句。

当有客户端发来连接请求后,select在某次轮询检测时就会检测到监听套接字的读事件就绪,此时select函数便会成功返回,并将我们设置的提示语句进行打印输出。

如果服务器在调用select函数时将timeout的值设置为特定的时间值,比如我们这里将timeout的值设置为5秒,那么select函数调用后的5秒内会进行阻塞等待,如果5秒后依旧没有读事件就绪,那么select函数将会进行超时返回。

我们可以将select函数超时返回和成功返回时timeout的值进行打印,以验证timeout是一个输入输出型参数。 

运行服务器后如果没有客户端发来连接请求,那么每次select函数调用5秒后都会进行超时返回,并且每次打印输出timeout的值都是0,也就意味着timeout的时间是被耗尽了的。

当有客户端发来连接请求后,在某次调用select函数时就会检测到监听套接字的读事件就绪,此时select函数便会成功返回,并将我们设置的提示语句进行打印输出。

因为当前程序并没有对就绪事件进行处理,因此在第一次select检测到读事件就绪后,之后每次select函数一调用就会检测到读事件就绪并成功返回,因此会看到屏幕不断打印输出提示语句,并且后续打印输出timeout的值都是4,表示本次select检测到读事件就绪时timeout的剩余时间为4秒。
因为timeout和readfds、writefds与exceptfds一样,它们都是输入输出型参数,因此如果要使用timeout参数,那么在每次调用select函数之前也都需要对timeout的值进行重新设置。

事件处理

当select检测到有文件描述符的读事件就绪并成功返回后,接下来就应该对就绪事件进行处理了,这里编写一个HandlerEvent函数,当读事件就绪后就调用该函数进行事件处理。

  • 在进行事件处理时需要遍历fd_array数组当中的文件描述符,依次判断各个文件描述符对应的读事件是否就绪,如果就绪则需要进行事件处理。
  • 当一个文件描述符的读事件就绪后,还需要进一步判断该文件描述符是否是监听套接字,如果是监听套接字的读事件就绪,那么就应该调用accept函数将底层的连接获取上来。但是光光调用accept将连接获取上来还不够,为了下一次调用select函数时能够让select帮我们监视新连接的读事件是否就绪,在连接获取上来后还应该将该连接对应的文件描述符添加到fd_array数组当中,这样在下一次调用select函数前对readfds重新设置时就能将该文件描述符添加进去了。
  • 如果是与客户端建立的连接对应的读事件就绪,那么就应该调用read函数读取客户端发来的数据,如果读取成功则将读到的数据在服务器端进行打印。如果调用read函数读取失败或者客户端关闭了连接,那么select服务器也应该调用close函数关闭对应的连接,但此时光光关闭连接也是不够的,还应该将该连接对应的文件描述符从fd_array数组当中清除,否则后续调用的select函数还会帮我们监视该连接的读事件是否就绪,但实际已经不需要了。

说明一下:

  •  当调用accept函数从底层获取上来连接后,不能立即调用read函数读取该连接当中的数据,因为此时新连接当中的数据可能并没有就绪,如果直接调用read函数可能需要进行阻塞等待,我们应该将这个等待过程交给select函数来完成,因此在获取完连接后直接将该连接对应的文件描述符添加到fd_array数组当中就行了,当该连接的读事件就绪时select函数会告诉我们,那个时候我们在进行数据读取就不会再被阻塞住了。
  • 添加文件描述符到fd_array数组当中,本质就是遍历fd_array数组,找到一个没有被使用的位置将该文件描述符添加进去即可。但有可能fd_array数组中全部的位置都已经被占用了,那么文件描述符就会添加失败,此时就只能将刚刚获取上来的连接对应的套接字进行关闭,因为此时服务器已经没有能力处理这个连接了。

select服务器测试

至此select服务器编写完毕,重新编译后运行服务器,并用telnet工具连接我们的服务器,此时通过telnet向服务器发送的数据就能够被服务器读到并且打印输出了。

此外,虽然当前的select服务器是一个单进程的服务器,但它却可以同时为多个客户端提供服务,根本原因就是因为select函数调用后会告知select服务器是哪个客户端对应的连接事件就绪了,此时select服务器就可以读取对应客户端发来的数据,读取完后又会调用select函数等待某个客户端连接的读事件就绪。

当服务器检测到客户端退出后,也会关闭对应的连接,并将对应的套接字从fd_array数组当中清除。

存在的一些问题

当前的select服务器实际还存在一些问题:

  • 服务器没有对客户端发进行响应。select服务器如果要向客户端发送数据,不能直接调用write函数,因为调用write函数时实际也分为“等”和“拷贝”两步,我们也应该将“等”的这个过程交给select函数,因此在每次调用select函数之前,除了需要重新设置readfds还需要重新设置writefds,并且还需要一个数组来保存需要被监视写事件是否就绪的文件描述符,当某一文件描述符的写事件就绪时我们才能够调用write函数向客户端发送数据。
  • 没有定制协议。代码中读取数据时并没有按照某种规则进行读取,此时就可能造成粘包问题,根本原因就是因为我们没有定制协议,比如HTTP协议规定在读取底层数据时读取到空行就表明读完了一个HTTP报头,此时再根据HTTP报头当中的Content-Length属性得知正文的长度,最终就能够读取到一个完整的HTTP报文,HTTP协议通过这种方式就避免了粘包问题。
  • 没有对应的输入输出缓冲区。代码中直接将读取的数据存储到了字符数组buffer当中,这是不严谨的,因为本次数据读取可能并没有读取到一个完整的报文,此时服务器就不能进行数据的分析处理,应该将读取到的数据存储到一个输入缓冲区当中,当读取到一个完整的报文后再让服务器进行处理。此外,如果服务器要能够对客户端进行响应,那么服务器的响应数据也不应该直接调用write函数发送给客户端,应该先存储到一个输出缓冲区当中,因为响应数据可能很庞大,无法一次发送完毕,可能需要进行分批发送。
     

select的优点

  • 可以同时等待多个文件描述符,并且只负责等待,实际的IO操作由accept、read、write等接口来完成,这些接口在进行IO操作时不会被阻塞。
  • select同时等待多个文件描述符,因此可以将“等”的时间重叠,提高了IO的效率。

当然,这也是所有多路转接接口的优点。

select的缺点

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

select可监控的文件描述符个数

调用select函数时传入的readfds、writefds以及exceptfds都是fd_set结构的,fd_set结构本质是一个位图,它用每一个比特位来标记一个文件描述符,因此select可监控的文件描述符个数是取决于fd_set类型的比特位个数的。

我们可以通过以下代码来看看fd_set类型有多少个比特位。

#include <iostream>
#include <sys/types.h>

int main()
{
	std::cout << sizeof(fd_set)* 8 << std::endl;
	return 0;
}

 运行代码后可以看到,其实select可监控的文件描述符个数就是1024个。

因此我们实现的select服务器当中将fd_array数组的大小设置为1024是足够的,因为readfds当中最多就只能添加1024个文件描述符,但不同环境下fd_set的大小可能是不同的,并且fd_set的大小也是可以调整的(涉及重新编译内核),因此之前select服务器当中对NUM的宏定义正确写法应该是这样的。
 

#define NUM (sizeof(fd_set)*8)

一个进程能打开的文件描述符个数

进程控制块task_struct当中有一个files指针,该指针指向一个struct files_struct结构,进程的文件描述符表fd_array就存储在该结构当中,其中文件描述符表fd_array的大小定义为NR_OPEN_DEFAULTNR_OPEN_DEFAULT的值实际就是32。


但并不意味着一个进程最多只能打开32个文件描述符,进程能打开的文件描述符个数实际是可以扩展的,比如我当前使用的云服务器默认就是把进程能打开的文件描述符设置得很高的,通过ulimit -a命令就可以看到进程能打开的文件描述符上限。

因此select可监控的文件描述符个数太少是一个很大的问题,比如select可监控的文件描述符个数是1024,除去其中的一个监听套接字,那么select服务器最多只能连接1023个客户端。

select的适用场景

多路转接接口select、poll和epoll,需要在一定的场景下使用,如果场景选择的不适宜,可能会适得其反。 

  • 多路转接接口一般适用于多连接,且多连接中只有少部分连接比较活跃。因为少量连接比较活跃,也就意味着几乎所有的连接在进行IO操作时,都需要花费大量时间来等待事件就绪,此时使用多路转接接口就可以将这些等的事件进行重叠,提高IO效率。
  • 对于多连接中大部分连接都很活跃的场景,其实并不适合使用多路转接。因为每个连接都很活跃,也就意味着任何时刻每个连接上的事件基本都是就绪的,此时根本不需要动用多路转接接口来帮我们进行等待,毕竟使用多路转接接口也是需要花费系统的时间和空间资源的。

多连接中只有少量连接是比较活跃的,比如聊天工具,我们登录QQ后大部分时间其实是没有聊天的,此时服务器端不可能调用一个read函数阻塞等待读事件就绪。

多连接中大部分连接都很活跃,比如企业当中进行数据备份时,两台服务器之间不断在交互数据,这时的连接是特别活跃的,几乎不需要等的过程,也就没必要使用多路转接接口了。

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

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

相关文章

vmagent获取node-exporter配置

vmagent 使用以下命令添加图表 helm 存储库&#xff1a; helm repo add vm https://victoriametrics.github.io/helm-charts/helm repo update 列出vm/victoria-metrics-agent可供安装的图表版本&#xff1a; helm search repo vm/victoria-metrics-agent -l victoria-met…

iObit Uninstaller 最新版13 Pro更新了什么?

iObit Uninstaller 专业且易用的 Windows 卸载程序&#xff0c;它可以轻松删除不需要的程序&#xff0c;插件和 Windows 应用程序&#xff0c;还可以对电脑旧的应用一键更新。 功能和特点 安装监视器 安装监视器会检测并记录安装中的所有系统更改&#xff0c;以确保在将来彻底…

基于Mysqlrouter+MHA+keepalived实现高可用半同步 MySQL Cluster项目

目录 项目名称&#xff1a; 基于Mysqlrouter MHA keepalived实现半同步主从复制MySQL Cluster MySQL Cluster&#xff1a; 项目架构图&#xff1a; 项目环境&#xff1a; 项目环境安装包&#xff1a; 项目描述&#xff1a; 项目IP地址规划&#xff1a; 项目步骤: 一…

java不支持发行版本5

这篇文章主要给大家介绍了关于如何解决java错误:不支持发行版本5的相关资料,发行版本5是Java5,已经是十多年前的版本了,现在已经不再被支持,需要的朋友可以参考下 − 目录 问题描述&#xff1a;解决方法&#xff1a;永久解决方法&#xff1a;总结 问题描述&#xff1a; 在i…

小红书内容+直播数据分析,品牌打开秋天的正确方式

秋天的第一杯奶茶&#xff0c;你喝了吗&#xff1f;不知不觉&#xff0c;秋日以至。季节轮转&#xff0c;随之而来的还有变化的消费需求。目前&#xff0c;各大社媒已逐渐开启关于秋天的讨论&#xff0c;新的季节&#xff0c;品牌要如何布局内容呢&#xff1f;本期&#xff0c;…

点成案例丨比浊仪用于乳酸菌抑菌活性测定

乳酸菌概述 自1929年英国科学家弗莱明发现青霉素以来&#xff0c;抗生素为人类医学的进步做出了巨大贡献。然而&#xff0c;抗生素在临床上广泛且持续的使用导致病原微生物产生了耐药性。目前&#xff0c;病原微生物对抗生素的耐药性正在威胁人们的健康&#xff0c;寻找具有抑…

Scratch 之 制作超丝滑 FNF 推条

这个教程是不用画笔的&#xff0c;所以不用担心推条是最后一层了&#xff01; 导入素材 你以为真是这样吗&#xff1f;NO&#xff0c;NO&#xff0c;NO&#xff0c;其实是这样的 没错&#xff0c;中间是空的&#xff01;中间是空的&#xff01;中间是空的&#xff01;&#xf…

【Gazebo安装教程】2023年最新安装全流程详解!

安装 实验环境&#xff1a;ubuntu22.04LTS 安装 Gazebo 首先我们需要安装必须的工具 sudo apt-get update sudo apt-get install lsb-release wget gnupg之后修改源&#xff0c;并 update 一下来更新 sudo wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/shar…

实现无限创建敌人。(Unity)

首先我们创建一个空物体GameObject产怪点改名为Enemypoint&#xff0c;这个点是敌人出现的位置。我们首先在空物体里面的transform组件中重置这个点的位置为0&#xff0c;并且给这个点设置一个颜色&#xff0c;方便找到这个点。如果给这个点设置一个位置&#xff0c;修改它的tr…

移动硬盘只显示盘符不显示容量怎么办

在使用移动硬盘时&#xff0c;有时候可能会遇到移动硬盘只显示盘符而不显示容量的困扰。这种情况可能会引起数据无法读取或无法保存的问题&#xff0c;给用户带来不便。本文将详细解释这个问题的原因&#xff0c;并提供一些解决方法&#xff0c;让您能够轻松恢复移动硬盘的容量…

高忆管理:真金白银落到实处 27家沪市公司更新回购增持“进度条”

或抛出方案&#xff0c;或更新进展&#xff0c;一大批沪市公司及其股东正加速跑在回购增持的路上。仅8月14日晚&#xff0c;沪市便有27家上市公司会集发表回购增持类公告&#xff0c;以真金白银持续提振商场决心。 据上海证券报记者计算&#xff0c;到现在&#xff0c;在回购方…

网络基础--ARP协议介绍

1、ARP作用 ARP&#xff08; Address Resolution Protocol&#xff0c;地址解析协议&#xff09;是将 IP 地址解析为以太网 MAC 地址&#xff08;或称物理地址&#xff09;的协议。在局域网中&#xff0c;当主机或其它网络设备有数据要发送给另一个主机或设备时&#xff0c;它必…

云上社群学习系统部分接口设计详解

目录 一、项目简介 二、技术选型 三、数据库设计 四、接口设计及思考 回复帖子部分 4.1 回复帖子 4.1.1.1 实现逻辑 4.1.1.2创建Service接⼝ 4.1.1.3 实现Service接⼝ 4.1.1.4 实现Controller 4.1.1.5 测试接口 4.1.1.6 实现前端页面 4.2 点赞帖子 4.2.1.1 参数要求…

MySQL 根据多字段查询重复数据

MySQL 根据多字段查询重复数据 在实际的数据库应用中&#xff0c;我们经常需要根据多个字段来查询重复的数据。MySQL 提供了一些方法来实现这个功能&#xff0c;让我们能够快速准确地找到和处理重复数据。本文将介绍如何使用 MySQL 来根据多字段查询重复数据&#xff0c;并提供…

LeetCode 38题:外观数列

题目 给定一个正整数 n &#xff0c;输出外观数列的第 n 项。 「外观数列」是一个整数序列&#xff0c;从数字 1 开始&#xff0c;序列中的每一项都是对前一项的描述。 你可以将其视作是由递归公式定义的数字字符串序列&#xff1a; countAndSay(1) "1"countAnd…

【【verilog典型电路设计之复数乘法器】】

verilog典型电路设计之复数乘法器 典型电路设计之复数乘法器 复数乘法的算法是&#xff1a; 设复数xabi ; ycdi; 则复数乘法结果 x.y(abi)(cdi)(ac-bd) i(adbc) 复数乘法器 我们可以将复数x y 的实部与虚部相乘&#xff0c;减去 x与y的虚部相乘 得到输出结果的实部 就是ac-bd…

DC电源模块减小输入电源与输出负载之间的能量损失

BOSHIDA DC电源模块减小输入电源与输出负载之间的能量损失 随着电子产品的普及&#xff0c;DC电源模块已成为现代电子设备中不可或缺的组成部分。DC电源模块可以将交流电转化为直流电&#xff0c;并根据需要&#xff0c;以适当的电压和电流提供给输出负载。然而&#xff0c;在输…

css3-flex布局:基础使用 / Flexbox布局

一、理解flex 二、理解Flex布局&#xff08;又称Flexbox布局&#xff09; Flex布局&#xff08;又称Flexbox布局&#xff09;是一种基于Web的CSS3布局模式&#xff0c;其目的是为了更加灵活和自适应地布置各种各样的网页元素。Flex布局通过将一个父容器分割为一个或多个弹性项…

gitlab修改远程仓库地址

目录 背景&#xff1a; 解决&#xff1a; 1.删除本地仓库关联的远程地址&#xff0c;添加新的远程仓库地址 2.直接修改本地仓库关联的远程仓库地址 3.打开.git隐藏文件修改远程仓库地址 4.拉取代码报错(git host key verification failed) 背景&#xff1a; 公司搬家&#…

item_review-获得TB商品评论

一、接口参数说明&#xff1a; item_review-获得TB商品评论&#xff0c;点击更多API调试&#xff0c;请移步注册API账号点击获取测试key和secret 公共参数 请求地址: https://api-gw.onebound.cn/taobao/item_review 名称类型必须描述keyString是调用key&#xff08;点击获取…