个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】

目录

1、I/O 多路转接之 select

1.1、初识 select

1.2、select 函数原型

1.3、测试timeout

1.3.1、主函数

1.3.2、SelectServer类

1.4、处理事件

1.4.1、版本一

1.4.2、版本二

1.4.3、版本三

1.5、补充select的理解

1.6、select 的特点

1.7、select 缺点

1.8、完整代码

1.8.1、SelectServer.hpp

1.8.2、Main.cc


1、I/O 多路转接之 select

1.1、初识 select

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

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

定位:只负责进行等,不进行拷贝!

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

1.2、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结构

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

关于 fd_set 结构

测试代码:

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

int main()
{
    // fd_set;
    std::cout << "fd_set位数容量: " << sizeof(fd_set) * 8 << std::endl;
    return 0;
}

其实这个结构就是一个整数数组, 更严格的说, 是一个 "位图". 使用位图中对应的位来表示要监视的文件描述符.
提供了一组操作 fd_set 的接口, 来比较方便的操作位图.

// 用来清除描述词组 set 中相关fd 的位
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 的全部位
void FD_ZERO(fd_set *set);

函数返回值

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

错误值可能为:

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

1.3、测试timeout

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

1.3.1、主函数

同前面一样,我们先写主函数再依次实现主函数中需要使用到的类型和函数,调用该可执行程序使用 文件名 + 端口号

// ./select_server 8888
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    EnableScreen(); // 开启日志
    std::unique_ptr<SelectServer> svr = std::make_unique<SelectServer>(port);
    svr->InitServer();
    svr->Loop();

    return 0;
}

1.3.2、SelectServer类

SelectServer类 的成员需要用到端口号 和 套接字成员函数暂时实现 InitServer() 和 Loop(),此处的套接字使用前面封装的Socket类,即需要使用到Socket.hpp,Log.hpp,LockGuard.hpp,InetAddr.hpp这四个文件!

基本结构

SelectServer类基本结构包括成员变量端口号和套接字成员函数包括构造,析构,初始化和轮询函数!

class SelectServer
{
public:
    SelectServer(uint16_t port);
    void InitServer();
    void Loop();
    ~SelectServer();

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

构造析构函数

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

SelectServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>())
{
    _listensock->BuildListenSocket(_port);
}

~SelectServer()
{}

Loop()

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

void Loop()
{
    while (true)
    {
        // 临时
        fd_set rfds;
        FD_ZERO(&rfds);                       // 清除 rfds 中相关fd的位
        FD_SET(_listensock->Sockfd(), &rfds); // 将fd设置进rfds中

        struct timeval timeout = {3, 0}; // 每隔3秒timeout一次
        // 不能直接获取连接,listensock && accept 我们把他也看做IO类的函数,只关心新链接到来,等价于读事情就绪!
        // _listensock->Accepter();
        // int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, &timeout); // 临时(第一个参数),只关心读
        int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, nullptr);
        switch (n)
        {
        case 0:
            LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
            break;
        case -1:
            LOG(ERROR, "select error\n");
            break;
        default:
            LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
            break;
        }
    }
}

InitServer()

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

运行结果

timeout参数填写实际数值时!

 timeout参数填写nullptr时!

等待时间设置为30秒,并3秒执行一次任务! 

 

1.4、处理事件

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

1.4.1、版本一

Loop()

Loop()进行轮询判断如果事件就绪,就调用处理任务的函数

void Loop()
{
    while (true)
    {
        // 临时
        fd_set rfds;
        FD_ZERO(&rfds);                       // 清除 rfds 中相关fd的位
        FD_SET(_listensock->Sockfd(), &rfds); // 将fd设置进rfds中

        // 不能直接获取连接,listensock && accept 我们把他也看做IO类的函数,只关心新链接到来,等价于读事情就绪!
        // _listensock->Accepter();
        int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, nullptr);
        switch (n)
        {
        case 0:
            LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
            break;
        case -1:
            LOG(ERROR, "select error\n");
            break;
        default:
            // 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
            LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
            HandlerEvent(rfds);
            break;
        }
    }
}

HandlerEvent()

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

