muduo的基础架构采用的就是Reactor模型,最基本的Reactor如下图中所示,简单点来说就是一个I/O线程内部会有一个一直处于循环状态的事件循环(EventLoop),这个EventLoop会一直监听是否有外部的事件触发(比如说客户端向服务器端发来的连接请求),如果有就调用对应的回调函数进行处理。
但是更准确点来说,muduo网络库采用的时multiple reactor + threadpool的形式,所谓的multiple reactor,就是指有主从reactor之分,Main Reactor只用于监听新的连接,在accept之后就会将这个连接分配到Sub Reactor上,由子Reactor负责连接的事件处理。
而线程池中维护了两个队列,一个队伍队列,一个线程队列,外部线程将任务添加到任务队列中,如果线程队列非空,则会唤醒其中一只线程进行任务的处理,相当于是生产者和消费者模型。
这边截取了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件事情:
最后启用了loop.loop()这个来开启整个EventLoop事件循环,由此lfd能够不断监听外来的连接请求。
以下内容仅作简要介绍,详细部分建议去看源码,只要我自己复习的时候看得懂就行啦。
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类是作为一个基类,会根据环境变量的设置选择是使用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,就是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_来进行读事件处理。
在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内部则主要是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_又是一个用来接收上层回调函数的“容器”。
那么传给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(); // 开启事件循环
}
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);
}
...
// 创建一个新的连接(一个新的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的监听中
}
channel_->enableReading(); // 将TcpConnection所对应的通道加入到Poller关注
也就是说,所有的回调函数,基本均来自与TcpServer,TcpServer类将自身成员函数TcpServer::newConncetion()作为lfd事件处理的回调,并且在这个函数中将用户自定义的函数注册为对cfd事件处理的回调,同时实现了将cfd注册到Poller下进行管理和监听。
参考资料:
muduo
muduo 源码剖析
muduo网络库代码剖析——Channel类
muduo库整体架构简析
以上均作为本人学习记录,如有错误请您务必指出