云计算百科
云计算领域专业知识百科平台

【linux】高级IO,I/O多路转接多路转接之epoll,epoll版本的TCP服务器的实现

小编个人主页详情<—请点击 小编个人gitee代码仓库<—请点击 linux系统编程专栏<—请点击 linux网络编程专栏<—请点击 倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己! 在这里插入图片描述


目录

    • 前言
    • 一、前置知识
    • 二、nocopy
    • 三、Epoller
    • 四、Main.cc
    • 五、EpollServer.hpp
    • 六、源代码
      • Epoller.hpp
      • EpollServer.hpp
      • Log.hpp
      • Main.cc
      • makefile
      • nocopy.hpp
      • Socket.hpp
    • 总结

前言

【linux】高级IO,I/O多路转接之epoll,接口和原理讲解,epoll_create,epoll_wait,epoll_ctl——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习 本文由小编为大家介绍——【linux】高级IO,I/O多路转接多路转接之epoll,epoll版本的TCP服务器的实现


一、前置知识

  • 上一篇文章,详情请点击<—— 那么在有了上一篇文章中,关于epoll的三个接口epoll_create,epoll_wait,epoll_ctl的铺垫性讲解,以及epoll原理的讲解后,接下来小编就要实现是epoll版本的TCP服务器,我们需要使用到日志,套接字,链接如下 关于Log.hpp日志的讲解,详情请点击<—— 关于Socket.hpp套接字的讲解,在第三点的TCP服务器的Socket.hpp进行的讲解,详情请点击<—— 在这里插入图片描述
  • 那么如上我们的epoll版本的TCP服务器需要建立以及包含的源文件和头文件概况如上 (一)Epoller.hpp,由于我们要实现的是epoll版本的TCP服务器,所以就要创建,等待,控制epoll模型,那么使用到epoll的三个接口,那么关于epoll的三个接口以及文件描述符epfd我们期望进行一定的封装,所以我们就要实现一个Epoller类放在Epoller.hpp文件中 (二)EpollerServer.hpp,用于放置epoll版本的TCP服务器对应的EpollerServer类,包含初始化服务器,启动服务器等 (三)Log.hpp是日志,其中定义了Log lg,所以我们可以直接使用lg打印日志 (四)Main.cc是主函数,包含调用服务器的逻辑 (五)makefile用于编译构建可执行程序 (六)nocopy.hpp用于方式nocopy类,作为一款服务器,这个服务器只能有一份,所以我们并不期望服务器被拷贝,所以我们让服务器EpollServer继承nocopy即可,同样的Epoller这个类是用于创建,等待,控制epoll模型,那么我们期望创建的epoll模型不可被拷贝,所以我们也让Epoller继承nocopy (七)Socket.hpp用于封装套接字的原生接口
  • 二、nocopy

  • nocopy是处于nocopy.hpp的类,那么对于nocopy这个类要实现的是禁止拷贝这个功能,所以我们只需要将拷贝构造和赋值运算符使用delete关键字禁用,这样凡是公有继承自这个nocopy的类都无法进行拷贝了
  • #pragma once

    class nocopy
    {
    public:
    nocopy(){};

    nocopy(const nocopy& ) = delete;

    const nocopy& operator=(const nocopy& ) = delete;
    };

  • 所以此时我们在main函数中,尝试一下对公有继承自nocopy的类Epoller进行拷贝构造
  • #include "Epoller.hpp"

    class Epoller : public nocopy
    {};

    int main()
    {
    Epoller ep;
    Epoller ep1 = ep;

    return 0;
    }

    运行结果如下 在这里插入图片描述

  • 所以我们可以让Epoller类公有继承自nocopy,所以这样Epoller创建的epoll模型就无法被拷贝,让epoll模型只为它所对应的epoll提供一次等待多个fd的工作
  • 同样的,我们也要让EpollerServer类公有继承自nocopy,所以这样epoll版本的TCP服务器只能有一份,不能被拷贝
  • 三、Epoller

  • Epoller是处于Epoller.hpp的类,Epoller是对epoll的三个系统调用函数,分别是epoll_create,epoll_wait,epoll_ctl以及文件描述符epfd进行封装,那么关于epoll的三个系统调用分别是epoll_create,epoll_wait,epoll_ctl如何使用小编在上一篇中进行讲解,本文和上一篇文章的耦合度很高,所以请读者友友学习完上一篇文章之后再来学习Epoller的封装,上一篇文章,详情请点击<——
  • 那么接下来我们就要实现类Epoller了,所以这个类是用于创建epoll模型的,对于epoll模型我们不期望被拷贝,所以这里我们不期望Epoller类被拷贝,所以这里我们就让Epoller类公有继承自nocopy即可
  • 此时我们来看一下类的私有成员变量,那么首先就要有一个_epfd作为我们找到epoll模型的入口,epoll可以一次等待多个fd,所以对于超时时间timeout这里我们初始化为3000微秒,即对应3秒
  • Epoller类的私有成员变量中还应该有一个静态const的int类型size,对于size的初始值我们随便设置,这个size是用于给epoll_create进行传参的,对于epoll_create的参数在现在这个标准下已经废弃不使用了,所以这里对于size我们可以设置为任意值,那么这里小编就将size设置为128 在这里插入图片描述
  • 那么在Epoller构造函数中我们就可以创建epoll模型了,那么如何创建呢?调用epoll_create即可,epoll_create可以创建epoll模型,并且将epoll模型关联的文件描述符epfd返回,如果失败,那么返回-1,此时我们打印日志,并且将错误描述也使用strerror打印出来
  • 如果创建epoll模型成功,那么返回的是一个大于等于0的数,此时我们同样打印日志,将这个epfd打印出来即可
  • 那么在Epoller的析构函数中,如果_epfd大于等于0,那么我们使用close关闭这个_epfd即可,当_epfd被关闭的时候,底层对应的epoll模型就会被释放,即底层的红黑树和就绪队列就会被释放,即其中的节点就都会被释放
  • #include <iostream>
    #include <cstring>
    #include <unistd.h>
    #include <sys/epoll.h>
    #include "Log.hpp"
    #include "nocopy.hpp"

    class Epoller : public nocopy
    {
    static const int size = 128;

    public:
    Epoller()
    {
    _epfd = epoll_create(size);
    if(_epfd == 1)
    {
    lg(Error, "epoll_create error: %s", strerror(errno));
    }
    else
    {
    lg(Info, "epoller_create success, epfd: %d", _epfd);
    }
    }

    ~Epoller()
    {
    if(_epfd > 0)
    {
    close(_epfd);
    }
    }

    private:
    int _epfd;
    int _timeout{3000};
    };

    在这里插入图片描述

  • 接下来我们开始封装epoll_wait,epoll_wait可以从epoll模型中的就绪数组中获取就绪节点,那么对于epoll_wait我们将其封装在EpollerWait这个成员函数中即可,那么对于EpollerWait的参数需要一个struct epoll_event类型的数组revents,用于将epoll_wait等待到的fd及其事件带出去,即revents这个数组是一个输出型参数,参数中还有一个num用于给maxevents传参,也是期望上层传入,所以num是一个输入型参数
  • 所以此时我们就可以调用epoll_wait然后传入文件描述符_epfd,事件数组revents,num最大就绪事件的fd的个数,超时时间_timeout,然后对于返回值n我们不进行处理,将n带出去,由外部进行处理,epoll_wait的返回值n表示本次获取的事件数组revents中就绪的fd的个数
  • int EpollerWait(struct epoll_event revents[], int num)
    {
    int n = epoll_wait(_epfd, revents, num, _timeout);

    return n;
    }

    在这里插入图片描述

  • 接下来我们封装epoll_ctl,epoll_ctl是对epoll模型中的红黑树新增,删除,修改节点中的fd以及event事件的,所以我们将epoll_ctl封装在EpollerUpdate,那么对于EpollerUpdate的参数要包括要对节点进行操作oper,对哪一个文件描述符sock进行操作,对文件描述符sock上的哪一个事件event进行操作
  • 所以我们观察上图,oper分为三种,分别是EPOLL_CTL_DEL删除节点,EPOLL_CTL_ADD新增节点,EPOLL_CTL_MOD修改节点,那么其实我们思考一下,epoll_ctl本质上是操作的epoll模型中的红黑树,并且红黑树节点的键值key是文件描述符sock,节点value中包含文件描述符sock和event事件
  • 那么如果我们仅仅想要删除一个节点,所以我们只需要知道这个节点的键值key即可在红黑树中找到这个节点进行删除,并不需要对event事件进行操作,但是如果我们想要新增节点或者修改节点,除了要使用文件描述符sock作为键值新增或者修改找到这个节点之外,还需要对节点中包含的event事件进行新增或者修改的操作
  • 所以我们由此就可以进行分类了,将EPOLL_CTL_DEL删除节点是一类,将EPOLL_CTL_ADD新增节点和EPOLL_CTL_MOD修改节点归为一类,所以此时我们定义一个返回值n默认为0,如果epoll_ctl对节点操作成功那么返回值为0,如果操作失败那么返回值为-1,所以此时接下来我们就可以根据上面的讲解进行if-else语句的判断了 在这里插入图片描述
  • 如果当前要进行的操作oper是EPOLL_CTL_DEL删除节点,那么我们就调用epoll_ctl依次传入_epfd,要进行的操作oper,删除的文件描述符sock,最后由于我们仅仅是删除节点,所以我们只需要利用文件描述符sock找到红黑树的节点进行删除即可,并不需要对节点中的事件进行新增或者修改操作,所以这里对于事件event我们可以不进行传入,仅仅传入nullptr即可,那么如果返回值为-1,那么我们打印日志即可
  • 如果当前要进行的操作oper是EPOLL_CTL_ADD新增节点或者EPOLL_CTL_MOD修改节点,那么此时我们就调用epoll_ctl依次传入_epfd,要进行的操作oper,要操作的文件描述符sock,最后由于我们是要新增或者修改节点,所以我们需要利用文件描述符sock新增或者修改红黑树的节点,并且由于是新增或者修改节点,所以对于节点中的event我们也要进行新增或者修改 在这里插入图片描述
  • 所以此时我们还要传入event,但是这里特别注意,我们要传入的event并不是EpollerUpdate参数中uint32_t类型的event,而是上图中struct epoll_event类型的ev,并且这个uint32_t类型的event是struct epoll_event类型的第一个成员变量,那么对于第二个成员变量data,那么我们只需要设置其中的参数文件描述符fd为sock即可
  • 那么这时候有的读者友友可能会想,epoll_ctl的参数中明明已经有文件描述符sock了,为什么还要在struct epoll_event中的data中也要包含文件描述符sock呢?
  • 其实epoll_ctl参数中包含的文件描述符sock是作为键值key用于新增或找到红黑树中的节点的,而此时这个struct epoll_event中既包含fd也包含event事件,那么将来event_wait获取的数组中的对象epoll_event,这个epoll_event才可以让我们得知是哪个fd就绪了,fd上的哪个event就绪了
  • 所以此时我们就初始化完成了struct epoll_event类型的对象ev了,那么我们将ev取地址直接进行传入到epoll_ctl的最后一个参数即可,那么如果epoll_ctl的返回值为-1,说明此时oper操作进行新增或修改节点失败了,所以此时我们打印日志即可,最后在if-else语句外面返回epoll_ctl的返回值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);
    if(n == 1)
    {
    lg(Error, "epoll_ctl delete error");
    }
    }
    else
    {
    // EPOLL_CTL_ADD || EPOLL_CTL_MOD
    struct epoll_event ev;
    ev.data.fd = sock;
    ev.events = event;

    n = epoll_ctl(_epfd, oper, sock, &ev);
    if(n == 1)
    {
    lg(Error, "epoll_ctl error");
    }
    }

    return n;
    }

  • 所以此时我们在Epoller类中就完成了对epoll的三个系统调用接口epoll_create,epoll_wait,epoll_ctl的封装,并且也将epoll模型对应的文件描述符epfd也封装在了Epoller类的私有成员变量中
  • 那么从此以后,如果我们想要使用epoll等待多个fd,那么只需要包含"Epoller.hpp"这个头文件,然后实例化Epoller类对象就完成了创建epoll模型,那么进行调用成员变量可以完成将epoll等待的多个fd获取上来,还可以控制节点中对应的fd和event的操作
  • 四、Main.cc

  • 那么在main函数中包含对服务器的调用逻辑,所以我们使用智能指针unique_ptr管理epoll版本的TCP服务器,那么在new服务器对象的时候,进行传参服务器要绑定的端口号8080即可
  • 接下来服务器要首先调用Init进行初始化,接下来之后再调用Start将服务器启动起来
  • #include "EpollServer.hpp"
    #include <memory>

    int main()
    {
    std::unique_ptr<EpollServer> epoll_svr(new EpollServer(8080));

    epoll_svr->Init();
    epoll_svr->Start();

    return 0;
    }

    五、EpollServer.hpp

  • 那么对于一款服务器来讲,我们不期望这个服务器可以被拷贝,所以我们让EpollServer公有继承nocopy即可,接下来我们来看EpollServer的私有成员变量,首先作为一款服务器应该有服务器所绑定的端口号port,所以这里的私有成员变量要有端口号_port
  • 并且我们要实现的服务器是epoll版本的TCP服务器,所以要进行socket编程,那么对于socket相关的接口,我们已经封装在了Socket.hpp中的类Sock中了,所以这里我们要实例化一个Sock的类对象,那么我们期望这个类对象可以自动管理生命周期,所以这里我们在私有成员变量中使用shared_ptr对应的_listensock_ptr管理Sock的类对象
  • 同样的既然是epoll版本的TCP服务器,所以这里就要使用epoll的相关接口,小编在前面已经将epoll的相关接口封装在了Epoller.hpp中的Epoller类中了,所以这里我们就要实例化一个Epoller的列对象,那么同样的我们期望这个类对象可以自动管理生命周期,所以这里我们在私有成员变量中使用shared_ptr对应的_epoller_ptr管理Epoller的类对象
  • 那么私有成员变量我们还要定义一个static和const修饰的int类型的变量num,初始值我们给64,用于给Epoller中的EpollerWait的第二个参数传参,即num表示将来我们获取就绪的fd的最大个数,所以此时我们就完成了对EpollServer类的私有成员变量的声明
  • 那么接下来,我们在构造函数中对私有成员变量进行初始化,我们期望服务器所绑定的端口号是外部传入的,所以这里在构造函数中我们接收端口号port,然后给_listensock_ptr这个成员变量new一个Sock对象,给_epoller_ptr这个成员变量new一个Epoller对象,然后使用接收到的端口号port初始化成员变量_port
  • 在析构函数中,我们调用_listensock_ptr中的Close关闭listen文件描述符即可,那么我们要关心的是文件描述符的读事件EPOLLIN和写事件EPOLLOUT,那么这里我们规范一点,将读事件EPOLLIN定义为EVENT_IN,将写事件EPOLLOUT定义为EVENT_OUT
  • #include <iostream>
    #include <memory>
    #include "Log.hpp"
    #include "Socket.hpp"
    #include "nocopy.hpp"
    #include "Epoller.hpp"

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

    class EpollServer : public nocopy
    {
    static const int num = 64;
    public:
    EpollServer(uint16_t port)
    :_listensock_ptr(new Sock())
    ,_epoller_ptr(new Epoller())
    ,_port(port)
    {}

    void Init()
    {}

    void Start()
    {}

    ~EpollServer()
    {
    _listensock_ptr->Close();
    }

    private:
    std::shared_ptr<Sock> _listensock_ptr;
    std::shared_ptr<Epoller> _epoller_ptr;
    uint16_t _port;
    };

  • 接下来我们就开始编写Init初始化服务器了,依次需要完成套接字的创建,服务器的绑定,将套接字设置为监听状态,那么我们依次调用_listensock_ptr中的成员函数Socket完成套接字的创建,调用Bind完成服务器的绑定,调用Listen将_listensock设置为监听状态,那么此时服务器已经初始化完成,并且此时Epoll模型的创建在EpollServer构造函数中已经完成了,并且在Epoller的构造函数中已经打印了日志,那么在这里我们也打印创建listen套接字完成这个日志,并且将listen套接字对应的fd也一并打印出来
  • void Init()
    {
    _listensock_ptr->Socket();
    _listensock_ptr->Bind(_port);
    _listensock_ptr->Listen();

    lg(Info, "create listen socket success, fd: %d", _listensock_ptr->Fd());
    }

  • 接下来我们开始编写Start启动服务器,那么服务器一旦启动就要源源不断的接收来自客户端的连接请求,服务器一旦启动几乎都在运行,所以也就意味着服务器的逻辑要基于一个死循环运行,所以这里我们使用for(;;)进行死循环,listensock上的读事件就绪代表有连接到来,那么我们能否一上来就让服务器accept对应的listensock呢?
  • 不能,因为我们要实现的是epoll版本的TCP服务器,说好的使用epoll一次等待多个fd,那么一旦这里服务器上来就accept等待客户端的连接,此时在单进程中服务器一次只能等待一个listensock这个一个fd ,别忘了epoll可以一次等待多个fd,所以等待的事情我们就要交给epoll来做,那么使用了epoll我们就可以在单进程场景中,一次等待多个fd,让单进程服务器一次服务多个客户端
  • 所以此时在进行for循环之前,我们要将listensock添加到epoll中,那么本质就是将listensock和它关心的事件,添加到内核epoll模型中的红黑树rb_tree中,所以此时我们调用_epoller_ptr中的EpollerUpdata进行EPOLL_CTL_ADD新增操作,将listensock对应的fd以及关心的读事件EVENT_IN添加到epoll模型的rb_tree中
  • 那么在for循环中我们此时就只需要调用_epoller_ptr中的EpollerWait传入struct epoll_event类型的revs数组以及num最大获取就绪fd的个数,将就绪的fd通过数组的形式获取上来即可,那么通过返回值n我们可以获悉有多少个fd就绪了,这个fd的值小于等于num
  • 所以此时我们EpollerWait的返回值n进行判断即可,如果n大于0,说明此时底层有fd就绪了,所以我们这里将revs数组中的第一个fd打印一下日志,别忘了有可能就绪的fd可能不止一个,而是都处于数组中,具体有多少个,有n个,所以我们就可以对这些就绪的处于数组中的fd及其就绪的事件event进行处理了,那么如何处理?交给派发器Dispatcher处理即可
  • void Dispatcher(epoll_event revs[], int n)
    {

    }

    void Start()
    {
    _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, _listensock_ptr->Fd(), EVENT_IN);
    struct epoll_event revs[num];
    for(;;)
    {
    int n = _epoller_ptr->EpollerWait(revs, num);
    if(n > 0)
    {
    lg(Debug, "event happened, fd: %d", revs[0].data.fd);

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

  • 就绪的fd的事件分为读事件就绪和写事件就绪,那么为了简便学习,目前在epoll中我们暂时不讨论对写事件就绪进行处理,我们仅对fd就绪事件为的读事件就绪的情况进行处理,那么此时fd的读事件已经就绪了,别忘了fd分为listensock和普通的sock
  • 对于listensock那么则是读事件就绪了要使用accept将来自客户端的连接从底层获取上来,对于普通sock则是读事件就绪了要将客户端发来的数据进行读取处理,两者的处理逻辑不同,所以listensock的代码处理封装为Accepter连接管理器,将sock的代码处理封装为Recver数据读取器
  • 那么我们要处理的就绪的fd都在revs数组中放着,数组内有多少个就绪的fd,我们如何知道?那么就是通过EpollerWait的返回值n,n表示数组内的就绪的fd的个数,所以Dispatcher的第二个参数我们就将n传入了,所以我们知道了要遍历revs这个数组,要遍历的上限范围是n 在这里插入图片描述
  • 所以此时我们就可以开始遍历一下revs了,那么别忘了数组的中的对象的类型是struct epoll_event,其中的第一个字段是uint32_t类型的events,即就绪的事件,第二个字段是epoll_data_t类型的data,而在epoll_data_t类型中有fd这个字段,所以我们单单从一个struct epoll_event类型中就可以获悉哪个fd上的哪个event事件就绪了,所以此时我们就可以理解了当初在epoll_ctl传参的时候要传参struct epoll_event类型
  • 那么此时我们已经可以知道哪个fd上的哪个event事件就绪了,所以我们此时就可以进行判断了,如果是写事件或者其它事件就绪了,在本文的处理逻辑中我们暂时忽略,为了简便学习,我们仅考虑if判断读事件就绪了,即此时fd上的读事件就绪了,那么fd分为listensock和其它的普通sock,那么如果fd是listensock我们就交给连接管理器Accepter处理,否则fd就是其它的普通sock,那么我们就交给数据读取器Recver处理
  • void Accepter()
    {

    }

    void Recver(int fd)
    {

    }

    void Dispatcher(struct epoll_event revs[], int n)
    {
    for(int i = 0; i < n; i++)
    {
    int fd = revs[i].data.fd;
    uint32_t event = revs[i].events;
    if(event & EVENT_IN)
    {
    if(fd == _listensock_ptr->Fd())
    {
    Accepter();
    }
    else
    {
    Recver(fd);
    }
    }
    else if(event & EVENT_OUT)
    {

    }
    else
    {

    }
    }
    }

  • 所以接下来我们编写一下连接管理器Accepter,那么当调用Accepter的时候,意味着此时的listensock上的读事件就绪了,意味着此时有客户端来连接服务器,服务器需要将连接从底层的全连接队列使用accept获取上来,所以此时我们就调用_listensock_ptr中的Accept即可
  • 那么如果返回的文件描述符sock大于0,说明此时accept获取连接成功,那么此时我们可以直接read读取sock上的数据吗?不可以,因为如果客户端仅仅只是建立连接而不发送数据,所以此时read读取底层的tcp接收缓冲区就会由于没有数据而导致read阻塞等待,所以此时服务器对于其它客户端的连接或者发来的数据就会不响应了
  • 所以要等待sock上有数据的本质上就是等待sock的读事件就绪,别忘了本文小编要实现的是epoll版本的TCP服务器,要等待的事情都交给epoll即可,epoll可以一次等待多个fd,所以此时我们不能直接read读取数据,而是应该将关心等待sock上的读事件就绪通过EpollerUpdate交给epoll模型中的红黑树rb_tree,让epoll关心这个sock和对应的event事件,接下来我们打印客户端信息的日志即可
  • void Accepter()
    {
    std::string clientip;
    uint16_t clientport;

    int sock = _listensock_ptr->Accept(&clientip, &clientport);
    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);
    }
    }

    运行结果如下 在这里插入图片描述

  • 所以此时如上,那么我们在Epoller中在EpollerWait中给epoll_wait设置的超时时间time是3000微秒,即3秒,所以如上图左边前9秒,小编没有让客户端连接服务器,所以左侧每个3秒就会超时一次,那么9秒过后,小编让右侧的telnet充当客户端连接服务器,此时我们的服务器中的epoll可以检测到listensock上的读事件就绪,并且正常调用了连接管理器Accepter可以正常将连接获取上来添加到epoll中让epoll关心这个连接对应的文件描述符sock上的读事件,并且打印了日志,无误
  • 那么接下来小编在右侧的客户端上进行了输入,即此时客户端给服务器通过tcp连接发送了数据,所以此时左侧服务器中的epoll就会检测到sock上的读事件就绪,那么就会告诉上层sock上的读事件就绪了,赶快来读取吧,epoll会一直通知到上层进行读取
  • 可是在上层此时小编并没有对数据读取器Recver进行编写,所以自然也就无法读取sock上的数据,进而我们每次在Start中的死循环中调用EpollerWait中封装的epoll_wait都会直接进行返回,然后上层打印fd就绪日志,可是上层由于没有编写Recver无法对sock上的数据进行读取,底层由于上层没有读取就会一直返回通知,所以此时在左侧就会出现满屏的日志信息
  • 那么下面小编想验证一下非阻塞,即在Epoller中的EpollerWait中封装的epoll_wait的超时时间为0,那么此时epoll就为非阻塞等待,那么由于小编在代码中没有进行任何sleep休眠,所以服务器一旦运行,那么由于是非阻塞,那么一进行调用那么就会返回,超时,然后在调用再返回,超时,所以这样循环,那么超时的日志就会一瞬间将屏幕打满,所以如下,小编在Epoller中的EpollerWait中封装的epoll_wait的超时时间为0,我们期望看到超时的日志一瞬间打满屏幕的现象
  • int EpollerWait(struct epoll_event revents[], int num)
    {
    // int n = epoll_wait(_epfd, revents, num, _timeout);
    int n = epoll_wait(_epfd, revents, num, 0);

    return n;
    }

    运行结果如下,非阻塞情况下一瞬间超时日志就会将屏幕打满 在这里插入图片描述

  • 那么下面小编想验证一下阻塞等待,所以小编就将Epoller中的EpollerWait中封装的epoll_wait的超时时间为-1,那么此时epoll就为阻塞等待直到fd上的事件就绪
  • int EpollerWait(struct epoll_event revents[], int num)
    {
    // int n = epoll_wait(_epfd, revents, num, _timeout);
    // int n = epoll_wait(_epfd, revents, num, 0);
    int n = epoll_wait(_epfd, revents, num, 1);

    return n;
    }

    运行结果如下 在这里插入图片描述

  • 所以此时左侧服务器一旦运行,那么就是阻塞式等待fd上的事件就绪,当小编在右侧使用telnet充当客户端连接服务器的时候,此时服务器的epoll上的listensock上的读事件就绪了,那么此时epoll就会返回,然后告诉上层处理listensock的读事件,所以此时上层处理完成,将连接获取上来放到epoll中,然后打印完成日志,此时左侧服务器又去阻塞式等待fd上的事件就绪了
  • 所以此时小编也编写一下数据读取器Recver,将数据读取上来,那么首先我们创建用户级缓冲区buffer,然后使用read将数据读取上来,如果read的返回值n大于0,那么代表此时成功的读取了数据,所以此时我们在数据的末尾放上‘\\0’(0等于‘\\0’),然后将这个字符串打印出来,接下来我们也给客户端发送消息,所以此时我们就构建echo_svr为"server echo$ ",然后我们将来客户端的消息添加在echo_svr的后面即可,然后使用write将消息通过连接发送回给客户端
  • 接下来如果n等于0,说明对方将连接关闭,所以此时服务器也想要将连接关闭,所以服务器这边先打印日志,这里细节来了,epoll要求如果想要使用epoll_ctl的EPOLL_CTL_DEL关闭fd,那么首先要求这个fd有效,所以如果这里我们贸然的上来先使用close关闭fd,然后再调用eopll_ctl就会出错,所以这里我们一定要先使用epoll_ctl的EPOLL_CTL_DEL在内核epoll模型中的rb_tree中删除sock对应的节点,所以这里我们只需要找到sock,并不需要传参event,所以这里的event我们传入0即可,然后再调用close关闭fd
  • 接下来如果n小于0,那么说明此时读取数据发生错误,那么此时打印日志,使用epoll_ctl的EPOLL_CTL_DEL删除内核epoll模型中rb_tree中对应的sock节点,然后再调用close关闭fd即可,所以此时我们就将数据读取器Recver也编写出来了,所以此时我们来验证一下,服务器是否可以成功的将来自客户端的数据读取,并且给客户端写回数据
  • void Recver(int fd)
    {
    char buffer[1024];
    ssize_t n = read(fd, buffer, sizeof(buffer) 1);
    if(n > 0)
    {
    buffer[n] = 0;
    std::cout << "get a message: " << buffer << std::endl;

    std::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 to, close fd: %d", fd);
    _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
    close(fd);
    }
    else
    {
    lg(Warning, "recv error, fd: %d", fd);
    _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
    close(fd);
    }
    }

    运行结果如下 在这里插入图片描述

  • 所以运行结果如上,别忘了此时的Epoller中的EpollerWait封装的epoll_wait的超时时间timeout仍然为-1阻塞等待直到fd上的事件就绪,所以此时左侧运行服务器之后,服务器启动后会阻塞等待直到fd上的事件就绪,那么此时右侧使用telnet充当客户端来连接服务器,所以此时服务器的epoll就会检测到listensock上有读事件就绪,然后告诉上层
  • 所以此时上层经过判断是listensock上的读事件就绪了,所以此时就会将fd交给连接管理器Accepter将连接对应的文件描述符sock获取上来并且添加到epoll中关心读事件,然后打印日志,此时左侧的客户端发送消息,本质上是右侧的服务器检测到sock上的读事件就绪了,所以就会告诉上层
  • 那么此时小编已经编写了数据读取器Recver将数据读取上来,并且构建服务器的消息发送回给客户端,所以此时客户端就收到了来自服务器的消息并打印,那么此时右侧的客户端想要断开连接,所以就quit退出了telnet,所以此时右侧的客户端将连接断开了,连接的断开相当于左侧的服务器epoll关心的sock上的读事件就绪
  • 那么左侧的服务器的epoll就会检测到sock的读事件就绪了,所以就会告诉上层,那么上层经过判断,发现就绪的fd不是listensock,就绪的fd是普通sock,所以就会交给数据读取器将数据读取上来,可是此时read一进行读取,发现对方将连接关闭了,所以此时read就会返回0,告诉上层此时连接已经关闭了,无法进行读取了
  • 所以上层就会打印日志,然后先使用epoll_ctl的EOPLL_CTL_DEL将连接对应的文件描述符sock删除,本质是在内核epoll模型中红黑树rb_tree中找到对应的sock节点进行删除,所以删除完成后,那么返回上层然后再调用close关闭sock,释放维护连接的文件打开对象
  • 六、源代码

    Epoller.hpp

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

    class Epoller : public nocopy
    {
    static const int size = 128;

    public:
    Epoller()
    {
    _epfd = epoll_create(size);
    if(_epfd == 1)
    {
    lg(Error, "epoll_create error: %s", strerror(errno));
    }
    else
    {
    lg(Info, "epoller_create success, epfd: %d", _epfd);
    }
    }

    int EpollerWait(struct epoll_event revents[], int num)
    {
    // int n = epoll_wait(_epfd, revents, num, _timeout);
    // int n = epoll_wait(_epfd, revents, num, 0);
    int n = epoll_wait(_epfd, revents, num, 1);

    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);
    if(n == 1)
    {
    lg(Error, "epoll_ctl delete error");
    }
    }
    else
    {
    // EPOLL_CTL_ADD || EPOLL_CTL_MOD
    struct epoll_event ev;
    ev.data.fd = sock;
    ev.events = event;

    n = epoll_ctl(_epfd, oper, sock, &ev);
    if(n == 1)
    {
    lg(Error, "epoll_ctl error");
    }
    }

    return n;
    }

    ~Epoller()
    {
    if(_epfd >= 0)
    {
    close(_epfd);
    }
    }

    private:
    int _epfd;
    int _timeout{3000};
    };

    EpollServer.hpp

    #include <iostream>
    #include <memory>
    #include <string>
    #include <unistd.h>
    #include "Log.hpp"
    #include "Socket.hpp"
    #include "nocopy.hpp"
    #include "Epoller.hpp"

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

    class EpollServer : public nocopy
    {
    static const int num = 64;
    public:
    EpollServer(uint16_t port)
    :_listensock_ptr(new Sock())
    ,_epoller_ptr(new Epoller())
    ,_port(port)
    {}

    void Init()
    {
    _listensock_ptr->Socket();
    _listensock_ptr->Bind(_port);
    _listensock_ptr->Listen();

    lg(Info, "create listen socket success, fd: %d", _listensock_ptr->Fd());
    }

    void Accepter()
    {
    std::string clientip;
    uint16_t clientport;

    int sock = _listensock_ptr->Accept(&clientip, &clientport);
    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;
    std::cout << "get a message: " << buffer << std::endl;

    std::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 to, close fd: %d", fd);
    _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
    close(fd);
    }
    else
    {
    lg(Warning, "recv error, fd: %d", fd);
    _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
    close(fd);
    }
    }

    void Dispatcher(struct epoll_event revs[], int n)
    {
    for(int i = 0; i < n; i++)
    {
    int fd = revs[i].data.fd;
    uint32_t event = revs[i].events;
    if(event & EVENT_IN)
    {
    if(fd == _listensock_ptr->Fd())
    {
    Accepter();
    }
    else
    {
    Recver(fd);
    }
    }
    else if(event & EVENT_OUT)
    {

    }
    else
    {

    }
    }
    }

    void Start()
    {
    _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, _listensock_ptr->Fd(), EVENT_IN);
    struct epoll_event revs[num];
    for(;;)
    {
    int n = _epoller_ptr->EpollerWait(revs, num);
    if(n > 0)
    {
    lg(Debug, "event happened, fd: %d", revs[0].data.fd);

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

    ~EpollServer()
    {
    _listensock_ptr->Close();
    }

    private:
    std::shared_ptr<Sock> _listensock_ptr;
    std::shared_ptr<Epoller> _epoller_ptr;
    uint16_t _port;
    };

    Log.hpp

    #pragma once

    #include <iostream>
    #include <string>
    #include <ctime>
    #include <cstdio>
    #include <cstdarg>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>

    #define SIZE 1024

    #define Info 0
    #define Debug 1
    #define Warning 2
    #define Error 3
    #define Fatal 4

    #define Screen 1 //输出到屏幕上
    #define Onefile 2 //输出到一个文件中
    #define Classfile 3 //根据事件等级输出到不同的文件中

    #define LogFile "log.txt" //日志名称

    class Log
    {
    public:
    Log()
    {
    printMethod = Screen;
    path = "./log/";
    }

    void Enable(int method) //改变日志打印方式
    {
    printMethod = method;
    }

    ~Log()
    {}

    std::string levelToString(int level)
    {
    switch(level)
    {
    case Info:
    return "Info";
    case Debug:
    return "Debug";
    case Warning:
    return "Warning";
    case Error:
    return "Error";
    case Fatal:
    return "Fatal";
    default:
    return "";
    }
    }

    void operator()(int level, const char* format, ...)
    {
    //默认部分 = 日志等级 + 日志时间
    time_t t = time(nullptr);
    struct tm* ctime = localtime(&t);
    char leftbuffer[SIZE];
    snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
    ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
    ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

    va_list s;
    va_start(s, format);
    char rightbuffer[SIZE];
    vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
    va_end(s);

    char logtxt[2 * SIZE];
    snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

    printLog(level, logtxt);
    }

    void printLog(int level, const std::string& logtxt)
    {
    switch(printMethod)
    {
    case Screen:
    std::cout << logtxt << std::endl;
    break;
    case Onefile:
    printOneFile(LogFile, logtxt);
    break;
    case Classfile:
    printClassFile(level, logtxt);
    break;
    default:
    break;
    }
    }

    void printOneFile(const std::string& logname, const std::string& logtxt)
    {
    std::string _logname = path + logname;
    int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
    if(fd < 0)
    return;
    write(fd, logtxt.c_str(), logtxt.size());
    close(fd);
    }

    void printClassFile(int level, const std::string& logtxt)
    {
    std::string filename = LogFile;
    filename += ".";
    filename += levelToString(level);

    printOneFile(filename, logtxt);
    }

    private:
    int printMethod;
    std::string path;
    };

    Log lg;

    Main.cc

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

    int main()
    {
    std::unique_ptr<EpollServer> epoll_svr(new EpollServer(8080));

    epoll_svr->Init();
    epoll_svr->Start();

    // Epoller ep;
    // Epoller ep1 = ep;

    return 0;
    }

    makefile

    epoll_server:Main.cc
    g++ o $@ $^ std=c++11
    .PHONY:clean
    clean:
    rm f epoll_server

    nocopy.hpp

    #pragma once

    class nocopy
    {
    public:
    nocopy(){};

    nocopy(const nocopy& ) = delete;

    const nocopy& operator=(const nocopy& ) = delete;
    };

    Socket.hpp

    #pragma once

    #include <iostream>
    #include <string>
    #include <cstring>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include "Log.hpp"

    const int backlog = 10;

    enum{
    SocketErr = 1,
    BindErr,
    ListenErr,
    };

    class Sock
    {
    public:
    Sock()
    {}

    void Socket()
    {
    sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd_ < 0)
    {
    lg(Fatal, "socket error, %s : %d", strerror(errno), errno);
    exit(SocketErr);
    }

    int opt = 1;
    setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
    }

    void Bind(uint16_t port)
    {
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = INADDR_ANY;
    socklen_t len = sizeof(local);

    if(bind(sockfd_, (struct sockaddr*)&local, len) < 0)
    {
    lg(Fatal, "bind error, %s : %d", strerror(errno), errno);
    exit(BindErr);
    }
    }

    void Listen()
    {
    if(listen(sockfd_, backlog) < 0)
    {
    lg(Fatal, "listen error, %s : %d", strerror(errno), errno);
    exit(ListenErr);
    }
    }

    int Accept(std::string* clientip, uint16_t* clientport)
    {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);

    int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
    if(newfd < 0)
    {
    lg(Warning, "accept error, %s : %d", strerror(errno), errno);
    return 1;
    }

    char ipstr[128];
    inet_ntop(AF_INET, &(peer.sin_addr), ipstr, sizeof(ipstr));
    *clientip = ipstr;
    *clientport = ntohs(peer.sin_port);

    return newfd;
    }

    bool Connect(const std::string& serverip, uint16_t serverport)
    {
    struct sockaddr_in peer;
    memset(&peer, 0, sizeof(peer));
    peer.sin_family = AF_INET;
    peer.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(peer.sin_addr));
    socklen_t len = sizeof(peer);

    int n = connect(sockfd_, (struct sockaddr*)&peer, len);
    if(n == 1)
    {
    std::cerr << "connect to " << serverip << ':' << serverport << "error" << std::endl;
    return false;
    }

    return true;
    }

    void Close()
    {
    if(sockfd_ > 0)
    {
    close(sockfd_);
    }
    }

    int Fd()
    {
    return sockfd_;
    }

    ~Sock()
    {}
    private:
    int sockfd_;
    };


    总结

    以上就是今天的博客内容啦,希望对读者朋友们有帮助 水滴石穿,坚持就是胜利,读者朋友们可以点个关注 点赞收藏加关注,找到小编不迷路!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【linux】高级IO,I/O多路转接多路转接之epoll,epoll版本的TCP服务器的实现
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!