muduo网络库学习:基本架构及流程分析
之前一直在学习陈硕大佬的muduo库,一直考虑找个时间总结一下(不然没过多久全忘光了)。muduo库是一种基于Reactor模型的C++网络库,仅支持Linux平台,相关学习可以参考《Linux 多线程服务器端编程》。本文仅做个人学习记录及分享用途,如果错误请您不吝赐教。
基本架构
Basic Reactor
muduo的基础架构采用的就是Reactor模型,最基本的Reactor如下图中所示,简单点来说就是一个I/O线程内部会有一个一直处于循环状态的事件循环(EventLoop),这个EventLoop会一直监听是否有外部的事件触发(比如说客户端向服务器端发来的连接请求),如果有就调用对应的回调函数进行处理。
Mutiple Reactor + ThreadPool
但是更准确点来说,muduo网络库采用的时multiple reactor + threadpool的形式,所谓的multiple reactor,就是指有主从reactor之分,Main Reactor只用于监听新的连接,在accept之后就会将这个连接分配到Sub Reactor上,由子Reactor负责连接的事件处理。
而线程池中维护了两个队列,一个队伍队列,一个线程队列,外部线程将任务添加到任务队列中,如果线程队列非空,则会唤醒其中一只线程进行任务的处理,相当于是生产者和消费者模型。
muduo库的基本使用
这边截取了EchoServer_unittest.cc文件中的部分程序,并且做了一定的简化:
void onMessage(const muduo::net::TcpConnectionPtr& conn, muduo::net::Buffer* buf, muduo::Timestamp time) {
conn->send(buf);
}
int main(int argc, char* argv[])
{
EventLoop loop; // 创建事件循环
InetAddress listenAddr(2000, false, ipv6); // 创建Server端的地址结构
EchoServer server(&loop, listenAddr); // server绑定所属的EventLoop并指定地址
server.setMessageCallback(onMessage); // 绑定消息到来时的回调函数
server.start(); // 启动线程
loop.loop(); // 开启事件循环
}
首先声明对应的EventLoop类对象,以及使用InetAdderss类构建对应的地址结构,然后在初始化TcpServer类对象中,传入所指向的loop以及需要绑定的地址结构,并server对象中的start方法完成3件事情:
- 启动线程池;
- 在监听用的socket文件描述符(lfd)上启动listen();
- 将监听用的文件描述符的可读事件注册到EventLoop中进行关注。
最后启用了loop.loop()这个来开启整个EventLoop事件循环,由此lfd能够不断监听外来的连接请求。
基本结构介绍
以下内容仅作简要介绍,详细部分建议去看源码,只要我自己复习的时候看得懂就行啦。
EventLoop类
muduo库在使用的时候,会有一个最基本的IO用的线程,内部有且仅有一个EventLoop事件循环,这就是主Reactor。这个事件循环主要就是用来监听是否有新的连接到来,EventLoop类中最主要的就是包含了一个Poller的类对象,这个就是用来实现IO复用的。
// 事件循环,该函数不能跨线程调用,只能在创建该对象的线程中调用
void EventLoop::loop()
{
assert(!looping_);
assertInLoopThread(); // 断言当前处于创建该对象的线程中
looping_ = true; // 标志开始循环
quit_ = false; // FIXME: what if someone calls quit() before loop() ?
LOG_TRACE << "EventLoop " << this << " start looping";
while (!quit_)
{
activeChannels_.clear(); // 将活动通道清除
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_); // 调用poll来返回活动的通道,第一个参数是超时时间
++iteration_;
if (Logger::logLevel() <= Logger::TRACE)
{
printActiveChannels(); // 打印活动通道
}
// TODO sort channel by priority
eventHandling_ = true; // 开启事件处理标志位
for (Channel* channel : activeChannels_) // 遍历处理活动通道
{
currentActiveChannel_ = channel;
currentActiveChannel_->handleEvent(pollReturnTime_);
}
currentActiveChannel_ = NULL; // 全部处理完之后当前正在处理的通道置为空
eventHandling_ = false; // 关闭事件处理标志位
doPendingFunctors();
}
LOG_TRACE << "EventLoop " << this << " stop looping";
looping_ = false; // 停止循环
}
Poller类
Poller类是作为一个基类,会根据环境变量的设置选择是使用epoll还是poll方法,向下有2个派生类,PollPoller和EpollPoller,这也是muduo库中唯一使用面向对象的地方,其余都是基于对象的实现方式。
// DefaultPoller.cc文件中
Poller* Poller::newDefaultPoller(EventLoop* loop)
{
if (::getenv("MUDUO_USE_POLL")) // 查看环境变量是否使用poll
{
return new PollPoller(loop);
}
else
{
return new EPollPoller(loop);
}
}
以EpollPoller为例,分别用epoll中最基本的epoll_create()、epoll_ctl()和epoll_wait()来对内部进行解释。
epoll_create():EpollPoller类的构造函数中就会调用epoll_create()来构建一棵监听用的红黑树。
EPollPoller::EPollPoller(EventLoop* loop)
: Poller(loop),
epollfd_(::epoll_create1(EPOLL_CLOEXEC)),
events_(kInitEventListSize)
{
if (epollfd_ < 0)
{
LOG_SYSFATAL << "EPollPoller::EPollPoller";
}
}
epoll_wait():EpollPoller类内部最主要的就是poll()函数,要求传入超时时间和活跃通道列表。函数内部调用epoll_wait()来返回所有已经就绪的事件个数,然后遍历这些事件,并且为此构建Channel通道,将这些Channel压入到函数传入的活跃通道列表activeChannels中。
Timestamp EPollPoller::poll(int timeoutMs, ChannelList* activeChannels)
{
LOG_TRACE << "fd total count " << channels_.size();
int numEvents = ::epoll_wait(epollfd_, // 最主要关注这边的函数调用
&*events_.begin(),
static_cast<int>(events_.size()),
timeoutMs);
int savedErrno = errno;
Timestamp now(Timestamp::now());
if (numEvents > 0)
{
LOG_TRACE << numEvents << " events happened";
fillActiveChannels(numEvents, activeChannels); // 函数内部会遍历就绪事件并且创建通道注册到活跃通道中
if (implicit_cast<size_t>(numEvents) == events_.size())
{
events_.resize(events_.size()*2);
}
}
else if (numEvents == 0) // 超过定时时间也没有事件发生
{
LOG_TRACE << "nothing happened";
}
else
{
// error happens, log uncommon ones
if (savedErrno != EINTR)
{
errno = savedErrno;
LOG_SYSERR << "EPollPoller::poll()";
}
}
return now;
}
epoll_ctl():在EpollPoller类中封装了update()方法来实现epoll_ctl()的调用
void EPollPoller::update(int operation, Channel* channel) // 传入节点操作以及对应通道
{
struct epoll_event event;
memZero(&event, sizeof event);
event.events = channel->events();
event.data.ptr = channel; // 指针指向通道
int fd = channel->fd();
LOG_TRACE << "epoll_ctl op = " << operationToString(operation)
<< " fd = " << fd << " event = { " << channel->eventsToString() << " }";
if (::epoll_ctl(epollfd_, operation, fd, &event) < 0) // epoll_ctl修改节点
{
if (operation == EPOLL_CTL_DEL)
{
LOG_SYSERR << "epoll_ctl op =" << operationToString(operation) << " fd =" << fd;
}
else
{
LOG_SYSFATAL << "epoll_ctl op =" << operationToString(operation) << " fd =" << fd;
}
}
}
但是在muduo中,主要还是通过操作通道来进行相应的事件处理修改的,也就是下面这个函数,由于内容较长也就不放上来了。但只要记住这个函数是调用update()来实现节点属性修改的即可。
void EPollPoller::updateChannel(Channel* channel)
但是要先记住下面这张图,细节部分先不过于探究,首先来看一下什么是Channel。
Channel类
所谓的Channel,就是muduo中对于文件描述符行为的一种封装,内部还包含了对于fd的一些事件处理回调等。尽管Channel类中包含了fd,但是它并不实际拥有fd,也不负责这个fd的生存周期的管理,因为他相当于仅是对这个fd行为的一个封装。其内部最主要的就是handleEvent()这个函数,事件到来时便调用这个函数进行处理,内部会根据具体的读写事件来决定使用的回调函数。
// 事件到来时调用该函数进行处理
void Channel::handleEvent(Timestamp receiveTime)
{
std::shared_ptr<void> guard;
if (tied_)
{
guard = tie_.lock();
if (guard)
{
handleEventWithGuard(receiveTime);
}
}
else
{
handleEventWithGuard(receiveTime);
}
}
handleEventWithGuard()这个函数会根据事件类型来进行相应回调函数的调用。
void Channel::handleEventWithGuard(Timestamp receiveTime)
{
eventHandling_ = true;
LOG_TRACE << reventsToString();
if ((revents_ & POLLHUP) && !(revents_ & POLLIN))
{
if (logHup_)
{
LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLHUP";
}
if (closeCallback_) closeCallback_();
}
if (revents_ & POLLNVAL) // 文件描述符没有打开或者不是合法的文件描述符
{
LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLNVAL";
}
if (revents_ & (POLLERR | POLLNVAL)) // 错误处理
{
if (errorCallback_) errorCallback_(); // 回调函数
}
if (revents_ & (POLLIN | POLLPRI | POLLRDHUP)) // 读/紧急数据/对等关闭处理
{
if (readCallback_) readCallback_(receiveTime);
}
if (revents_ & POLLOUT) // 可写事件
{
if (writeCallback_) writeCallback_();
}
eventHandling_ = false;
}
上面这个函数细节不必过于深究,只要知道在Channel类中会调用以下几个函数来实现对相应事件回调函数的注册即可。
// 回调函数注册
void handleEvent(Timestamp receiveTime);
void setReadCallback(ReadEventCallback cb)
{ readCallback_ = std::move(cb); }
void setWriteCallback(EventCallback cb)
{ writeCallback_ = std::move(cb); }
void setCloseCallback(EventCallback cb)
{ closeCallback_ = std::move(cb); }
void setErrorCallback(EventCallback cb)
{ errorCallback_ = std::move(cb); }
前面说了在EpollPoller中在调用了poll()这个方法之后会将事件构建成通道,也就是说这些通道会根据这个事件来调用这个Channel::handleEvent()来进行相应的事件处理,比如说如果是读事件就会调用readCallback_来进行读事件处理。
TcpConnection类
在muduo库中,Channel类一般都不是单独使用的,通常都是用作其他类的成员,比如TcpConnection和Acceptor这两个类。
TcpConnection相当于是对服务器和客户端之间连接的一种抽象。类中主要是EventLoop、Socket以及Channel这三个类对象指针,其中第一个用于指向当前连接所属的事件循环,而 Socket用来指明用于网络通信的cfd,Channel表示本次连接的通道,用来封装cfd的各种行为,在TcpConnection这个类构造的时候,便会将对应的读写回调处理函数注册进Channel中。
譬如在TcpConnection的构造函数中节选出一段来,这段函数就实现了对Channel中读事件回调函数的注册:
channel_->setReadCallback(
std::bind(&TcpConnection::handleRead, this, _1));
bind是函数适配器,不懂的可以自己去搜一下,这边就不再赘述,注册的回调函数为TcpConnection::handleRead(),在类中可以看到其具体实现:
void TcpConnection::handleRead(Timestamp receiveTime)
{
loop_->assertInLoopThread();
int savedErrno = 0;
ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
if (n > 0)
{
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime); // 主要关注这里即可,又是一个回调函数
}
else if (n == 0)
{
handleClose();
}
else
{
errno = savedErrno;
LOG_SYSERR << "TcpConnection::handleRead";
handleError();
}
}
messageCallback_是类中的一个通用多态函数封装器,可以简单理解为是一个“容器”,用来接收上层传给他的回调函数,也就是说TcpConncetion一般也是作为其他类的成员,接收其传给它的回调函数。
Acceptor类
Acceptor内部则主要是loop_类对象指针,acceptSocket_监听用套接字以及acceptChannel通道
EventLoop* loop_;
Socket acceptSocket_; // 监听套接字
Channel acceptChannel_; // 用于观察此socket的可读事件
不同于TcpConncetion,Acceptor主要负责的就是对lfd的读事件的处理,当有读事件到来的时候,即会调用handleRead()这个函数来调用构造时传给他的回调函数。
void Acceptor::handleRead()
{
loop_->assertInLoopThread();
InetAddress peerAddr;
//FIXME loop until no more
int connfd = acceptSocket_.accept(&peerAddr);
if (connfd >= 0)
{
// string hostport = peerAddr.toIpPort();
// LOG_TRACE << "Accepts of " << hostport;
if (newConnectionCallback_)
{
newConnectionCallback_(connfd, peerAddr); // 主要关注此处即可,这里又是一个回调
}
else
{
sockets::close(connfd);
}
}
else
{
LOG_SYSERR << "in Acceptor::handleRead";
// Read the section named "The special problem of
// accept()ing when you can't" in libev's doc.
// By Marc Lehmann, author of libev.
if (errno == EMFILE)
{
::close(idleFd_);
idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
::close(idleFd_);
idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
}
}
}
newConnectionCallback_又是一个用来接收上层回调函数的“容器”。
TcpServer类
那么传给TcpConnection和Acceptor这两个类的回调函数从哪里来?那就是TcpServer。类成员如下,内部维护了一个ConncetionMap来管理多个Connection。
EventLoop* loop_; // the acceptor loop
const string ipPort_; // 服务端口
const string name_; // 服务名
std::unique_ptr<Acceptor> acceptor_; // avoid revealing Acceptor
std::shared_ptr<EventLoopThreadPool> threadPool_; // EventLoop线程池
ConnectionCallback connectionCallback_; // 连接到来的回调函数
MessageCallback messageCallback_; // 消息到来的回调函数
WriteCompleteCallback writeCompleteCallback_;
ThreadInitCallback threadInitCallback_;
AtomicInt32 started_;
// always in loop thread
int nextConnId_; // 下一个连接ID
ConnectionMap connections_; // 连接列表
TcpServer在构造的时候会为acceptor指定他的回调函数,也就是newConnection这个函数。
TcpServer::TcpServer(EventLoop* loop,
const InetAddress& listenAddr,
const string& nameArg,
Option option)
: loop_(CHECK_NOTNULL(loop)), // 检查loop不是空指针
ipPort_(listenAddr.toIpPort()), // 获取端口号
name_(nameArg),
acceptor_(new Acceptor(loop, listenAddr, option == kReusePort)), // 只能指针管理Acceptor
threadPool_(new EventLoopThreadPool(loop, name_)),
connectionCallback_(defaultConnectionCallback),
messageCallback_(defaultMessageCallback),
nextConnId_(1)
{ // 注册acceptor_的回调函数,在poller返回事件之后调用TcpServer的newConnection
acceptor_->setNewConnectionCallback(
std::bind(&TcpServer::newConnection, this, _1, _2));
}
在这个函数内部,将会从EventLoop线程池中取得一个新的EventLoop线程,然后构建一个新的TcpConncetion,并将这个新的EventLoop和Acceptor处理得到的cfd传递给他,那么就得到了一个新的客户端连接,并在newConnection这个函数内部对其进行各种读写事件回调函数的注册,而这些函数,则是由我们用户来提供的,也就是说,系统提供acceptor的默认读回调,回调时创建新的连接,而cfd,与客户端通信时的一些读写事件的处理,这些回调函数由用户自身提供。
// 创建一个新的连接(一个新的newConnction对象)
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
loop_->assertInLoopThread();
EventLoop* ioLoop = threadPool_->getNextLoop(); // 从EventLoop线程池中获取到新的EventLoop传递给TcpConnection
char buf[64];
snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
++nextConnId_; // 下一个连接的id++
string connName = name_ + buf; // 连接的名称
LOG_INFO << "TcpServer::newConnection [" << name_
<< "] - new connection [" << connName
<< "] from " << peerAddr.toIpPort();
InetAddress localAddr(sockets::getLocalAddr(sockfd)); // 构造本地地址
// FIXME poll with zero timeout to double confirm the new connection
// FIXME use make_shared if necessary 创建一个连接
TcpConnectionPtr conn(new TcpConnection(ioLoop, // 构建Connection的时候传入新的EventLoop,即将cfd事件处理分配给Sub Reactor
connName,
sockfd,
localAddr,
peerAddr));
connections_[connName] = conn; // 将这个连接放在连接列表中
conn->setConnectionCallback(connectionCallback_); // 绑定回调函数
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
conn->setCloseCallback(
std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn)); // 调用注册的函数将cfd添加到Poller的监听中
}
TcpClient这个类内部主要是Connector并且维护了一个单独的TcpConnection成员对象,此处不再过多解释。
流程梳理
那么让我们来重新梳理一下整个流程。首先明确一点,用户可以为TcpServer提供自定义的回调函数,但是这些回调函数均是用于cfd,也就是与客户端通信时的读写事件回调处理。细节部分将不再赘述:
void onMessage(const muduo::net::TcpConnectionPtr& conn, muduo::net::Buffer* buf, muduo::Timestamp time) {
conn->send(buf);
}
int main(int argc, char* argv[])
{
EventLoop loop; // 创建事件循环
InetAddress listenAddr(2000, false, ipv6); // 创建Server端的地址结构
EchoServer server(&loop, listenAddr); // server绑定所属的EventLoop并指定地址
server.setMessageCallback(onMessage); // 绑定消息到来时的回调函数
server.start(); // 启动线程
loop.loop(); // 开启事件循环
}
- 主程序中会先创建一个EventLoop对象以及一个TcpServer对象,TcpServer对象在创建时会调用其构造函数,内部会实现一个Acceptor对象的构造并注册它的回调函数TcpServer::newConnection(),也就是当accept()接收到客户端连接请求时(lfd接收到读事件)会调用的回调函数;
- 用户自定义实现的函数onMessage(),通过TcpServer对象的setMessageCallback()方法进行回调函数注册,这个注册的函数会在accept()接收到新的连接返回cfd时自动注册到它的通道中作为相应事件发生时的回调函数调用。
- server.start()开启TcpServer,在start()中会调用Acceptor::listen()来对lfd对应通道acceptChannel的可读事件注册到Poller中进行关注,最终会调用到Poller::update(),由于是新的节点,因此会将其添加到Poller的关注中进行监听(相当于会在loop启动之后就将lfd挂载到epoll的监听红黑树中),具体函数调用过程如下:
- loop.loop()启动EventLoop,初始时仅有一个IO线程,线程内部有且仅有一个EventLoop在不断循环运行,它也是唯一的Main Reactor。以下过程看图说话。
- 循环时执行的是EventLoop::loop()函数,在内部的while()循环中,会先将当前活跃的activeChannels清空,然后调用poller对象的poll方法(此处我们假定使用的时默认的epoll());
- 在这个poll方法中实现了epoll_wait()监听,并且根据获取到的就绪事件数量创建相等数量的channel通道,并且将它们压入到活跃通道集合activeChannels中;
- poll方法完成功能之后,就会依次遍历这些channel,并且调用channel类中的成员函数handleEvent()来进行事件处理;
- handleEvent()中会调用Channel::handleEventWithGuard()对读写等事件类型进行判断,并且调用对应的回调函数;
- 假设当前还没有客户端建立连接,线程中只有一个lfd在监听,那么此时如果有事件到来,那就是lfd的读事件,activeChannels中仅有一个就绪事件的channel,也就是lfd的读事件发生,此时就会调用这个lfd通道的读事件回调;
- lfd的读事件回调过程如下:当前活跃通道为acceptChannel,调用这个channel对象中的 handleEvent() ,判断为读事件,则调用回调函数readCallback_(),这个函数的实体是来自于Acceptor类中注册进acceptChannel的Accptor::handleRead()函数,在使用accept()创建完cfd之后,就会调用从TcpServer传递进来的回调函数TcpServer::newConncetion();(哈哈哈,有点绕,函数放在这边便于大家理解)
void Acceptor::handleRead()
{
loop_->assertInLoopThread();
InetAddress peerAddr;
//FIXME loop until no more
int connfd = acceptSocket_.accept(&peerAddr);
if (connfd >= 0)
{
// string hostport = peerAddr.toIpPort();
// LOG_TRACE << "Accepts of " << hostport;
if (newConnectionCallback_)
{
newConnectionCallback_(connfd, peerAddr);
}
...
- 在TcpServer::newConncetion()中,会从EventLoop线程池中按顺序获取一个EventLoop,并将当前的cfd和获取到的EventLoop用来构建一个新的TcpConnection类对象,并且为其注册用户自定义的相关与客户端通信用的读写事件回调函数;
// 创建一个新的连接(一个新的newConnction对象)
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
...
TcpConnectionPtr conn(new TcpConnection(ioLoop, // 构建Connection的时候传入新的EventLoop,即将cfd事件处理分配给Sub Reactor
connName,
sockfd,
localAddr,
peerAddr));
connections_[connName] = conn; // 将这个连接放在连接列表中
conn->setConnectionCallback(connectionCallback_); // 绑定回调函数
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
conn->setCloseCallback(
std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn)); // 调用注册的函数将cfd添加到Poller的监听中
}
- 然后在TcpServer::newConnection()最后会调用TcpConnection::connectEstablished(),内部调用下面的函数,执行结果就是会将cfd注册到Poller中进行监听(简单来说就是挂载到epoll的监听红黑树下)。
channel_->enableReading(); // 将TcpConnection所对应的通道加入到Poller关注
- 也就是说,当Main Reactor接收到了客户端的连接请求之后,就会从线程池中取出一个EventLoop线程,创建了一个Sub Reactor将cfd的读写通信事件交给它来处理,将cfd注册到Poller进行管理,而自己就只是负责lfd的监听,并且会将创建的连接保存进TcpServer中的ConnectionMap中用来管理。
- 本次loop()循环一次之后,重新回到步骤7中,不同的是,此时线程中已经有两个文件描述符lfd和cfd,那么如果两者均有就绪事件发生,此时在activeChannels就会有2个就绪的channel对象,lfd的处理如上所示,而cfd的处理则是会在handleEvent()的时候调用用户自己定义注册的回调函数。
也就是说,所有的回调函数,基本均来自与TcpServer,TcpServer类将自身成员函数TcpServer::newConncetion()作为lfd事件处理的回调,并且在这个函数中将用户自定义的函数注册为对cfd事件处理的回调,同时实现了将cfd注册到Poller下进行管理和监听。
参考资料:
muduo
muduo 源码剖析
muduo网络库代码剖析——Channel类
muduo库整体架构简析
以上均作为本人学习记录,如有错误请您务必指出