>>>Acceptor用于accept(2)接受TCP连接。
>>>Acceptor的数据成员包括Socket、Channel。
>>>Acceptor的socket是listening socket(即server socket)。
>>>Channel用于观察此socket的readable事件,并Acceptor::handleRead(),后者调用accept(2)来接受连接,并回调用户callback。
不过,Acceptor类在上层应用程序中我们不直接使用,而是把它封装作为TcpServer的成员。
Acceptor的主要成员有:
private:
void handleRead(); //可读回调函数
EventLoop* loop_; //loop指针
Socket acceptSocket_; //监听套接字
Channel acceptChannel_; //和监听套接字绑定的通道
NewConnectionCallback newConnectionCallback_; //一旦有新连接发生执行的回调函数
bool listenning_; //acceptChannel所处的EventLoop是否处于监听状态
int idleFd_; //用来解决文件描述符过多引起电平触发不断触发的问题,后文会有解释
然后看它的构造函数:
Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
: loop_(loop),
acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())), //创建监听套接字
acceptChannel_(loop, acceptSocket_.fd()), //绑定Channel和socketfd
listenning_(false),
idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC)) //预先准备一个空闲文件描述符
{
assert(idleFd_ >= 0); //>=0
acceptSocket_.setReuseAddr(true);
acceptSocket_.setReusePort(reuseport);
acceptSocket_.bindAddress(listenAddr);
acceptChannel_.setReadCallback(
boost::bind(&Acceptor::handleRead, this)); //设置Channel的fd的读回调函数
}
Acceptor通过一个EventLoop指针,一个InetAddress对象,以及一个是否复用端口的标志构造,构造函数中会创建监听套接字并绑定响应的Channel,但是并不会启动关注Channel的可读事件,这是listen()中应该做的。并且还创建了一个指向空的文件描述符,这待会会说。
创建完套接字我们可以设置新连接来到时的回调函数:
void setNewConnectionCallback(const NewConnectionCallback& cb)
{ newConnectionCallback_ = cb; }
那它是什么时候执行呢,是触发监听套接字可读事件的时候:
void Acceptor::handleRead() //读回调函数
{
loop_->assertInLoopThread();
InetAddress peerAddr;
//FIXME loop until no more
int connfd = acceptSocket_.accept(&peerAddr); //获得已连接套接字
if (connfd >= 0)
{
// string hostport = peerAddr.toIpPort();
// LOG_TRACE << "Accepts of " << hostport;
if (newConnectionCallback_) //如果设置了新连接回调函数
{
newConnectionCallback_(connfd, peerAddr); //那么就执行它
}
else
{
sockets::close(connfd); //否则就关闭,sockets是全局函数
}
}
else //如果<0失败了?
{
LOG_SYSERR << "in Acceptor::handleRead";
// Read the section named "The special problem of
// accept()ing when you can't" in libev's doc.
// By Marc Lehmann, author of livev.
if (errno == EMFILE) //太多的文件描述符
{
::close(idleFd_); //先关闭空闲文件描述符,让它能够接收。否则由于采用电平触发,不接收会一直触发。
idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL); //那就腾出一个文件描述符,用来accept
::close(idleFd_); //accept之后再关闭
idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC); //然后再打开成默认方式
}
}
}
在监听套接字可读事件触发时,我们会调用accept接受连接。如果此时注册过回调函数,就执行它。如果没有就直接关闭!
另一方面,如果已用文件描述符过多,accept会返回-1,我们构造函数中注册的idleFd_就派上用场了。当前文件描述符过多,无法接收新的连接。但是由于我们采用LT模式,如果无法接收,可读事件会一直触发。那么在这个地方的处理机制就是,关掉之前创建的空心啊idleFd_,然后去accept让这个事件不会一直触发,然后再关掉该文件描述符,重新将它设置为空文件描述符。
这种机制可以让网络库在处理连接过多,文件描述符不够用时,不至于因为LT模式一直触发而产生坏的影响。
设置完回调函数,就可以listen()了,这也是Acceptor类的最终任务:
//Acceptor是在listen中开始关注可读事件
void Acceptor::listen()
{
loop_->assertInLoopThread();
listenning_ = true;
acceptSocket_.listen();
acceptChannel_.enableReading(); //关注可读事件
}
enableReading()不必多说,会在Poller中开始关注Channel对应fd的可读时间,所以当有连接到来时,可读事件发生,我们就可以愉快的调用上面的handleRead()函数,进而调用newConnectionCallback()函数了。而newConnectionCallback()函数是客端负责自定义的,所以客端就可以在一个新连接到来时,随心所欲的做自己想做的事情了。
下面是所有源码分析,后面会给出示例 :
头文件:
/// Acceptor of incoming TCP connections.
///
class Acceptor : boost::noncopyable
{
public:
typedef boost::function NewConnectionCallback;
Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport);
~Acceptor();
void setNewConnectionCallback(const NewConnectionCallback& cb)
{ newConnectionCallback_ = cb; }
bool listenning() const { return listenning_; }
void listen();
private:
void handleRead(); //可读回调函数
EventLoop* loop_;
Socket acceptSocket_;
Channel acceptChannel_;
NewConnectionCallback newConnectionCallback_;
bool listenning_; //acceptChannel所处的EventLoop是否处于监听状态
int idleFd_;
};
实现文件:
Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
: loop_(loop),
acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())), //创建监听套接字
acceptChannel_(loop, acceptSocket_.fd()), //绑定Channel和socketfd
listenning_(false),
idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC)) //预先准备一个空闲文件描述符
{
assert(idleFd_ >= 0); //>=0
acceptSocket_.setReuseAddr(true);
acceptSocket_.setReusePort(reuseport);
acceptSocket_.bindAddress(listenAddr);
acceptChannel_.setReadCallback(
boost::bind(&Acceptor::handleRead, this)); //设置Channel的fd的读回调函数
}
Acceptor::~Acceptor()
{
acceptChannel_.disableAll(); //需要把所有事件都disable掉,才能调用remove函数
acceptChannel_.remove();
::close(idleFd_); //关闭文件描述符
}
//Acceptor是在listen中开始关注可读事件
void Acceptor::listen()
{
loop_->assertInLoopThread();
listenning_ = true;
acceptSocket_.listen();
acceptChannel_.enableReading(); //关注可读事件
}
void Acceptor::handleRead() //读回调函数
{
loop_->assertInLoopThread();
InetAddress peerAddr;
//FIXME loop until no more
int connfd = acceptSocket_.accept(&peerAddr); //获得已连接套接字
if (connfd >= 0)
{
// string hostport = peerAddr.toIpPort();
// LOG_TRACE << "Accepts of " << hostport;
if (newConnectionCallback_) //如果设置了新临街回调函数
{
newConnectionCallback_(connfd, peerAddr); //那么就执行它
}
else
{
sockets::close(connfd); //否则就关闭
}
}
else //如果<0失败了?
{
LOG_SYSERR << "in Acceptor::handleRead";
// Read the section named "The special problem of
// accept()ing when you can't" in libev's doc.
// By Marc Lehmann, author of livev.
if (errno == EMFILE) //太多的文件描述符
{
::close(idleFd_); //先关闭空闲文件描述符,让它能够接收。否则由于采用电平触发,不接收会一直触发。
idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL); //那就腾出一个文件描述符,用来accept
::close(idleFd_); //accept之后再关闭
idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC); //然后再打开成默认方式
}
}
}
一个典型的例子:
#include
#include
#include
#include
#include
using namespace muduo;
using namespace muduo::net;
void newConnection(int sockfd, const InetAddress& peerAddr)
{
printf("newConnection() : accepted a new connection from %s\n",
peerAddr.toIpPort().c_str());
::write(sockfd, "How are you?\n", 13);
sockets::close(sockfd);
}
int main()
{
printf("main(): pid = %d\n", getpid());
InetAddress listenAddr(8888);
EventLoop loop;
Acceptor acceptor(&loop, listenAddr, true);
acceptor.setNewConnectionCallback(newConnection);
acceptor.listen();
loop.loop();
}
输出: