【网络】高级IO

目录

一、五种IO模型

1、阻塞IO

2、非阻塞IO

3、信号驱动

4、IO多路转接

5、异步IO

6、总结

二、高级IO重要概念

1、同步通信与异步通信

2、阻塞 vs 非阻塞

三、非阻塞IO

1、fcntl

2、实现函数SetNoBlock

四、IO多路转接select

1、select

1.1、参数解释

1.2、参数timeout取值

1.3、select函数返回值

1.4、关于fd_set结构

1.5、常见的程序片段

2、selectserver

2.1、 select执行过程

2.2、socket就绪条件

2.2.1、读就绪

2.2.2、写就绪

2.3、实现代码

2.4、select的缺点

五、IO多路转接poll

1、poll函数接口

2、poll的特点

2.1、优点

2.2、缺点

3、pollserver

六、IO多路转接epoll

1、epoll

1.1、epoll_create

1.2、epoll_ctl

1.3、epoll_wait

2、epoll原理总结

3、epoll的优点

4、epollserver 基础版本

5、epoll的工作方式

5.1、水平触发Level Triggered 工作模式

5.2、边缘触发Edge Triggered工作模式

5.3、对比LT和ET

6、epollserver ET工作方式

7、Reactor


IO = 等待资源 + 拷贝资源。

等待资源:等待要拷贝的数据、拷贝数据需要的空间。当资源就绪后,该状态称为IO事件就绪。在 TCP协议中,有一个状态标志位 PSH,本质上就是把资源设置为就绪状态。

一、五种IO模型

1、阻塞IO

阻塞IO:在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。

阻塞IO是最常见的IO模型:

2、非阻塞IO

 非阻塞IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。

 非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用。

3、信号驱动

信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。

4、IO多路转接

 IO多路转接:虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。

5、异步IO

 异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。

6、总结

 任何IO过程中,都包含两个步骤。第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。让IO更高效,最核心的办法就是让等待的时间尽量少。

二、高级IO重要概念

1、同步通信与异步通信

同步和异步关注的是消息通信机制:

  •  所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
  •  异步则是相反, 调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后, 被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

2、阻塞 vs 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

  •  阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
  •  非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

三、非阻塞IO

1、fcntl

一个文件描述符,默认都是阻塞IO。

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

 传入的cmd的值不同,后面追加的参数也不相同

fcntl函数有5种功能:

  • 复制一个现有的描述符(cmd=F_DUPFD) .
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)

我们此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞。

2、实现函数SetNoBlock

基于fcntl,我们实现一个SetNoBlock函数,将文件描述符设置为非阻塞。

void SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFD); //获取指定文件的状态标志位
    if(fl < 0)
    {
        std::cerr << "error string : " << strerror(error) << "error code: " << error << std::endl;
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
  • 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)。
  • 然后再使用F_SETFL将文件描述符设置回去。设置回去的同时,加上一个O_NONBLOCK参数。

 如果我们将文件设置为非阻塞状态,那么一旦底层没有数据就绪,就会以出错的形式返回,但是不算真正的出错,错误码会被设置为 11 ,代表临时资源没有就绪。

void SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFD); //获取指定文件的状态标志位
    if(fl < 0)
    {
        std::cerr << "error string : " << strerror(errno) << " error code: " << errno << std::endl;
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int main()
{
    char buffer[64];
    SetNonBlock(0);
    while(1)
    {
        printf(">>> ");
        fflush(stdout);
        ssize_t n = read(0, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n - 1] = 0;
            std::cout << "echo# " << buffer << std::endl;
        }
        else if(n == 0)
        {
            std::cout << "end file" << std::endl;
            break;
        }
        else
        {
            if(errno == EAGAIN || errno == EWOULDBLOCK)
            {
                //因为底层数据没有准备好,希望下次继续来检查
                std::cout << "data not really" << std::endl;
                sleep(1);
                continue;
            }

            else if(errno == EINTR)
            {
                //这次IO被信号中断了,也需要重新读取
                continue;
            }
            else
            {
                //再下面才是真正的读取失败了
                std::cout << "read error" << " error string : " << strerror(errno) << " error code: " << errno << std::endl;
                break;
            }
        }
    }
}

四、IO多路转接select

1、select

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

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

1.1、参数解释

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

1.2、参数timeout取值

  • NULL:阻塞等待。表示select()没有timeout, select将一直被阻塞,直到某个文件描述符上发生了事件。
  • {0,0}:非阻塞等待。仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段里没有事件发生, select将超时返回。
  • timeout是输入输出型参数,被返回时,值为剩余的时间。

1.3、select函数返回值

  • 返回值n > 0:表示有 n 个文件描述符就绪。
  • 返回值n = 0:等待超时,指定时间内没有文件描述符就绪。
  • 返回值n < 0:有等待失败的情况,错误原因存于errno,此时参数readfds, writefds,exceptfds和timeout的值变成不可预测。

 错误值可能为:

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

1.4、关于fd_set结构

 其实这个结构就是一个整数数组,更严格的说,是一个 "位图"使用位图中对应的位来表示要监视的文件描述符。这个位图的上限在Linux系统中是 1024 。

提供了一组操作 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 函数中的 fd_set 类型的参数都是输入输出型参数。 

 用户首先通过 fd_set 类型参数输入,告诉内核哪些 fd 需要内核关心。内核通过这些 fd_set 类型参数输出,告诉用户哪些 fd 的对应事件已经就绪了。

1.5、常见的程序片段

fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){……}

2、selectserver

2.1、 select执行过程

 理解select模型的关键在于理解fd_set。

 为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

  1.  执行fd_set set。FD_ZERO(&set)。则set用位表示是0000,0000。
  2.  若fd= 5,执行 FD_SET(fd,&set) 后set变为 0001,0000 (第5位置为1)
  3.  若再加入fd= 2, fd=1,则set变为 0001,0011
  4.  执行select(6,&set,nullptr,nullptr,nullptr)阻塞等待
  5.  若fd=1,fd=2上都发生可读事件,则select返回,此时set变为 0000,0011。注意:没有事件发生的fd=5被清空。

2.2、socket就绪条件

2.2.1、读就绪

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

2.2.2、写就绪

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

2.3、实现代码

select服务器在使用的时候,需要程序员自己维护一个第三方数组,对已经获取的sock做管理。

  •  一是用于在select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
  •  二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
typedef struct FdEvent
{
    int fd;
    uint8_t event;
    std::string clientip;
    uint16_t clientport;
} type_t;

type_t _fdarray[N];

具体服务器代码如下:

#pragma once

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

#include "Sock.hpp"
#include "log.hpp"

const static int gport = 8888;

#define READ_EVENT (0x1 << 0)
#define WRITE_EVENT (0x1 << 1)
#define EXCEPT_EVENT (0x1 << 2)

typedef struct FdEvent
{
    int fd;
    uint8_t event;
    std::string clientip;
    uint16_t clientport;
} type_t;

// typedef int type_t;
// static const int defaultfd = -1;

