Linux网络 | 多路转接epoll

        前言:本节内容讲述多路转接的epoll。 epoll是在poll的基础上再一次的进化, 但是epoll其实和poll与select在底层已经不是一个"物种"了。 这种差别就类似于进程间通信中管道和共享内存。 里面最重要的一个知识点博主认为就是epoll模型。友友们在学习的时候务必将epoll模型搞懂, 然后你会发现接口变得非常简单。

        ps:本节内容友友们最好自己实现一下select和poll以后再来哦!

目录

快速认识epoll接口

epoll_wait

epoll_wait

epoll_ctl

epoll原理

epoll模型

epoll模型与三个接口 

epoll_create

epoll_ctl

 epoll_wait

epoll优点

epoll代码

准备文件

Epoller

Epollserver.hpp


快速认识epoll接口

        epoll接口有三个:epoll_create、epoll_ctl、epoll_wait。

epoll_wait

        epoll_create的参数我们不管。被忽略,只要设置成大于0即可。

        epoll_create的返回值是一个文件描述符。返回值成功返回文件描述符,失败-1被返回。

epoll_wait

        epollwait就是获取已经就绪的文件描述符。第一个参数就是epoll_create返回值。接下来两个参数是我们自己定义的用户缓冲区,用来把已经就绪的文件描述符及其事件返回。int timeout单位是毫秒,含义和我们的poll一摸一样。返回值代表已经就绪的文件描述符个数。之前的select和poll的返回值,其实意义不大,但是这个返回值是能够用上的。用来表示已经就绪的fd的个数。

        这里面我们要看到是epoll_event这个类型:

        epoll_event第一个成员就是表示什么事件。第二个成员就是表示是一个联合体,可以选择上面字段任意当中的一个。

epoll_ctl

        epoll_ctl是用来比如说我想向系统中新增一个文件描述符及其要关心的事件,我想修改对特定文件描述符进行关心的事件,比如说读事件,想要改成写事件。        

        epoll_ctl第一个参数就是我们对应的epoll_create所对应的返口值。第二个OP代表的就是一些选项,这些选项如下:

        增加就是用来增加一个事件, 修改就是修改一个事件, 删除就是删除一个事件。 

        第三个和第四个就是代表的操作的是哪一个文件描述符上面的哪一个事件。

epoll原理

        这一板块主要学习epoll模型, epoll模型博主认为是学习epoll最重要的。现在我们先来看一下我们的体系结构:

        上面其实就是我们网络IO的时候报文需要经过的四层结构。 自底向上是网卡(硬件)、网卡驱动(数据链路层)、OS(网络层和传输层)、用户(用户层)以及OS和用户中间有一层系统调用。 

        数据进行传输的时候, 假如我们接收到了一个报文, 这个报文一定是从下往上依次交付的。 问题是, 数据是怎么从网卡向上交付的呢?(交付肯定是操作系统进行管理, 所以也可以换个问题, 即操作系统在硬件层面上, 怎么知道网卡上有数据了呢?)——这里就用到了硬件中断。或者说调用网卡(硬件层)的方法, 将数据读到数据链路层的网卡驱动。

         交给了网卡驱动之后, 网卡驱动又是如何将数据交给操作系统的。 这个就是我们后面要着重讲解的:epoll模型!

epoll模型

  •         首先,操作系统层面, 在我们的操作系统自身内部会维护一颗红黑树。这颗红黑树,里面的字段如下:红黑树的节点里面有要关心的fd,有要关心的事件。如下图左边:

  •         然后同样,操作系统也会维护一个就绪队列。一旦红黑树中有特定的一个节点就绪了,我们就可以把该节点连接到就绪队列里面,这个队列节点同样有fd和event。只是此时event设置成了EPOLLIN。如下图的右边:

  •         我们对应的操作系统的底层网卡,允许操作系统去注册一些回调机制。此时操作系统内部会提供一个回调函数。

        这个回调函数是干什么的。网卡以中断的方式把数据从网卡搬到驱动层。然后驱动层发现自己有数据就绪了,那么驱动层就会自动调用callback。 然后就可以理解为callback将驱动层的数据搬到了操作系统内部, 只不过实际上比这个复杂。 下面我们看一下这个callback函数,首先看一下callback所在的位置:

        callback的执行大概逻辑如下:

       所以,未来底层数据就绪,都已经放到我们的就绪队列里面了。所以,未来对于用户,只需要从就绪队列当中拿我们已经就绪的事件就可以了。

        所以,上面这一整套机制, 就叫做epoll模型。

        最后, epoll模型怎么被进程找到呢?我们就可以给epoll模型再创建一个:struct file。因为linux中一切皆文件,所以epoll模型本质就是文件。那么struct file中一个指针就指向这个文件。

        然后strcut file也是一个文件,未来这个strcut file的地址就被保存到了文件描述符数组里,充当一个文件描述符。

