目录
[Acceptor 类](#Acceptor 类)
在上一节中,我们实现了 Reactor模式 的核心结构,创建了一个 Server
类,并且注意到许多逻辑集中在这个类中。一个简单的服务器程序主要由两部分组成:一部分是通过 accept(2)
函数与客户端建立连接 ,另一部分是处理与已经连接的客户端之间的服务(即 TCP 连接)。
为了提高代码的清晰度和可维护性,我们可以将 Server
类的这些功能进行分离。首先,我们可以创建一个新的类,称为 Acceptor,专门处理建立连接的逻辑。
Acceptor 类
Acceptor
类的主要职责是接收新的 TCP 连接。它是一个内部类,外部无法直接访问,只能在 Server
类内部使用,其生命周期由 Server
类控制。
这个类应该包含以下几个重要成员:
- sockfd: 该成员是服务器用来监听的文件描述符(serverFd)。
- channel : 此成员用于将其添加到 epoll 实例中。当该文件描述符被激活时,相关的
handleEvent()
方法将被调用,从而执行对应的回调函数。这个回调函数的作用是处理来自客户端的连接请求。
通过将连接建立的逻辑独立到 Acceptor
类中,我们可以使 Server
类更加简洁,同时提高代码的可维护性和可读性。
cpp
class Acceptor {
public:
// 定义一个回调类型,函数签名为接受一个整数类型的套接字描述符
using NewConnectionCallback = std::function<void(int sockfd)>;
public:
// 构造函数:接受一个监听地址和事件循环指针
Acceptor(const InetAddr& listenAddr, EventLoop* eventloop);
~Acceptor(); // 析构函数
// 设置新连接的回调函数
void setNewconnectionCallback(const NewConnectionCallback& cb) {
newConnectionCallback_ = cb; // 绑定回调函数,供后续使用
}
// 启动监听
void listen();
private:
// 处理可读事件的私有方法
void handleRead();
EventLoop* loop_; // 指向事件循环对象的指针
Socket acceptSocket_; // 服务器的监听套接字(fd),用于接收新连接
Channel acceptChannel_; // 与 epoll 实例交互的 Channel 对象
NewConnectionCallback newConnectionCallback_; // 存储用户设置的新连接回调函数
bool listen_; // 记录是否正在监听
};
1.Acceptor类的构造函数
Acceptor的构造函数和Acceptor::listen()函数会调用socket(2),bind(2),listen(2)等Socket API,也即是把在第6节中Server的构造函数的实现(创建TCP服务端的传统步骤)放到Acceptor构造函数中来实现。
cpp
// Acceptor 类的构造函数
Acceptor::Acceptor(const InetAddr& listenAddr, EventLoop* eventloop)
: loop_(eventloop) // 初始化事件循环指针
, acceptSocket_(Socket()) // 创建 Socket 对象,表示监听套接字
, acceptChannel_(loop_, acceptSocket_.fd()) // 使用事件循环和监听套接字文件描述符创建 Channel 对象
, listen_(false) // 将监听状态标志初始化为 false
{
// 绑定监听地址,将指定的地址与监听套接字相关联
acceptSocket_.bind(listenAddr);
// 设置回调函数,handleRead() 方法将在监听的套接字可读时被调用
auto cb = [this]() { handleRead(); }; // 使用捕获的 `this` 指针来访问当前对象
acceptChannel_.SetReadCallback(cb); // 将回调函数设置到 acceptChannel 中
this->listen(); // 调用 listen 方法启动监听
}
// 启动监听函数
void Acceptor::listen()
{
// 开始监听连接请求,准备接受客户端的连接
acceptSocket_.listen();
// 启用可读事件,使 acceptChannel 在监听套接字可读时触发事件
acceptChannel_.enableReading();
}
在构造函数中设置好回调函数后,acceptChannel_**在可读的时候会回调Acceptor::handleRead(),**handleRead()内部会调用accept()来接受新连接,并回调newConnectionCallback_。
2.handleRead()的实现
cpp
// Acceptor 类中的 handleRead 方法
void Acceptor::handleRead() {
InetAddr peerAddr; // 定义一个 InetAddr 对象,用于存储连接的客户端地址信息
// 调用 accept() 方法接收新的连接,并将客户端地址信息存储在 peerAddr 中
int connfd = acceptSocket_.accept(&peerAddr);
// 检查接收到的文件描述符是否有效
if (connfd >= 0) {
// 如果存在用户设置的新连接回调函数
if (newConnectionCallback_) {
// 调用新连接回调函数,将新连接的文件描述符传递给它
newConnectionCallback_(connfd); // 这个回调函数是在 Server 构造函数中进行设置的
// 这将触发 Server::newConnection() 方法
}
}
}
// Server 类中的 newConnection 方法
void Server::newConnection(int sockfd) {
// 将新连接的套接字设置为非阻塞模式
setNonblock(sockfd);
// 创建一个新的 Channel 对象,关联新连接的文件描述符
Channel* channel = new Channel(loop_, sockfd); // 当前版本没有 delete,可能导致内存泄漏,之后版本会进行修改
// 定义一个回调函数,当事件发生时处理对应的 Channel
auto cb = [this, channel]() { handleEvent(channel); }; // 使用捕获的 this 和 channel
// 将定义的回调函数设置到 Channel 中,以便处理可读事件
channel->SetReadCallback(cb); // 设置回调
// 启用可读事件,开始监听此 Channel 的可读状态
channel->enableReading();
}
这里可能会出现一个疑惑:为什么要在Server构造函数中去设置newConnectionCallback_回调呢?
先来看看上一节的Server::newConnection(Socket* serv_sock)的实现
cpp
// 上一节的 Server::newConnection(Socket* serv_sock) 函数
void Server::newConnection(Socket* serv_sock) {
// 创建一个 InetAddr 对象,用于存储客户端地址信息
InetAddr cliaddr;
// 接受新的客户端连接,返回一个新的套接字,cliaddr 会被填充为客户端的地址
Socket* cli_socket = new Socket(serv_sock->accept(&cliaddr));
// 将新客户端连接的套接字设置为非阻塞模式
cli_socket->setNonblock();
// 创建一个新的 Channel 对象,传入事件循环和新连接的文件描述符
Channel* channel = new Channel(loop_, cli_socket->fd());
// 定义一个回调函数,处理该连接上的事件
auto cb = [this, channel]() { handleEvent(channel); }; // 使用 lambda 捕获 this 和 channel
// 将定义的回调函数设置到 Channel 中
channel->SetCallback(cb); // 设置回调
// 启用可读事件,开始监听这个 Channel 的可读状态
channel->enableReading();
}
上一节的Server::newConnection()函数是不是和现在的Acceptor::handleRead()是差不多的呢。
都是先进行accpet()建立新连接,接着创建新客户对应的channel,并设置其回调函数,之后把channel添加到epoll实例内。
那么,我们现在有了Acceptor类,那进行accpet()的操作就可以放在Accpetor类中实现了啦。
那为什么不把创建新客户对应的channel这些操作也放到Accpetor类中呢。
因为建立连接后的channel都代表一个新客户,这些channel应该由Server类去管理。Acceptor类只负责accpet()建立连接,不负责管理建立连接之后的chananel客户,要分工明确。
前面说了,Server类主要有两部分嘛,一部分是用accept(2)函数来与客户端进行连接 ,另一部分是与已连接的客户端(即是TCP连接)进行服务处理等等。第一部分对应是Acceptor类,另外的对应就是建立连接之后的channel。
目前这一节Server类还没有把新创建的channel保存下来,之后会修改好这情况(就是可以用一个成员变量保存客户端们的channels)。
所以这个函数需要是放置在Server类中,那Acceptor类对象想要用该函数,那就需要在Server类中去进行绑定,把Server::newConnection()绑定给Acceptor::newConnectionCallback_。
Sever类的改变
首先是添加了Acceptor类变量。
cpp
class Server {
public:
// 构造函数,接收服务器的地址和事件循环
Server(const InetAddr& serverAddr, EventLoop* eventloop);
~Server(); // 析构函数
// 处理事件的函数
void handleEvent(Channel* ch);
// 新连接的处理函数,接收一个 socket 文件描述符
void newConnection(int sockfd);
private:
EventLoop* loop_; // 指向事件循环的指针
std::unique_ptr<Acceptor> acceptor_; // 封装 Acceptor 对象的智能指针
};
// Server 类的构造函数实现
Server::Server(const InetAddr& listenAddr, EventLoop* eventloop)
: loop_(eventloop) // 用传入的事件循环参数初始化成员变量
, acceptor_(std::make_unique<Acceptor>(listenAddr, loop_)) // 创建 Acceptor 对象,绑定监听地址和事件循环
{
// 定义一个回调函数,当有新连接时调用 newConnection 方法
auto cb = [this](intsockfd) { newConnection(sockfd); };
// 将回调函数设置到 Acceptor 中,当有新的连接时将自动调用
acceptor_->setNewconnectionCallback(cb); // 绑定 Server::newConnection() 函数到 Acceptor 的新连接回调
// 以下代码为注释,提供与以前方法的比较学习
/*
// Previous implementation:
// serv_socket_->bind(listenAddr); // 绑定监听地址
// serv_socket_->listen(); // 启动监听
// serv_socket_->setNonblock(); // 设置非阻塞模式
// serv_channel_ = new Channel(loop_, serv_socket_->fd()); // 创建 Channel 关联监听的 socket
// auto cb = [this]() { newConnection(serv_socket_); }; // 定义回调函数
// serv_channel_->SetCallback(cb); // 设置回调
// serv_channel_->enableReading(); // 启用可读事件
*/
}
目前可以说,Acceptor类中有两个设置回调的地方,一是在Server构造函数中使用Acceptor类对象时需要设置newConnectionCallback_,二是在Acceptor类内部的成员变量acceptChannel_需要设置回调函数SetReadCallback()。这两点需要记住,分清楚。
Acceptor类主要实现创建TCP服务端的传统步骤,并且接受客户端的连接accept()。这样,Server类更加简洁,也分离出了Acceptor类。
接着我们还可以继续完善,还有连接这部分,下一节再介绍。