static const int defaultevent = 0;

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].fd = defaultfd;
            _fdarray[i].event = defaultevent;
            _fdarray[i].clientport = 0;
        }
    }

    void Accepter()
    {
        std::cout << "有一个新连接" << std::endl;
        // 这里进行Accept就不会再被阻塞了
        std::string clientip;
        uint16_t clientport;
        int sock = _listensock.Accept(&clientip, &clientport);
        if (sock < 0)
            return;

        // 不能直接读,因为不知道sock有没有数据就绪
        // 需要将sock交给select,让select进行管理。
        logMessage(DEBUG, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);

        // 只要把获得的sock添加到_fdarray数组里就可以了
        int pos = 1;
        for (; pos < N; pos++)
        {
            if (_fdarray[pos].fd == defaultfd)
                break;
        }
        if (pos >= N)
        {
            // 数组满了
            close(sock);
            logMessage(WARNING, "sockfd array[] full");
        }
        else
        {
            _fdarray[pos].fd = sock;
            _fdarray[pos].event = READ_EVENT;
            _fdarray[pos].clientip = clientip;
            _fdarray[pos].clientport = clientport;
        }
    }

    void Recver(int index)
    {
        // serverIO();
        int fd = _fdarray[index].fd;
        char buffer[1024];
        ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0); // 这里读取不会被阻塞
        if (s > 0)
        {
            buffer[s - 1] = 0;
            std::cout << _fdarray[index].clientip << " : " <<  _fdarray[index].clientport << " : " << buffer << std::endl;

            // 把数据发送回去也要被select管理
            std::string echo = buffer;
            echo += "[select server echo]";
            send(fd, echo.c_str(), echo.size(), 0);
        }
        else
        {
            if (s == 0)
                logMessage(INFO, "client quit, _fdarray[i] -> defaultfd: %d->%d", fd, defaultfd);
            else
                logMessage(WARNING, "recv error, _fdarray[i] -> defaultfd: %d->%d", fd, defaultfd);

            close(_fdarray[index].fd);
            _fdarray[index].fd = defaultfd;
            _fdarray[index].event = defaultevent;
            _fdarray[index].clientip.resize(0);
            _fdarray[index].clientport = 0;
        }
    }

    void HandlerEvent(fd_set &rfds, fd_set &wfds)
    {
        for (int i = 0; i < N; i++)
        {
            if (_fdarray[i].fd == defaultfd)
                continue;

            if ((_fdarray[i].event & READ_EVENT) && (FD_ISSET(_fdarray[i].fd, &rfds)))
            {
                // 处理读取,1.accept 2、recv
                if (_fdarray[i].fd == _listensock.Fd())
                {
                    Accepter();
                }
                else if (_fdarray[i].fd != _listensock.Fd()) 
                {
                    Recver(i);
                }
                else
                {}
            }
            else if((_fdarray[i].event & WRITE_EVENT) && (FD_ISSET(_fdarray[i].fd, &wfds)))
            {
                
            }
            else
            {}
        }
    }

    void Start()
    {
        // 在网络中,新连接到来被当作读事件就绪。
        //_listensock.Accept(); 不能直接进行accept,因为如果没有链接到来,程序会被阻塞住

        // 此时,服务端只有一个文件描述符

        // 这种写法不正确,因为直接把rfds写死了,应该是动态变化的
        //  struct timeval timeout = {0, 0};
        //  fd_set rfds;
        //  FD_ZERO(&rfds);
        //  FD_SET(_listensock.Fd(), &rfds);

        _fdarray[0].fd = _listensock.Fd();
        _fdarray[0].event = READ_EVENT;

        while (1)
        {
            // 因为rfds是一个输入输出行参数,注定了每次都要对rfds进行重置。
            // 重置就必须要知道历史上有哪些fd
            // 因为服务器在运行中,sockfd的值一直在动态变化,所以maxfd也在一直变化
            fd_set rfds;
            fd_set wfds;
            FD_ZERO(&rfds);
            FD_ZERO(&wfds);
            int maxfd = _fdarray[0].fd;
            for (int i = 0; i < N; i++)
            {
                if (_fdarray[i].fd == defaultfd)
                    continue;
                // 合法fd
                if (_fdarray[i].event & READ_EVENT)
                    FD_SET(_fdarray[i].fd, &rfds);

                if (_fdarray[i].event & WRITE_EVENT)
                    FD_SET(_fdarray[i].fd, &wfds);

                if (maxfd < _fdarray[i].fd)
                    maxfd = _fdarray[i].fd;
            }

            int n = select(maxfd + 1, &rfds, &wfds, nullptr, nullptr);
            switch (n)
            {
            case 0:
                logMessage(DEBUG, "time out, %d: %s", errno, strerror(errno));
                break;
            case -1:
                logMessage(WARNING, "%d: %s", errno, strerror(errno));
                break;
            default:
                logMessage(DEBUG, "有一个就绪事件发生了 : %d", n);
                HandlerEvent(rfds, wfds);
                DebugPrint();
                break;
            }
            sleep(1);
        }
    }

    void DebugPrint()
    {
        std::cout << "fdarray[]: ";
        for (int i = 0; i < N; i++)
        {
            if (_fdarray[i].fd == defaultfd)
                continue;
            std::cout << _fdarray[i].fd << " ";
        }
        std::cout << "\n";
    }

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

private:
    uint16_t _port;
    Sock _listensock;
    type_t _fdarray[N];
};

2.4、select的缺点

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

五、IO多路转接poll

1、poll函数接口

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

// pollfd结构
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events */
    short revents; /* returned events */
};

参数说明

  •  fds是一个poll函数监听的结构列表。每一个元素中,包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合。
  •  nfds表示fds数组的长度。
  •  timeout表示poll函数的超时时间,单位是毫秒(ms)。

events和revents的取值

 返回结果

  • 返回值小于0, 表示出错;
  • 返回值等于0, 表示poll函数等待超时;
  • 返回值大于0, 表示poll由于监听的文件描述符就绪而返回

2、poll的特点

2.1、优点

不同于 select 使用三个位图来表示三个 fdset 的方式, poll 使用一个 pollfd 的指针实现。

  •  pollfd 结构包含了要监视的 event 和发生的 event ,不再使用select “参数-值” 传递的方式。接口使用比select更方便。
  •  poll并没有最大数量限制 (但是数量过大后性能也是会下降)。

2.2、缺点

poll中监听的文件描述符数目增多时:

  •  和select函数一样,poll返回后,需要轮询 pollfd 来获取就绪的描述符。
  •  每次调用 poll 都需要把大量的 pollfd 结构从用户态拷贝到内核中。
  •  同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

3、pollserver

实现代码:

#pragma once

#include <iostream>
#include <string>
#include <sys/poll.h>
#include <cstring>

#include "Sock.hpp"
#include "log.hpp"

const static int gport = 8888;
static const int N = 4096;
const static short defaultevent = 0;

typedef struct pollfd type_t;
// static const int defaultfd = -1;

class PollServer
{

public:
    PollServer(uint16_t port = gport)
        : _port(port), _fdarray(nullptr)
    {
    }

    void InitServer()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();
        _fdarray = new type_t[N];

        for (int i = 0; i < N; i++)
        {
            _fdarray[i].fd = defaultfd;
            _fdarray[i].events = defaultevent;
            _fdarray[i].revents = defaultevent;
        }
    }

    void Accepter()
    {
        std::cout << "有一个新连接" << std::endl;
        // 这里进行Accept就不会再被阻塞了
        std::string clientip;
        uint16_t clientport;
        int sock = _listensock.Accept(&clientip, &clientport);
        if (sock < 0)
            return;

        // 不能直接读,因为不知道sock有没有数据就绪
        // 需要将sock交给select,让select进行管理。
        logMessage(DEBUG, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);

        // 只要把获得的sock添加到_fdarray数组里就可以了
        int pos = 1;
        for (; pos < N; pos++)
        {
            if (_fdarray[pos].fd == defaultfd)
                break;
        }
        if (pos >= N)
        {
            // 数组满了,可以进行动态扩容
            close(sock); 
            logMessage(WARNING, "sockfd array[] full");
        }
        else
        {
            _fdarray[pos].fd = sock;
            _fdarray[pos].events = POLLIN; //POLLIN | POLLOUT
            _fdarray[pos].revents = defaultevent;
        }
    }

    void HandlerEvent()
    {
        for (int i = 0; i < N; i++)
        {
            int fd = _fdarray[i].fd;
            short revent = _fdarray[i].revents;

            if (fd == defaultfd)
                continue;
            if ((fd == _listensock.Fd()) && (revent & POLLIN))
            {
                Accepter();
            }
            else if ((fd != _listensock.Fd()) && (revent & POLLIN))
            {
                // serverIO();
                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管理
                    //向event里添加写事件
                    _fdarray[i].events = POLLIN | POLLOUT;
                    std::string echo = buffer;
                    echo += "[select server echo]";
                    send(fd, echo.c_str(), echo.size(), 0);
                }
                else
                {
                    if (s == 0)
                        logMessage(INFO, "client quit, _fdarray[i] -> defaultfd: %d->%d", fd, defaultfd);
                    else
                        logMessage(WARNING, "recv error, _fdarray[i] -> defaultfd: %d->%d", fd, defaultfd);

                    close(fd);
                    _fdarray[i].fd = defaultfd;
                    _fdarray[i].events = defaultevent;
                    _fdarray[i].revents = defaultevent;
                }
            }
        }
    }

    void Start()
    {
        // 在网络中,新连接到来被当作读事件就绪。
        //_listensock.Accept(); 不能直接进行accept,因为如果没有链接到来,程序会被阻塞住

        // 此时,服务端只有一个文件描述符

        // 这种写法不正确,因为直接把rfds写死了,应该是动态变化的
        //  struct timeval timeout = {0, 0};
        //  fd_set rfds;
        //  FD_ZERO(&rfds);
        //  FD_SET(_listensock.Fd(), &rfds);

        _fdarray[0].fd = _listensock.Fd();
        _fdarray[0].events = POLLIN;

        while (1)
        {
            // 因为rfds是一个输入输出行参数,注定了每次都要对rfds进行重置。
            // 重置就必须要知道历史上有哪些fd
            // 因为服务器在运行中,sockfd的值一直在动态变化,所以maxfd也在一直变化

            int timeout = -1; //设为-1,表示阻塞式调用
            int n = poll(_fdarray, N, timeout); //可以对_fdarrat内容进行管理,合法的fd、event全部放入_fdarray的最左侧。
            switch (n)
            {
            case 0:
                logMessage(DEBUG, "time out, %d : %s", errno, strerror(errno));
                break;
            case -1:
                logMessage(WARNING, "%d: %s", errno, strerror(errno));
                break;
            default:
                logMessage(DEBUG, "有一个就绪事件发生了 : %d", n);
                HandlerEvent();
                DebugPrint();
                break;
            }
            // sleep(1);
        }
    }

    void DebugPrint()
    {
        std::cout << "fdarray[]: ";
        for (int i = 0; i < N; i++)
        {
            if (_fdarray[i].fd == defaultfd)
                continue;
            std::cout << _fdarray[i].fd << " ";
        }
        std::cout << "\n";
    }

    ~PollServer()
    {
        _listensock.Close();
        if(_fdarray)
            delete[] _fdarray;
    }

private:
    uint16_t _port;
    Sock _listensock;
    type_t *_fdarray;
};

六、IO多路转接epoll

1、epoll

 epoll 是为处理大批量句柄而作了改进的 poll

 它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)。它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

epoll 有3个相关的系统调用:

1.1、epoll_create

int epoll_create(int size);

 创建一个epoll的句柄

  • 自从linux2.6.8之后, size参数是被忽略的。
  • 用完之后,必须调用close()关闭。

 调用 epll_create 函数后,会在OS内核中创建一个红黑树。红黑树节点管理的数据类型是 struct epoll_event ,里面包含用户让OS管理的文件描述符以及对应的事件。

 创建红黑树的同时,也会创建一个就绪队列。

 函数调用成功后,返回文件描述符,该文件中保存一个eventpoll结构体,结构体中就存放红黑树的根节点与就绪队列的头节点。

1.2、epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

 epoll_ctl 函数的本质,是通过epoll模型对红黑树做操作,向红黑树中新增、删除或修改某一个节点。即对红黑树进行增删改。

 epoll_ctl函数除了对红黑树进行增删改外,还要给节点结构体里文件描述符所指向的file结构体注册回调机制。

 file结构体中本来就有一个回调函数指针,并在拷贝操作完成后自动调用。只不过一般这个指针都被设置为NULL,现在给改结构体注册一个回调函数,并把函数指针指向这个函数。

 回调函数的功能:在文件中数据拷贝完成之后,把本文件描述符对应的红黑树节点添加到就绪队列中。

 epoll的事件注册函数

  •  它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
  •  第一个参数是 epoll_create() 的返回值(epoll的句柄)。
  •  第二个参数表示动作,用三个宏来表示。
  •  第三个参数是需要监听的fd。
  •  第四个参数是告诉内核需要监听什么事。

第二个参数的取值:

  • EPOLL_CTL_ADD :注册新的fd到epfd中。
  • EPOLL_CTL_MOD :修改已经注册的fd的监听事件。
  • EPOLL_CTL_DEL :从epfd中删除一个fd。

struct epoll_event 结构如下:

1.3、epoll_wait

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

 OS 会把已经就绪的文件描述符对应的红黑树节点中的epoll_event结构体也添加到到就绪队列里(此时这个节点及隶属于红黑树,也隶属于队列)。

 epoll_wait 负责以时间复杂度为O(1)的方式,检测有没有事件就绪,即检测队列是否为空。

 epoll_wait收集在epoll监控的事件中已经发送的事件:

  • 参数events是分配好的epoll_event结构体数组。
  • epoll将会把发生的事件从就绪队列里拷贝到events数组中 (events不可以是空指针,内核只负责把数据就绪队列里拷贝到这个events数组中,不会去帮助我们在用户态中分配内存)。
  • maxevents告知内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size。
  • 参数timeout是超时时间 (毫秒, 0会立即返回, -1是永久阻塞)。
  • 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败。

2、epoll原理总结

 当某一进程调用epoll_create方法时, Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。

truct eventpoll{
    ....
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
    ....
};
  •  每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。
  •  这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
  •  而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法。
  •  这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
  •  在epoll中,对于每一个事件,都会建立一个epitem结构体。
struct epitem{
    struct rb_node rbn;//红黑树节点
    struct list_head rdllink;//双向链表节点
    struct epoll_filefd ffd; //事件句柄信息
    struct eventpoll *ep; //指向其所属的eventpoll对象
    struct epoll_event event; //期待发生的事件类型
}
  •  当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。
  •  如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1)。

总结一下, epoll的使用过程就是三部曲:

  • 调用epoll_create创建一个epoll句柄;
  • 调用epoll_ctl, 将要监控的文件描述符进行注册;
  • 调用epoll_wait, 等待文件描述符就绪

3、epoll的优点

  •  接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效。不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开。
  •  数据拷贝轻量:只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)。
  •  事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪。这个操作时间复杂度O(1)。即使文件描述符数目很多,效率也不会受到影响。
  •  没有数量限制:文件描述符数目无上限。

4、epollserver 基础版本

实现代码:

//Epoller.hpp
#pragma once

#include <iostream>
#include <string>
#include <sys/epoll.h>
#include <cstring>
#include <stdlib.h>

#include "log.hpp"
#include "error.hpp"

static const int defaultepfd = -1;
static const int gsize = 128;

class Epoller
{
public:
    Epoller()
    :_epfd(defaultepfd)
    {

    }

    void Create()
    {
        _epfd = epoll_create(gsize);
        if(_epfd < 0)
        {
            logMessage(FATAL, "listen error, code: %d, errstring: %s", errno, strerror(errno));
            exit(EPOLL_CREATE_ERR);
        }
    }

    //向epoll中添加事件
    bool AddEvent(int fd, uint32_t events)
    {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = fd;    //用户数据,epoll底层不对该数据做任何处理,就是为了给未来返回准备的

        int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
        if(n < 0)
        {
            logMessage(FATAL, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));
            return false;
        }
        return true;
    }

    bool DelEvent(int fd)
    {
        //epoll在操作的时候,fd必须得合法
        return epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr) == 0;
    }

    int Wait(struct epoll_event* revs, int num, int timeout)
    {
        return epoll_wait(_epfd, revs, num, timeout);
    }

    int Fd()
    {
        return _epfd;
    }

    void Close()
    {
        if(_epfd != defaultepfd)
            close(_epfd);
    }

    ~Epoller()
    {}
private:
    int _epfd;
};



//Epollserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <assert.h>
#include <functional>

