前两篇写muduo网络框架线程处理,这两篇通过一个小的echo服务器来完整说明这个网络事件处理的流程。echo是muduo自带的例子,十分简单:
int main()
{
LOG_INFO << "pid = " << getpid();
muduo::net::EventLoop loop;
muduo::net::InetAddress listenAddr(2007);
EchoServer server(&loop, listenAddr);
server.start();
loop.loop();
}
EchoServer::EchoServer(muduo::net::EventLoop* loop,
const muduo::net::InetAddress& listenAddr)
: server_(loop, listenAddr, "EchoServer")
{
server_.setConnectionCallback(
std::bind(&EchoServer::onConnection, this, _1));
server_.setMessageCallback(
std::bind(&EchoServer::onMessage, this, _1, _2, _3));
}
void EchoServer::start()
{
server_.start();
}
void EchoServer::onConnection(const muduo::net::TcpConnectionPtr& conn)
{
LOG_INFO << "EchoServer - " << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
}
void EchoServer::onMessage(const muduo::net::TcpConnectionPtr& conn,
muduo::net::Buffer* buf,
muduo::Timestamp time)
{
muduo::string msg(buf->retrieveAllAsString());
LOG_INFO << conn->name() << " echo " << msg.size() << " bytes, "
<< "data received at " << time.toString();
conn->send(msg);
}
muduo的主线程有一个在栈上显示声明的EventLoop实例,我们看到EchoServer有个TcpServer类型的组合变量server_(注意muduo提倡组合方式,而非派生的方式),server_会引用到这个EventLoop 实例。进而是server_开始运行start。start中线程池也开始运行start,线程池的start分析,前面已经讲过,这里就不再赘述了。接着是监听socket对象Acceptor的监听工作,这个没什么好说的。为什么要用loop_->runInLoop的方式,这个下面会说到。
当有新的连接到来时,会触发事件,调用TcpServer::newConnection,这个是在TcpServer构造函数中就绑定好了的,函数如下:
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
loop_->assertInLoopThread();
EventLoop* ioLoop = threadPool_->getNextLoop();
char buf[64];
snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
++nextConnId_;
string connName = name_ + buf;
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,
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));
}
前面说过新的连接事件处理会在工作线程的EventLoop(如果有的话)中。如何获取新的新的线程,调用threadPool_->getNextLoop()即可,他采用简单的round-robin方式来获取。接着会创建一个TcpConnection实例,设置各种回调函数,例如连接成功之后的回调函数,收到消息之后的回调函数,他们已经在EchoServer构造函数中绑定。然后是运行ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn)):
void EventLoop::runInLoop(Functor cb)
{
if (isInLoopThread())
{
cb();
}
else
{
queueInLoop(std::move(cb));
}
}
void EventLoop::queueInLoop(Functor cb)
{
{
MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(std::move(cb));
}
if (!isInLoopThread() || callingPendingFunctors_)
{
wakeup();
}
}
runInLoop的目的就是,如果新连接获得EventLoop在主线程中,那么直接执行该绑定的函数,否则EventLoop先暂存其回调函数,叫作未决的函数PendingFunctors。什么时候调用呢?前面说到新线程的可能处于epoll的阻塞中。所以要唤醒新的线程,或者已经是在处理之前的回调函数状态,为了快速处理新的回调函数,也再次发出唤醒的事件。
唤醒后EventLoop继续循环下去,处理未决函数doPendingFunctors。TcpConnection::connectEstablished()函数就被调用了:
void TcpConnection::connectEstablished()
{
loop_->assertInLoopThread();
assert(state_ == kConnecting);
setState(kConnected);
channel_->tie(shared_from_this());
channel_->enableReading();
connectionCallback_(shared_from_this());
}
他要做的是添加到epoll的监管之中。然后回调函数触发,前面说过在EchoServer中绑定过了。当有数据来到时,会触发其读回调函数(这个是在通道Channel类中绑定的,会另写一篇文章来说明)。
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_在EchoServer中绑定了,他会给客户端原封不动的发送收到的消息。其实接收与发送消息时涉及到的缓存处理还是有点复杂的,这个有机会再剖析。
至此整个流程就走完了。