epoll模型与三个接口 

        了解了epoll模型之后我们再来看三个接口就比较简单。

epoll_create

        其实,创建epoll_create的时候其实是干什么呢?其实就是创建struct file。同时填充字段(epoll模型, 以及其他字段),然后把这个strcut file放到文件描述符表里面,将fd返回。 

epoll_ctl

        epoll_ctl(int epfd,int op,int fd, struct epollevent* event);其实就是刚刚create返回的fd作为第一个参数。这样就能找到对应的epoll模型所在的struct file,进而找到epoll模型。然后op有增加修改和删除,就是在操作红黑树!所以epoll_ctl就是在修改epoll模型里面的红黑树!

 epoll_wait

        最后,epoll_wait是什么呢?int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); 这里面的events是一个输出型参数。epoll_wait就是拿到fd然后找到epoll模型,将就绪队列里面的节点一个一个的放到events数组里面。maxevents就是最多拿到上层几个节点。假如队列里有很多节点,但是maxevents只能最多拿上去一部分。那么剩下的就等下一次再拿上去。

epoll优点

        1、检测就绪的时间是O(1),获取就绪的时间复杂度是O(n)。检测就绪只需要看队列是否为空,获取某个fd是否就绪就要遍历队列检测了!

        2、fd,event没有上限。

        3、这颗红黑树本质就相当于在写select和poll的时候我们维护的数组,所以我们epoll就不需要自己维护一个数组了。epoll_wait的返回值n,表示有几个fd就绪了,而就绪事件是连续的!!有返回值个输出型参数,那么当我们在上层进行处理就绪事件的时候,遍历整个events数组就不会有浪费的动作!!

epoll代码

准备文件

        epoll我们要多准备一个Epoller文件, 在这个文件里面我们定义一个Epoller类, 将epoll的接口封装起来。

        

Epoller


#include"nocopy.hpp"
#include"Log.hpp"
#include<cstring>
#include<sys/epoll.h>

class Epoller : public nocopy
{
    static const int size = 128;  //作为create的参数, 只要大于零即可。

public:
    Epoller()
    {

    }
    
    ~Epoller()
    {

    }


    int EpollerWait(struct epoll_event revents[], int num)    
    {

    }

    int EpollerUpdate(int oper, int sock, uint32_t event)
    {

    }


private:
    int epfd_;

    int timeout_{3000};


};

         Epoller封装epoll的三个函数, 其中构造函数封装epoll_create, EpollerWait封装epoll_wait, EpollerUpdate封装epoll_ctl。成员变量中断epfd_就是epoll模型所在的fd。 以后就可以通过这个fd找到epoll模型了。 

        封装代码如下:

#include"nocopy.hpp"
#include"Log.hpp"
#include<cstring>
#include<sys/epoll.h>

class Epoller : public nocopy  //epoll模型, 设置成不可拷贝。
{
    static const int size = 128;  //这个用来create传参, 无意义, 大于0即可。 

public:
    Epoller()
    {
        //创建epoll模型
        epfd_ = epoll_create(size);  //返回值就相当于fd, -1代表创建错误。 
        if (epfd_ == -1)  
        {
            lg(Error, "epoll_create error : %s", strerror(errno));
        }
        else
        {
            lg(Info, "epoll_create success: %d", epfd_);
        }
    }
    
    ~Epoller()  //关闭fd
    {
        if (epfd_ >= 0) 
        {
            close(epfd_);
        }
    }

    //epoll_wait等待函数, 第一个参数就是将就绪的节点拿出去, 第二个参数就是最多拿几个。
    int EpollerWait(struct epoll_event revents[], int num)    
    {
        int n = epoll_wait(epfd_, revents, num, timeout_);
        return n;
    }

    int EpollerUpdate(int oper, int sock, uint32_t event)
    {
        int n = 0;

        if(oper == EPOLL_CTL_DEL)
        {   
            n = epoll_ctl(epfd_, oper, sock, nullptr);  //将事件设置为nullptr, 就是删除事件了。

            if (n != 0)
            {
                lg(Error, "delete epoll_ctl delete error!");
            }
        }
        else
        {
            //在用户曾定义对某个文件fd关心的那个事件。
            struct epoll_event ev;   
            ev.events = event;
            ev.data.fd = sock;     //方便后期得知是哪一个文件fd就绪了。

            n = epoll_ctl(epfd_, oper, sock, &ev);   //将事件设置进入内核
            if (n != 0)
            {
                lg(Error, "epoll_ctl error!");
            }
        }   

        return n;
    }

private:
    int epfd_;

    int timeout_{3000};

};

Epollserver.hpp

        

#include <iostream>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Log.hpp"
#include "Epoller.hpp"
#include <memory>

uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);

class Epollserver : nocopy
{

    static const int num = 64;

public:
    Epollserver()
    {
    }

    ~Epollserver()
    {

    }

    void Init()
    {
    
    }


    void Start()
    {
    }



private:
    shared_ptr<Socket> _listensocket_ptr;
    shared_ptr<Epoller> _epoller_ptr;

    uint16_t port_;
};

         然后Epollserver.hpp用来创建epoll服务。 成员变量就是有两个特殊, 一个是_listensocket_ptr。一个是_epoller_ptr。_listensocket_ptr用来保存监听fd, 监听新连接。然后_epoller_ptr就是找到epoll模型的fd,操作epoll模型。

        实现如下:

#include <iostream>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Log.hpp"
#include "Epoller.hpp"
#include <memory>

uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);

class Epollserver : nocopy
{

    static const int num = 64;

public:
    Epollserver(uint16_t port)
        : port_(port),
          _listensocket_ptr(new Socket()),
          _epoller_ptr(new Epoller())
    {
    }

    ~Epollserver()
    {
        _listensocket_ptr->Close();
    }

    void Init()
    {
        _listensocket_ptr->InitSocket();
        _listensocket_ptr->Bind(port_);
        _listensocket_ptr->Listen();

        lg(Info, "create listen socket success: %d\n", _listensocket_ptr->Fd());
    }


    void Start()
    {
        // 将listensock添加到epoll中 -> listensock和他关心的事件, 添加到内核epoll模型中rb_tree中。 
        _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, _listensocket_ptr->Fd(), EVENT_IN);
        struct epoll_event revs[num];

        for (;;)
        {
            int n = _epoller_ptr->EpollerWait(revs, num);   //EpollerWait等待函数封装

            if (n > 0)
            {
                // 有事件就绪, 那么第0号下标一定是有数据的
                lg(Debug, "event happend, fd is : %d", revs[0].data.fd);

                Dispatcher(revs, n);
            }
            else if (n == 0)
            {
                lg(Info, "time out ...");
            }
            else
            {
                lg(Error, "epoll wait error");
            }
        }
    }



private:
    shared_ptr<Socket> _listensocket_ptr;
    shared_ptr<Epoller> _epoller_ptr;

    uint16_t port_;
};

        主要是Start, Start函数一开始就是要先向epoll模型的红黑树里面添加一个listensock和他关心的事件, 所以用到了Update函数。 然后进入for循环后就是循环检测就绪队列里面的就绪事件。 如果存在就绪事件, 就进入Dispatcher函数。 下面看Dispatcher函数:

    void Dispatcher(struct epoll_event revs[], int n)
    {
        for (int i = 0; i < n; i++)
        {
            // 知道了哪个文件描述符的哪个事件就绪了。处理所有的就绪的事件
            uint32_t event = revs[i].events;
            int fd = revs[i].data.fd;

            //判断就绪条件
            if (event & EVENT_IN)
            {
                if (fd == _listensocket_ptr->Fd())   //监听fd就绪, 获取了一个新链接
                {
                    Accepter();
                }
                else  // 其他fd上面的普通读取事件就绪
                {
                    Recver(fd);
                }
            }

        }
    }

        进入Dispatcher函数后, 就应该根据判断条件, 如果fd和listensock的fd相同, 那么就说明来了一个新连接, 就是进入连接管理器, 否则就进入读取管理器。

        下面是两个代码:


    void Accepter()
    {
        // 获取了一个新连接
        string clientip;
        uint16_t clientport;
        int sock = _listensocket_ptr->Accept(&clientip, &clientport);  //成功连接, 然后将新获得的sock添加到关心事件中,即rb_tree中。
        if (sock > 0)
        {
            // 我们能直接读取吗?不能, 因为获取新连接和事件就绪不一样
            _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
            lg(Info, "get a new link, client info@ %s:%d", clientip.c_str(), clientport);
        }
    }

    void Recver(int fd)
    {
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); 
        if (n > 0)
        {
            buffer[n] = 0;

            cout << "get a message: " << buffer << endl;



            //write
            string echo_str = "server echo $ ";
            echo_str += buffer;
            write(fd, echo_str.c_str(), echo_str.size());

        }
        else if (n == 0)  //等待超时,
        {
            lg(Info, "client quit, me too, close fd is : %d", fd);
            
            //读时间已经关了, 就不需要关心了, 就移除事件
            _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
            close(fd);
        }
        else
        {
            lg(Waring, "recv error: fd is : %d", fd);   
            
            _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
            close(fd);
        }

    }

        然后我们的主函数:

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

int main()
{
    unique_ptr<Epollserver> epoll_svr(new Epollserver(8080));
    epoll_svr->Init();
    epoll_svr->Start();
    
}

        以上。

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!