接着之前我们剖析的muduo库三大核心组件Channel类,Poller/EpollPoller类,EventLoop类,我们接下来继续看muduo库中的其他类,这一篇,我们先来介绍Acceptor类。
Acceptor类
主要功能
Acceptor类用于创建套接字,设置相关处理方法,调用listen函数,接受(accept)客户端连接, 接受新用户连接并分发连接给SubReactor(SubEventLoop)。注意: 它只在muduo网络库内部的TcpServer使用,由TcpServer控制它的生命期。
重要成员变量
EventLoop *loop_; //Acceptor用的就是用户定义的那个baseloop_,也就是mainloop
Socket acceptSocket_;
Channel acceptChannel_;
NewConnectionCallback newConnetionCallback_;
bool listenning_;
loop_
监听套接字的fd由哪个EventLoop负责循环监听以及处理相应事件,其实这个EventLoop就是main EventLoop。acceptSocket_
服务器监听套接字的文件描述符acceptChannel_
把acceptSocket_及其感兴趣事件和事件对应的处理函数进行封装。newConnetionCallback_
这个是最重要的一个成员了,它的类型是using NewConnectionCallback = std::function<void(int sockfd, const InetAddress&)>;
,在TcpServer构造函数中通过acceptor_->setNewConnetionCallback(std::bind(&TcpServer::newConnection, this, std::placeholders::_1,std::placeholders::_2));
将TcpServer::newConnection
函数注册给了这个成员变量。这个TcpServer::newConnection
函数的功能是通过轮询EventLoop *ioLoop = threadPool_->getNextLoop();
选择一个subEventLoop,并把已经接受的连接分发给这个subEventLoop。listenning_
是一个标志位
重要成员函数
- 首先来提一下构造函数
Acceptor::Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool resuseport)
: loop_(loop)
, acceptSocket_(createNonblocking()) //创建sock
, acceptChannel_(loop, acceptSocket_.fd()) //封装成channel
, listenning_(false)
{
acceptSocket_.setReuseAddr(true); //更改TCP选项
acceptSocket_.setReusePort(true);
acceptSocket_.bindAddress(listenAddr); //绑定套接字
acceptChannel_.setReadCallback(std::bind(&Acceptor::headleRead,this));
}
1)要知道Acceptor是TcpServer管理的,在构造函数中,首先是TcpServer给的loop,这里实际就是mainloop;
2)其次通过createNonblocking
中利用socket
创建了一个套接字,这个socket是监听套接字;一个服务器通常只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。而内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字在accept()
函数中,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭;
3)将loop和socket封装成channel
4)给listenning_
一个初始化
5)通过调用Socket中的setReuseAddr
、setReusePort
更改了一些选项。
6)绑定了套接字
7)绑定一个回调,用于新用户的连接
listen( )
函数
void Acceptor::listen()
{
listenning_ = true;
acceptSocket_.listen(); //listen
acceptChannel_.enableReading(); //acceptChannel_=> Poller
}
在这里,我们可以看出实际上调用了Socket
的listen( )
,然后底层调用了系统的listen( )
,开启对acceptSocket_的监听同时将acceptChannel及其感兴趣事件(可读事件)注册到main EventLoop的事件监听器上。换言之就是让mainLoop事件监听器去监听acceptSocket_;
headleRead()
函数
void Acceptor::headleRead()
{
InetAddress peerAddr;
int connfd = acceptSocket_.accept(&peerAddr);
if(connfd >= 0)
{
if (newConnetionCallback_)
{
newConnetionCallback_(connfd,peerAddr);//轮询找到SUBLOOP唤醒,分发当前的新客户端的Channel
}
else
{
::close(connfd);
}
}
else
{
LOG_ERROR("%s:%s:%d accept err:%d \n", __FILE__,__FUNCTION__,__LINE__,errno);
if (errno == ENFILE)
{
LOG_ERROR("%s:%s:%d sockfd reached limit! \n", __FILE__,__FUNCTION__,__LINE__);
}
}
}
这里主要是建立连接,通过调用Socket的accept
函数,底层调用系统的accept
函数,返回一个已连接的socket描述字,这样连接就建立了。同时内部还调用了成员变量newConnectionCallback_保存的函数,当mainLoop监听到acceptChannel_
上发生了可读事件时(新用户连接事件),就是调用这个handleRead( )
方法,内部调用newConnetionCallback_
,也就是TcpServer设置的一个回调函数setNewConnetionCallback
,绑定了TcpServer::newConnection
,通过 轮询算法,选择一个subloop,分发当前的新客户端的Channel,并且绑定了一些回调。
需要注意的是:在muduo库的源码中,实际上还创建了一个空的文件描述符idleFd_
这个思想也是很巧妙的,在调用accept的过程中,如果已用文件描述符过多,accept会返回-1,构造函数中注册的idleFd_就派上用场了。当前文件描述符过多,无法接收新的连接。但是由于我们采用LT模式,如果无法接收,可读事件会一直触发。那么在这个地方的处理机制就是,关掉之前创建的空的idleFd_,然后去accept,这样进行了连接,让这个事件不会一直触发,然后再关掉该文件描述符,重新将它设置为空文件描述符。这样就优雅的解决 EMFIFE 问题。
代码地址:https://github.com/Cheeron955/mymuduo/tree/master