
引言
在网络编程中,服务器端程序需要能够监听客户端的连接请求并进行处理。Acceptor
类在这个过程中扮演着至关重要的角色,它负责创建监听套接字、绑定地址、开始监听以及处理新的连接请求。在本文中,我们将详细剖析手写 muduo 网络库中的Acceptor
类,探讨其实现原理和工作流程。
整体功能概述
Acceptor
类的主要功能是在指定的地址和端口上监听客户端的连接请求,并在有新的连接到来时调用相应的回调函数进行处理。它封装了套接字的创建、绑定、监听以及接受连接等操作,使得上层代码可以更方便地处理网络连接。
代码结构分析
头文件Acceptor.h
cpp
#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 &)>;
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_;
};
代码解释
- 类型定义 :
NewConnectionCallback
:这是一个函数对象类型,用于处理新的连接。当有新的客户端连接到来时,会调用这个回调函数,并传递新连接的套接字描述符和客户端地址。
- 构造函数 :
Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport)
:接受一个EventLoop
指针、一个InetAddress
对象和一个布尔值reuseport
作为参数。EventLoop
用于事件循环,InetAddress
表示监听的地址和端口,reuseport
用于设置是否重用端口。
- 析构函数 :
~Acceptor()
:负责清理资源,如禁用Channel
的所有事件并移除Channel
。
- 成员函数 :
setNewConnectionCallback
:用于设置新连接的回调函数。listenning
:返回当前是否正在监听。listen
:开始监听客户端的连接请求。
- 私有成员函数 :
handleRead
:处理读事件,当有新的连接到来时会触发该函数。
- 私有成员变量 :
loop_
:指向EventLoop
的指针。acceptSocket_
:用于监听的套接字对象。acceptChannel_
:用于处理套接字事件的Channel
对象。NewConnectionCallback_
:新连接的回调函数。listenning_
:表示当前是否正在监听的布尔值。
源文件Acceptor.cpp
cpp
#include "Acceptor.h"
#include "LogStream.h"
#include "InetAddress.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
static int createNonblocking()
{
int sockfd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP);
if (sockfd < 0)
{
LOG_ERROR << "listen socket create err: " << errno;
exit(-1);
}
return sockfd;
}
Acceptor::Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport)
: loop_(loop)
, acceptSocket_(createNonblocking())
, acceptChannel_(loop, acceptSocket_.fd())
, listenning_(false)
{
acceptSocket_.setReuseAddr(true);
acceptSocket_.setReusePort(true);
acceptSocket_.bindAddress(listenAddr);
acceptChannel_.setReadCallback(
std::bind(&Acceptor::handleRead, this));
}
Acceptor::~Acceptor()
{
acceptChannel_.disableAll();
acceptChannel_.remove();
}
void Acceptor::listen()
{
listenning_ = true;
acceptSocket_.listen();
acceptChannel_.enableReading();
}
void Acceptor::handleRead()
{
InetAddress peerAddr;
int connfd = acceptSocket_.accept(&peerAddr);
if (connfd >= 0)
{
if (NewConnectionCallback_)
{
NewConnectionCallback_(connfd, peerAddr);
}
else
{
::close(connfd);
}
}
else
{
LOG_ERROR << "accept err: " << errno;
if (errno == EMFILE)
{
LOG_ERROR << "sockfd reached limit ";
}
}
}
代码解释
- 辅助函数
createNonblocking
:- 该函数用于创建一个非阻塞的套接字。使用
::socket
函数创建一个AF_INET
类型的流式套接字,并设置为非阻塞和CLOEXEC
模式。如果创建失败,会输出错误日志并退出程序。
- 该函数用于创建一个非阻塞的套接字。使用
- 构造函数
Acceptor::Acceptor
:- 调用
createNonblocking
函数创建一个非阻塞的套接字。 - 设置套接字的地址重用和端口重用选项。
- 将套接字绑定到指定的地址和端口。
- 为
acceptChannel_
设置读事件的回调函数,当有新的连接到来时会调用handleRead
函数。
- 调用
- 析构函数
Acceptor::~Acceptor
:- 禁用
acceptChannel_
的所有事件,并从EventLoop
中移除acceptChannel_
。
- 禁用
- 成员函数
Acceptor::listen
:- 将
listenning_
标志设置为true
,表示开始监听。 - 调用
acceptSocket_.listen()
开始监听客户端的连接请求。 - 启用
acceptChannel_
的读事件,以便在有新的连接到来时触发handleRead
函数。
- 将
- 成员函数
Acceptor::handleRead
:- 调用
acceptSocket_.accept
函数接受新的连接,并获取客户端的地址。 - 如果接受连接成功,且设置了新连接的回调函数,则调用该回调函数处理新连接;否则,关闭新连接的套接字。
- 如果接受连接失败,输出错误日志,并在
errno
为EMFILE
时提示套接字描述符达到上限。
- 调用
工作流程总结
- 创建
Acceptor
对象 :在服务器启动时,创建一个Acceptor
对象,并传入EventLoop
指针、监听地址和端口信息。 - 设置回调函数 :调用
setNewConnectionCallback
函数设置新连接的回调函数。 - 开始监听 :调用
listen
函数开始监听客户端的连接请求。 - 处理新连接 :当有新的连接到来时,
acceptChannel_
会触发读事件,调用handleRead
函数。在handleRead
函数中,接受新的连接,并调用预先设置的回调函数处理新连接。
总结
Acceptor
类通过封装套接字的创建、绑定、监听和接受连接等操作,为上层代码提供了一个简单易用的接口来处理客户端的连接请求。通过使用EventLoop
和Channel
,可以实现高效的事件驱动的网络编程。在实际应用中,我们可以根据需要修改NewConnectionCallback
函数,以实现不同的业务逻辑。
希望本文对你理解Acceptor
类的实现原理和工作流程有所帮助。在后续的文章中,我们将继续深入探讨手写 muduo 网络库的其他部分。