手写muduo网络库(终):从实际案例出发详解muduo各模块间调用关系

一、引言

在之前的系列文章中,我们已经详细探讨了手写 muduo 网络库各个模块的实现细节。然而,仅仅了解模块的实现逻辑是不够的,还需要深入理解各个模块之间的调用关系。本文将以一个回声服务器的代码编写为例,详细剖析 muduo 网络库各模块间的调用关系。通过模拟代码执行过程,深入探究连接建立、事件处理、回调函数设置与调用等关键环节,为读者揭示 muduo 底层原理。

二、程序启动与初始化

#include 
#include 
#include "TcpServer.h"
#include "LogStream.h"

class EchoServer
{
public:
    EchoServer(EventLoop *loop, const InetAddress &addr, const std::string &name)
        : server_(loop, addr, name)
        , loop_(loop)
    {
        // 注册回调函数
        server_.setConnectionCallback(
            std::bind(&EchoServer::onConnection, this, std::placeholders::_1));
        
        server_.setMessageCallback(
            std::bind(&EchoServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

        // 设置合适的subloop线程数量
        server_.setThreadNum(3);
    }
    void start()
    {
        server_.start();
    }

private:
    // 连接建立或断开的回调函数
    void onConnection(const TcpConnectionPtr &conn)   
    {
        if (conn->connected())
        {
            LOG_INFO << "Connection UP :  " << conn->peerAddress().toIpPort().c_str();
        }
        else
        {
            LOG_INFO << "Connection DOWN : " << conn->peerAddress().toIpPort().c_str();
        }
    }

    // 可读写事件回调
    void onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp time)
    {
        std::string msg = buf->retrieveAllAsString();
        conn->send(msg);
    }
    TcpServer server_;
    EventLoop *loop_;

};

int main() {
    g_logger = new Logger();
    g_logger->SetLogLevel(kInfo);
    EventLoop loop;
    InetAddress addr(8080);
    EchoServer server(&loop, addr, "EchoServer");
    std::cout << "SERVER START..."  << std::endl;
    server.start();

    loop.loop();
    return 0;
}

初始化流程: 

1. EchoServer 构造函数:初始化服务器基础

在 testserver.cpp 中,EchoServer 类的构造函数负责初始化服务器的基本配置。其中,通过 server_(loop, addr, name) 创建了 TcpServer 对象,这是整个服务器架构的核心组件之一。

// testserver.cpp
EchoServer::EchoServer(EventLoop *loop, const InetAddress &addr, const std::string &name)
    : server_(loop, addr, name)
    , loop_(loop)
{
    // 注册回调函数
    server_.setConnectionCallback(
        std::bind(&EchoServer::onConnection, this, std::placeholders::_1));
    
    server_.setMessageCallback(
        std::bind(&EchoServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

    // 设置合适的subloop线程数量
    server_.setThreadNum(3);
}

2. TcpServer 构造函数:初始化关键组件

TcpServer 构造函数在服务器启动时起到了关键的初始化作用。它不仅绑定了主事件循环 mainloop,还创建了 Acceptor 和线程池 EventLoopThreadPool

// TcpServer.cpp
TcpServer::TcpServer(EventLoop *loop,
                     const InetAddress &listenAddr,
                     const std::string &nameArg,
                     Option option)
    : loop_(CheckLoopNotNull(loop))
    , ipPort_(listenAddr.toIpPort())
    , name_(nameArg)
    , acceptor_(new Acceptor(loop, listenAddr, option == kReusePort))
    , threadPool_(new EventLoopThreadPool(loop, name_))
    , connectionCallback_()
    , messageCallback_()
    , nextConnId_(1)
    , started_(0)
{
    acceptor_->setNewConnectionCallback(
        std::bind(&TcpServer::newConnection, this, std::placeholders::_1, std::placeholders::_2));
}

3. Acceptor 构造函数:处理新连接接受

Acceptor 类负责监听新的连接请求,并在有新连接到来时进行处理。在构造函数中,它绑定了主事件循环 mainloop,创建了一个非阻塞的套接字 acceptSocket_,并关联了一个 Channel 对象 acceptChannel_。同时,为 acceptChannel_ 设置了读回调事件 handleRead,当有新连接到来时,会触发该回调。

// Acceptor.cpp
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));
}

