Linux高级I/O:Reactor反应堆模式 | Epoll ET模式


全文约 8696 字,预计阅读时长: 25分钟


在这里插入图片描述


Reactor反应堆模式

  • Reactor:负责检测关心的IO事件,当检测到IO事件时,分发给Handlers处理(对数据的读写处理+对数据的分析处理)。
  • epoll ET工作方式:只支持非阻塞IO文件。

简易的Reactor epoll ET TCP服务器

单进程基于ET非阻塞设计的一个Reactor模式+数据读写+数据分析。

socket封装

监听套接字的创建、绑定、监听、设置非阻塞I/O

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include<fcntl.h>
using std::cerr;
using std::cin;
using std::cout;
using std::endl;

namespace ns_sock
{
    enum
    {
        SOCK_ERR = 2,
        BIND_ERR,
        LISTEN_ERR
    };

    const int g_backlog = 5;

    struct sock_package
    {
        static int SockCreate()
        {
            int sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                cerr << "socket  failed" << endl;
                exit(SOCK_ERR);
            }
            //设置可以立即重启绑定。
            int opt = 1;
            setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
            return sock;
        }
        static void Bind(const int &sock_fd, const uint16_t &port)
        {
            struct sockaddr_in local;
            bzero(&local, sizeof(local));
            local.sin_addr.s_addr = INADDR_ANY;
            local.sin_port = htons(port);
            local.sin_family = AF_INET;
            if (bind(sock_fd, (sockaddr *)&local, sizeof(local)) < 0)
            {
                cerr << "Bind Error" << endl;
                exit(BIND_ERR);
            }
        }
        static void Listen(const int &sock_fd)
        {
            if (listen(sock_fd, g_backlog) < 0)
            {
                cerr << "Listen Error" << endl;
                exit(LISTEN_ERR);
            }
        }
        static void set_noblock(int listen_sk)
        {
            int ret_sock = fcntl(listen_sk,F_GETFL);
            if(ret_sock<0)
            {
                cerr<<"fcntl error"<<endl;
                exit(5);
            }
            fcntl(listen_sk,F_SETFL,ret_sock | O_NONBLOCK);
        }
    };

}

epoll。hpp

一:epoll文件描述符的创建,往epoll文件描述里、添加需要关心的I/O文件 、以及关心的I/O文件的事件;检测事件的发生。

二:当前I/O文件的错误回调功能具有:哈希表删除当前的fd值,epoll_del从epoll红黑树中删除该fd节点;最后关闭fd。

三:检测到所有的异常事件发生时,都交付给当前I/O文件的错误回调处理。

四:当检测有事件就绪时应检测当前I/O文件的FD是否还存在。因为可能发送事件触发时,上一秒该文件有异常,从epoll中删除了。

五:具有一个可以让:当前I/O文件读取完毕后,构建发送报文时,可以修改当前该文件的关心事件的功能。因为读事件时一直需要关心的,写事件需要服务器返回数据时开启,发送完毕取消关心。sock文件描述符的反应堆指针,是为了后续可以使用其内部相关功能。

#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <unordered_map>
#include <string>
#include <utility>
#include <unistd.h>

using namespace std;
namespace ns_reactor
{
    class reactor_sv;
    class sock_pack;
    typedef void (*sock_callback)(sock_pack &);//函数指针类型
    class sock_pack//sock_fd配套设施。
    {
    public:
        int sock_fd_;
        reactor_sv *r;
        string inbuffer;
        string outbuffer;

        sock_callback read_hand;//函数指针类型 声明 一个函数指针
        sock_callback write_hand;
        sock_callback error_hand;

    public:
        sock_pack() : sock_fd_(-1), r(nullptr)
        {
            read_hand = nullptr;
            write_hand = nullptr;
            error_hand = nullptr;
        }
        ~sock_pack()
        {
            // if (sock_fd_ >= 0)
            // {
            //     close(sock_fd_);
            // }
            cout << "dont Deconstruction me" << endl;
        }
        void sock_events_handler(sock_callback rc, sock_callback wc, sock_callback ec)
        {
            read_hand = rc;//函数指针格式一样即可。
            write_hand = wc;
            error_hand = ec;
        }
    };
----------
    class reactor_sv//反应堆核心
    {
    private:
        int epfd_;
        unordered_map<int, sock_pack> react_mp;

