muduo : TcpConnection

引用

前面学习了TcpServer的实现,TcpServer对每个连接都会新建一个TcpConnection(使用shared_ptr管理)。接下来学习一下TcpConnection的设计细节。

连接状态

muduo对于一个连接的从生到死进行了状态的定义,类似一个状态机。

enum States { kDisconnected, kConnecting, kConnected, kDisconnecting };

分别代表:已经断开、初始状态、已连接、正在断开

成员变量

private
  EventLoop* loop_; // 神一样的存在,EventLoop
  string name_;
  States state_;  // FIXME: use atomic variable
  // we don't expose those classes to client.
  boost::scoped_ptr<Socket> socket_;   // 已连接套接字
  boost::scoped_ptr<Channel> channel_; // 已连接套接字对应的Channel
  InetAddress localAddr_;
  InetAddress peerAddr_;
  ConnectionCallback connectionCallback_;
  MessageCallback messageCallback_;
  WriteCompleteCallback writeCompleteCallback_;
  ConnectionCallback closeCallback_;
  Buffer inputBuffer_;  // 输入缓冲区
  // MutexLock mutex_;
  Buffer outputBuffer_; // 输出缓冲区
  boost::any context_;  // 存放上下文

先理解上面的 loop_, socket_, channel_好了,不明白请翻阅前几篇文章。

构造/析构函数

TcpConnection::TcpConnection(EventLoop* loop,
                             const string& name__,
                             int sockfd,
                             const InetAddress& localAddr,
                             const InetAddress& peerAddr)
  : loop_(CHECK_NOTNULL(loop)),
    name_(name__),
    state_(kConnecting),          // 初始状态为kConnecting
    socket_(new Socket(sockfd)),  // RAII管理已连接套接字
    channel_(new Channel(loop, sockfd)), // 使用Channel管理套接字上的读写
    localAddr_(localAddr),
    peerAddr_(peerAddr)
{
  // 设置一些回调函数(好的,这很muduo)
  // 在已连接套接字可读时,调用TcpConnection::handleRead,进而调用用户设置的回调函数messageCallback_
  channel_->setReadCallback(
      boost::bind(&TcpConnection::handleRead, this, _1));
  channel_->setWriteCallback(
      boost::bind(&TcpConnection::handleWrite, this));
  channel_->setCloseCallback(
      boost::bind(&TcpConnection::handleClose, this));
  channel_->setErrorCallback(
      boost::bind(&TcpConnection::handleError, this));
  LOG_DEBUG << "TcpConnection::ctor[" <<  name_ << "] at " << this
    << " fd=" << sockfd;
}

TcpConnection::~TcpConnection()
{
  LOG_DEBUG << "TcpConnection::dtor[" <<  name_ << "] at " << this
    << " fd=" << channel_->fd();
}

构造函数在初始化列表中对socket、channel等进行了初始化,在函数体中设置了回调函数。

TcpConnection::handleRead

void TcpConnection::handleRead(Timestamp receiveTime)
{
  loop_->assertInLoopThread();
  int savedErrno;
  ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
  if (n > 0)
  {
    // 调用回调函数,使用shared_from_this()得到自身的shared_ptr, 延长了该对象的生命期,保证了它的生命期长过messageCallback_函数,messageCallback_能安全的使用它。
    messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
  }
  else if (n == 0)
  {
    handleClose();
  }
  else
  {
    // check savedErrno
  }
}

前面提到了,在已连接套接字可读时,调用TcpConnection::handleRead,进而调用用户设置的回调函数messageCallback_

连接的断开

这是muduo提到的三个半事件中的另一个事件,即处理连接的断开。这个过程比较绕,是这样事儿的,图中的X表示TcpConnection通常在此析构:

这里写图片描述

为什么看上去这么绕?

主要是由EventLoop::loop的结构造成的,再次贴出事件循环的主体:

void EventLoop::loop()
{
  // ...
  while (!quit_)
  {
    activeChannels_.clear();
    // 1. 通过poller获取就绪的channel,放到activeChannels_中,poller会将发生的事件类型填写到channel的revents_中,供Channel::handleEvent使用
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
    ++iteration_;

    eventHandling_ = true;
    for (ChannelList::iterator it = activeChannels_.begin();
        it != activeChannels_.end(); ++it)
    {
      currentActiveChannel_ = *it;
      // 2. 调用channel的事件处理函数handleEvent,根据poller设置的发生的事件类型,调用相应的用户回调函数
      currentActiveChannel_->handleEvent(pollReturnTime_);
    }
    currentActiveChannel_ = NULL;
    eventHandling_ = false;

    // 3. 处理其他函数,由runInLoop加入到线程的任务容器
    doPendingFunctors();

  }
//...
}

