muduo库的TcpServer和TcpConnection用法

(本文地址: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:

连接关闭:



好吧,这篇更长。









  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值