秒懂Linux之多路转接 select

fe594ea5bf754ddbb223a54d8fb1e7bc.gif

目录

I/O 多路转接之 select

初识 select

select 函数原型

fd_set 结构

 timeval 结构

函数返回值

select 使用示例: 检测标准输入输出

基本框架

事件处理HandlerEvent

select优缺点


I/O 多路转接之 select

初识 select

系统提供 select 函数来实现多路复用输入 / 输出模型 . 可以对应到前文的钓鱼例子~
select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的 ;
程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

select 函数原型

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

参数解释: 

  • 参数 nfds 是需要监视的最大的文件描述符值+1
  • rdset,wrset,exset 分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
  • 参数 timeout 为结构 timeval,用来设置 select()等待时间

 参数 timeout 取值:

  • NULL:则表示 select()没有 timeoutselect 将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。相当于轮询多个fd~
  • 特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回。例如设置(5,0)若是第3s有fd,则返回2代表剩余时间

fd_set 结构

fd_set:文件描述符集合的概念,也可以理解为是位图的标识。这样就意味着fd的存放就跟比特位上的位置与数据有关了。

把需要监视的fd设置进去(等待监视),然后它就会返回出已经就绪好的fd(钓鱼)。

操作 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 结构

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

函数返回值

  • 执行成功则返回文件描述词状态已改变的个数  (检测多条鱼竿中钓鱼的数量)
  • 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回
  • 当有错误发生时则返回-1,错误原因存于 errno,此时参数 readfdswritefds, exceptfds 和 timeout 的值变成不可预测。
———————————————————————————————————————————
前面都是关于select相关的概念解释,接下来就开始模拟实现出select的功能~
在开始之前我们先设想一下select到底有什么用呢?目前在我看来就只是一个可以查看多个fd状态的工具,什么是fd的状态呢?就是指这个文件fd的准备工作,例如标准输入fd:0,标准输出fd:1是否由阻塞等待工作转化为就绪工作来实现自身fd的职能。 select:一个监控?待定~

select 使用示例: 检测标准输入输出

模拟一下客户端输入数据,服务端输出数据,加入了select作为对整个过程fd文件状态的监视工程

基本框架

SelectServerMain.cc 

#include "SelectServer.hpp"
#include <memory>


// ./selectserver port
int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        std::cout<<"Usage: "<<argv[0]<<" port" <<std::endl;
    }
    uint16_t port = std::stoi(argv[1]);
    EnableScreen();
    
    std::unique_ptr<SelectServer> svr = std::make_unique<SelectServer>(port);
    svr->Loop();

    return 0;
}

SelectServer.hpp 

#pragma once
#include <iostream>
#include <string>
#include <memory>
#include "Socket.hpp"

using namespace socket_ns;

class SelectServer
{
public:
    SelectServer(uint16_t port)
        : _port(port),
          _listensock(std::make_unique<TcpSocket>())
    {
        InetAddr addr("0", _port);
        _listensock->BuildListenSocket(addr);
    }

    void Loop()
    {
        while (true)
        {
            //_listensock等待客户端的连接请求,这就相当于对方给我发送数据,我们要触发IO(读取),所以要作为读事件处理
            // 即:新连接的到来就意味着一次读事件,而这是select所在监视的~
            fd_set rfds;                          // 设置读事件集合
            FD_ZERO(&rfds);                       // 清空集合
            FD_SET(_listensock->SockFd(), &rfds); // 把_listensock对应的fd加入读事件集合
         

            struct timeval timeout = {1, 0};

            // 关注rfds集合里面的fd,一旦有就绪状态就设置比特位,并且把关注fd状态改变的个数返回给n
            int n = select(_listensock->SockFd() + 1, &rfds, nullptr, nullptr, &timeout);

            // 根据fd个数进行判定
            switch (n)
            {
            case 0:
                //显示每一次等待的时间
                LOG(INFO, "timeout, %d.%d\n", timeout.tv_sec, timeout.tv_usec);
                break;
            case -1:
                //出现错误
                LOG(ERROR, "select error...\n");
                break;
            default:
                //表示有事件就绪
                LOG(DEBUG, "Event Happen. n : %d\n", n); 
                HandlerEvent(rfds);
                break;
            }
        }
    }

    ~SelectServer()
    {
    }

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;
};

我们设置好需要让内核关注的事件集合rfds后用select开始进行事件的监视~

ps:如果发现事件就绪了,但不去处理的话就会一直反复提醒“Event Happen"直到处理完毕。

所以我们再写一个事件处理函数~这里已经初见端倪了,rfds集合里面是已经就绪的事件,我们可以一次性处理这些事件,按照以前是需要多进程/线程的,现在用一个函数select就搞定了~

事件处理HandlerEvent

    void HandlerEvent(fd_set &rfds)
    {
        InetAddr clientaddr;
        //这里调用accpet并不会阻塞,因为由客户端发来的事件已经就绪
        int sockfd = _listensock->Accepter(&clientaddr);
        if(sockfd>=0)
        {
            LOG(DEBUG,"Get new link,sockfd: %d,client info %s: %d\n",sockfd,clientaddr.Ip().c_str(),clientaddr.Port());
        }
    }

我们在监视到客户端发来的连接请求事件后立马进行处理——服务端accept进行连接~

那么我们这个新的sockfd该如何管理呢?直接拿去用吗?recv(sockfd)/send(sockfd)——都不是,而是要托管给select~因为只有select知道读事件是否就绪,然后才会去合理使用系统调用~