一个对象在被使用时,必然是不能被析构的。这里的currentActiveChannel->handleEvent在执行时必须保证currentActiveChannel不会被析构,一般小的程序当然不会有这么显而易见的错误。不过对于稍微复杂点的程序是需要考虑的。

TcpConnection是拥有一个Channel的,它负责管理该Channel的生命周期,但是却必须把Channel的裸指针暴露给EventLoop(确切的讲是暴露给Poller),因为Poller需要对Channel的事件进行管理(添加、修改、删除)。

此外,TcpConnection是在TcpServer中创建的,但是连接断开事件是被TcpConnection得知的。TcpConnection需要将TcpServer中存放自己shared_ptr的容器中的对应响删除掉,然后才能析构。为了在TcpServer进行erase时不会将TcpConnection析构,需要使用一定的手段(shared_from_this

而且上面讲到了,TcpConnection的Channel的裸指针暴露给了EventLoop的Poller。根据上图的调用链,不能在使用Channel时将TcpConnection析构,因为TcpConnection析构时,Channel也会被析构,这就造成正在使用一个对象时,它确被析构了,将导致严重的问题。所以muduo使用queueInLoop使得TcpConnection的析构点位于使用完channel之后的位置(见EventLoop::loop)。

看来,对于对象的生命期管理有时还是很负责。

下面会继续进行分析。

TcpConnection::handleClose

void TcpConnection::handleClose()
{
  loop_->assertInLoopThread();
  // we don't close fd, leave it to dtor, so we can find leaks easily.
  setState(kDisconnected); // 设置状态为kDisconnected,表示已断开
  channel_->disableAll();  // 移除注册的事件,使用epoll时是EPOLL_CTL_DEL

  TcpConnectionPtr guardThis(shared_from_this()); // 延长本对象的生命周期,引用计数为2

  // 调用用户回调函数
  connectionCallback_(guardThis); // 参数为shared_ptr,保证了 connectionCallback_能安全的使用本对象

  // 调用TcpServer::removeConnection
  // must be the last line
  closeCallback_(guardThis);
}

连接断开时,会调用TcpConnection::handleClose;接着调用用户回调connectionCallback_;最后调用closeCallback_,即TcpServer::removeConnection(TcpServer创建TcpConnection时设置的)

TcpServer::removeConnection

void TcpServer::removeConnection(const TcpConnectionPtr& conn)
{
  loop_->runInLoop(boost::bind(&TcpServer::removeConnectionInLoop, this, conn));
}

void TcpServer::removeConnectionInLoop(const TcpConnectionPtr& conn)
{
  loop_->assertInLoopThread();
  LOG_INFO << "TcpServer::removeConnectionInLoop [" << name_
           << "] - connection " << conn->name();

  // 根据conn的name,从map容器中删除,此时引用计数会减1。erase之前引用计数为2(由前面的shared_from_this()保证),所以执行完erase,引用计数变为1
  size_t n = connections_.erase(conn->name());
  assert(n == 1);

  // 然后调用conn->connectDestroyed
  EventLoop* ioLoop = conn->getLoop();
  ioLoop->queueInLoop(
      boost::bind(&TcpConnection::connectDestroyed, conn)); // bind延长了conn的生命期,connectDestroyed完成后,TcpConnection被析构。
  // FIXME wake up ?
}

TcpServer先将该conn从map容器中删除,因为erase之前使用了shared_from_this,所以erase之前引用计数为2,那么erase之后引用计数将变为1。
如果没用shared_from_this,仅仅传递了一个裸指针过来,erase之后引用计数变为0,那么该TcpConnection会被析构!这意味着TcpConnection的Channel也会被析构,可是你现在正在使用该Channel啊(结合上图看),怎么能在使用某个对象的时候把它析构呢,这是严重的错误。所以muduo使用shared_ptr管理TcpConnection,避免了上述问题。

最后queueInLoop就是将TcpConnection::connectDestroyed函数移动到EventLoop中执行,执行位置就是在Channel->handleEvent之后,此时可以安全的析构TcpConnection。(这么做的原因见前面)

注意上面最后的boost::bind,它让TcpConnection的生命期长到调用connectDestroyed的时刻。在connectDestroyed执行完之后,TcpConnection才被析构。

TcpConnection::connectDestroyed

void TcpConnection::connectDestroyed()
{
  loop_->assertInLoopThread();
  if (state_ == kConnected)
  {
    setState(kDisconnected);
    channel_->disableAll();

    connectionCallback_(shared_from_this());
  }

  // 将EventLoop.Poller中的该channel从容器中删除
  loop_->removeChannel(get_pointer(channel_));
}

TcpConnection::connectDestroyed是该对象析构前调用的最后一个成员函数,它会通知用户连接已经断开。

我已经用了洪荒之力来理解TcpConnection,实在是太绕了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值