#include "Sock.hpp"
#include "log.hpp"
#include "error.h"
#include "Epoller.hpp"

const static int gport = 8888;

using func_t = std::function<std::string(std::string)>;

class EpollServer
{
    const static int gnum = 64;

public:
    EpollServer(func_t func, uint16_t port = gport)
        :_func(func) 
        ,_port(port)
    {
    }

    void InitServer()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();
        _epoller.Create();

        logMessage(DEBUG, "initserver success");
    }

    void Start()
    {
        // 1. 将listensock添加到epoll中
        bool r = _epoller.AddEvent(_listensock.Fd(), EPOLLIN);
        assert(r);
        (void)r;

        int timeout = 1000;
        while (1)
        {
            int n = _epoller.Wait(_revs, gnum, timeout);
            switch (n)
            {
            case 0:
                logMessage(DEBUG, "timeout");
                break;
            case -1:
                logMessage(WARNING, "epoll_wait filed");
                break;
            default:
                // n就是就绪事件的个数
                logMessage(DEBUG, "有%d个事件就绪了", n);
                HandlerEvents(n);
                break;
            }
        }
    }

    void HandlerEvents(int num)
    {
        for (int i = 0; i < num; i++)
        {
            int fd = _revs[i].data.fd;
            uint32_t events = _revs[i].events;

            if (events & EPOLLIN)
            {
                // 读事件就绪
                if (fd == _listensock.Fd())
                {
                    // 1.新连接事件到来
                    //  logMessage(DEBUG, "get a new line ... ");
                    std::string clientip;
                    uint16_t clientport;
                    int sock = _listensock.Accept(&clientip, &clientport);

                    if (sock < 0)
                        continue;

                    logMessage(DEBUG, "%s:%d 已经连接上了服务器", clientip.c_str(), clientport);

                    // 1.1.此时,不能直接recv/read,因为需要使用多路转接
                    bool r = _epoller.AddEvent(sock, EPOLLIN);
                    assert(r);
                    (void)r;
                }
                else
                {
                    // 2.读取事件
                    char request[1024];
                    ssize_t s = recv(fd, request, sizeof(request) - 1, 0); // 这里读取不会被阻塞
                    if (s > 0)
                    {
                        request[s - 1] = 0; //  \r\n的形式为结尾的
                        request[s - 2] = 0;

                        std::string response = _func(request);

                        send(fd, response.c_str(), response.size(), 0);
                    }
                    else
                    {
                        if (s == 0)
                            logMessage(INFO, "client quit... ");
                        else
                            logMessage(WARNING, "recv error ...");

                        //在处理异常的时候,先移除,再关闭
                        _epoller.DelEvent(fd);
                        close(fd);
                    }
                }
            }
        }
    }

    ~EpollServer()
    {
        _listensock.Close();
        _epoller.Close();
    }

private:
    uint16_t _port;
    Sock _listensock;
    Epoller _epoller;
    struct epoll_event _revs[gnum];
    func_t _func;
};

 因为TCP协议中,数据是以字节流的方式发送读取的,完整报文由应用层协议规定。所以我们直接读取是没有办法保证读取到完整的报文的。

 为了解决这个问题,我们就需要自定义应用层协议,并通过回调函数来处理数据,得到完整报文。

5、epoll的工作方式

5.1、水平触发Level Triggered 工作模式

 在我们使用 select、poll、epoll的时候,在最基本的情况下,一旦有事件就绪,如果上层不取, 底层就会一直通知用户事件已经就绪。这种工作方式为LT(水平触发)工作模式。

epoll默认状态下就是LT工作模式。

  •  当epoll检测到socket上事件就绪的时候,可以不立刻进行处理,或者只处理一部分。
  •  如果缓冲区中还有数据,在第二次调用 epoll_wait 时,epoll_wait仍然会立刻返回并通知socket读事件就绪。
  •  直到缓冲区上所有的数据都被处理完,epoll_wait 才不会立刻返回。
  •  支持阻塞读写和非阻塞读写。

5.2、边缘触发Edge Triggered工作模式

如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志,epoll进入ET工作模式。

  •  当epoll检测到socket上事件就绪时,必须立刻处理。
  •  如果缓冲区中还有数据,在第二次调用 epoll_wait 的时候,epoll_wait 不会再返回了。
  •  也就是说,ET模式下,文件描述符上的事件就绪后,只有一次处理机会。
  •  ET的性能比LT性能更高( epoll_wait 返回的次数少了很多)。Nginx默认采用ET模式使用epoll。
  •  只支持非阻塞的读写,因为要强逼着程序员必须要将本轮数据全部读取完毕。因为缓冲区大小有限,为了保证把数据读完,采用的策略是进行循环读取,直到某一次读取到的数据量少于预期值,就说明数据已经读完,没有剩余数据了。这时如果采用的是阻塞读取,如果全部数据的数据量刚刚好是缓冲区大小的整数倍,那么最后一次读完之后,因为全部数据已经读完,但是循环读取却没有遇到过读取数据量少于预期的情况,还会继续读取,又因为没有数据了,从而陷入阻塞状态。

5.3、对比LT和ET

  •  LT是 epoll 的默认行为。使用 ET 能够减少 epoll 触发的次数。但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完。
  •  相当于一个文件描述符就绪之后,不会反复被提示就绪,看起来就比 LT 更高效一些。但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理,不让这个就绪被重复提示的话,其实性能也是一样的。
  •  另一方面,ET 的代码复杂程度更高了。

 ET强逼程序员尽快取走所有的数据,本质上是让TCP底层更新出更大的接收窗口,从而在较大概率上,提供对方的滑块窗口的大小,提高发送效率。

6、epollserver ET工作方式

//Epoll.hpp

#pragma once

#include <iostream>
#include <string>
#include <sys/epoll.h>
#include <cstring>
#include <stdlib.h>

#include "log.hpp"
#include "error.hpp"

static const int defaultepfd = -1;
static const int gsize = 128;

class Epoller
{
public:
    Epoller()
    :_epfd(defaultepfd)
    {

    }

    void Create()
    {
        _epfd = epoll_create(gsize);
        if(_epfd < 0)
        {
            logMessage(FATAL, "listen error, code: %d, errstring: %s", errno, strerror(errno));
            exit(EPOLL_CREATE_ERR);
        }
    }

    //合并添加和修改
    bool AddModEvent(int fd, uint32_t events, int op)
    {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = fd;    //用户数据,epoll底层不对该数据做任何处理,就是为了给未来返回准备的

        int n = epoll_ctl(_epfd, op, fd, &ev);
        if(n < 0)
        {
            logMessage(FATAL, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));
            return false;
        }
        return true;
    }

    bool DelEvent(int fd)
    {
        //epoll在操作的时候,fd必须得合法
        return epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr) == 0;
    }


    int Wait(struct epoll_event* revs, int num, int timeout)
    {
        return epoll_wait(_epfd, revs, num, timeout);
    }

    int Fd()
    {
        return _epfd;
    }

    void Close()
    {
        if(_epfd != defaultepfd)
            close(_epfd);
    }

    ~Epoller()
    {}
private:
    int _epfd;
};


//EpollServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <assert.h>
#include <functional>
#include <unordered_map>

#include "Sock.hpp"
#include "log.hpp"
#include "error.h"
#include "Epoller.hpp"
#include "util.hpp"
#include "Protocol.hpp"

using namespace Protocol_ns;

const static int gport = 8888;
const static int bsize = 1024;
class Connection;
class EpollServer;

using func_t = std::function<void(Connection *, const Request &)>;
using callback_t = std::function<void(Connection *)>;

// 大号的结构体
class Connection
{
public:
    Connection(const int &fd, const std::string &clientip, const uint16_t &clientport)
        : _fd(fd), _clientip(clientip), _clientport(clientport)
    {
    }
    void Register(callback_t recver, callback_t sender, callback_t excepter)
    {
        _recver = recver;
        _sender = sender;
        _excepter = excepter;
    }
    ~Connection()
    {
    }

public:
    // IO信息
    int _fd;
    std::string _inbuffer;
    std::string _outbuffer;
    // IO处理函数
    callback_t _recver;
    callback_t _sender;
    callback_t _excepter;

