多路转接-epoll上

epoll是改进的poll,但底层毫无关联

epoll的核心定位:基于对多个fd等待的就绪时间通知机制

1.快速认识epoll的接口

接口也分离了,epoll_ctl只做内核告诉用户,epoll_wait只做用户告诉内核

2.epoll的原理

a.示意图

为什么 epoll_creat的返回值是文件描述符?epoll_ctl、epoll_wait 的第一个参数是该文件描述符?

如何和文件系统关联上?

怎么能通过文件描述符fd 找到epoll模型?

b.内核源码

回调机制怎么做的?
回调机制什么时候被激活的?

epoll_ctl:1.在红黑树中 插入节点 2.向底层回调机制,注册回调方法,每注册一个 红黑树节点,还要注册一个回调节点

为什么select和poll慢?

3.编写epoll server demo

当前我们的代码和select、poll差不多

至于为什么epoll_event中还要有epoll_data_t,是因为要把 fd和 对应的事件关联起来,维护的是用户 的数据;而之前设置的 fd,是内核维护的数据,是红黑树节点的 key值!

epoll server也是单进程,处理了多个IO请求!

a.epoll的优点:

  • 拆了三个函数,接口方便
  • 数据拷贝轻量化,通过EPOLL_CTL_ADD,将要关心的 fd拷贝到内核,之后我们直接通过wait将数据拷贝到用户就可以;而select 和 poll,每次调用,都需要将关心的事件 拷贝给内核,有事件就绪还要再拷贝给用户。
  • 事件回调,避免再内核中遍历所有fd,而是直接将就绪的 fd,加入到就绪队列中,检测是否有事件就绪O(1)。
  • 可以同时监测的文件描述符没有上限

b.epoll的缺点:认为没有

demo

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include "Socket.hpp"
#include "Log.hpp"
#include <sys/epoll.h>

using namespace SocketModule;
using namespace LogModule;

class EpollServer
{
    const static int size = 64;
    const static int defaultfd = -1;

public:
    EpollServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false), _epfd(defaultfd)
    {
        // 1.创建listen套接字
        _listensock->BulidListenSocket(port); // 3
        // 2.创建epoll模型
        _epfd = epoll_create(256);
        if (_epfd < 0)
        {
            LOG(LogLevel::FATAL) << "epoll_create error";
            exit(EPOLL_CREATE_ERR);
        }
        LOG(LogLevel::INFO) << "epoll_create_success: " << _epfd; // 4

        // 3.让 epoll 关心 listensockfd
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = _listensock->Fd();                                  // todo
        int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->Fd(), &ev); // 新增了节点
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "add listensockfd error";
            exit(EPOLL_CTL_ERR);
        }
    }

    void Start()
    {
        _isrunning = true;
        int timeout = -1;
        while (_isrunning)
        {
            // 不能直接accept
            struct epoll_event ev;
            // 获取就绪队列中的事件
            int n = epoll_wait(_epfd, _revs, size, timeout);
            switch (n)
            {
            case 0:
                LOG(LogLevel::DEBUG) << "timeout ";
                break;
            case -1:
                LOG(LogLevel::ERROR) << "epoll error";
                break;
            default:           // 就绪事件放在数组里了
                Dispatcher(n); // 有n个就绪了
                break;
            }
        }
        _isrunning = false;
    }

    // 有新连接到来,或者 客户端发数据了!
    // 事件派发器!
    void Dispatcher(int rnum) // LT模式,没一次性全拿走,就一直通知,水平触发模式,默认行为
    {
        LOG(LogLevel::DEBUG) << "event ready";
        // n个就绪事件,都在那个数组里!依次拷贝的,从下标0 开始!
        for (int i = 0; i < rnum; i++)
        {
            int sockfd = _revs[i].data.fd;     // 哪个fd就绪
            uint32_t revent = _revs[i].events; // 这个fd上 哪个事件就绪
            if (revent & EPOLLIN)              // 读事件就绪
            {
                // 可能是 listensockfd就绪,可能是 普通fd就绪
                // 1.listen 就绪
                if (sockfd == _listensock->Fd())
                {
                    Accepter(); // 获取新连接
                }
                else // 2.普通fd 就绪
                {
                    Recver(sockfd);
                }
            }
            if (revent & EPOLLOUT) // 写事件就绪
            {
            }
        }
    }

    // 连接管理器!
    void Accepter()
    {
        InetAddr client;
        // 至少有一个连接到来,accept一次,绝对不会阻塞!
        int fd = _listensock->Accept(&client); // 这回accept不会阻塞,select已经解决了等 的问题
        if (fd >= 0)
        {
            LOG(LogLevel::INFO) << "get a new link success,sockfd:"
                                << fd << ",client is" << client.StringAddr();
            // 新的fd,让epoll关心!
            struct epoll_event ev;
            ev.events = EPOLLIN;
            ev.data.fd = fd;
            int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
            if (n < 0)
            {
                LOG(LogLevel::WARNING) << "add 普通fd error";
            }
            else
            {
                LOG(LogLevel::INFO) << "add 普通fd success,fd: " << fd;
            }
        }
    }

    // IO处理器
    void Recver(int sockfd)
    {
        char buffer[1024];
        // 普通 fd就绪,本次读取,不会被阻塞!
        ssize_t n = recv(sockfd, &buffer, sizeof(buffer) - 1, 0); // bug
        if (n > 0)
        {
            std::cout << "client say@ " << buffer << std::endl;
        }
        else if (n == 0) // 客户端不写了
        {
            LOG(LogLevel::INFO) << "client quit";

            // epoll_ctl只能移除合法的 fd!,先移除,再关闭!
            int m = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
            if (m == 0)
            {
                LOG(LogLevel::INFO) << "epoll_ctl remove success";
            }
            close(sockfd);
        }
        else // 读取出错
        {
            LOG(LogLevel::ERROR) << "recv error";

            int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
            if (ret == 0)
            {
                LOG(LogLevel::INFO) << "epoll_ctl remove success";
            }
            close(sockfd);
        }
    }

    void Stop()
    {
        _isrunning = false;
    }
    ~EpollServer()
    {
        _listensock->Close();
        if (_epfd > 0)
            close(_epfd);
    }

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    int _epfd;
    struct epoll_event _revs[size]; // 拿取就绪事件的缓冲区数组,最大捞多少个事件
};

完。