void HandlerEvent(fd_set &rfds)
{
    // 判断fd是否在rfds集合中
    if(FD_ISSET(_listensock->Sockfd(), &rfds))
    {
        // 链接事件就绪,等价于读事件就绪
        InetAddr addr;
        int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
        if(sockfd > 0)
        {
            LOG(DEBUG,"get a new link,client info %s:%d\n",addr.Ip().c_str(),addr.Port());
            // 已经获得了一个新的sockfd
            // 接下来我们可以读取?绝对不能读,条件不一定满足
            // 谁最清楚底层fd的数据是否就绪了呢?通过select
            // 想办法把新的fd添加给select,由select统一进行监管
            // select 为什么等待的fd越来越多呢? 因为要把新的fd添加给select
        }
        else
        {
            return;
        }
    }
}

注意:此处为了更加原生,需要修改Socket类的Accepter()函数的返回值!

Socket类和TcpSocket类 

class Socket
{
public:
    virtual int Accepter(InetAddr *cliaddr) = 0; // 获取连接
}

class TcpSocket : public Socket
{
public:
    // 获取连接
    int Accepter(InetAddr *cliaddr) override
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        // 获取新连接
        int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);
        // 获取失败继续获取
        if (sockfd < 0)
        {
            LOG(WARNING, "sccept reeor\n");
            // return nullptr;
            return -1;
        }
        *cliaddr = InetAddr(client);
        LOG(INFO, "get a new link,client info: %s,sockfd:%d\n", cliaddr->AddrStr().c_str(), _sockfd);

        // return std::make_shared<TcpSocket>(sockfd); // C++14
        return sockfd;
    }
}

运行结果

1.4.2、版本二

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

SelectServer类结构

class SelectServer
{
    const static int gnum = sizeof(fd_set) * 8;
    const static int gdefaultfd = -1;
private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;

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

InitServer()

InitServer()函数将数组初始化为默认fd,并将listensockfd添加到数组的第一个元素!

void InitServer()
{
    for (int i = 0; i < gnum; i++)
    {
        fd_array[i] = gdefaultfd;
    }
    fd_array[0] = _listensock->Sockfd(); // 默认直接添加sockfd到数组中
}

Loop()

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

1、文件描述符初始化

2、合法的fd添加到rfds集合中   2.1、更新出最大的fd的值

3、检查读条件是否就绪

void Loop()
{
    while (true)
    {
        // 1.文件描述符初始化
        fd_set rfds;
        FD_ZERO(&rfds); // 清除 rfds 中相关fd的位
        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); // 将fd设置进rfds中
            // 2.1 更新出最大的fd的值
            if (max_fd < fd_array[i])
            {
                max_fd = fd_array[i];
            }
        }

        struct timeval timeout = {30, 0}; // 等待30秒

        // 3.检查读条件是否就绪
        int n = ::select(max_fd + 1, &rfds, nullptr, nullptr, nullptr);
        switch (n)
        {
        case 0:
            LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
            break;
        case -1:
            LOG(ERROR, "select error\n");
            break;
        default:
            // 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
            LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
            HandlerEvent(rfds);
            PrintDebug();
            sleep(1);
            break;
        }
    }
}

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中内容

就绪是listensockfd  