    public:
        reactor_sv() : epfd_(-1)
        {
        }
        ~reactor_sv() {}
        void epfd_create()//创建
        {
            epfd_ = epoll_create(128);
            if (epfd_ < 0)
            {
                cerr << "epoll create  error..." << endl;
                return;
            }
            cout << "初始化完成..." << endl;
        }
        void epll_add(const sock_pack &sk, const uint32_t ext_events)//添加sock——fd至epoll中
        {
            epoll_event ske;
            ske.events = ext_events;
            ske.data.fd = sk.sock_fd_;
            if (epoll_ctl(epfd_, EPOLL_CTL_ADD, sk.sock_fd_, &ske) < 0)
            {
                cerr << "epol_ctl failed.." << endl;
                return;
            }
            else
            {
                react_mp.insert({sk.sock_fd_, sk});
            }
            cout << "ep_add success...!" << endl;
        }

        void Epoll_Del(int sock)//出错清除关心的IO文件。
        {
            auto iter = react_mp.find(sock);
            if (iter == react_mp.end())
            {
                return;
            }
            epoll_ctl(epfd_, EPOLL_CTL_DEL, sock, nullptr);
            react_mp.erase(iter);
            close(sock);
            cout << "Epoll_Del called..资源清理成功" << endl;
        }
		//当读取完毕修改写事件的关心。发送完毕以后,对写事件的取消关注。
        void EventMod(int sock, bool isread, bool iswrite)
        {
            epoll_event ev;
            ev.events = (EPOLLET | (isread ? EPOLLIN : 0) | (iswrite ? EPOLLOUT : 0));
            ev.data.fd = sock;

            if (epoll_ctl(epfd_, EPOLL_CTL_MOD, sock, &ev) == 0)
            {
                cout << "EventMod success!..." << endl;
            }
        }

        bool Is_Exits(int sock)//判断fd是否还在。
        {
            auto iter = react_mp.find(sock);
            return iter == react_mp.end() ? false : true;
        }

#define NUM 10
        //事件就绪时的事件派发
        void dispather(int &timeout)
        {
            epoll_event epev[NUM];
            int n = epoll_wait(epfd_, epev, NUM, timeout);//检测
            for (size_t i = 0; i < n; i++)
            {
                // cout << "epoll_wait ready...!" << endl;
                uint32_t events1 = epev[i].events;
                int sock1 = epev[i].data.fd;

                //所有的异常交给sock的错误回调
                if (events1 & EPOLLERR)
                {
                    events1 |= (EPOLLIN | EPOLLOUT);
                }
                if (events1 & EPOLLHUP)//客户挂断
                {
                    events1 |= (EPOLLIN | EPOLLOUT);
                }
                if ((Is_Exits(sock1)) && (events1 & EPOLLIN))
                    if (react_mp[sock1].read_hand)
                        react_mp[sock1].read_hand(react_mp[sock1]);

                if ((Is_Exits(sock1)) && events1 & EPOLLOUT)
                    if (react_mp[sock1].write_hand)
                        react_mp[sock1].write_hand(react_mp[sock1]);
            }
        }
    };

}

epoll。cc

申请一个反应堆核心。创建绑定套接字。该文件相关设施的配置。堆核心的创建,添加、循环检测。
最开始添加的是监听套接字相关。

#include "epoll.hpp"
#include "sock.hpp"
#include "Accept.hpp"
using namespace ns_sock;
using namespace ns_reactor;

