【Linux】高级IO

前言

        本篇笔记记录五种IO模型的基本概念,重点针对多路转接IO模型对我们的网络服务器进行设计,从而达到提升效率的效果。

我的上一篇Linux笔记:【Linux】网络基础(3)_柒海啦的博客-CSDN博客

         让我们开始吧~

目录

一、五种IO模型

1.阻塞IO

2.非阻塞IO

3.信号驱动IO

4.多路转接IO

5.异步IO

IO模型之间的联系与区别

fcntl设置非阻塞IO

二、select与poll多路转接方案

1.select接口介绍

2.编写select的一般模式

3.总结一下select的优缺点:

4.poll接口介绍:

5.poll的优缺点

三、epoll多路转接方案

epoll_create

1.epoll的工作原理

epoll_ctl

epoll_wait

2.epoll的工作模式

四、基于epoll的Reactor模式


一、五种IO模型

        在本机通信的时候,实际上进程间通信就是一个IO的过程。

        学习了网络通信后,我们知道网络实际上也是一个进程向另一个进程进行通信,也是一个IO的过程。

        那么不妨重新介绍一下IO的概念

IO = input && output

        依据于冯诺依曼结构来说,就是访问外设的过程。输入和输出,所以一般情况就是先等数据就绪,就绪完后就对齐进行拷贝。

        所以,IO还可以如下的进行表示:

IO = 等 + 数据拷贝

        对于输入来说,等就是等待对应层将数据放入接收缓冲区的过程,等待完成后在拷贝给应用层。

        对于输出来说,等就是等待发送缓冲区为空或者有剩余的过程,等待完成后从应用层拷贝到缓冲区内。

        对于上面的等 + 数据拷贝,我们可以以网络套接字编程中recv/read为例子。

        1.当我们read/recv的时候,如果底层缓冲区没有数据怎么办?阻塞进行等待。

        2.当我们read/recv的时候,如果底层缓冲区有数据,read/recv会怎么办?拷贝。

        但是往往拷贝就是一瞬间的事情,所以IO的时间取决于等待的过程。一个最简单的例子:C/C++使用scanf或者cin的时候就是输入IO操作,此时运行会发现运行窗口一直再等待阻塞。阻塞什么?等待输入数据!

        所以,我们想要提高IO的效率的话,就要从等这方面下手脚。这样也就存在下面的五种IO模型进行优化操作。

1.阻塞IO

        阻塞IO就是最常见的IO。我们之前的使用的IO绝大多数都是使用的阻塞IO。

        阻塞IO就是自己去等,一旦等待完成就完成拷贝。

2.非阻塞IO

        非阻塞IO就不同了,它会在等待数据的过程中不去阻塞等待,而是直接返回,下一次再次调用再去检查释放准备好,没有准备好也是直接返回.....直到数据等待好了就进行拷贝。

3.信号驱动IO

        我们将等待数据的过程交给专门的信号处理程序,一旦等待完成信号处理程序通知上层(SIGIO),上层便就调用接口直接进行拷贝。

4.多路转接IO

        通过一个系统调用,我们一次性批量等待大量的IO接口。一旦存在数据等待完成,上层一次性将批量等待的IO接口等待完成的完成拷贝数据。

        核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。

5.异步IO

        内核帮助应用程序对特定文件描述符进行等待IO资源,当等待成功,内核拷贝数据完成后,在通知应用程序并且拷贝到用户空间。

IO模型之间的联系与区别

        首先,我们需要理解一下异步IO和同步IO之间的关系。

        这里的同步IO不同于线程中讲解的线程同步。此同步表示在IO过程中参与了其中的一个步骤(阻塞IO:等 + 拷贝,信号驱动IO:拷贝),在发起调用后,最终是自己获取的结果

        但是异步IO完全是交给别人去做,自己不参与IO的过程。也就是说它发起调用,会立即返回但是不会存在结果,是由别人-真正的被调用者通过状态、通知来通知调用者,处理别人获取的结果

        对于阻塞IO和非阻塞IO来说,阻塞IO在等待的过程中,是会被cpu链入对于资源的等待队列中的,所以当前线程实际上是会被挂起的。但是非阻塞IO如果不能得到结果,该调用是不会阻塞当前进程的。

        针对非阻塞IO的这种特性,我们代码书写的时候一般是循环来写的。那么我们有没有一种统一的方法对文件描述符设置为非阻塞IO的形式呢?(通过各个接口设置的方式不一样,并且都默认是阻塞IO,所以非阻塞IO是需要自己手动设置的)并且在实际操作中,我们如何对非阻塞IO究竟是出错还是-输入已经空了、输出已经满了进行判断呢?

fcntl设置非阻塞IO

        我们可以使用fcntl函数对文件描述符的属性进行设置。

man fcntl

头文件

       #include <unistd.h>
       #include <fcntl.h>
函数描述

        int fcntl(int fd, int cmd, ... /* arg */ );

        fcntl()执行一个操作下面描述的fd打开的文件描述符。操作由cmd决定。

函数参数

        fd:操作的文件描述符。

        cmd:

                F_GETFL   获取文件访问模式和文件状态标志;Arg被忽略。

                F_SETFL   将文件状态标志设置为arg指定的值。文件访问方式(O_RDONLY, O_WRONLY, O_RDWR)

                ......

        arg:

                arg中的文件创建标志(即O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC)被忽略。在Linux上这个命令只能修改O_APPEND、O_ASYNC、O_DIRECT、O_NOATIME和O_NONBLOCK标志。

                其中O_NONBLOCK标志就是非阻塞标志。

返回值

        出错了返回-1,并且设置error。

        设置了非阻塞IO后,由于是如果失败返回-1,设置错误码,这个失败包含两种情况:

        1.等待失败(比如in的时候接收缓冲区为空,或者out的时候发送缓冲区满了),这种情况是正常的。errno会被设置为EWOULDBLOCK或者EAGAIN。

        2.IO中断。这个调用被一个信号给中断。也就是说可能在等文件资源的时候,被系统唤醒,等待中断了。errno会被设置为EINTR。这也是正常的,重新进行即可,但是概率非常小。

        3.真正的错误。进行差错处理。

        我们下面利用我们经常使用的标准输入描述符进行实验,查看设置非阻塞IO的基本流程以及如何去使用非阻塞IO接口。

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>