我们把这个辅助数组当成rfds集合~

代码做出以下调整:

void HandlerEvent(fd_set &rfds)
    {
        for (int i = 0; i < N; i++)
        {
            InetAddr clientaddr;
            // 这里调用accpet并不会阻塞,因为由客户端发来的事件已经就绪
            int sockfd = _listensock->Accepter(&clientaddr);
            if (sockfd >= 0)
            {
                LOG(DEBUG, "Get new link,sockfd: %d,client info %s: %d\n", sockfd, clientaddr.Ip().c_str(), clientaddr.Port());
            }
            // 将新的fd以加入集合的方式托管给select进行监视
            int pos = 1;
            // 寻找数组内空闲的位置
            for (; pos < N; pos++)
            {
                if (_fd_array[pos] == defaultfd)
                    break;
            }
            // 若数组位置满了,则关闭当前的sockfd,因为无法处理事件
            if (pos == N)
            {
                close(sockfd);
                LOG(WARNING, "server is full\n");
                return;
            }
            // 把sockfd加入到辅助数组中,后续再从辅助数组里拿出放进rfds
            else
            {
                _fd_array[pos] = sockfd;
                LOG(DEBUG, "%d add to select array!\n", sockfd);
            }
        }
    }

因为select可以一次性监视多个fd是否就绪并同时进行处理,所以事件处理函数内不仅仅只是处理一次,而是保证能够全部处理完事件~因此加入了for循环~不然第一次也只是处理了Listen套接字而已。

但又引入了新的问题,除了listensock,我们还有其他的sockfd,也并不是全部的sockfd都是要走accept这一套的,所以我们还得进行区分~前者是为了获取新连接,而后者是为了读写~

HandlerEvent

void HandlerEvent(fd_set &rfds)
    {
        for (int i = 0; i < N; i++)
        {
            if (_fd_array[i] == defaultfd)
                continue;

            // 判断在辅助数组内的fd是否为rfds所关注的fd,若是则说明对方事件就绪,判断对方的事件处理类型
            if (FD_ISSET(_fd_array[i], &rfds))
            {
                // 若关注fd为_listensock,则给其安排accept的业务
                if (_fd_array[i] == _listensock->SockFd())
                {
                    AcceptClient();
                }
                else
                {
                    // 普通读写事件就绪,使用相应的系统调用~
                    ServiceIO(i);
                }
            }
        }
    }

AcceptClient

void AcceptClient()
    {
        InetAddr clientaddr;
        // 这里调用accpet并不会阻塞,因为由客户端发来的事件已经就绪
        int sockfd = _listensock->Accepter(&clientaddr);
        if (sockfd >= 0)
        {
            LOG(DEBUG, "Get new link,sockfd: %d,client info %s: %d\n", sockfd, clientaddr.Ip().c_str(), clientaddr.Port());
        }
        // 将新的fd以加入集合的方式托管给select进行监视
        int pos = 1;
        // 寻找数组内空闲的位置
        for (; pos < N; pos++)
        {
            if (_fd_array[pos] == defaultfd)
                break;
        }
        // 若数组位置满了,则关闭当前的sockfd,因为无法处理事件
        if (pos == N)
        {
            close(sockfd);
            LOG(WARNING, "server is full\n");
            return;
        }
        // 把sockfd加入到辅助数组中,后续再从辅助数组里拿出放进rfds
        else
        {
            _fd_array[pos] = sockfd;
            LOG(DEBUG, "%d add to select array!\n", sockfd);
        }
    }

ServiceIO

void ServiceIO(int pos)
    {
        char buffer[1024];
        // 这里的读取一定不会阻塞,因为读事件已经就绪了,否则也不会来到这里了
        ssize_t n = recv(_fd_array[pos], buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say# " << buffer << std::endl;
            std::string echo_string = "[server echo]# ";
            echo_string += buffer;
            send(_fd_array[pos], echo_string.c_str(), echo_string.size(), 0);
        }
        // 下面普通事件处理完会被去除出辅助数组,但listensockfd永远不会被去除,因为它需要持续地去监听新的连接
        else if (n == 0)
        {
            LOG(DEBUG, "%d is closed\n", _fd_array[pos]);
            close(_fd_array[pos]);
            _fd_array[pos] = defaultfd;
            LOG(DEBUG, "curr fd_array[] fd list : %s\n", RfdsToString().c_str());
        }
        else
        {
            LOG(DEBUG, "%d recv error\n", _fd_array[pos]);
            close(_fd_array[pos]);
            _fd_array[pos] = defaultfd;
            LOG(DEBUG, "curr fd_array[] fd list : %s\n", RfdsToString().c_str());
        }
    }

RfdsToString

    std::string RfdsToString()
    {
        std::string fdstr;
        for (int i = 0; i < N; i++)
        {
            if (_fd_array[i] == defaultfd)
                continue;
            fdstr += std::to_string(_fd_array[i]);
            fdstr += " ";
        }
        return fdstr;
    }

 最后我们再来测试一下~

select优缺点

优点:显而易见,可以实现多线程/多进程的效果,同时处理多个请求事件~

缺点:也很明显,rfds是有大小限制的,并且由于输入输出混合型参数的特性,每次调用都需要重新做设定,然后就是在监视多个fd的时候,OS需要遍历所有的fd,至少一个准备就绪就立即返回~

全部代码

select的使用 · 29fcf13 · 玛丽亚后/keep - Gitee.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值