    // 用户信息, only debug
    std::string _clientip;
    uint16_t _clientport;

    // 也可以给conn带上自己要关心的事件
    uint32_t events;

    // 回指指针
    EpollServer *R;
};

class EpollServer
{
    const static int gnum = 64;

public:
    EpollServer(func_t func, uint16_t port = gport) : _func(func), _port(port)
    {
    }

    void InitServer()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();
        _epoller.Create();
        AddConnection(_listensock.Fd(), EPOLLIN | EPOLLET);
        logMessage(DEBUG, "init server success");
    }

    // 事件派发器
    int Dispatcher() // 名字要改
    {
        int timeout = -1;
        while (true)
        {
            LoopOnce(timeout);
        }
    }

    void LoopOnce(int timeout)
    {
        int n = _epoller.Wait(_revs, gnum, timeout);

        for (int i = 0; i < n; i++)
        {
            int fd = _revs[i].data.fd;
            uint32_t events = _revs[i].events;

            if ((events & EPOLLERR) || (events & EPOLLHUP))
                //_connections[fd]->_excepter(_connections[fd]);
                events |= (EPOLLIN | EPOLLOUT);
                //这一步是将所有的异常情况,最后都转为recv, send的异常。

            if ((events & EPOLLIN) && ConnIsExists(fd))  //判断fd还存不存在,因为有可能在处理异常的时候把fd关掉了,下面再使用就会报错
                _connections[fd]->_recver(_connections[fd]);

            if ((events & EPOLLOUT) && ConnIsExists(fd))
                _connections[fd]->_sender(_connections[fd]);
        }
    }
    void AddConnection(int fd, uint32_t events, const std::string &ip = "127.0.0.1", uint16_t port = gport)
    {
        // 设置fd是非阻塞
        if (events & EPOLLET)
            Util::SetNonBlock(fd);

        // 为listensock创建对应的connect对象
        Connection *conn = new Connection(fd, ip, port);

        if (fd == _listensock.Fd())
        {
            conn->Register(std::bind(&EpollServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
        }
        else
        {
            conn->Register(std::bind(&EpollServer::Recver, this, std::placeholders::_1),
                           std::bind(&EpollServer::Sender, this, std::placeholders::_1),
                           std::bind(&EpollServer::Excepter, this, std::placeholders::_1));
        }

        // 把events赋值给conn对象,以便在下面进行处理数据的时候,判断是ET的还是LT的
        conn->events = events;
        conn->R = this;

        // 将listensock,connection对象添加到connections中
        _connections.insert(std::pair<int, Connection*>(fd, conn));

        // 添加事件
        bool r = _epoller.AddModEvent(fd, events, EPOLL_CTL_ADD);
        assert(r);
        (void)r;

        logMessage(DEBUG, "addConnection success, fd: %d, clientinfo: [%s:%d]", fd, ip.c_str(), port);
    }

    bool EnableReadWrite(Connection* conn, bool readable, bool writeable)
    {
        uint32_t events = EPOLLET;
        events |= (readable ? EPOLLIN : 0);
        events |= (writeable ? EPOLLOUT : 0);

        conn->events = events;
        return _epoller.AddModEvent(conn->_fd, conn->events, EPOLL_CTL_MOD);
    }

    // 连接管理器
    void Accepter(Connection *conn)
    {
        // 1.新连接事件到来
        //  logMessage(DEBUG, "get a new line ... ");

        // 可能有多个连接同时到来,因此需要循环读取,保证每一个连接都被读取到了
        do
        {
            int err = 0; // 用作Accept的输出型参数,如果监听失败了,用于返回accept函数的错误码
            std::string clientip;
            uint16_t clientport;
            int sock = _listensock.Accept(&clientip, &clientport, &err);

            if (sock > 0)
            {
                logMessage(DEBUG, "%s:%d 已经连接上了服务器", clientip.c_str(), clientport);

                AddConnection(sock, EPOLLIN | EPOLLET, clientip, clientport);
            }

            else
            {
                if(err == EAGAIN || err == EWOULDBLOCK) //说明读完了,新数据没来
                    break;


                //下面的都没读完
                else if(err == EINTR)
                    continue;
                else
                {
                    logMessage(WARNING, "errstring: %s, errcode: %d", strerror(err), err);
                    continue;
                }
            }

        } while (conn->events & EPOLLET);
    }


    void Recver(Connection *conn)
    {
        // 读取完毕本轮数据!
        do
        {
            char buffer[bsize];
            ssize_t n = recv(conn->_fd, buffer, sizeof(buffer) - 1, 0);
            if (n > 0)
            {
                buffer[n] = 0;
                conn->_inbuffer += buffer;
                // 根据基本协议,进行数据分析 -- 自己定过一个!
                std::string requestStr;
                int n = Protocol_ns::ParsePackage(conn->_inbuffer, &requestStr);
                if(n > 0) 
                {
                    requestStr = RemoveHeader(requestStr, n);
                    Request req;
                    req.Deserialize(requestStr);
                    _func(conn, req); // request 保证是一个完整的请求报文!
                }

                // logMessage(Debug, "inbuffer: %s, [%d]", conn->inbuffer_.c_str(), conn->fd_);
            }
            else if (n == 0)
            {
                conn->_excepter(conn);
            }
            else
            {
                if (errno == EAGAIN || errno == EWOULDBLOCK)
                    break;
                else if (errno == EINTR)
                    continue;
                else
                    conn->_excepter(conn);
            }
        } while (conn->events & EPOLLET);
    }

   void Sender(Connection *conn)
    {
        do
        {
            ssize_t n = send(conn->_fd, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);
            if(n > 0) //发送成功,n是这次发送,发送了多少数据
            {
                conn->_outbuffer.erase(0, n);  //移除已经发送了的数据。
                std::cout << "you can see me" << std::endl;
                if(conn->_outbuffer.empty())   //如果数据已经发送完了,就把写关心去掉
                {
                    EnableReadWrite(conn, true, false);
                    break;
                }
                else
                {
                    EnableReadWrite(conn, true, true);
                }
            }
            else
            {
                if(errno == EAGAIN || errno == EWOULDBLOCK)
                    break;
                else if(errno == EINTR)
                    continue;
                else
                {
                    conn->_excepter(conn);
                    break;
                }
            }
        }while(conn->events & EPOLLET);
    }

    void Excepter(Connection *conn)
    {
        logMessage(DEBUG, "Excepter..., fd: %d, clientinfo: [%s:%d]", conn->_fd, conn->_clientip.c_str(), conn->_clientport);
    }

    bool ConnIsExists(int fd)
    {
        return _connections.find(fd) != _connections.end();
    }

    ~EpollServer()
    {
        _listensock.Close();
        _epoller.Close();
    }

private:
    uint16_t _port;
    Sock _listensock;
    Epoller _epoller;
    struct epoll_event _revs[gnum];
    func_t _func;
    std::unordered_map<int, Connection *> _connections;
};

//main.cc
#include "EpollServer.hpp"
#include <memory>

Response calculaterHelper(const Request& req)
{
    Response resp(0, 0);
    switch(req._op)
    {
        case '+':
            resp._result = req._x + req._y; 
            break;
        case '-': 
            resp._result = req._x - req._y; 
            break;
        case '*':
            resp._result = req._x * req._y; 
            break;
        case '/':
            if(req._y == 0)
                resp._code = 1;
            else
                resp._result = req._x / req._y; 
            break;
        case '%':
            if(req._y == 0)
                resp._code = 2;
            else
                resp._result = req._x % req._y; 
            break;
        default:
            resp._code = 3;
            break;
    }

    return resp;
    
}

void Calculate(Connection* conn, const Request& req)
{
    Response resp = calculaterHelper(req);
    std::string sendStr;
    resp.Serialize(&sendStr);
    sendStr = Protocol_ns::AddHeader(sendStr);

    //发送
    //在epoll中,关于fd的读取,一般要常设置(一直要让epoll关心)
    //对于fd的写入,一般是按需设置(不能常设值),只有需要发的时候,才设置
    
    //V1
    conn->_outbuffer += sendStr;
    //开启对写事件的关心
    conn->R->EnableReadWrite(conn, true, true);  //一般初次设置对写事件的关心,对应的fd会立刻触发一次就绪(因为发送buffer一定是空的)
}

int main()
{   
    std::unique_ptr<EpollServer> svr(new EpollServer(Calculate));

    svr->InitServer();
    svr->Dispatcher();
    return 0;
}

7、Reactor

Reactor是基于多路转接,包含事件派发器、连接管理器等的半同步,半异步的IO服务器。

 半同步,半异步体现在,Reactor可以只负责事件派发,这是同步的,而数据读写、业务处理交给上层的线程池来处理,这是异步的。

由epoll进行驱动,并进行事件派发,这种服务器就是Reactor服务器。

 Reactor模式翻译过来是反应堆模式,他就像一个反应堆,连接接了很多connection对象,由epoll进行管理,如果那个connction对象就绪了,epoll就会通知上层,进行处理。

几个重要的注意事项:

  •  在多路转接中,对于任何文件描述符,读要进行常设值,写要进行按需设置。
  •  在进行写入时,把写使能一次,就对应了写入一次。
  •  在多路转接中,读需要交给epoll等待,而写可以直接写。这是因为读缓冲区默认为空,即事件不满足,因此需要等待。而写事件不满足是因为写缓冲区满了,我们第一次发送的时候写缓冲区应该是空的,所以可以直接写,不用在epoll里等待,如果一次没写完,再等。

 关于连接管理,在客户端进行服务器连接时,服务器要把所有的客户端连接都管理起来,这无疑占用了服务器很多资源。由于可能由很多客户端仅仅与服务器建立了连接,却没有与服务器进行信息交互,因此服务器为了节省资源需要定期清理这些超时的客户端。 

 为了实现这个功能,在连接类中增加了一个时间变量lasttime,用于记录最后一次读取的时间,每次进行读取时更新这个时间。在事件派发器中进行检测工作,判断客户端最后一次访问的时间并进行与超时时间对比。

 一个服务器中,多数客户端剩余的连接时间都是不同的,所以把epoll的等待时间timeout设置为所有客户端中剩余时间最短的那一个就好了。这个逻辑可使用最小堆来实现。

Reactor服务器:

//Epoll.hpp
#pragma once

#include <iostream>
#include <string>
#include <sys/epoll.h>
#include <cstring>
#include <stdlib.h>

#include "log.hpp"
#include "error.hpp"

static const int defaultepfd = -1;
static const int gsize = 128;

class Epoller
{
public:
    Epoller()
    :_epfd(defaultepfd)
    {

    }

    void Create()
    {
        _epfd = epoll_create(gsize);
        if(_epfd < 0)
        {
            logMessage(FATAL, "listen error, code: %d, errstring: %s", errno, strerror(errno));
            exit(EPOLL_CREATE_ERR);
        }
    }

    //合并添加和修改
    bool AddModEvent(int fd, uint32_t events, int op)
    {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = fd;    //用户数据,epoll底层不对该数据做任何处理,就是为了给未来返回准备的

        int n = epoll_ctl(_epfd, op, fd, &ev);
        if(n < 0)
        {
            logMessage(FATAL, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));
            return false;
        }
        return true;
    }

    bool DelEvent(int fd)
    {
        //epoll在操作的时候,fd必须得合法
        return epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr) == 0;
    }


    int Wait(struct epoll_event* revs, int num, int timeout)
    {
        return epoll_wait(_epfd, revs, num, timeout);
    }

    int Fd()
    {
        return _epfd;
    }

    void Close()
    {
        if(_epfd != defaultepfd)
            close(_epfd);
    }

    ~Epoller()
    {}