4. EventLoopThreadPool 构造函数:初始化线程池

线程池 EventLoopThreadPool 的构造函数主要负责绑定主事件循环 baseLoop_,并初始化一些线程池的参数,如 started_numThreads_ 和 next_

// EventLoopThreadPool.cpp
EventLoopThreadPool::EventLoopThreadPool(EventLoop *baseLoop, const std::string &nameArg)
    : baseLoop_(baseLoop), name_(nameArg), started_(false), numThreads_(0), next_(0)
{
}

5. 绑定 Acceptor 的新连接回调

在模块构造结束后回到在 TcpServer 构造函数中,通过 acceptor_->setNewConnectionCallback 方法,将 Acceptor 的新连接回调函数设置为 TcpServer::newConnection。当有新连接到来时,Acceptor 的 handleRead 函数会调用该回调。

// TcpServer.cpp
acceptor_->setNewConnectionCallback(
    std::bind(&TcpServer::newConnection, this, std::placeholders::_1, std::placeholders::_2));

 6. EchoServer::EchoServer 函数:设置回调

    在server_及其相关子组件构造结束后,调用TcpServer接口传入用户指定的业务处理函数 (EchoServer::onConnection用于连接相关处理;EchoServer::onMessage用于消息业务处理),并设置线程池线程个数。

    EchoServer(EventLoop *loop, const InetAddress &addr, const std::string &name)
            : server_(loop, addr, name)
            , loop_(loop)
        {
            // 注册回调函数
            server_.setConnectionCallback(
                std::bind(&EchoServer::onConnection, this, std::placeholders::_1));
            
            server_.setMessageCallback(
                std::bind(&EchoServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
    
            // 设置合适的subloop线程数量
            server_.setThreadNum(3);
        }

    三、服务器启动与监听

    TcpServer::start 调用

    void TcpServer::start()
    {
        if (started_.fetch_add(1) == 0)
        {
            threadPool_->start(threadInitCallback_);
            loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));
        }
    }
    
    • 线程池启动:调用 EventLoopThreadPool::start 启动线程池。
    • Acceptor::listen 调用:在主事件循环中调用 Acceptor::listen 开始监听新的连接。

    Acceptor::listen 调用

    void Acceptor::listen()
    {
        listenning_ = true;
        acceptSocket_.listen();
        acceptChannel_.enableReading();
    }
    
    • Socket::listen 调用:调用 Socket::listen 开始监听新的连接。
    • Channel::enableReading 调用:启用 Channel 的读事件,将其注册到 Poller 中。

    四、新连接建立

    Acceptor::handleRead 调用

    当有新的连接到来时,Poller 会触发 Acceptor 的 Channel 的读事件,从而调用 Acceptor::handleRead

    void Acceptor::handleRead()
    {
        InetAddress peerAddr;
        int connfd = acceptSocket_.accept(&peerAddr);
        if (connfd >= 0)
        {
            if (NewConnectionCallback_)
            {
                NewConnectionCallback_(connfd, peerAddr);
            }
            else
            {
                ::close(connfd);
            }
        }
        else
        {
            LOG_ERROR << "Acceptor::handleRead";
        }
    }
    
    • Socket::accept 调用:调用 Socket::accept 接受新的连接,返回新连接的文件描述符。
    • 回调函数调用:调用 NewConnectionCallback,即 TcpServer::newConnection

    TcpServer::newConnection 调用

    当有新连接到来时,Acceptor 的 handleRead 函数会调用 TcpServer::newConnection 函数。该函数的主要功能是接受新连接,并将其封装成 TcpConnection 对象,同时为其设置各种回调函数。

    // TcpServer.cpp
    void TcpServer::newConnection(int sockfd, const InetAddress &peerAddr)
    {
        // 从线程池中获取一个EventLoop用于处理新连接
        EventLoop *ioLoop = threadPool_->getNextLoop();
    
        // 生成新连接的名称
        char buf[64] = {0};
        snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
        ++nextConnId_; 
        std::string connName = name_ + buf;
    
        LOG_INFO << "TcpServer::newConnection [" << name_.c_str() << "] - new connection [" << connName.c_str() << "] from" << peerAddr.toIpPort().c_str();
    
        // 获取本地地址
        sockaddr_in local;
        ::memset(&local, 0, sizeof(local));
        socklen_t addrlen = sizeof(local);
        if(::getsockname(sockfd, (sockaddr *)&local, &addrlen) < 0)
        {
            LOG_ERROR << "sockets::getLocalAddr";
        }
        InetAddress localAddr(local);
    
        // 创建TcpConnection对象
        TcpConnectionPtr conn(new TcpConnection(ioLoop,
                                                connName,
                                                sockfd,
                                                localAddr,
                                                peerAddr));
    
        // 将新连接添加到连接映射中
        connections_[connName] = conn;
    
        // 设置TcpConnection的各种回调函数
        conn->setConnectionCallback(connectionCallback_);
        conn->setMessageCallback(messageCallback_);
        conn->setWriteCompleteCallback(writeCompleteCallback_);
    
        // 设置TcpConnection的关闭回调函数
        conn->setCloseCallback(
            std::bind(&TcpServer::removeConnection, this, std::placeholders::_1));
    
        // 在ioLoop中执行连接建立的回调函数
        ioLoop->runInLoop(
            std::bind(&TcpConnection::connectEstablished, conn));
    }
    
    • TcpConnection 创建:创建 TcpConnection 对象,用于管理新连接。
    • 回调函数设置:将 TcpServer 的回调函数设置到 TcpConnection 中。
    • TcpConnection::connectEstablished 调用:在 EventLoop 中调用 TcpConnection::connectEstablished 建立连接。

    newConnection中的模块调用顺序

    • 获取 EventLoop:通过 threadPool_->getNextLoop() 从线程池中获取一个 EventLoop 用于处理新连接。线程池采用轮询的方式选择 EventLoop,确保连接均匀分布到各个 EventLoop 中。
    • 生成连接名称:为新连接生成一个唯一的名称,方便后续管理和日志记录。
    • 获取本地地址:使用 getsockname 函数获取本地地址信息。
    • 创建 TcpConnection 对象:创建一个 TcpConnection 对象,将新连接的相关信息封装在其中。在 TcpConnection 的构造函数中,会创建一个 Channel 对象,并将其与 ioLoop 和新连接的文件描述符关联。同时,为 Channel 设置各种回调函数,如读回调、写回调、关闭回调和错误回调。
    • 设置 TcpConnection 的回调函数:为 TcpConnection 设置各种回调函数,包括连接建立或断开的回调、消息到达的回调、数据发送完成的回调和连接关闭的回调。
    • 执行连接建立的回调函数:通过 ioLoop->runInLoop 方法,在 ioLoop 中执行 TcpConnection::connectEstablished 函数。在该函数中,将连接状态设置为 kConnected,将 Channel 与 TcpConnection 对象绑定,并向 Poller 注册 Channel 的读事件(EPOLLIN)。最后,执行连接建立的回调函数。

    TcpConnection::connectEstablished 调用

    void TcpConnection::connectEstablished()
    {
        setState(kConnected);
        channel_->tie(shared_from_this());
        channel_->enableReading();
    
        connectionCallback_(shared_from_this());
    }
    
    • 状态设置:将 TcpConnection 的状态设置为 kConnected
    • Channel::tie 调用:将 Channel 与 TcpConnection 绑定。
    • Channel::enableReading 调用:启用 Channel 的读事件,将其注册到 Poller 中。
    • 回调函数调用:调用 connectionCallback,即 EchoServer::onConnection

    通过以上步骤,接收到的事件描述符就一步一步成为了 TcpConnection 对象,并为其 Channel 设置了各种回调函数,同时将 Channel 注册到线程池 EventLoop 的 Poller 中,等待后续事件的处理。 

     后续channel与poller交互相关细节请看:手写muduo网络库(四):实现线程 ID 管理与事件循环并分析EventLoop,Poller,Channel关系-CSDN博客

    五、总结

    通过对 EchoServerTcpServerAcceptorEventLoopThreadPool 和 TcpConnection 等组件的深入分析,我们详细了解了网络服务器如何处理新连接的过程。各个组件之间相互协作,通过回调函数和事件循环机制,实现了高效、可靠的网络通信。这种设计模式不仅提高了服务器的性能和可扩展性,还使得代码的维护和调试更加方便。

    你可能感兴趣的:(手写muduo网络库(终):从实际案例出发详解muduo各模块间调用关系)