int main()
{
    //动态开辟一个反应堆核心 
    reactor_sv *re_sv = new reactor_sv;

    //创建监听套接字
    int listen_sock_ = sock_package::SockCreate();
    sock_package::Bind(listen_sock_, 8080);
    sock_package::Listen(listen_sock_);
    sock_package::set_noblock(listen_sock_);

    //创建一个sock相关的对象
    sock_pack sk;
    sk.sock_fd_ = listen_sock_;
    sk.r = re_sv;
    sk.sock_events_handler(accpt_listen, nullptr, nullptr);

    //堆核心初始化,添加需要关心的套接字文件以及读写事件
    re_sv->epfd_create();
    re_sv->epll_add(sk, EPOLLIN | EPOLLOUT);
    int time1 = 1000;
    //cout << "开始循环服务 " << endl;
    while (true)
    {
        //循环检测就绪队列是否有事件就绪
        //sleep(2);
        re_sv->dispather(time1);
    }
    // re_sv->dispather(time1);
    return 0;
}

Accept。hpp连接器

有连接到来时,添加进epoll中,设置新连接相关属性。

#pragma once

#include "sock.hpp"
#include "epoll.hpp"
#include "Awe_call.hpp"

using namespace ns_sock;
using namespace ns_reactor;

void accpt_listen(sock_pack &sk)
{
    while (true)
    {
        //cout << "获取客户套接字..." << sk.sock_fd_ << endl;
        sockaddr_in peer;
        socklen_t len = sizeof(peer);

        int sock1 = accept(sk.sock_fd_, (sockaddr *)&peer, &len);
        if (sock1 > 0) //新连接
        {
            sock_pack new_sk;
            sock_package::set_noblock(sock1);
            new_sk.sock_fd_ = sock1;
            new_sk.r = sk.r;
            new_sk.sock_events_handler(read_call, write_call, error_call);

            (sk.r)->epll_add(new_sk, EPOLLIN | EPOLLET);
            cout<<"添加的套接字:..."<<sock1<<endl;
        }
        else //全局错误码被设置
        {
            if (errno == EINTR)
            {
                continue; //被信号中断,并不代表底层没有新连接。
            }
            else if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                //错误码这样的是因为 底层没有链接了。
                break;
            }
            else
            {
                //真错了
                cerr << "accept error..." << endl;
                continue;
            }
        }
    }
}

读事件就绪回调

按照协议对接收到的报文进行读取。模拟协议:100+100X120+120X;X是报文之间的分隔符;网络计算器。这里只考虑加法。
一:一个函数只负责专心读。从网络sock中读取到自己的输入缓冲区中。
二:一个函数对批量读取到的数据进行粘包问题处理,分成多个报文。
三:再对单个报文进行分析。
四:分析完毕,计算结果。
五:发送响应给客户端;实际是写入到自己的输出缓冲区。
六:自己文件关心事件修改:关心写入事件(send)。
出错则调用己方的错误回调。

专心负责读。
static int ReadHelp(sock_pack &sk)
{
    while (true)
    {
        char buff[1024];
        ssize_t s = recv(sk.sock_fd_, buff, sizeof(buff) - 1, 0);
        if (s > 0)
        {
            buff[s] = 0;
            sk.inbuffer += buff;
        }
        else if (s < 0)
        {
            if (errno == EINTR) //信号错误
            {
                continue;
            }
            else if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                return 0; //把数据读完了。
            }
            else
            {
                return -1;
            }
        }
        else // == 0  客户端下线
        {
            return -1;
        }
    }
}

// 1+1 分析单个报文 x,y,op、输入输出参数
static bool AnalyseData(string &scut, int *x, int *y, char *op)
{
    auto pos = scut.find("+");
    if (pos == string::npos)
    {
        return false;
    }
    *x = atoi(scut.substr(0, pos).c_str()); //[)不包含pos
    *y = atoi(scut.substr(pos + 1).c_str());
    *op = '+';
    return true;
}
//缓冲区数据分成多个报文   
//输入输出参数:vector<string> *out 
static void StringUncouple(string &in, vector<string> *out, const string &op)
{
    while (true)
    {
        auto pos = in.find(op);
        if (pos == string::npos)
        {
            break;
        }
        string sub = in.substr(0, pos);
        out->push_back(sub);
        in.erase(0, pos + op.size());
    }
}