int main()
{
    // 注意标准输入描述符是默认打开的,并且是0号fd哦,当然也可以直接stdin
    int fl = fcntl(0, F_GETFL);  // 首先获取原本的文件标志状态,保证修改的时候不动原来的状态
    int flag = fcntl(0, F_SETFL, fl | O_NONBLOCK);  // 设置此文件的文件标志状态,增加非阻塞状态
    printf("请输入: ");
    char buffer[1024];
    while (true)
    {
        ssize_t n = read(0, buffer, sizeof(buffer) - 1);  // 0此时没有作用了,因为标准输入文件描述符标志状态设置为非阻塞了
        if (n > 0)
        {
            // 此时是读取成功的,进行打印
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
        else if (n < 0)
        {
            // 读取失败,判断两种情况
            if (errno == EWOULDBLOCK || errno == EAGAIN)
            {
                // 此时只是读取缓冲区为空而已
                std::cout << "null" << std::endl;
                sleep(1);
            }
            else if (errno == EINTR)
            {
                // 此时小概率被IO中断了
                continue;
            }
            else{
                // 此时才是真正的错误
                std::cout << "read error:" << errno << strerror(errno) << std::endl;
                break;
            }
        }
    }
    return 0;
}

         现象果然如上面所说。

         那么,现在我们需要在我的自定义组件MySock文件中加入设置非阻塞的选项,供之后使用,就需要增加setNonBlock接口。针对于MySock文件,详细介绍可以在我的这两篇文章寻找答案,实际上就只是对套接字编程进行一个基本封装而已,加上了log日志打印文件而已。

【Linux】网络基础(1)_柒海啦的博客-CSDN博客

【Linux】网络套接字编程_柒海啦的博客-CSDN博客

        源文件可以在我的仓库中提出哦:linux日常练习/EpollTcpTest/ET/MySock.hpp · 刘凌宇/Linux_Test - Gitee.com 

        增加接口代码:

// 设置对此资源设置非阻塞IO,出错会设置致命出错日志
        static bool setNonBlock(int sock)
        {
            int fl = fcntl(sock, F_GETFL);  // 获取当前sock底层的读写标记位 - 保持当前的不变
            if (fl > 0)
            {
                int flag = fcntl(sock, F_SETFL, fl | O_NONBLOCK);  // 在不动原来的基础上进行一个非阻塞
                if (flag != -1) return true;
            }
            // 这里都是出错了
            logMessage(FATAL, "fcntl error %d:%s", errno, strerror(errno));
            return false;
        }

        观察上面的五种IO模型,在等待一批量的文件描述符的时候,除开多路转接IO外,其余都是一个一个进行等待的,但是多路转接IO是同时等待。这样的话它就在单位时间,等的比重就会非常低,所以效率就会很高。

        所以,下面我们针对多路转接,操作系统为我们提供了select、poll、epoll等接口实现,我们逐步用这些方案实现一个成熟的服务器出来。使用多路转接后,就不像之前服务器一样需要开多线程或者多进程执行,减少操作系统的压力。

二、select与poll多路转接方案

        前面我们已经提到过多路转接方案了。它这种IO模型是一次性等待一批文件描述符,从而缩短整体等待的时间,达到提高效率的效果。

        select接口就是系统给我们的一个多路转接方案,select只解决等的问题。所以它会:1.帮用户一次等待多个文件sock。2.当哪些文件sock就绪了,select通知用户,就绪的sock有哪些,用户调用io接口读取即可。

1.select接口介绍

man select

头文件

        #include <sys/select.h>

函数描述

        int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

        select()允许程序监视多个文件描述符,直到一个或多个文件描述符为某些类型的I/O操作(例如,可能的输入)“准备好了”。如果一个文件描述符能够在不阻塞的情况下执行相应的I/O操作(例如,read(2)),那么它就被认为是就绪的。

函数参数

        nfds:监听中最大的文件描述符+1。

        readfds、writefds、exceptfds

                操作系统提供的类型:fd_set。本质是一个位图,表示文件描述符集,并且存在上限:1024个。这三种,分别将fd设进去表示操作系统需要帮助我们关心什么IO操作:(读、写、异常是否就绪),然后返回的时候,对应fd_set如果存在对应的fd表示事件就绪了。所以它是一个输入输出参数

                对于fd_set这个位图结构,我们不能直接操作,需要利用系统提供的接口对位图进行扩展操作。

                void FD_CLR(int fd, fd_set *set);  // 删除位图中对应的fd。
                int  FD_ISSET(int fd, fd_set *set);  //检查对应fd是否存在位图中,存在返回1否则0.         
                void FD_SET(int fd, fd_set *set);  // 将对应fd添加到为位图中去
                void FD_ZERO(fd_set *set);  // 将此位图结构清空

        timeout:是一个结构体,用来描述时间 - 阻塞的时长:

                struct timeval-获取当前时区的时间。
                {time_t tv_sec - 时间戳
                suseconds_t tv_usec;  // 微妙级别的

                }

                输入参数的时候,表示阻塞一次需要多少时间。输出参数的时候表示基于需要的时间还剩下多少时间。

返回值

        如果成功,select()和pselect()返回三个返回的描述符中包含的文件描述符的数量。如果出错,返回-1并且设置errno。

2.编写select的一般模式

        通过上面的接口介绍,我们可以总结一下通过select编写IO的一般模式:

        1、nfds,随着获取的sock的越来越多,注定了nfds都可能发生变化。-最大文件描述符+1。

        2、rfds、wfds、efds都是输入输出型参数。输入输出不一定是一样的。每一次对rfds重新添加。
        3、timeout也是输入输出,每一次需要重置,前提是你需要的话。
        通过1、2将我们自己合法的文件描述符全部保存起来,用来支持:1-更新最大fd,2.更新fds-位图结构。
        所以我们就需要需要一个第三方数组,保存合法的fd。针对这个数组:1、遍历数组,更新最大值。2、遍历数组,添加关心的fd到set中。3、事件检测。4、遍历数组,找到就绪的事件,完成对应的动作:连接、读取。扩展:引入写入。(现在不好改写,epoll解决。)数组需要1024个即可。对于数组的处理:只要获取了sock,添加到数组里即可。如果满了,表示已经超过select监管个数了,满了,你没有位置了,丢弃此链接!如果存在,就添加到我们管理的文件描述表中即可。
        处理就绪套接字资源的时候,需要串行执行:原本是-1,跳过 ,如果FD_ISSET(fd, &fd_set)就是判断合法文件描述符是否事件就绪了。读事件就绪了就一定是read嘛,自然还有监听套接字。判断是否是监听套接字,监听套接字就获取链接添加sock,否则就是read!

        对于IO事件的处理,就read来说,分为三种情况,针对这三种情况分别要做不同的处理,编码的时候需要特别注意。

        下面,我们首先基于之前的log、mysock文件进行编写一个select实现的多路转接服务器,但是只处理读的事件,也就是说利用客户端进行测试的时候,服务器只需要打出信息即可,不做其他的处理。

#ifndef _SELECT_SERVER_H_
#define _SELECT_SERVER_H_

#include "MySock.hpp"
#include <sys/select.h>
#include <unistd.h>
#include <unordered_map>

const int NUM = (sizeof(fd_set) * 8);  // 定义fd_set最多接收的fd个数 - fd_set是一个位图 一个字节是八个比特位(一个比特位从右到左表示0123.....)
const int FD_NONE = -1;  // 设定fd的初始值为-1,表示此处fd空缺,可以填入或者跳过

class SelectServer
{
public:
    SelectServer(const uint16_t port = 8080)
    {
        _listenSock = QiHai::Sock::socket();
        QiHai::Sock::bind(_listenSock, port);  // 服务器默认绑定0.0.0.0 ip
        QiHai::Sock::listen(_listenSock);
        // 首先对_fdsArray进行初始化,好存放我们需要处理的fd,并且处理后的结果
        for (int i = 0; i < NUM; ++i) _fdsArray[i] = FD_NONE;
        _fdsArray[0] = _listenSock;  // 默认监听套接字为第一个
    }

    void Start()
    {
        // 首先,将通过监听套接字获取链接视为一种IO资源,我们是读取其中的链接的
        fd_set rfds;
        while(true)
        {
            // 因为rfd为输入输出参数,所以每次循环需要对其进行更新
            FD_ZERO(&rfds);  // 将文件集清空 - 本质是一个位图
            // 每次需要重新将输入的fd_set位图结果进行更新
            int maxFd = _listenSock;
            for(int i = 0; i < NUM; ++i)
            {
                if (_fdsArray[i] != FD_NONE)
                {
                    FD_SET(_fdsArray[i], &rfds);
                    if (_fdsArray[i] > maxFd) maxFd = _fdsArray[i];
                } 
            }
            int n = select(maxFd + 1, &rfds, nullptr, nullptr, nullptr);  // 最后一个参数nullptr为阻塞进行处理
            switch(n)
            {
            case 0:
                // 表示当前链接的所有fd没有就绪的 - 还需要进行等待 但是阻塞等待的话就不存在了
                logMessage(DEBUG, "select timeout......");
                break;
            case -1:
                logMessage(WARNING, "select error:%d-%s", errno, strerror(errno));
                break;
            default:
                // 出现文件就绪成功 IO = 等待 + 拷贝,等待成功!
                HandlerEvent(rfds);                
                break;
            }
        }
    }

    ~SelectServer()
    {
        if (_listenSock >= 0) close(_listenSock);
    }
private:
    // 此时select负责的多个fd中存在就绪的了,需要对其进行处理
    void HandlerEvent(fd_set& rfds)
    {
        //明确rfds是一个输入输出型参数,此时是输出型参数,如果对应fd等待资源就绪,内核会进行一个设置
        for (int i = 0; i < NUM; ++i)
        {
            if (_fdsArray[i] == FD_NONE) continue;
            if (FD_ISSET(_fdsArray[i], &rfds))  // 表示此时对应的就绪了
            {
                if (_fdsArray[i] == _listenSock) Accepter();  // 表示连接服务
                else Recver(i); // 正常的读服务
            }
        }
    }

    void Accepter()
    {
        // 连接服务
        int sock = QiHai::Sock::accept(_listenSock);  // 此时是就绪的,不用阻塞进行等待了 debug模式里面会打印客户端ip和port
        int pos = 1;  //从1开始
        for (; pos < NUM; ++pos)
        {
            if (_fdsArray[pos] == FD_NONE) break;
        }

        if (pos == NUM)
        {
            // 此时select托管的fd满了,不好意思,只能抛弃此连接了
            close(sock);
            logMessage(WARNING, "accept error,_fdsArray overload......");
        }
        else _fdsArray[pos] = sock;
    }

    void Recver(int pos)
    {
        // 读取服务
        // 当前应该配合协议进行读取,但是当前为了实现简单没有对数据进行处理
        char buffer[1024];
        int n = recv(_fdsArray[pos], buffer, sizeof(buffer) - 1, 0);  // 应该也是无需阻塞等待,直接读取
        if (n > 0)
        {
            buffer[n] = '\0';
            logMessage(DEBUG, "fds[%d]# %s", pos, buffer);
        }
        else if (n == 0){
            // 对方关闭连接
            close(_fdsArray[pos]);
            _fdsArray[pos] = FD_NONE;
            logMessage(WARNING, "fds[%d] close, me too ......", pos);
        }
        else{
            // 读取出现错误
            logMessage(ERROR, "recv error %d-%s", errno, strerror(errno));
            close(_fdsArray[pos]);
            _fdsArray[pos] = FD_NONE;
        }
    }
private:
    int _listenSock;
    int _fdsArray[NUM];
};



#endif

        运行结果:

3.总结一下select的优缺点:

优点:-任何一个多路转接都是这样
        a、效率高!-和之前的比。单位事件内,等的比重小了。
        b、应用场景:有大量的连接,但是只有少量是活跃的!
        
缺点:
        a、为了维护第三方数组,select服务器充满了大量的遍历 - On OS底层关心的时候也是存在大量的遍历的。
        b、每一次都要对select输出参数重新设定。
        c、能够同时管理的fd的个数是存在上限的。-fd_set。-1024bit
        d、因为几乎每一个参数都是输入输出型的,决定了select一定频繁的用户到内核,内核到用户参数数据拷贝。
        e、编码比较复杂。(前四个缺点导致的)

         针对于select缺点中的fd的上限个数,以及参数是输入输出型的,poll函数对齐进行了优化。

4.poll接口介绍:

man poll

头文件

        #include <poll.h>

函数描述

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

        Poll()执行与select(2)类似的任务:它等待一组文件描述符中的一个准备好执行I/O。

函数参数

        fds:类型为pollfd,为内核提供:

                struct pollfd{

                        int fd;  // 不会对fd 用户和内核都不会改其
                        short events;  // 请求事件  -- 用户写的,内核只对其读取
                        short revents; // 响应事件,内核告诉用户哪些事件就绪了。

                };

                基于select,实际上把之前fd_set三个位图fd表示的两种意思分离了。就使用一个数组进行搞定。由于fd_set是固定的大小所以只能是固定的fd个数。而现在数组个数完全由用户决定,理论上没有上限的。并且由于输入输出参数的分离,就不用每次对参数进行重新设定了。

                其中请求和响应事件存在对应的宏作为读、写、异常的关心:

                POLLIN、POLLOUT、POLLERR、POLLPRI、    POLLRDHUP....

                (可读    可写    错误       高优先级数据可读    TCP对方关闭连接)

        nfds:决定等待的文件描述符个数。

        timeout:输入型参数,单位为毫秒,表示阻塞一次的时间。(同理,0为非阻塞,-1为阻塞,其余就是阻塞一次的时间)

返回值

        -1表示错误。否则就是等待就绪的文件描述符个数。

        poll的工作模式和select类似,这里不在过多赘述,我们可以直接在之前那份代码上简单修改几处就能修改为poll的工作模式。

#ifndef _POLL_SERVER_H_
#define _POLL_SERVER_H_

#include "MySock.hpp"
#include "poll.h"
#include <unistd.h>
#include <unordered_map>

const int FD_NONE = -1;  // 设定fd的初始值为-1,表示此处fd空缺,可以填入或者跳过

class PollServer
{
public:
    PollServer(const uint16_t port = 8080, int nfds = 1024):_nfds(nfds)
    {
        _listenSock = QiHai::Sock::socket();
        QiHai::Sock::bind(_listenSock, port);  // 服务器默认绑定0.0.0.0 ip
        QiHai::Sock::listen(_listenSock);

        _pollFds = new pollfd[_nfds];
        for (int i = 0; i < _nfds; ++i)
        {
            _pollFds[i].fd = FD_NONE;
            _pollFds[i].events = _pollFds[i].revents = 0;  // 置空
        }
        _pollFds[0].fd = _listenSock;
        _pollFds[0].events = POLLIN;  // 只读事件
    }

    void Start()
    {
        // 首先,将通过监听套接字获取链接视为一种IO资源,我们是读取其中的链接的
        while(true)
        {
            int n = poll(_pollFds, _nfds, -1); // 最后一个参数-1为阻塞的进行读取哦
            switch(n)
            {
            case 0:
                // 表示当前链接的所有fd没有就绪的 - 还需要进行等待 但是阻塞等待的话就不存在了
                logMessage(DEBUG, "poll timeout......");
                break;
            case -1:
                logMessage(WARNING, "poll error:%d-%s", errno, strerror(errno));
                break;
            default:
                // 出现文件就绪成功 IO = 等待 + 拷贝,等待成功!
                HandlerEvent();                
                break;
            }
        }
    }

    ~PollServer()
    {
        if (_listenSock >= 0) close(_listenSock);
        delete[] _pollFds;
    }
private:
    // 此时select负责的多个fd中存在就绪的了,需要对其进行处理
    void HandlerEvent()
    {
        //明确rfds是一个输入输出型参数,此时是输出型参数,如果对应fd等待资源就绪,内核会进行一个设置
        for (int i = 0; i < _nfds; ++i)
        {
            if (_pollFds[i].fd == FD_NONE) continue;
            if (_pollFds[i].revents & POLLIN)  // 表示此时对应的就绪了
            {
                if (_pollFds[i].fd == _listenSock) Accepter();  // 表示连接服务
                else Recver(i); // 正常的读服务
            }
        }
    }

    void Accepter()
    {
        // 连接服务
        int sock = QiHai::Sock::accept(_listenSock);  // 此时是就绪的,不用阻塞进行等待了 debug模式里面会打印客户端ip和port
        int pos = 1;  //从1开始
        for (; pos < _nfds; ++pos)
        {
            if (_pollFds[pos].fd == FD_NONE) break;
        }

        if (pos == _nfds)
        {
            // 此时select托管的fd满了,不好意思,只能抛弃此连接了
            close(sock);
            logMessage(WARNING, "accept error,_fdsArray overload......");
        }
        else{
            _pollFds[pos].fd = sock;
            _pollFds[pos].events = POLLIN;
        }
    }

    void Recver(int pos)
    {
        // 读取服务
        // 当前应该配合协议进行读取,但是当前为了实现简单没有对数据进行处理
        char buffer[1024];
        int n = recv(_pollFds[pos].fd, buffer, sizeof(buffer) - 1, 0);  // 应该也是无需阻塞等待,直接读取
        if (n > 0)
        {
            buffer[n] = '\0';
            logMessage(DEBUG, "fds[%d]# %s", pos, buffer);
        }
        else if (n == 0){
            // 对方关闭连接
            close(_pollFds[pos].fd);
            _pollFds[pos].fd = FD_NONE;
            logMessage(WARNING, "fds[%d] close, me too ......", pos);
        }
        else{
            // 读取出现错误
            logMessage(ERROR, "recv error %d-%s", errno, strerror(errno));
            close(_pollFds[pos].fd);
            _pollFds[pos].fd = FD_NONE;
        }
    }
private:
    int _listenSock;
    int _nfds;
    struct pollfd* _pollFds;
};



#endif

5.poll的优缺点

    优点:
        1.效率高
        2.有大量的链接,只有少量的是活跃的-节省资源
        3.输入输出参数分离,不需要进行大量的重置。
        4.poll参数级别,没有可以管理fd的上限。
    缺点:        
        ***1.poll仍然避免不了结构体数组,还是需要进行遍历的,在用户从检测事件就绪,内核检测fd就绪,都是一样。
        2.poll需要内核到用户的拷贝。 -- 少不了
        *3.poll编码也不容易。--比select容易。

        可以看到,虽然在一定程度上做了简化,参数分离,提高上限等操作,但是还是避免不大量的遍历。这些问题的原因就是用户在对这一批fd进行组织管理的。

        那么我们能不能将管理这一批fd也交给操作系统呢?并且操作系统做相关的回调设置,减少遍历的操作吗?当然可以,epoll就是这样完美解决这些问题的方案。

三、epoll多路转接方案

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

        epoll针对poll的缺点,-目的是为了解决用户来维护数组,并且不断循环拷贝(内核->用户)的过程,解决这些只能在内核层自己处理。

        所以首先在操作系统内部创建这个管理系统加了epoll_create进行创建,epoll_ctl提供了是否添加、修改、删除对应fd的一个监控模式,而epoll_wait则提供了拿出就绪文件描述符的功能。

epoll_create

man epoll_create

头文件

        #include <sys/epoll.h>

函数描述

        int epoll_create(int size);

        epoll_create()创建一个epoll实例。从Linux 2.6.8开始,size参数被忽略,但必须大于零;

        epoll_create()返回指向新的epoll实例的文件描述符。该文件描述符用于对epoll接口的所有后续调用。当不再需要时,该文件epoll_create()返回的描述符应该使用close关闭。当所有引用epoll实例的文件描述符都已关闭时,内核将销毁该实例并释放用于重用的关联资源。

        如果失败返回-1,并且设置errno。

1.epoll的工作原理

        因为操作系统需要在内核为我们维护,那么自然需要先组织。

        针对于用户想要关心的fd是否就绪,内核采用的是红黑树进行组织。红黑树是自平衡的搜索二叉树,可以通过此链接进行了解哦(红黑树的插入实现)~

        所以,这样就避免了像select、poll,利用用户的数组进行遍历,内核查看哪些需要进行关心,而是用户直接到内核进行注册,这样OS每次就对这颗树进行检查,避免频繁的遍历以及切换状态。

        另外,用户曾经在红黑树注册的fd的事件,会在底层网卡的驱动程序中设置一个回调函数(OS是如何直到网卡内存在数据呢?--TCP/IP协议栈,想传给上层网络层、传输层的时候  采用硬件中断的方式。网卡-中断通知>cpu-执行void(*hanlder[])()-中断向量表->read_netcard()此时就可以从网卡内搬到操作系统内部了。-此时在经过网络协议栈进行一步步,最后通知上层。转化为一个值保存到寄存器里-索引),设置了回调函数后,如果对应的事件就绪,此时不用关心红黑树,插入进就绪队列。此时不用再关心红黑树,直接队列送上去即可。-内核告诉用户哪些事件就绪了.

         针对于poll、select,他们的工作原理如下,可以做一个更好的比对:

    1.无论是select、poll都是需要用户自己维护数组进行保存fd和特定事件的。--成本、程序员承担
    2.select、poll都需要遍历。(内核和用户都要遍历)
    3.poll、select工作模式:
        a、通过系统调用,用户告诉内核哪些fd上的哪些事件
        b、通过系统的返回,内核告诉用户哪些fd的哪些事件已经发生了。

        所以,我们往epoll模型中的红黑树进行插入、修改、删除对应fd关心时,使用的接口就是epoll_ctl。

epoll_ctl

man epoll_ctl

头文件

        #include <sys/epoll.h>

函数描述

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

        这个系统调用对文件描述符epfd引用的epoll实例执行控制操作。它请求对目标文件描述符fd执行操作op。

函数参数

        epfd:epoll_create获得的epoll模型的文件描述符。

        op:对对应的fd如何操作。

                EPOLL_CTL_ADD  // 添加fd关心事件

                EPOLL_CTL_MOD  //更改更改与目标文件描述符fd关联的事件事件

                EPOLL_CTL_DEL   //从epfd引用的epoll实例中删除(取消注册)目标文件描述符fd。(需要注意,一般时先从epoll模型中删除fd,然后在close,否则可能存在bug)

        fd:添加对应关心的事件描述符。

        event:关心的事件。

                struct epoll_event // 系统创建的类型

                {

                        uint32_t events;  // epoll事件

                        epoll_data_t data;  // 用户数据变量

                };

                epoll事件:

                        EPOLLIN  读事件

                        EPOLLOUT  写事件

                        EPOLLHUP  关联文件描述符挂起。没有必要等它。(网络中对方关闭连接)

                        EPOLLERR  相关文件描述符发生错误。

                        EPOLLET  为关联的文件描述符设置边缘触发行为。(默认为水平触发行为,此行为为多路转接的工作模式,后续会讲)

                epoll_data_t 为一个联合体,用户自己设定想要返回什么信息:

                                   typedef union epoll_data {
                                               void        *ptr;
                                               int          fd;
                                               uint32_t     u32;
                                               uint64_t     u64;
                                    } epoll_data_t;

返回值

        成功返回0,失败返回-1,设置errno。

        当监视的fd存在关心事件就绪的时候,从epoll模型维护的就绪队列中提出来的方法时epoll_wait。

epoll_wait

man epoll_wait

头文件:

        #include <sys/epoll.h>

函数描述:

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

        epoll_wait()系统调用等待文件描述符epfd引用的epoll实例上的事件。由事件指向的内存区域将包含调用者可用的事件。epoll_wait()最多返回maxevents。maxevents参数必须大于零。

函数参数:

        epfd:epoll模型的文件描述符。

        events:用户保存就绪事件的数组。

        maxevents:期望就绪事件个数。(返回来的事件个数最多不超过期望个数)

        timeout:和poll类似,大于0设置阻塞时间,等于0表示非阻塞,小于0表示阻塞等待。

返回值:

        当成功时,epoll_wait()返回为请求的I/O准备好的文件描述符的数量,如果在请求的超时毫秒内没有文件描述符准备好,则返回零。当一个发生错误时,epoll_wait()返回-1,并适当设置errno。(这个数量就是存在在数组的整个个数,从0下标开始到此返回数 - 1)

        实际上,人们针对于这个内核等待fd就绪事件发生,如何通知用户有着不同的操作模式。这里就涉及到了多路转接的工作模式了。

2.epoll的工作模式

        epoll针对事件就绪后通知用户的行为,增加了其工作模式,以便应对不同的IO模式。

        我们以快递小哥发送快递为例。

        假设快递小哥A拿到多个人的包裹,他在发送快递的时候,每一个快递都是等别人真正从他手中拿过的时候,他才进行下一个快递的发放。

        但是快递小哥B不同,他拿到多个包裹后,不同的快递通知不同的人时,每个只是通知一遍,通知后他会放到一个位置走了,继续下一个快递的发放。如果同一个人的快递临时多了一个,他会在通知这个人一遍。

        实际上,快递小哥A的工作模式就是水平触发模式(Level Triggered),简称LT。而快递小哥B的工作模式就是边缘触发模式(Edge Triggered),简称ET。

        默认条件下,epoll模型设置的是LT模式,对于poll和select的工作模式也是LT模式。就是说一旦存在关心的事件就绪了,会不断的通知你(通过wait接口返回的就绪个数)。但是ET只有数据首次到达或者发生变化的时候才会通知。

        针对于这两种模式,很明显,显然ET模式的工作效率会很高。但是即使是LT模式,我每次拿数据的时候也一次性拿完,效率是和ET模式下差不多的。

LT模式 VS ET模式原则上谁更高效?
    ET模式。
    1.更少的返回次数。
    2.ET模式会倒逼程序员将接收缓冲区中的数据全部取走,应用层就更快的取走了缓冲区的数据,单位时间下,此服务器在一定程度上会给发送方一个更大的接收窗口,对方就可以有更大的滑动窗口,提高IO吞吐。
    ET模式的代码复杂度比较高。

        所以,我们通过ET模式可以发现一个问题,它每次通知我们对应的fd关心事件的时候,如果是读取。没有读完的话,那么下次就读不到了,获取不到完整的数据(LT模式下下次还能继续)。所以在ET模式下,我们需要读取一次完整的读完。

        那么,这里就需要对IO的特性做出区别了。由于读取的时候,可能因为用户的接收缓冲区过小或者发生信号中断的话,就不能一次性的从接收缓冲区中读取完全,所以我们需要循环的去读。但是,如果是阻塞IO的话,读取完后发现接收缓冲区为空会发生阻塞,此时此进程就会被挂起。这怎么可以呢?所以需要非阻塞IO进行轮询读取,最后根据错误码进行判断是缓冲区空了还是失败了。

        所以,基于ET的工作环境,我们需要非阻塞IO进行读取。

四、基于epoll的Reactor模式

        那么,现在我们综合实现一个完善的服务器。利用epoll的多路转接接口和ET工作模式。

        需求如下:

1.实现网络版的计算器。

2.服务器对于超时连接-比如超时20s没有发送任何请求,需要主动断开。

3.采用epoll的多路转接,不出现任何多执行流。

        综合上述需求,我们实际编码的时候,需要自己定制协议(协议可以采用现成的Json,自己解决粘包即可)。之前的代码中,因为只解决了读的需求,每个fd都是共享一个自己的用户缓冲区,现在在ET、并且要处理读和写事件下,需要每个fd都能维护两个缓冲区,并且采用非阻塞IO的方式,实现TCP网络版的计算器。

        参考代码如下:(注:MySock、log头文件没有提供编码)

// Epoll.hpp

#ifndef __EPOLL_HPP__
#define __EPOLL_HPP__

#include <sys/epoll.h>
#include <unistd.h>
#include "log.hpp"

// 创建epoll模型,使用此epoll模型进行操作
class Epoll
{
    const static int gsize = 256;
    const static int gtimeout = 10000;  // 10s
private:
    int _epfd;
public:
    Epoll():_epfd(-1)
    {}

    // 创建epoll模型
    bool EpollCreate()
    {
        _epfd = epoll_create(gsize);  // gsize需要注意是一个废弃的参数,这里随便设置即可
        if (_epfd < 0) logMessage(FATAL, "epoll_create error %d:%s", errno, strerror(errno));
        return _epfd != -1;
    }

    // 增添监听对象到epoll模型中来,并且说明需要关心的事件 如果需要使用ET模式进行监听,需要在event选项添加EPOLLET,需要注意,默认就是LT模式
    bool AddSockToEpoll(int sock, int event)
    {
        struct epoll_event epev;
        epev.events = event;
        epev.data.fd = sock;

        int flag = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &epev);
        if (flag < 0) logMessage(FATAL, "epoll_ctl error %d:%s", errno, strerror(errno));
        return flag != -1;
    }

    // 修改sock在epoll所关心的状态
    bool CtlSockToEpoll(int sock, int event)
    {
        // 进来先添加ET模式
        event |= EPOLLET;
        struct epoll_event epev;
        epev.events = event;
        epev.data.fd = sock;

        int flag = epoll_ctl(_epfd, EPOLL_CTL_MOD, sock, &epev);
        if (flag < 0) logMessage(FATAL, "epoll_ctl error %d:%s", errno, strerror(errno));
        return flag != -1;
    }

    // 删除sock在epoll对应的监听
    bool DelFromEpoll(int sock)
    {
        int flag = epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
        if (flag < 0) logMessage(FATAL, "epoll_ctl error %d:%s", errno, strerror(errno));
        return flag != -1;
    }

    // 从epoll模型的就绪队列取出num个(num是期望个数),并且以timeout毫秒的时候进行一次阻塞。-1为阻塞,0非阻塞,默认5s一次阻塞
    // 需要注意的是,如果是ET模型,只有等待对象的缓存区出现变化后才会通知,并且只通知一次,如果通知后没有取走或者取完,此就绪队列就不会存储其相关的就绪事件了
    int WaitEpoll(epoll_event events[], int num, int timeout = gtimeout)  // 默认5s进行进行一次阻塞
    {
        return epoll_wait(_epfd, events, num, timeout);
    }

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

#endif
// Protocal.hpp 自己定制协议
#ifndef __PROTOCAL_HPP__
#define __PROTOCAL_HPP__

#include <cstring>
#include <string>
#include <vector>
#include<jsoncpp/json/json.h>
// 自定义协议 - 使用json封装一下

namespace QiHai
{
    static const char* SEP_BEGIN = "{";
    static const char* SEP_END = "}";
    static const int SEP_LEN = strlen(SEP_END);

    // 请求
    class Request
    {
    public:
        Request() = default;
        Request(int x, int y, char op)
        :_x(x), _y(y), _op(op)
        {}

        // 序列化
        std::string serialization()
        {
            Json::Value root;
            root["x"] = _x;
            root["y"] = _y;
            root["op"] = _op;

            Json::FastWriter writer;
            return writer.write(root);
        }

        // 反序列化
        bool deserialization(std::string request)
        {
            Json::Reader read;
            Json::Value root;
            if (!read.parse(request, root)) return false;

            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _op = root["op"].asInt();
            return true;
        }

        int _x;
        int _y;
        char _op;
    };

    // 响应
    class Response
    {
    public:
        Response() = default;
        Response(int state, int result)
        :_state(state), _result(result)
        {}
        
        // 序列化
        std::string serialization()
        {
            Json::Value root;
            root["state"] = _state;
            root["result"] = _result;

            Json::FastWriter writer;
            return writer.write(root);
        }

        // 反序列化
        bool deserialization(std::string request)
        {
            Json::Reader read;
            Json::Value root;
            if (!read.parse(request, root)) return false;

            _state = root["state"].asInt();
            _result = root["result"].asInt();
            return true;
        }

        int _state;  // 状态:1正确 0异常
        int _result;  // 结果:正常就是计算结果,否则就是错误编号
    };

    // 解决粘包以及不是独立的一个报文问题:这里采用的是Json的格式{"1":1}...
    void SpliteMessage(std::string& message, std::vector<std::string>& messages)
    {
        while (true)
        {
            // 首先找到第一个{的位置
            auto begin = message.find(SEP_BEGIN);
            if (begin == std::string::npos) break;  // 没有找到一个完整报文
            auto end = message.find(SEP_END);
            if (end == std::string::npos) break;  // 同理

            std::cout << "DEBUG: " << message.substr(0, end + 1) << std::endl;
            messages.push_back(message.substr(0, end + 1));  // {}
            message.erase(0, end + SEP_LEN);
        }
    }
}

#endif
// TCPServer.hpp
#ifndef __TCPSERVER_HPP__
#define __TCPSERVER_HPP__

#include <functional>
#include <unordered_map>
#include <ctime>
#include "MySock.hpp"
#include "log.hpp"
#include "Epoll.hpp"
#include "Protocal.hpp"

namespace QiHai
{
    // 用户管理
    class User
    {
    public:
        std::string _ip;
        std::uint16_t _port;

        User(std::string ip, uint16_t port)
        :_ip(ip), _port(port)
        {}
    };

    class TcpServer;
    class Connection;
    using func_t = std::function<void(Connection*)>;  // 注意前面需要声明哦
    using service_logic = std::function<void(Connection*, std::string)>;
    static const time_t MAX_TIME = 20;  // 最大时间间隔
    // 每个sock对应一个连接类进行管理,里面存在其输入输出缓冲区,以及对应读取、写入、异常的回调方法
    class Connection
    {
    public:
        int _sock;

        // 三个回调函数
        func_t _recv_cb;
        func_t _send_cb;
        func_t _except_cb;

        // 接收缓冲却和发送缓冲区
        std::string _inbuffer;  // 当前存在bug,不能处理二进制流,但是文本是可以的
        std::string _outbuffer;

        TcpServer* _tsvr;  // 对服务器的一个回调指针,后续会使用到

    public:
        time_t _time;  // 当前的时间戳

        Connection(int sock, TcpServer* tsvr)
        :_sock(sock), _tsvr(tsvr)
        {}

        // 设置三种回调函数
        void SetCallBack(func_t recv_cb, func_t send_cb, func_t except_cb)
        {
            _recv_cb = recv_cb;
            _send_cb = send_cb;
            _except_cb = except_cb;
        }

        ~Connection()
        {}
    };

    class TcpServer
    {
        const static uint16_t gport = 8080;
        const static int gnum = 128;
    private:
        int _listensock;
        uint16_t _port;
        Epoll _poll;
        std::unordered_map<int, Connection*> _connections;  // 组织链接
        std::unordered_map<int, User*> _users;  // 组织用户
        struct epoll_event* _revs;  // 读取就绪事件的缓冲区
        int _revs_num;  // 每次读就绪事件的大小,这里初始化的时候固定大小即可
        service_logic _call;  // 处理业务逻辑的函数,由上层提供
    private:
        // 将此sock建立一个连接对象描述起来,并且连接到map。设置三个回调函数。默认关心的事件是读取,写入和异常后续设置
        // 任何多路转接的服务器,一般默认只会打开对读取事件的关心,写入事件按需打开 
        void AddConnection(int sock, func_t recv_cb, func_t send_cb, func_t except_cb)
        {
            if(!QiHai::Sock::setNonBlock(sock)) exit(2);  // 设置其属性为非阻塞
            Connection* connection = new Connection(sock, this);
            connection->_time = time(nullptr);  // 初始时间戳
            connection->SetCallBack(recv_cb, send_cb, except_cb);
            _connections.insert(std::make_pair(sock, connection));
            // 添加入epoll模型中,默认关系读取事件
            if (!_poll.AddSockToEpoll(sock, EPOLLIN | EPOLLET)) exit(6);  // 主动设置为ET模式对其进行监听
        }

        // 专门处理监听套接字的读取回调函数
        void Accepter(Connection* conn)
        {
            logMessage(DEBUG, "Accepter wait......");
            std::string client_ip;
            uint16_t client_port;
            int err_num = 0;
            while(true)
            {
                int sock = QiHai::Sock::accept(conn->_sock, &err_num, &client_ip, &client_port);
                // 注意,sock设置的是非阻塞读取,所以循环的进行绑定,一次性读完
                if (sock < 0)
                {
                    if (err_num == EAGAIN || err_num == EWOULDBLOCK) break;  // 此时只是没有链接了,读完了
                    else if(err_num == EINTR) continue;  // 此时是被中断了
                    else{
                        logMessage(WARNING, "accept error...... %d:%s", err_num, strerror(err_num));
                        break;  // 此时错误了,但是影响不大,break即可
                    }
                }
                _users.insert(std::make_pair(sock, new User(client_ip, client_port)));
                AddConnection(sock, std::bind(&TcpServer::Recver, this, std::placeholders::_1), \
                                    std::bind(&TcpServer::Sender, this, std::placeholders::_1), \
                                    std::bind(&TcpServer::Excpeter, this, std::placeholders::_1));

                logMessage(DEBUG, "is %d connection! add Connection success!", sock);
            }
        }

        // 三大回调方法
        // 读回调
        void Recver(Connection* conn)
        {
            // 此链接读的事件响应,处理读的事件
            // 首先保证将本次从底层的读取缓冲区拿完到用户的缓冲区
            conn->_time = time(nullptr);  // 更新时间戳
            bool err = false;
            const int nums = 1024;
            while(true)
            {
                char buffer[nums];
                int n = recv(conn->_sock, buffer, sizeof(buffer) - 1, 0);  // 此时最后一个0没有多大意义。因为sock始终保证的就是非阻塞进行读取
                if(n > 0)
                {
                    // 读取成功
                    buffer[n] = '\0';
                    conn->_inbuffer += buffer;
                }
                else if(n == 0)
                {
                    // 对方关闭连接
                    logMessage(DEBUG, "sock-%d[%s:%d] close connection, server close connection!", \
                                       conn->_sock, _users[conn->_sock]->_ip.c_str(), _users[conn->_sock]->_port);
                    conn->_except_cb(conn);  // 调用异常处理方法
                    err = true;
                    break;
                }
                else
                {
                    // 小于0判断是否读取完毕
                    if (errno == EAGAIN || errno == EWOULDBLOCK) break;  // 读取完毕正常退出
                    else if (errno == EINTR) continue;  // 中断了,在继续读取
                    else
                    {
                        err = true;
                        // 此时真的出错了
                        logMessage(FATAL, "recv error %d:%s", errno, strerror(errno));
                        conn->_except_cb(conn);  // 调用异常处理方法
                        break;
                    }
                }
            }
            if (!err)
            {
                logMessage(DEBUG, "sock:%d[%s:%d]# %s", \
                           conn->_sock, _users[conn->_sock]->_ip.c_str(), _users[conn->_sock]->_port, conn->_inbuffer.c_str());
                // 此时需要保证读取的数据是一个一个独立完整的报文,然后传递给上层逻辑进行处理
                std::vector<std::string> messages;
                QiHai::SpliteMessage(conn->_inbuffer, messages);  // test:{"x":1,"y":2,"op":43}{"x":2,"y":3,"op":47}{"x":0,"y":0,"op":37}{"x":1,
                for (auto& msg : messages)
                {
                    // 调用上层逻辑进行处理,服务器只是帮你提取出报文出来
                    _call(conn, msg);
                }
            }
        }

        void Sender(Connection* conn)
        {
            // 写也是非阻塞的写,也就是说,如果写入缓冲区满了也是会直接返回
            while(true)
            {
                int n = send(conn->_sock, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);
                if (n > 0)
                {
                    // 写入成功
                    conn->_outbuffer.erase(0, n);
                    if (conn->_outbuffer.empty()) break;  // 写完了,退出
                }
                else{
                    if (errno == EAGAIN || errno == EWOULDBLOCK) break;  //  此时底层的发送缓冲区可能满了,不可发送,等下次机会 - 只要空了就会在执行这里
                    else if (errno == EINTR) continue;  // IO中断了,重新试一次
                    else{
                        // 此处出错了
                        logMessage(FATAL, "send error %d:%s", errno, strerror(errno));
                        conn->_except_cb(conn);
                        break;
                    }
                }
            }

            // 注意,如果将缓冲区内的数据发完了,需要关闭epoll对其写入事件的关心 - 因为一旦发送缓冲区变化了-变空 会通知的
            if (conn->_outbuffer.empty()) EnableReadWrite(conn, true, false);
            else EnableReadWrite(conn, true, true);
                // 保险起见,没发完的下次还要继续发
        }

        void Excpeter(Connection* conn)
        {
            User* user = _users[conn->_sock];
            _users.erase(conn->_sock);
            delete user;

            if (!_poll.DelFromEpoll(conn->_sock)) exit(6);  // 从epoll 模型中移除
            _connections.erase(conn->_sock);
            close(conn->_sock);  // 关闭此文件描述符
            delete conn;
        }

        bool IsConnectionExists(int sock)
        {
            auto res = _connections.find(sock);  // 可能后续因为异常,存在没有映射到此管理的表中的,这里需要检测一下
            return res != _connections.end();
        }
        
        void LoopOnce()
        {
            // 具体去就epoll就绪队列中获取
            int n = _poll.WaitEpoll(_revs, _revs_num);  // 默认10s阻塞一次
            if (n == 0)
            {
                logMessage(DEBUG, "events wait......");
            }
            else if (n < 0)
            {
                logMessage(FATAL, "epoll_wait error %d:%s", errno, strerror(errno));
                exit(7);
            }
            else{
                logMessage(DEBUG, "have events!");
                for (int i = 0; i < n; ++i)
                {
                    int sock = _revs[i].data.fd;
                    int event = _revs[i].events;

                    if (event & EPOLLERR) _revs[i].events |= (EPOLLIN | EPOLLOUT);  // 直接掉用recv、send报错后调用异常处理
                    if (event & EPOLLHUP) _revs[i].events |= (EPOLLIN | EPOLLOUT);  // 对方断开连接同理

                    if (event & EPOLLIN)
                    {
                        if(IsConnectionExists(sock) && _connections[sock]->_recv_cb)  // 首先是否找得到,然后判断是否注册了读取回调函数
                            _connections[sock]->_recv_cb(_connections[sock]);
                    }
                    if (event & EPOLLOUT)
                    {
                        if (IsConnectionExists(sock) && _connections[sock]->_send_cb)
                            _connections[sock]->_send_cb(_connections[sock]);
                    }
                }
            }
        }

        void Survivallink()  // 生存链接时间,发现大于MAX_TIME,主动断开连接
        {
            auto res = _connections.begin();
            while (res != _connections.end())
            {
                time_t t = time(nullptr);
                Connection* conn = res->second;
                ++res;
                if (t - conn->_time >= MAX_TIME && conn->_except_cb)  // 排除了sock
                {
                    logMessage(DEBUG, "sock:%d[%s:%d] No communication for a long time,close sock!",\
                                       conn->_sock, _users[conn->_sock]->_ip.c_str(), _users[conn->_sock]->_port);
                    conn->_except_cb(conn);
                }
            }
        }
    public:
        TcpServer(uint16_t port = gport)
        :_port(port), _revs(nullptr), _revs_num(gnum)
        {
            _listensock = QiHai::Sock::socket();
            if (_listensock < 0) exit(1);

            // 绑定
            if (!QiHai::Sock::bind(_listensock, _port)) exit(3);

            // 设置监听状态
            if (!QiHai::Sock::listen(_listensock)) exit(4);
            logMessage(DEBUG, "_listensock init success!");

            // 创建epoll模型
            if (!_poll.EpollCreate()) exit(5);
            logMessage(DEBUG, "epoll init success!");

            // 此时能否向以前那样,所有sock共享一个缓冲区呢(即裸的sock)?自然不能,可能存在多个sock是阶段性发送的,报文数据不完全。
            // 所以我们需要维护sock的每一个缓冲区,先描述在组织 使用类connection进行描述,使用哈希表实现的map进行一个组织
            // 在添加监听套接字对象到epoll之前,首先维护好一个就绪缓冲区
            _revs = new epoll_event[_revs_num];

            // 正式添加,我们增加一个接口,后续sock添加到里面都通过此接口 - 要建立连接类之类的,解耦一下
            AddConnection(_listensock, std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
            logMessage(DEBUG, "listensock connection add success!");
        }

        // 修改连接在epoll模型的读写状态
        void EnableReadWrite(Connection* conn, bool readable, bool writeable)
        {
            int event = (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0);
            if (!_poll.CtlSockToEpoll(conn->_sock, event)) exit(6);
        }

        // 服务器开始对特定事件等待并且进行一个派发
        void Dispather(service_logic func)
        {
            _call = func;  // 业务处理函数
            while(true)
            {
                Survivallink();  // 每次循环检查一次
                LoopOnce();  // 一次
            }
        }

        ~TcpServer()
        {
            close(_listensock);
            if (_revs != nullptr) delete[] _revs;
        }
    };


}

#endif
// main.cc 程序入口
#include "TcpServer.hpp"
#include <memory>

void business(QiHai::Request& req, QiHai::Response& res)
{
    res._state = 1;
    switch (req._op)
    {
    case '+':
        res._result = req._x + req._y;
        break;
    case '-':
        res._result = req._x - req._y;
        break;
    case '*':
        res._result = req._x * req._y;
        break;
    case '/':
        if (req._y == 0)
        {
            res._state = 0;
            res._result = 1;  // 除0异常
        }
        else res._result = req._x / req._y;
        break;
    case '%':
        if (req._y == 0)
        {
            res._state = 0;
            res._result = 2;  // 模0异常
        }
        else res._result = req._x % req._y;
        break;
    default:
        res._state = 0;
        res._result = 3;  // 未知运算符
        break;
    }
}

void Natcb(QiHai::Connection* conn, std::string message)
{
    // 上层业务处理
    // 接收到一个报文后,因为是一个结构化的东西,首先进行反序列化
    QiHai::Request req;
    if (!req.deserialization(message))
    {
        logMessage(WARNING, "Request deserialization error!");
        return;
    }

    QiHai::Response res;
    business(req, res);
    conn->_outbuffer += res.serialization();
    conn->_tsvr->EnableReadWrite(conn, true, true);  // 修改一次写的状态,剩下由服务器进行处理
}

int main()
{
    std::unique_ptr<QiHai::TcpServer> server(new QiHai::TcpServer);
    server->Dispather(Natcb);

    // QiHai::Response res;
    // res.deserialization("{\"state\":1,\"result\":2}");
    // std::cout << res.serialization() << std::endl;

    // QiHai::Request req;
    // std::string msg = "{\"op\":43,\"x\":1,\"y\":2}";
    // req.deserialization(msg);
    // std::cout << req.serialization() << std::endl;

    // QiHai::Request req(1, 2, '+');
    // std::cout << req.serialization() << std::endl;
    return 0;
}

实现效果:

        由于没有实现此服务器的客户端,所以使用telnet模拟发送一个自己定制协议的报文,查看结果,并且在创建一个进行连接,长时间不请求,是否会断开:

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值