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 <functional>

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 <sys/types.h>    
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>


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…)一下监听套接字以便再次触发。也就是重新注册监听套接字的可读事件(当然没有上面的方案好,但也是一种思路)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贪睡的蜗牛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值