void read_call(sock_pack &sk)
{
    // cout<<"ready to read.."<<endl;
    //专心负责读。出错了你告诉我。我使用错误回调。-1 失败 0 完全读完
    if (ReadHelp(sk) < 0)
    {
        if (sk.error_hand)
            sk.error_hand(sk);
        return;
    }
    //缓冲区数据分成多个报文    1+1X2+3X...x切割符
    vector<string> msgcut;
    StringUncouple(sk.inbuffer, &msgcut, "X");
    //分析单个报文
    for (auto &scut : msgcut)
    {
    //反序列化
    	// Task t(sk.sock_, scut); 也可以采用线程池的方法
        // ThreadPool.push(t);
        int x = 0;
        int y = 0;
        char op = 0;
        if (!AnalyseData(scut, &x, &y, &op))
        {
            continue;
        }
        //计算结果。
        int result = 0;
        switch (op)
        {
        case '+':
            result = x + y;
            break;
        default:
            break;
        }
        //构建单个响应报文
        string res = scut;
        res += "=";
        res += to_string(result);
        res += "X"; //添加分隔符

        //发送
        sk.outbuffer += res;
        //设置写事件关心
        (sk.r)->EventMod(sk.sock_fd_, true, true);
    }
}

写事件就绪回调

发送时区分:自己的输出缓冲区是否全部发送完毕,是则清空;取消对写入事件的关心。
因为TCP发送缓冲区导致的,己方发送缓冲区未发送完毕,则清空己方书输出缓冲区已经发送的内容。还需持续关心写事件。
出错则调用己方的错误回调。

static int SendHelp(int sock,string& wrt_string)
{
    int total =0;//一共发了多少个字节
    int size= wrt_string.size();
    //防止协议缓冲区满了,而自己缓冲区还有数据,删除已经发送的。
    const char* start = wrt_string.c_str();
    while (true)
    {
        //实际上发送的字节数是取决于返回值
        ssize_t s = send(sock,start+total,size-total,0);
        if(s>0)
        {
            total+=s;
            if(total == size)
            {
                wrt_string.clear();
                return 1;
            }
        }
        else //错误的几种情况
        {
            if(errno == EINTR)//信号错误
            {
                continue;
            }
            else if(errno == EAGAIN || errno == EWOULDBLOCK)
            {
                //TCP发送缓冲区满了。清除已经发送的了。
                wrt_string.erase(0,total);
                return 0;
            }
            else
            {
                return -1;
            }           
        } 
    }
}

void write_call(sock_pack &sk)
{
     //cout << "ready to write.." << endl;
     //-1,失败;1,发送完毕;0,未完全发送。
    int ret = SendHelp(sk.sock_fd_, sk.outbuffer);
    if (ret == -1)
    {
        if (sk.error_hand)
            sk.error_hand(sk);
    }
    else if(ret ==1)//发送完毕
    {
        (sk.r)->EventMod(sk.sock_fd_,true,false);
    }
    else if(ret == 0)//需持续关心写事件就绪,一旦就绪就可以从自己缓冲区发。
    {
        (sk.r)->EventMod(sk.sock_fd_,true,true);
    }
}

错误事件回调

调用核心中的Epoll_Del函数。

void error_call(sock_pack &sk)
{
    cout << " error_call..." << endl;
    sk.r->Epoll_Del(sk.sock_fd_);
}

总结与拓展

  • substr():前必后开[),返回起始位置到end参数(或直到字符串的结尾)的string对象。
  • vector:push_back()、auto for...,容器的find使用。
  • 有一个意义清晰的变量命名和函数名。函数名首字母大写,下划线,首字母大写…Acceptor_Connector
  • 根据接受、发送错误时;全局错误码的设置。采取对应的处理方式。一般情况是:信号错误继续;网络套接字接受区的数据读完了,返回;发送缓冲区满了,清楚己方输出缓冲区的数据。
  • 线程池、任务队列、多进程、多线程的考虑;STL容器及其功能的熟悉。

拓展

  • 格言:All problems in computer science can be solved by another level of indirection.。计算机科学中的所有问题都可以通过增加一个间接层来解决。—David Wheeler(剑桥大学计算机科学教授)
  • epoll的惊群问题
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值