// 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
void HandlerEvent(fd_set &rfds)
{
    for (int i = 0; i < gnum; i++)
    {
        // 1.判断fd是否合法
        if (fd_array[i] == gdefaultfd)
            continue;
        // 2.判断fd是否就绪
        // fd一定是合法的fd
        // 合法的fd不一定就绪,判断fd是否就绪?
        if (FD_ISSET(fd_array[i], &rfds))
        {
            // 读事件就绪
            // 2.1 listensockfd 2.2 normal sockfd
            if (_listensock->Sockfd() == fd_array[i])
            {
                // listensockfd
                // 链接事件就绪,等价于读事件就绪
                InetAddr addr;
                int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
                if (sockfd > 0)
                {
                    LOG(DEBUG, "get a new link,client info %s:%d\n", addr.Ip().c_str(), addr.Port());
                    // 已经获得了一个新的sockfd
                    // 接下来我们可以读取?绝对不能读,条件不一定满足
                    // 谁最清楚底层fd的数据是否就绪了呢?通过select
                    // 想办法把新的fd添加给select,由select统一进行监管--怎么做到?
                    // select 为什么等待的fd越来越多呢? 因为要把新的fd添加给select
                    // 只要将新的fd添加到fd_array中即可!
                    bool flag = false;
                    for (int pos = 1; pos < gnum; pos++)
                    {
                        if (fd_array[pos] == gdefaultfd)
                        {
                            flag = true;
                            fd_array[pos] = sockfd; // 把新的fd放入数组中
                            LOG(INFO,"add %d to fd_array success \n",sockfd);
                            break;
                        }
                    }
                    // 数组满了
                    if (!flag)
                    {
                        LOG(WARNING, "Server Is Full\n");
                        ::close(sockfd);
                    }
                }
            }
            else
            {
                // normal sockfd,正常的读写

            }
        }
    }
}

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 << "\n";
}

测试listenfd

就绪是normal sockfd

// 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
void HandlerEvent(fd_set &rfds)
{
    for (int i = 0; i < gnum; i++)
    {
        // 1.判断fd是否合法
        if (fd_array[i] == gdefaultfd)
            continue;
        // 2.判断fd是否就绪
        // fd一定是合法的fd
        // 合法的fd不一定就绪,判断fd是否就绪?
        if (FD_ISSET(fd_array[i], &rfds))
        {
            // 读事件就绪
            // 2.1 listensockfd 2.2 normal sockfd
            if (_listensock->Sockfd() == fd_array[i])
            {
                // listensockfd
                // ...
            }
            else
            {
                // normal sockfd,正常的读写
                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 echo_str = "[server echo info]";
                    echo_str += buffer;
                    // 读写是空的  写就绪: 发送缓冲区有没有空间
                    ::send(fd_array[i],echo_str.c_str(),echo_str.size(),0); // 可以直接发,临时方案
                }
                else if(n == 0)
                {
                    LOG(INFO,"client quit...\n");
                    // 关闭fd
                    ::close(fd_array[i]);
                    // select 不再关心这个fd了
                    fd_array[i] = gdefaultfd;
                }
                else
                {
                    LOG(ERROR,"recv error\n");
                    // 关闭fd
                    ::close(fd_array[i]);
                    // select 不再关心这个fd了
                    fd_array[i] = gdefaultfd;
                }
            }
        }
    }
}

上面的代码确实能够测试普通sockfd,但是浏览器不能显示内容,可以在发送给客户端时,增加一点显示效果!

// normal sockfd,正常的读写
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);
}

注意:此处的读写消息是不太严谨的,根据我们前面学过的协议,我们读取的内容是包含报头和有效数据的,但是此处重点是为了测试select,协议不是这里的重点! 

1.4.3、版本三

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

HandlerEvent()

// 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
void HandlerEvent(fd_set &rfds)
{
    // 事件派发
    for (int i = 0; i < gnum; i++)
    {
        // 1.判断fd是否合法
        if (fd_array[i] == gdefaultfd)
            continue;
        // 2.判断fd是否就绪
        // fd一定是合法的fd
        // 合法的fd不一定就绪,判断fd是否就绪?
        if (FD_ISSET(fd_array[i], &rfds))
        {
            // 读事件就绪
            // 2.1 listensockfd 2.2 normal sockfd
            if (_listensock->Sockfd() == fd_array[i])
            {
                // listensockfd
                // 链接事件就绪,等价于读事件就绪
                Accepter();
            }
            else
            {
                // normal sockfd,正常的读写
                HandlerIO(i);
            }
        }
    }
}

Accepter()

