在之前的系列文章中,我们已经详细探讨了手写 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;
}
初始化流程:
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);
}
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));
}
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));
}
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)
{
}
Acceptor
的新连接回调在模块构造结束后回到在 TcpServer
构造函数中,通过 acceptor_->setNewConnectionCallback
方法,将 Acceptor
的新连接回调函数设置为 TcpServer::newConnection
。当有新连接到来时,Acceptor
的 handleRead
函数会调用该回调。
// TcpServer.cpp
acceptor_->setNewConnectionCallback(
std::bind(&TcpServer::newConnection, this, std::placeholders::_1, std::placeholders::_2));
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博客
通过对 EchoServer
、TcpServer
、Acceptor
、EventLoopThreadPool
和 TcpConnection
等组件的深入分析,我们详细了解了网络服务器如何处理新连接的过程。各个组件之间相互协作,通过回调函数和事件循环机制,实现了高效、可靠的网络通信。这种设计模式不仅提高了服务器的性能和可扩展性,还使得代码的维护和调试更加方便。