private:
    int _epfd;
};


//EpollServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <assert.h>
#include <functional>
#include <unordered_map>
#include <ctime>

#include "Sock.hpp"
#include "log.hpp"
#include "error.h"
#include "Epoller.hpp"
#include "util.hpp"
#include "Protocol.hpp"

using namespace Protocol_ns;

const static int gport = 8888;
const static int bsize = 1024;
const static int linkTimeOut = 30;  //连接保持时间

class Connection;
class EpollServer;

using func_t = std::function<const Response(const Request &)>;
using callback_t = std::function<void(Connection *)>;

// 大号的结构体
class Connection
{
public:
    Connection(const int &fd, const std::string &clientip, const uint16_t &clientport)
        : _fd(fd), _clientip(clientip), _clientport(clientport)
    {
    }
    void Register(callback_t recver, callback_t sender, callback_t excepter)
    {
        _recver = recver;
        _sender = sender;
        _excepter = excepter;
    }
    ~Connection()
    {
    }

public:
    // IO信息
    int _fd;
    std::string _inbuffer;
    std::string _outbuffer;
    // IO处理函数
    callback_t _recver;
    callback_t _sender;
    callback_t _excepter;

    // 用户信息, only debug
    std::string _clientip;
    uint16_t _clientport;

    // 也可以给conn带上自己要关心的事件
    uint32_t events;

    // 回指指针
    EpollServer *R;

    //时间戳
    time_t lasttime; //该connection最近一次就绪的时间
};

class EpollServer
{
    const static int gnum = 64;

public:
    EpollServer(func_t func, uint16_t port = gport) : _func(func), _port(port)
    {
    }

