(本文地址:LYanger的博客:http://blog.csdn.net/freeelinux/article/details/53574574)
TcpServer是muduo库很重要的一个类,它结合TcpConnection、Acceptor构成了一套完整的对I/O触发事件的处理机制。那么它们具体是怎么工作的呢?
先来看一个例子:
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>
#include <boost/bind.hpp>
#include <stdio.h>
using namespace muduo;
using namespace muduo::net;
class TestServer {
public:
TestServer(EventLoop* loop, const InetAddress& listenAddr, int numThreads)
: loop_(loop),
server_(loop, listenAddr, "TestServer"),
numThreads_(numThreads){
server_.setConnectionCallback(boost::bind(&TestServer::onConnection, this, _1));
server_.setThreadNum(numThreads);
}
void start(){
server_.start();
}
private:
void onConnection(const TcpConnectionPtr& conn){
if(conn->connected()){
printf("onConnection(): new connection [%s] from %s \n",
conn->name().c_str(),
conn->peerAddress().toIpPort().c_str());
}
else{
printf("onConnection() : connection [%s] is down\n",
conn->name().c_str());
}
}
EventLoop* loop_;
TcpServer server_;
int numThreads_;
};
int main()
{
printf("main() : pid = %d\n", getpid());
InetAddress listenAddr(8888);
EventLoop loop;
TestServer server(&loop, listenAddr, 4);
server.start();
loop.loop();
}
输出:
先不分析结构,我们来分析一下过程:
首先主函数中声明了一个InetAddress对象,构造参数是服务端的端口号,我们来看一下InetAddress的构造函数:
//仅仅指定port,不指定ip,则ip为INADDR_ANY(即0.0.0.0)
explicit InetAddress(uint16_t port = 0, bool loopbackOnly = false, bool ipv6 = false);
具体实现截取ipv4部分是这样的:
InetAddress::InetAddress(uint16_t port, bool loopbackOnly, bool ipv6)
{
if (ipv6)
{
...
}
else
{
bzero(&addr_, sizeof addr_);
addr_.sin_family = AF_INET;
in_addr_t ip = loopbackOnly ? kInaddrLoopback : kInaddrAny; //INADDR_ANY 或者 INADDY_LOOPBACK
addr_.sin_addr.s_addr = sockets::hostToNetwork32(ip);
addr_.sin_port = sockets::hostToNetwork16(port);
}
}
它起始就是我们编最简单socket程序时sockaddr_in结构体填充的一个封装。
然后主函数中声明了一个loop对象,EventLoop类的功能就不用说了,Reactor循环处理事件的一个封装。给出它的成员函数:
bool looping_; /* atomic */
bool quit_; /* atomic and shared between threads, okay on x86, I guess. */
bool eventHandling_; /* atomic */
bool callingPendingFunctors_; /* atomic */
int64_t iteration_;
const pid_t threadId_; //当前所属对象线程id
Timestamp pollReturnTime_; //时间戳,poll返回的时间戳
boost::scoped_ptr<Poller> poller_; //poller对象
boost::scoped_ptr<TimerQueue> timerQueue_;
int wakeupFd_; //用于eventfd
// unlike in TimerQueue, which is an internal class,
// we don't expose Channel to client.
boost::scoped_ptr<Channel> wakeupChannel_; //wakeupfd所对应的通道,该通道会纳入到poller来管理
boost::any context_;
// scratch variables
ChannelList activeChannels_; //Poller返回的活动通道,vector<channel*>类型
Channel* currentActiveChannel_; //当前正在处理的活动通道
MutexLock mutex_;
std::vector<Functor> pendingFunctors_; // @GuardedBy mutex_
我们来分析一下打开的描述符,接下来本文打开的描述符都用绿色标注。
1.首先poller_对象会打开文件描述符值为3的pollfd,一般是是epollfd。
2.打开timerfd,这和接下来的eventfd是muduo使用的较新的系统调用之一。
3.wakeupFd_,I/O线程自己和自己通信的eventfd。
4.server肯定会调用listen,所以将来肯定会打开Acceptor类的listenfd(不一定是这个名字)。
5.不要忘了在Ac'ceptor中的用来处理过多文件描述符时,防止LT模式不断出发的idleFd。它是这样的:
idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))
好了继续看函数:
我们看到主函数中又创建了一个server对象,它是自定义的TestServer类型,由于采用基于对象的编程方式,所以该类在内部声明了TcpServer对象,相当于对TcpServer的一个再封装。
TestServer构造函数是这样的:
TestServer(EventLoop* loop, const InetAddress& listenAddr, int numThreads)
: loop_(loop),
server_(loop, listenAddr, "TestServer"),
numThreads_(numThreads){
server_.setConnectionCallback(boost::bind(&TestServer::onConnection, this, _1));
server_.setThreadNum(numThreads);
}
loop是主函数中main Reactor的EventLoop对象的指针,代表了主I/O线程。利用构造函数创建了TcpServer类型的server_对象,看下它的实现:
TcpServer的成员变量有:
//typedef boost::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::map<string, TcpConnectionPtr> ConnectionMap; //使用map维护了一个连接列表
EventLoop* loop_; // the acceptor loop
const string ipPort_; //服务器端口
const string name_; //服务器名
boost::scoped_ptr<Acceptor> acceptor_; // avoid revealing Acceptor,是accept所属的EventLoop,不一定是TcpServer所属的EventLoopA
boost::shared_ptr<EventLoopThreadPool> threadPool_;
//typedef boost::function<void (const TcpConnectionPtr&)> ConnectionCallback;
ConnectionCallback connectionCallback_;
//typedef boost::function<void (const TcpConnectionPtr&,Buffer*, Timestamp)> MessageCallback;
MessageCallback messageCallback_;
//typedef boost::function<void (const TcpConnectionPtr&)> WriteCompleteCallback;
WriteCompleteCallback writeCompleteCallback_;
//typedef boost::function<void(EventLoop*)> ThreadInitCallback;
ThreadInitCallback threadInitCallback_;
AtomicInt32 started_; //启动标记实际上是bool量,只不过用原子操作在0和1之间切换
// always in loop thread
int nextConnId_; //下一个连接id
ConnectionMap connections_; //连接列表
可以看出TcpServer的拥有一个Acceptor和EventLoopThreadPool。Acceptor是对accept连接一系列事件的封装,EventThreadPoll是I/O线程的池式结构。这就是muduo库的思想,multi Reactor模型,主函数中一个main Reactor,I/O线程池中存放的都是有用Reactor的线程。来一个给分配一个。
所以,现在可以看出TcpServer具备了接受客户端连接,分配I/O线程给客户端的功能。它的构造函数:
TcpServer::TcpServer(EventLoop* loop,
const InetAddress& listenAddr,
const string& nameArg,
Option option)
: loop_(CHECK_NOTNULL(loop)), //不能为空,否则触发FATAL
ipPort_(listenAddr.toIpPort()), //端口号
name_(nameArg), //名称
acceptor_(new Acceptor(loop, listenAddr, option == kReusePort)), //创建Acceptor,使用scoped_ptr管理
threadPool_(new EventLoopThreadPool(loop, name_)), //I/O线程池
connectionCallback_(defaultConnectionCallback),
messageCallback_(defaultMessageCallback),
nextConnId_(1) //下一个已连接编号id
{
acceptor_->setNewConnectionCallback(
//Acceptor::handleRead函数中会回调用TcpServer::newConnection
//_1对应得socket文件描述符,_2对应的是对等方的地址
boost::bind(&TcpServer::newConnection, this, _1, _2)); //设置一个连接回调函数
}
在构造函数重我们给acceptor绑定了一个newConnection回调函数,所以当有客户端连接触发accept时,会调用该函数,我们来看一下该函数:
//新连接处理函数
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
loop_->assertInLoopThread();
//使用round-robin选组一个I/O loop
EventLoop* ioLoop = threadPool_->getNextLoop();
char buf[64];
snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_); //端口+连接id
++nextConnId_; //++之后就是下一个连接id
string connName = name_ + buf;
//构造本地地址
InetAddress localAddr(sockets::getLocalAddr(sockfd));
TcpConnectionPtr conn(new TcpConnection(ioLoop, //创建一个连接对象,ioLoop是round-robin选择出来的
connName,
sockfd,
localAddr,
peerAddr));
//TcpConnection的use_count此处为1,新建了一个Tcpconnection
connections_[connName] = conn;
//TcpConnection的use_count此处为2,因为加入到connections_中。
//实际TcpServer的connectionCallback等回调函数是对conn的回调函数的封装,所以在这里设置过去
conn->setConnectionCallback(connectionCallback_);
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
//将TcpServer的removeConnection设知道了TcpConnection的关闭回调函数中
conn->setCloseCallback(
boost::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
//调用TcpConenction:;connectEstablished函数内部会将use_count加一然后减一,此处仍为2
//但是本函数介绍结束后conn对象会析构掉,所以引用计数为1,仅剩connections_列表中存活一个
}
当TcpServer的Acceptor获得一个新连接时,会调用TcpServer的newConnection函数,函数开始就有这一句:
EventLoop* ioLoop = threadPool_->getNextLoop();
它实际上是使用round-robin算法为新连接选择一个从I/O线程池中选择一个sub Reactor(说I/O线程也行,下文都说sub Reactor),它的函数是这样的:
//round-robin
EventLoop* EventLoopThreadPool::getNextLoop()
{
baseLoop_->assertInLoopThread();
assert(started_);
EventLoop* loop = baseLoop_;
//如果loops_为空,说明我们没有创建其他EventLoopThread,只有一个main Reactor,那么直接返回baseLoop_
if (!loops_.empty())
{
// round-robin round-robin
loop = loops_[next_];
++next_;
if (implicit_cast<size_t>(next_) >= loops_.size())
{
next_ = 0;
}
}
return loop;
}
从I/O线程池中选择一个sub Reactor,如果之前没有设置I/O线程池,那就返回main Reactor。
给个图直观感受一下:
那么是什么时候设定的呢?本文举的的例子中就有:
TestServer(EventLoop* loop, const InetAddress& listenAddr, int numThreads)
: loop_(loop),
server_(loop, listenAddr, "TestServer"),
numThreads_(numThreads){
server_.setConnectionCallback(boost::bind(&TestServer::onConnection, this, _1));
server_.setThreadNum(numThreads);
调用TcpServer的setThreadNum函数,看一下:
void TcpServer::setThreadNum(int numThreads)
{
assert(0 <= numThreads);
threadPool_->setThreadNum(numThreads); //设置I/O线程个数,不包含main Reactor
}
他直接调用了TcpServer成员也就是I/O线程池,类型是 boost::shared_ptr<EventLoopThreadPool>的成员函数,不要看错,这个就是之前的I/O线程池,这名字起的老是让人不再上下文中误认为线程池。看看下EventLoopThreadPool的serTreadNum()函数,只有一句:
void setThreadNum(int numThreads) { numThreads_ = numThreads; }
但是,当客户端或者说本文所用的例子中主函数调用start(),就会调用TcpServer的start()函数:
//该函数可以重复调用
//该函数可以跨线程调用
void TcpServer::start()
{
if (started_.getAndSet(1) == 0) //先get然后得到结果是0,然后赋值为1,以后都为1就不会进入if语句
{
threadPool_->start(threadInitCallback_); //启动线程池
assert(!acceptor_->listenning());
//因为acceptor是指针指征,库函数get_pointer可以返回原生指针
loop_->runInLoop(
boost::bind(&Acceptor::listen, get_pointer(acceptor_)));
}
}
该函数就会启动I/O线程池,具体EventLoopThreadPool的start()函数是这样的:
//启动EventLoopThread池
void EventLoopThreadPool::start(const ThreadInitCallback& cb)
{
assert(!started_);
baseLoop_->assertInLoopThread();
started_ = true;
for (int i = 0; i < numThreads_; ++i)
{
char buf[name_.size() + 32];
snprintf(buf, sizeof buf, "%s%d", name_.c_str(), i);
EventLoopThread* t = new EventLoopThread(cb, buf); //创建若干个I/O线程
threads_.push_back(t); //压入到threads_
loops_.push_back(t->startLoop()); //启动每个EventLoopThread线程进入loop(),并且把返回的每个EventLoop指针压入到loops_
}
if (numThreads_ == 0 && cb) //创建0个也就是没有创建EventLoopThread线程
{
//只有一个EventLoop,在这个EventLoop进入事件循环之前,调用cb
cb(baseLoop_);
}
}
其中它会创建多个EventLoopThread并调用每个的startLoop()函数,这个需要看一下:
//启动EventLoopThread中的loop循环,内部实际调用thread_.start
EventLoop* EventLoopThread::startLoop()
{
assert(!thread_.started());
thread_.start();
{
MutexLockGuard lock(mutex_);
while (loop_ == NULL)
{
cond_.wait();
}
}
return loop_;
}
关于startloop()函数之前分析过,它启动了每个EventLoopThread的成员thread,是一个线程,启动后自然会执行线程函数:
//该函数是EventLoopThread类的核心函数,作用是启动loop循环
//该函数和上面的startLoop函数并发执行,所以需要上锁和condition
void EventLoopThread::threadFunc()
{
EventLoop loop;
if (callback_)
{
callback_(&loop); //构造函数传递进来的,线程启动执行回调函数
}
{
MutexLockGuard lock(mutex_);
loop_ = &loop; //然后loop_指针指向了这个创建的栈上的对象,threadFunc退出之后,这个指针就失效了
cond_.notify(); //该函数退出,意味着线程就推出了,EventLoopThread对象也就没有存在的价值了。但是muduo的EventLoopThread
//实现为自动销毁的。一般loop函数退出整个程序就退出了,因而不会有什么大的问题,
//因为muduo库的线程池就是启动时分配,并没有释放。所以线程结束一般来说就是整个程序结束了。
}
loop.loop(); //开始loop循环
//assert(exiting_);
loop_ = NULL;
}
倒数第四行很明显,开始loop循环,也就是到这里线程池中每个EventLoopThread都会进入自己的poll状态,成为一个真正的Reactor。
所以,muduo库选择单线程模式还是多线程模式就是一句话xxx.setThreadNum(xx)。
再回头看刚才的函数:
//新连接处理函数
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
loop_->assertInLoopThread();
//使用round-robin选组一个I/O loop
EventLoop* ioLoop = threadPool_->getNextLoop();
char buf[64];
snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_); //端口+连接id
++nextConnId_; //++之后就是下一个连接id
string connName = name_ + buf;
//构造本地地址
InetAddress localAddr(sockets::getLocalAddr(sockfd));
//typedef std::map<string, TcpConnectionPtr> ConnectionMap; //使用map维护了一个连接列表
TcpConnectionPtr conn(new TcpConnection(ioLoop, //创建一个连接对象,ioLoop是round-robin选择出来的
connName,
sockfd,
localAddr,
peerAddr));
//TcpConnection的use_count此处为1,新建了一个Tcpconnection
connections_[connName] = conn;
//TcpConnection的use_count此处为2,因为加入到connections_中。
//实际TcpServer的connectionCallback等回调函数是对conn的回调函数的封装,所以在这里设置过去
conn->setConnectionCallback(connectionCallback_);
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
//将TcpServer的removeConnection设知道了TcpConnection的关闭回调函数中
conn->setCloseCallback(
boost::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
//调用TcpConenction:;connectEstablished函数内部会将use_count加一然后减一,此处仍为2
//但是本函数介绍结束后conn对象会析构掉,所以引用计数为1,仅剩connections_列表中存活一个
}
执行完getNextLoop后,我们现在知道我们获得了一个Reactor,可能是main,也可能不是,取决于我们是否使用并启动了EventLoopThreadPool。
话说回来,我们之前一个在讨论Acceptor接收连接,TcpServer分配I/O线程,但是,一个服务端最基本的处理通信细节的功能在哪里呢?
还是在这个函数里,我们题目中有TcpConnection几个字,但是它到这里才正式登场。
我们前面说了,有连接到来,Acceptor调用TcpServer的newConnection()函数,那现在我们看到newConnection()函数创建了一个栈上的TcpConnection的智能指针对象conn,该对象把Acceptor传来的已连接套接字sockfd一封装,再设置各种回调函数,把自己放入map中保存,然后再把自己加入到(runInLoop可能导致异步加入)通过getNextLoop的Reactor之中!这是什么鬼?
实际上TcpConnection的功能已经很明显了,就是对已连接套接字的一个抽象。由于muduo库的设计,TcpServer对客端而言就是一个服务器,客端使用TcpServer自然会在它上面设置各种自己网络程序的回调函数,对各种连接的处理。所以TcpConnection就会在TcpServer中封装已连接套接字和回调函数,加入到round-robin所选择的Reactor之中,
然后该fd以后触发的所有可读可写事件都由该Reactor处理。这就是multi Reactor的思想。
那么runInLoop()函数我们知道实际上这样的:
//顾名思义,在I/O线程中调用某个函数,该函数可以跨线程调用
void EventLoop::runInLoop(const Functor& cb)
{
//如果是在当前I/O线程中调用,就同步调用cb回调函数
if (isInLoopThread())
{
cb();
}
else
{
//否则在其他线程中调用,就异步将cb添加到任务队列当中,以便让EventLoop真实对应的I/O线程执行这个回调函数
queueInLoop(cb);
}
}
如果是当前I/O线程立即执行cb,如果不是,会将它加入任务队列,然后通过queueInLoop函数中唤醒wakeFd_,使得I/O线程的loop()函数进行doPendingFunctors,其实就是从poll的wait状态退出,执行异步队列的回调函数,这些以前的博客都剖析过。
核心就在这一句:
ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
让ioLoop去调度runInLoop,这不正好保证了函数执行在当前EventLoop所在的I/O线程中吗?那么I/O线程会立即执行该回调函数。
所以核心还是这个回调函数,因为我们虽说给TcpConnection分配了一个Reactor,但是从来没有注册进入Poller啊,这有什么用?
那么就来看一下它注册的回调函数:
void TcpConnection::connectEstablished()
{
loop_->assertInLoopThread(); //断言处于loop线程
assert(state_ == kConnecting); //断言处于未连接状态
setState(kConnected); //将状态设置为已连接
//之前引用计数为2
channel_->tie(shared_from_this()); //将自身这个TcpConnection对象提升,由于是智能指针,所以不能直接用this
//shared_from_this()之后引用计数+1,为3,但是shared_from_this()是临时对象,析构后又会减一,
//而tie是weak_ptr并不会改变引用计数,所以该函数执行完之后引用计数不会更改
channel_->enableReading(); //一旦连接成功就关注它的可读事件,加入到Poller中关注
connectionCallback_(shared_from_this());
}
boom!事已至此,不必多说了,我们看到它调用了TcpConnection所拥有的Channel的enableReading()函数,该函数实际上底层是将Channel所拥有的fd注册进入Poller中的,所以现在我们的已连接套接字就这样成供地加入进了Reactor,那么,客户端连接,随便来吧,回调函数都注册好了,就等处理你了。
实际上函数还有一句:
channel_->tie(shared_from_this());
它底层是这么实现的:
void Channel::tie(const boost::shared_ptr<void>& obj)
{
tie_ = obj;
tied_ = true;
}
可见是把TcpConnection型智能指针存入了Channel之中。我们知道之前enableReading()已经把连接套接字加入了Reactor,那么发生可读时间是,会首先调用它的handleEvent()函数,是这样是实现的:
//处理所有发生的事件,如果活着,底层调用handleEventWithGuard
void Channel::handleEvent(Timestamp receiveTime) //事件到来调用handleEvent处理
{
boost::shared_ptr<void> guard;
if (tied_)
{
guard = tie_.lock();
if (guard)
{
handleEventWithGuard(receiveTime);
}
}
else
{
handleEventWithGuard(receiveTime);
}
}
handleEventWithGuard会进行recv处理,POLLIN可读就调用Channel之前注册的读回调函数,此外还有写回调,关闭回调等,这些都是TcpServer注册到TcpConnection在关联到TcpConnection所拥有的已连接套接字的,通过执行newConnection()函数之时,你可以回头看看。
可见,处理连接事件时,对TcpConnection的智能指针进行了提升,因为已连接套接字fd的生命期是由TcpConnection管理的,我们要确保处理事件时该对象还存活,否则使用一个已经死亡的对象,结果只有core dump。
还有一点是关闭该套接字,newConnection()函数中也注册了这个,handleEvent()是就会执行这个:
//该函数会在TcpConnection断开后放进I/O事件处理队列,等待Functors处理
void TcpConnection::connectDestroyed()
{
loop_->assertInLoopThread();
if (state_ == kConnected) //已删除函数
{
setState(kDisconnected);
channel_->disableAll();
connectionCallback_(shared_from_this()); //回调用户函数
}
channel_->remove(); //从通道和Poller中移除
}
最后上两张时序图,理解理解:
连接建立:
r:
连接关闭:
好吧,这篇更长。