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