// 处理新链接
void Accepter()
{
    InetAddr addr;
    int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
    if (sockfd > 0)
    {
        LOG(DEBUG, "get a new link,client info %s:%d\n", addr.Ip().c_str(), addr.Port());
        // 已经获得了一个新的sockfd
        // 接下来我们可以读取?绝对不能读,条件不一定满足
        // 谁最清楚底层fd的数据是否就绪了呢?通过select
        // 想办法把新的fd添加给select,由select统一进行监管--怎么做到?
        // select 为什么等待的fd越来越多呢? 因为要把新的fd添加给select
        // 只要将新的fd添加到fd_array中即可!
        bool flag = false;
        for (int pos = 1; pos < gnum; pos++)
        {
            if (fd_array[pos] == gdefaultfd)
            {
                flag = true;
                fd_array[pos] = sockfd; // 把新的fd放入数组中
                LOG(INFO, "add %d to fd_array success \n", sockfd);
                break;
            }
        }
        // 数组满了
        if (!flag)
        {
            LOG(WARNING, "Server Is Full\n");
            ::close(sockfd);
        }
    }
}

HandlerIO()

// 处理普通fd就绪
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 echo_str = "[server echo info]";
        // echo_str += buffer;
        // 读写是空的  写就绪: 发送缓冲区有没有空间
        // ::send(fd_array[i],echo_str.c_str(),echo_str.size(),0); // 可以直接发,临时方案

        // 测试二
        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(INFO, "client quit...\n");
        // 关闭fd
        ::close(fd_array[i]);
        // select 不再关心这个fd了
        fd_array[i] = gdefaultfd;
    }
    else
    {
        LOG(ERROR, "recv error\n");
        // 关闭fd
        ::close(fd_array[i]);
        // select 不再关心这个fd了
        fd_array[i] = gdefaultfd;
    }
}

1.5、补充select的理解

1、select要正常工作,需要借助一个辅助数组,来保存所有合法fd!

2、每次使用都要重置!

3、就绪了,循环检测处理所有事件!

1.6、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 的第一个参数。

1.7、select 缺点

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

1.8、完整代码

1.8.1、SelectServer.hpp

#pragma once

#include <iostream>
#include <sys/select.h>
#include "Socket.hpp"
#include "InetAddr.hpp"

using namespace socket_ns;

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

