• C++11重写muduo网络库6——Acceptor模块


    6.1Acceptor说明

    • Acceptor工作在mainReactor,用于监听新用户的连接,将与客户端通信的fd打包成Channel,muduo采用轮询算法找一个subloop,将其唤醒,把打包好的Channel给subloop。
    • Acceptor 是 TcpServer的一部分, 用于接受Tcp连接,主要完成服务端的 create,bind, listen,accept这几个阶段。
    • Acceptor类在上层应用程序中我们不直接使用,而是把它封装作为TcpServer的成员,生命期由后者控制。

    6.2 主要成员变量说明

    在这里插入图片描述
    在这里插入图片描述
    这个回调函数首先将新用户的地址保存在peerAddr,并获取和客户端通信的fd,然后传入Acceptor::newConnetionCallback_并执行,而这个回调就是TcpServer对象的成员acceptor_提前调用Acceptor::setNewConnectionCallback设置的
    在这里插入图片描述

    6.3 源码

    Acceptor.h

    #pragma once
    #include "noncopyable.h"
    #include "Socket.h"
    #include "Channel.h"
    
    #include 
    
    class EventLoop;
    class InetAddress;
    
    class Acceptor : noncopyable
    {
    public:
        using NewConnectionCallback = std::function<void(int sockfd, const InetAddress&)>;
    
        //构造函数,完成过create,bind这两个过程,并在channel中注册监听套接字的可读事件回调hangleRead。
        Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport);
        ~Acceptor();
    
        void setNewConnectionCallback(const NewConnectionCallback &cb) 
        {
            newConnectionCallback_ = cb;
        }
    
        bool listenning() const { return listenning_; }
    
        //执行listen这个过程,同时让channel监听可读事件(原理就是往epoll上挂在监听套接字的可读事件)
        void listen();
    private:
        //处理监听socket的可读事件,即新连接到来
        void handleRead();
        
        //loop 主线程循环,在主线程中进行监听连接
        EventLoop *loop_; // 通过事件循环监听listenfd,也就是mainLoop
        Socket acceptSocket_; //封装了listenfd,用于监听新用户的连接
        Channel acceptChannel_;//封装了listenfd,以及感兴趣的事件events和发生的事件revents
     
        //如果一个客户端连接成功,Acceptor返回一个Channel给TcpServer,TcpServer会通过轮询唤醒一个subLoop,
        //并把新客户端的Channel给subLoop,而这些事情都是交给一个回调函数做的,即newConnectionCallback_做的
        NewConnectionCallback newConnectionCallback_;
        bool listenning_;//监听套接字是否处于监听状态
    };
    

    Acceptor.cc

    #include "Acceptor.h"
    #include "Logger.h"
    #include "InetAddress.h"
    
    #include     
    #include 
    #include 
    #include 
    
    
    static int createNonblocking()
    {
        //创建listenfd
        int sockfd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
        if (sockfd < 0) 
        {
            LOG_FATAL("%s:%s:%d listen socket create err:%d \n", __FILE__, __FUNCTION__, __LINE__, errno);
        }
    }
    //完成过create,bind这两个过程,并在channel中注册监听套接字的可读事件回调hangleRead。
    Acceptor::Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport)
        : loop_(loop)
        , acceptSocket_(createNonblocking()) // 创建socket,socket的构造函数需要一个int参数。这里可以设置堵塞状态
        , acceptChannel_(loop, acceptSocket_.fd())
        , listenning_(false)
    {
        acceptSocket_.setReuseAddr(true);
        acceptSocket_.setReusePort(true);
        acceptSocket_.bindAddress(listenAddr); // bind
    //当新用户连接后需要执行一个回调,这个回调会和用户连接的fd包成channel然后交给subloop
    //下面就是注册了listenfd的cahnnel发生读事件后需要执行的回调函数,Acceptor只管理封装了listenfd的Channel
        acceptChannel_.setReadCallback(std::bind(&Acceptor::handleRead, this));
    }
    
    Acceptor::~Acceptor()
    {
        acceptChannel_.disableAll();//把通道acceptChannel_上所有事件都disableAll()掉
        acceptChannel_.remove();
    }
    
        //主要调用Socket::listen(),把listening_置为true,通道也开始监听读事件
    void Acceptor::listen()
    {
        listenning_ = true;
        acceptSocket_.listen(); // 调用底层的listen
        //acceptChannel_关注套接字的可读事件
        acceptChannel_.enableReading(); // 将acceptChannel_注册到Poller,或者在Poller更新自己感兴趣的事件 
    }
    
    // listenfd有事件发生了,就是有新用户连接了
    // 监听socket有事件发生
    // 调用Socket::accept()接受连接,连接成功的话,调用newConnectionCallback_()回调函数,处理这个连接,没有回调函数的话就直接关闭这个连接
    // 如果连接失败,失败原因是文件描述符太多的话
    void Acceptor::handleRead()
    {
        InetAddress peerAddr;//客户端地址
        int connfd = acceptSocket_.accept(&peerAddr);
        if (connfd >= 0)
        {
            if (newConnectionCallback_)
            {
                newConnectionCallback_(connfd, peerAddr); // 轮询找到subLoop,唤醒,分发当前的新客户端的Channel
            }
            else
            {
                //如果没有设置新用户连接的回调操作就会关闭连接
                ::close(connfd);
            }
        }
        else
        {
            LOG_ERROR("%s:%s:%d accept err:%d \n", __FILE__, __FUNCTION__, __LINE__, errno);
            if (errno == EMFILE)//打开的fd达到上限
            {
                LOG_ERROR("%s:%s:%d sockfd reached limit! \n", __FILE__, __FUNCTION__, __LINE__);
            }
        }
    }
    

    6.4 Accept出现EMFILE错误问题分析

    • Listen阶段把所有已经完成三次握手的tcp连接放到全连接队列中,并通知应用层accept(),这时候可能出现EMFILE问题
    • LT模式的EMFILE问题:由于 每次 accept 都失败了,相当于 listenfd 上的可读事件没有处理,epoll 会不停的触发 listenfd 上的可读事件,应用层也就会不停的调用 accept,然后又出现 accept 调用失败,如此这般不停的执行无效的循环,白白浪费了CPU的资源。
    • ET模式的EMFILE问题:第一次accept失败后,由于没有处理可读事件,epoll不会再通知listenfd的可读事件了,后面新的连接到来也就不会通知了,也就无法接收新的客户端连接了。
    解决方案:muduo的Acceptor handleRead做法
    • 事先准备一个空闲的文件描述符 idlefd,相当于先占一个"坑"位
    • 调用 close 关闭 idlefd,关闭之后,进程就会获得一个文件描述符名额
    • 再次调用 accept 函数, 此时就会返回新的文件描述符 clientfd, 立刻调用 close 函数,关闭 clientfd
    • 重新创建空闲文件描述符 idlefd,重新占领 “坑” 位,再出现这种情况的时候又可以使用

    还有一种解决方案:ET模式下通常的做法是发生EMFILE之后epoll_ctl(…MOD…)一下监听套接字以便再次触发。也就是重新注册监听套接字的可读事件(当然没有上面的方案好,但也是一种思路)

  • 相关阅读:
    透视虎牙斗鱼三季报:游戏直播在各自“求变”中见分晓
    vscode篇---设置python路径,项目主目录和添加参数
    2023十大精选炒伦敦金软件最新排名榜单汇总
    2020年亚太杯APMCM数学建模大赛B题美国总统的经济影响分析求解全过程文档及程序
    mysql中varchar长度为多少
    ChatGpt介绍和国产ChatGpt对比
    Tomcat发布WebService服务
    Java学习之常见易错点总结--第一期
    【分享】“飞书自建“在集简云平台集成应用的常见问题与解决方案
    nodejs+vue+elementui毕业设计选题系统express vscode
  • 原文地址:https://blog.csdn.net/baidu_41553551/article/details/127017744