    void InitServer()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();
        _epoller.Create();
        AddConnection(_listensock.Fd(), EPOLLIN | EPOLLET);
        logMessage(DEBUG, "init server success");
    }

    // 事件派发器
    int Dispatcher() 
    {
        int timeout = 1000;
        while (true)
        {
            LoopOnce(timeout);

            checkLink();
        }
    }

    void LoopOnce(int timeout)
    {
        int n = _epoller.Wait(_revs, gnum, timeout);

        for (int i = 0; i < n; i++)
        {
            int fd = _revs[i].data.fd;
            uint32_t events = _revs[i].events;

            if ((events & EPOLLERR) || (events & EPOLLHUP))
                //_connections[fd]->_excepter(_connections[fd]);
                events |= (EPOLLIN | EPOLLOUT);
            // 这一步是将所有的异常情况,最后都转为recv, send的异常。

            if ((events & EPOLLIN) && ConnIsExists(fd)) // 判断fd还存不存在,因为有可能在处理异常的时候把fd关掉了,下面再使用就会报错
                _connections[fd]->_recver(_connections[fd]);

            if ((events & EPOLLOUT) && ConnIsExists(fd))
                _connections[fd]->_sender(_connections[fd]);
        }
    }
    void AddConnection(int fd, uint32_t events, const std::string &ip = "127.0.0.1", uint16_t port = gport)
    {
        // 设置fd是非阻塞
        if (events & EPOLLET)
            Util::SetNonBlock(fd);

        // 为listensock创建对应的connect对象
        Connection *conn = new Connection(fd, ip, port);

        if (fd == _listensock.Fd())
        {
            conn->Register(std::bind(&EpollServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
        }
        else
        {
            conn->Register(std::bind(&EpollServer::Recver, this, std::placeholders::_1),
                           std::bind(&EpollServer::Sender, this, std::placeholders::_1),
                           std::bind(&EpollServer::Excepter, this, std::placeholders::_1));
        }

        // 把events赋值给conn对象,以便在下面进行处理数据的时候,判断是ET的还是LT的
        conn->events = events;
        conn->R = this;
        conn->lasttime = time(nullptr);

        // 将listensock,connection对象添加到connections中
        _connections.insert(std::pair<int, Connection *>(fd, conn));

        // 添加事件
        bool r = _epoller.AddModEvent(fd, events, EPOLL_CTL_ADD);
        assert(r);
        (void)r;

        logMessage(DEBUG, "addConnection success, fd: %d, clientinfo: [%s:%d]", fd, ip.c_str(), port);
    }

    // 在多路转接中,对于任何描述符,读要常设值,写要按需设置
    bool EnableReadWrite(Connection *conn, bool readable, bool writeable)
    {
        uint32_t events = EPOLLET;
        events |= (readable ? EPOLLIN : 0);
        events |= (writeable ? EPOLLOUT : 0);

        conn->events = events;
        return _epoller.AddModEvent(conn->_fd, conn->events, EPOLL_CTL_MOD);
    }

    // 连接管理器
    void Accepter(Connection *conn)
    {
        // 1.新连接事件到来
        //  logMessage(DEBUG, "get a new line ... ");

        // 可能有多个连接同时到来,因此需要循环读取,保证每一个连接都被读取到了
        do
        {
            int err = 0; // 用作Accept的输出型参数,如果监听失败了,用于返回accept函数的错误码
            std::string clientip;
            uint16_t clientport;
            int sock = _listensock.Accept(&clientip, &clientport, &err);

            if (sock > 0)
            {
                logMessage(DEBUG, "%s:%d 已经连接上了服务器", clientip.c_str(), clientport);

                AddConnection(sock, EPOLLIN | EPOLLET, clientip, clientport);
            }

            else
            {
                if (err == EAGAIN || err == EWOULDBLOCK) // 说明读完了,新数据没来
                    break;

                // 下面的都没读完
                else if (err == EINTR)
                    continue;
                else
                {
                    logMessage(WARNING, "errstring: %s, errcode: %d", strerror(err), err);
                    continue;
                }
            }

        } while (conn->events & EPOLLET);
    }

    void HandlerRequest(Connection *conn)
    {
        int quit = false;
        while (!quit)
        {
            std::string requestStr;

            // 1.提取完整报文
            int n = Protocol_ns::ParsePackage(conn->_inbuffer, &requestStr);
            if (n > 0)
            {
                // 2.提取有效载荷
                requestStr = RemoveHeader(requestStr, n);

                // 3.进行反序列化
                Request req;
                req.Deserialize(requestStr);

                // 4.进行业务处理
                Response resp = _func(req); // request 保证是一个完整的请求报文!

                // 5.序列化
                std::string RespStr;
                resp.Serialize(&RespStr);

                // 6.添加报头
                RespStr = AddHeader(RespStr);

                // 7.进行返回
                conn->_outbuffer += RespStr;
            }
            else
                quit = true;
        }
    }

    bool RecverHelper(Connection *conn)
    {
        int ret = true;
        conn->lasttime = time(nullptr); //更新conn最近访问的时间

        // 读取完毕本轮数据!
        do
        {
            char buffer[bsize];
            ssize_t n = recv(conn->_fd, buffer, sizeof(buffer) - 1, 0);
            if (n > 0)
            {
                buffer[n] = 0;
                conn->_inbuffer += buffer;
                // 根据基本协议,进行数据分析 -- 自己定过一个!
                // 可以边读取边分析,也可以读完之后,一起分析。

                // logMessage(Debug, "inbuffer: %s, [%d]", conn->inbuffer_.c_str(), conn->fd_);
            }
            else if (n == 0)
            {
                conn->_excepter(conn);
                ret = false;
                break;
            }
            else
            {
                if (errno == EAGAIN || errno == EWOULDBLOCK)
                    break;
                else if (errno == EINTR)
                    continue;
                else
                {
                    conn->_excepter(conn);
                    ret = false;
                    break;
                }
            }
        } while (conn->events & EPOLLET);

        return ret;
    }

    void Recver(Connection *conn)
    {
        // 读取数据
        if(!RecverHelper(conn))
            return;

        // 分析数据
        HandlerRequest(conn);

        // 一般在进行写入时,直接写入,没写完才交给epoll
        if (!conn->_outbuffer.empty())
        {
            conn->_sender(conn);
        }
    }

    void Sender(Connection *conn)
    {
        bool safe = true;
        do
        {
            ssize_t n = send(conn->_fd, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);
            if (n > 0) // 发送成功,n是这次发送,发送了多少数据
            {
                conn->_outbuffer.erase(0, n); // 移除已经发送了的数据。

                // 如果发送缓冲区已经为空,直接break;
                if (conn->_outbuffer.empty())
                    break;
            }
            else
            {
                // 把对方接收缓冲区写满了,退出
                if (errno == EAGAIN || errno == EWOULDBLOCK)
                {
                    break;
                }

                else if (errno == EINTR)
                    continue;

                else
                {
                    safe = false;
                    conn->_excepter(conn);
                    break;
                }
            }
        } while (conn->events & EPOLLET);

        if (!safe)
            return;

        // 如果对方接收缓冲区已经被写满了满了,写不下了,并且发送缓冲区中还有数据
        if (!conn->_outbuffer.empty())
        {
            // 设置写关心,交给epoll管理
            EnableReadWrite(conn, true, true);
        }
        else
        {
            EnableReadWrite(conn, true, false);
        }
    }

    //这个函数要防止重复调用,因此在上面很多函数中都添加了bool值进行判断
    void Excepter(Connection *conn)
    {
        // 1.先从epoll中移除fd
        _epoller.DelEvent(conn->_fd);

        // 2.移除unordered_map中的KV关系
        _connections.erase(conn->_fd);

        // 3.关闭fd
        close(conn->_fd);

        // 4.将connction对象释放
        delete conn;

        logMessage(DEBUG, "Excepter...done, fd: %d, clientinfo: [%s:%d]", conn->_fd, conn->_clientip.c_str(), conn->_clientport);
    }

    bool ConnIsExists(int fd)
    {
        return _connections.find(fd) != _connections.end();
    }

    //检查是否超时,断开连接
    void checkLink()
    {
        time_t curr = time(nullptr);
        for(auto& connection : _connections)
        {
            if(connection.second->lasttime + linkTimeOut < curr) //如果最后一次访问的时间 + 超时时间 > 当前时间,就要断开连接了。
                Excepter(connection.second);

            else 
                continue;
        }
    }

    ~EpollServer()
    {
        _listensock.Close();
        _epoller.Close();
    }

private:
    uint16_t _port;
    Sock _listensock;
    Epoller _epoller;
    struct epoll_event _revs[gnum];
    func_t _func;
    std::unordered_map<int, Connection *> _connections;
};

//main.cc
#include "EpollServer.hpp"
#include <memory>



Response Calculater(const Request& req)
{
    Response resp(0, 0);
    switch(req._op)
    {
        case '+':
            resp._result = req._x + req._y; 
            break;
        case '-': 
            resp._result = req._x - req._y; 
            break;
        case '*':
            resp._result = req._x * req._y; 
            break;
        case '/':
            if(req._y == 0)
                resp._code = 1;
            else
                resp._result = req._x / req._y; 
            break;
        case '%':
            if(req._y == 0)
                resp._code = 2;
            else
                resp._result = req._x % req._y; 
            break;
        default:
            resp._code = 3;
            break;
    }

    return resp;
    
}



int main()
{   
    std::unique_ptr<EpollServer> svr(new EpollServer(Calculater));

    svr->InitServer();
    svr->Dispatcher();
    return 0;
}

上面的Reactor服务器也可以实现成多线程版本。 

 在设计多线程服务器时,最需要防备的是多个线程同时对同一个文件描述符进行读写,这样会使数据混乱。

 因此在使用多线程进行Reactor设计时,有一个原则:一个fd以及connection,一定只能有一个线程来进行管理。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unity 编辑器资源导入处理函数 OnPostprocessAudio :深入解析与实用案例

Unity 编辑器资源导入处理函数 OnPostprocessAudio 用法 点击封面跳转下载页面 简介 在Unity中&#xff0c;我们可以使用编辑器资源导入处理函数&#xff08;OnPostprocessAudio&#xff09;来自定义处理音频资源的导入过程。这个函数是继承自AssetPostprocessor类的&#xff…

前后端分离------后端创建笔记(上)

本文章转载于【SpringBootVue】全网最简单但实用的前后端分离项目实战笔记 - 前端_大菜007的博客-CSDN博客 仅用于学习和讨论&#xff0c;如有侵权请联系 源码&#xff1a;https://gitee.com/green_vegetables/x-admin-project.git 素材&#xff1a;https://pan.baidu.com/s/…

在Centos环境中搭建Nginx环境

一、Nginx概念简介 Nginx是一个轻量级的高性能HTTP反向代理服务器&#xff0c;同时它也是一个通用类型的代理服务器&#xff0c;支持绝大部分协议&#xff0c;如TCP、UDP、SMTP、HTTPS等。 Nginx与redis相同&#xff0c;都是基于多路复用模型构建出的产物&#xff0c;因此它与R…

Redis心跳检测

在命令传播阶段&#xff0c;从服务器默认会以每秒一次的频率&#xff0c;向主服务器发送命令&#xff1a; REPLCON FACK <rep1 ication_ offset>其中replication_offset是从服务器当前的复制偏移量。 发送REPLCONF ACK命令对于主从服务器有三个作用&#xff1a; 检测主…

Arcgis中直接通过sde更新sqlserver空间数据库失败

问题 背景 不知道有没有人经历过这样一个情况,我们直接在Arcgis中通过sde更新serserver数据库会失败,就是虽然在sde更新sqlserver数据库,但是在Navicat中通过sql语句来查询,发现数据并没有更新,如:上图中,更新数据库后,第一张图是sde打开的sqlserver数据库,它的数据库…

solr迁移到另一个solr中(docker单机)

背景介绍 solr数据迁移&#xff0c;或者版本升级&#xff0c;需要用到迁移&#xff0c;此处记录一下迁移方法以及过程中遇到的问题。我这边使用的是docker环境&#xff0c;非docker部署的应该也是一样的。 solr部署教程 准备工作 ● solrA 版本&#xff1a; 8.11.2 (已有so…

使用 Packet Tracer 查看协议数据单元

练习 2.6.2&#xff1a;使用 Packet Tracer 查看协议数据单元 地址表 本练习不包括地址表。 拓扑图 学习目标 捕获从 PC 命令提示符发出的 ping运行模拟并捕获通信研究捕获的通信从 PC 使用 URL 捕获 Web 请求运行模拟并捕获通信研究捕获的通信 简介&#xff1a; Wiresha…

提升Element UI分页查询用户体验与交互:实现修改未保存提示

我实现的功能是在 element ui 的分页组件中进行分页查询时&#xff0c;如果当前有未保存的修改数据就提示用户&#xff0c;用户可以选择是否放弃未保存的数据。确认放弃就重新查询数据&#xff1b;选择不放弃&#xff0c;不重新查询&#xff0c;并且显示条数选择框保持原样&…

轻装上阵,不调用jar包,用C#写SM4加密算法【卸载IKVM 】

前言 记得之前写了一个文章&#xff0c;是关于java和c#加密不一致导致需要使用ikvm的方式来进行数据加密&#xff0c;主要是ikvm把打包后的jar包打成dll包&#xff0c;然后Nuget引入ikvm&#xff0c;从而实现算法的统一&#xff0c;这几天闲来无事&#xff0c;网上找了一下加密…

时序预测 | MATLAB实现CNN-BiGRU-Attention时间序列预测

时序预测 | MATLAB实现CNN-BiGRU-Attention时间序列预测 目录 时序预测 | MATLAB实现CNN-BiGRU-Attention时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLAB实现CNN-BiGRU-Attention时间序列预测&#xff0c;CNN-BiGRU-Attention结合注意力机制时…

uni-app实现图片上传功能

效果 代码 <uni-forms-item name"ViolationImg" label"三违照片 :"><uni-file-picker ref"image" limit"1" title"" fileMediatype"image" :listStyles"listStyles" :value"filePathsL…

UML-类图和对象图

目录 类图概述&#xff1a; 1.类: 2.属性: 3.类的表示&#xff1a; 4.五种方法: 类图的关系&#xff1a; 1.关联 2.聚合 3.组合 4.依赖 5.泛化 6.实现 对象图概述&#xff1a; 1. 对象图包含元素: 2. 什么是对象 3.对象的状态可以改变: 4.对象的行为 5.对象标…

ad+硬件每日学习十个知识点(30)23.8.10 (SDIO端口扩展器TXS02612RTWR,模数转换器ADC121C027)

文章目录 1.cpu->SDIO端口扩展器->SD卡槽->SD卡(当然也可以反向读取)2.SDIO端口扩展器介绍3.SDIO端口扩展器TXS02612RTWR4.SD卡槽5.什么是模数转换器&#xff1f;6.I2C模数转换器ADC121C0277.模数转换方案 1.cpu->SDIO端口扩展器->SD卡槽->SD卡(当然也可以反…

【JPCS出版】第五届能源、电力与电网国际学术会议(ICEPG 2023)

第五届能源、电力与电网国际学术会议&#xff08;ICEPG 2023&#xff09; 2023 5th International Conference on Energy, Power and Grid 最近几年&#xff0c;不少代表委员把目光投向能源电力领域&#xff0c;对促进新能源发电产业健康发展、电力绿色低碳发展&#xff0c;提…

Kubernetes(K8s)从入门到精通系列之十:使用 kubeadm 创建一个高可用 etcd 集群

Kubernetes K8s从入门到精通系列之十&#xff1a;使用 kubeadm 创建一个高可用 etcd 集群 一、etcd高可用拓扑选项1.堆叠&#xff08;Stacked&#xff09;etcd 拓扑2.外部 etcd 拓扑 二、准备工作三、建立集群1.将 kubelet 配置为 etcd 的服务管理器。2.为 kubeadm 创建配置文件…

【前端 | CSS】滚动到底部加载,滚动监听、懒加载

背景 在日常开发过程中&#xff0c;我们会遇到图片懒加载的功能&#xff0c;基本原理是&#xff0c;滚动条滚动到底部后再次获取数据进行渲染。 那怎么判断滚动条是否滚动到底部呢&#xff1f;滚动条滚动到底部触发时间的时机和方法又该怎样定义&#xff1f; 针对以上问题我…

QPainter - 八卦时钟

QPainter - 八卦时钟 上一篇我们在画时钟的时候&#xff0c;已经把基本的钟表指针和刻度都绘制过了 想要完成八卦时钟&#xff0c;就要绘制这个里面的八卦了。 先上个图&#xff1a; 有人和我说八卦不能转 再来一张图&#xff1a; 背景的绘制 我们需要删除之前所绘制的白色…

FFmpeg常见命令行(五):FFmpeg滤镜使用

前言 在Android音视频开发中&#xff0c;网上知识点过于零碎&#xff0c;自学起来难度非常大&#xff0c;不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》&#xff0c;结合我自己的工作学习经历&#xff0c;我准备写一个音视频系列blog。本文是音视频系…

2023年中国日志审计市场竞争格局、市场规模、下游应用领域及行业发展趋势[图]

日志是行为或状态详细描述的载体&#xff0c;其时效性与信息丰富程度在网络安全事件分析、事件回溯和取证过程中起到重要作用。在法律层&#xff0c;日志也是重要的电子证据&#xff0c;日志记录、监控、审计手段等&#xff0c;可以帮助有效地减少信息破坏、信息泄露的问题&…

【Python机器学习】实验10 支持向量机

文章目录 支持向量机实例1 线性可分的支持向量机1.1 数据读取1.2 准备训练数据1.3 实例化线性支持向量机1.4 可视化分析 实例2 核支持向量机2.1 读取数据集2.2 定义高斯核函数2.3 创建非线性的支持向量机2.4 可视化样本类别 实例3 如何选择最优的C和gamma3.1 读取数据3.2 利用数…