public:
    SelectServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>())
    {
        _listensock->BuildListenSocket(_port);
    }
    // 版本一
    // void InitServer()
    // {}
    // 版本一
    // void HandlerEvent(fd_set &rfds)
    // {
    //     // 判断fd是否在rfds集合中
    //     if(FD_ISSET(_listensock->Sockfd(), &rfds))
    //     {
    //         // 链接事件就绪,等价于读事件就绪
    //         InetAddr addr;
    //         int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
    //         if(sockfd > 0)
    //         {
    //             LOG(DEBUG,"get a new link,client info %s:%d\n",addr.Ip().c_str(),addr.Port());
    //             // 已经获得了一个新的sockfd
    //             // 接下来我们可以读取?绝对不能读,条件不一定满足
    //             // 谁最清楚底层fd的数据是否就绪了呢?通过select
    //             // 想办法把新的fd添加给select,由select统一进行监管
    //             // select 为什么等待的fd越来越多呢? 因为要把新的fd添加给select
    //         }
    //         else
    //         {
    //             return;
    //         }
    //     }
    // }
    // 版本一
    // void Loop()
    // {
    //     while (true)
    //     {
    //         // 临时
    //         fd_set rfds;
    //         FD_ZERO(&rfds);                       // 清除 rfds 中相关fd的位
    //         FD_SET(_listensock->Sockfd(), &rfds); // 将fd设置进rfds中

    //         struct timeval timeout = {3, 0}; // 每隔3秒timeout一次
    //         // 不能直接获取连接,listensock && accept 我们把他也看做IO类的函数,只关心新链接到来,等价于读事情就绪!
    //         // _listensock->Accepter();
    //         // int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, &timeout); // 临时(第一个参数),只关心读
    //         int n = ::select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, nullptr);
    //         switch (n)
    //         {
    //         case 0:
    //             LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
    //             break;
    //         case -1:
    //             LOG(ERROR, "select error\n");
    //             break;
    //         default:
    //             // LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
    //             // 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
    //             LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
    //             HandlerEvent(rfds);
    //             // sleep(3);
    //             break;
    //         }
    //     }
    // }
    void InitServer()
    {
        for (int i = 0; i < gnum; i++)
        {
            fd_array[i] = gdefaultfd;
        }
        fd_array[0] = _listensock->Sockfd(); // 默认直接添加sockfd到数组中
    }
    // 处理新链接
    void Accepter()
    {
        InetAddr addr;
        int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
        if (sockfd > 0)
        {
            LOG(DEBUG, "get a new link,client info %s:%d\n", addr.Ip().c_str(), addr.Port());
            // 已经获得了一个新的sockfd
            // 接下来我们可以读取?绝对不能读,条件不一定满足
            // 谁最清楚底层fd的数据是否就绪了呢?通过select
            // 想办法把新的fd添加给select,由select统一进行监管--怎么做到?
            // select 为什么等待的fd越来越多呢? 因为要把新的fd添加给select
            // 只要将新的fd添加到fd_array中即可!
            bool flag = false;
            for (int pos = 1; pos < gnum; pos++)
            {
                if (fd_array[pos] == gdefaultfd)
                {
                    flag = true;
                    fd_array[pos] = sockfd; // 把新的fd放入数组中
                    LOG(INFO, "add %d to fd_array success \n", sockfd);
                    break;
                }
            }
            // 数组满了
            if (!flag)
            {
                LOG(WARNING, "Server Is Full\n");
                ::close(sockfd);
            }
        }
    }
    // 处理普通fd就绪
    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 echo_str = "[server echo info]";
            // echo_str += buffer;
            // 读写是空的  写就绪: 发送缓冲区有没有空间
            // ::send(fd_array[i],echo_str.c_str(),echo_str.size(),0); // 可以直接发,临时方案

            // 测试二
            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(INFO, "client quit...\n");
            // 关闭fd
            ::close(fd_array[i]);
            // select 不再关心这个fd了
            fd_array[i] = gdefaultfd;
        }
        else
        {
            LOG(ERROR, "recv error\n");
            // 关闭fd
            ::close(fd_array[i]);
            // select 不再关心这个fd了
            fd_array[i] = gdefaultfd;
        }
    }
    // 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
    void HandlerEvent(fd_set &rfds)
    {
        // 事件派发
        for (int i = 0; i < gnum; i++)
        {
            // 1.判断fd是否合法
            if (fd_array[i] == gdefaultfd)
                continue;
            // 2.判断fd是否就绪
            // fd一定是合法的fd
            // 合法的fd不一定就绪,判断fd是否就绪?
            if (FD_ISSET(fd_array[i], &rfds))
            {
                // 读事件就绪
                // 2.1 listensockfd 2.2 normal sockfd
                if (_listensock->Sockfd() == fd_array[i])
                {
                    // listensockfd
                    // 链接事件就绪,等价于读事件就绪
                    Accepter();
                }
                else
                {
                    // normal sockfd,正常的读写
                    HandlerIO(i);
                }
            }
        }
    }
    void Loop()
    {
        while (true)
        {
            // 1.文件描述符初始化
            fd_set rfds;
            FD_ZERO(&rfds); // 清除 rfds 中相关fd的位
            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); // 将fd设置进rfds中
                // 2.1 更新出最大的fd的值
                if (max_fd < fd_array[i])
                {
                    max_fd = fd_array[i];
                }
            }

            struct timeval timeout = {30, 0}; // 等待30秒

            // 3.检查读条件是否就绪
            int n = ::select(max_fd + 1, &rfds, nullptr, nullptr, nullptr);
            switch (n)
            {
            case 0:
                LOG(DEBUG, "time out,%d.%d\n", timeout.tv_sec, timeout.tv_usec);
                break;
            case -1:
                LOG(ERROR, "select error\n");
                break;
            default:
                // 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
                LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
                HandlerEvent(rfds);
                PrintDebug();
                // sleep(1);
                break;
            }
        }
    }
    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 << "\n";
    }
    ~SelectServer()
    {
    }

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

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

1.8.2、Main.cc

#include <iostream>
#include <memory>

#include "SelectServer.hpp"

// ./select_server 8888
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    EnableScreen(); // 开启日志
    std::unique_ptr<SelectServer> svr = std::make_unique<SelectServer>(port);
    svr->InitServer();
    svr->Loop();

    return 0;
}

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