7.从Server到Acceptor,优化Reactor模式的实现

目录

[Acceptor 类](#Acceptor 类)

1.Acceptor类的构造函数

2.handleRead()的实现

Sever类的改变


在上一节中,我们实现了 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类。

接着我们还可以继续完善,还有连接这部分,下一节再介绍。

相关推荐
为你写首诗ge21 分钟前
【Unity网络编程知识】FTP学习
网络·unity
TDD_06282 小时前
【运维】Centos硬盘满导致开机时处于加载状态无法开机解决办法
linux·运维·经验分享·centos
x66ccff2 小时前
vLLM 启动 GGUF 模型踩坑记:从报错到 100% GPU 占用的原因解析
linux
神经毒素2 小时前
WEB安全--文件上传漏洞--一句话木马的工作方式
网络·安全·web安全·文件上传漏洞
William.csj3 小时前
Linux——开发板显示器显示不出来,vscode远程登录不进去,内存满了的解决办法
linux·vscode
慵懒学者3 小时前
15 网络编程:三要素(IP地址、端口、协议)、UDP通信实现和TCP通信实现 (黑马Java视频笔记)
java·网络·笔记·tcp/ip·udp
KeithTsui3 小时前
GCC RISCV 后端 -- 控制流(Control Flow)的一些理解
linux·c语言·开发语言·c++·算法
森叶4 小时前
linux如何与windows进行共享文件夹开发,不用来回用git进行拉来拉去,这个对于swoole开发者来说特别重要
linux·git·swoole
itachi-uchiha4 小时前
关于UDP端口扫描概述
网络·网络协议·udp
liulilittle4 小时前
Linux 高级路由策略控制配置:两个不同路由子网间通信
linux·网络·智能路由器