muduo網路庫學習筆記(13):TcpConnection生命期的管理

li27z發表於2016-11-13

本篇通過分析muduo中TcpConnection對斷開連線事件的處理,來學習muduo網路庫對TcpConnection生命期的管理。

TcpConnection對連線斷開事件的處理

首先,我們來看一下TcpConnection處理連線斷開事件時函式呼叫的流程:
這裡寫圖片描述
我們這裡所指的連線斷開,都是指被動關閉,即對方先關閉連線,本地read(2)返回0,觸發關閉邏輯。

分析:一個伺服器(TcpServer)維護了一個連線列表,當一個連線斷開時,TcpConnection中的通道處於活躍的狀態,EventLoop的事件迴圈返回了這個活躍的通道,然後呼叫通道的handleEvent()函式來處理。連線關閉是可讀事件,進而回撥了TcpConnection的handleRead()函式,handleRead()中又呼叫了read()返回為0,判斷read()返回為0又會呼叫handleClose()函式。handleClose()函式會回撥TcpServer的removeConnection()函式,其中會呼叫erase()將該連線從連線列表移除。

這裡我們需要注意的是——一般情況下,將連線從連線列表移除後,我們就可以將這個連線物件銷燬(delete)掉了,但是在這裡我們不能立即銷燬這個連線物件,原因如下:

如果我們銷燬了這個物件,TcpConnection所包含的Channel物件也就跟著被銷燬了,而我們當前正在呼叫Channel物件的handleEvent()函式,就會出現core dump。所以,我們必須保證TcpConnection的生存期長於Channel::handleEvent()函式。

muduo選擇用智慧指標shared_ptr來管理TcpConnection的生命期,並且讓TcpConnection類繼承自boost::enable_shared_from_this。

原始碼分析

具體程式碼改動如下:

程式碼片段1:Channel的改動
檔名:Channel.cc

// 解構函式中會判斷Channel是否仍處於事件處理狀態
// 在事件處理期間Channel物件不會析構
Channel::~Channel()
{
  assert(!eventHandling_);
}

// 開始處理事件之前,會將事件處理標誌位置為true
// 直到事件處理完,事件處理標誌位再置為false
void Channel::handleEventWithGuard(Timestamp receiveTime)
{
  eventHandling_ = true;
  if ((revents_ & POLLHUP) && !(revents_ & POLLIN))
  {
    if (logHup_)
    {
      LOG_WARN << "Channel::handle_event() POLLHUP";
    }
    if (closeCallback_) closeCallback_();
  }
  if (revents_ & POLLNVAL)
  {
    LOG_WARN << "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;
}
程式碼片段2:TcpConnection::handleRead()函式的改動
檔名:TcpConnection.cc

void TcpConnection::handleRead(Timestamp receiveTime)
{
  loop_->assertInLoopThread();
  int savedErrno = 0;
  char buf[65536];
  ssize_t n = ::read(channel_->fd(), buf, sizeof buf);
  // 根據read(2)的返回值分別呼叫messageCallback_()、handleClose()和handleError()
  if (n > 0)
  {
    messageCallback_(shared_from_this(), buf, n);
  }
  else if (n == 0)
  {
    handleClose();
  }
  else
  {
    errno = savedErrno;
    LOG_SYSERR << "TcpConnection::handleRead";
    handleError();
  }  
}
程式碼片段3:TcpConnection::handleClose()函式
檔名:TcpConnection.cc

void TcpConnection::handleClose()
{
  loop_->assertInLoopThread();
  LOG_TRACE << "fd = " << channel_->fd() << " state = " << state_;
  // 斷定此時連線處於已連線狀態
  assert(state_ == kConnected);
  // we don't close fd, leave it to dtor, so we can find leaks easily.
  channel_->disableAll();

  // must be the last line
  // 呼叫所設定的連線關閉回撥函式
  closeCallback_(shared_from_this()); 
}
程式碼片段4:TcpServer::removeConnection()函式
檔名:TcpServer.cc

void TcpServer::removeConnection(const TcpConnectionPtr& conn)
{
  loop_->assertInLoopThread();
  LOG_INFO << "TcpServer::removeConnectionInLoop [" << name_
           << "] - connection " << conn->name();
  // 將斷開的連線從連線列表中移除
  size_t n = connections_.erase(conn->name());
  (void)n;
  assert(n == 1);
  // 此處一定要用EventLoop::queueInLoop(),避免Channel物件被提前銷燬
  // 這裡用boost::bind讓TcpConnection的生命期長到呼叫connectDestroyed()的時刻
  // 使用boost::bind得到一個boost::function物件,會把conn傳遞進去,引用計數會加1
  loop_->queueInLoop(
      boost::bind(&TcpConnection::connectDestroyed, conn));
}

Channel::handleEvent()事件處理完後就會呼叫functors(見部落格“muduo網路庫學習筆記(11):有用的runInLoop()函式”)即呼叫TcpConnection::connectDestroyed()。

程式碼片段5:TcpConnection::connectDestroyed()函式
檔名:TcpConnection.cc

// connectDestroyed()是TcpConnection析構前最後呼叫的一個成員函式
// 它通知使用者連線已斷開
void TcpConnection::connectDestroyed()
{
  loop_->assertInLoopThread();
  assert(state_ == kConnected);
  setState(kDisconnected);
  channel_->disableAll();
  connectionCallback_(shared_from_this());  // 回撥使用者設定的回撥函式
  loop_->removeChannel(get_pointer(channel_));  // 從poll/epoll中移除channel
}

boost::enable_shared_from_this

由於TcpConnection模糊的生命期,我們用到了shared_ptr來管理它的生命期,並讓TcpConnection類繼承自boost::enable_shared_from_this。那麼,boost::enable_shared_from_this的作用是什麼呢?

使用示例:

#include <boost/enable_shared_from_this.hpp>
#include <boost/shared_ptr.hpp>
#include <cassert>

// class Y繼承自boost::enable_shared_from_this<Y>
class Y: public boost::enable_shared_from_this<Y>
{
public:
    boost::shared_ptr<Y> f()
    {
        return shared_from_this();  // 返回指向自身的shared_ptr
    }

    Y* f2()
    {
        return this; 
    }
};

int main()
{
    boost::shared_ptr<Y> p(new Y);  // p的引用計數為1
    boost::shared_ptr<Y> q = p->f();  // 將當前物件轉換為一個shared_ptr,賦值給q,此時p/q引用計數為2

    Y* r = p->f2(); 
    assert(p == q);  // 斷言正確
    assert(p.get() == r);  // 斷言正確

    std::cout << p.use_count() << std::endl;  // 輸出:2

    // 構造一個shared_ptr物件s,將r賦給s
    // 列印出的s的引用計數應該為1,而不是3
    // 因為此時構造了一個新的、獨立的shared_ptr物件,而不是將一個shared_ptr物件賦值給另一個shared_ptr物件
    boost::shared_ptr<Y> s(r); 
    std::cout << s.use_count() << std::endl;
    assert(p == s);  // 斷言失敗

    return 0;
}

測試結果如圖:
這裡寫圖片描述

所以,在TcpConnection的生命期管理過程中,如果我們直接用this指標傳遞物件,可能會構建一個新的shared_ptr物件,並不是直接將我們之前管理的物件的shared_ptr拷貝過去從而使引用計數加1,故我們需要用到boost::enable_shared_from_this的shared_from_this()函式。

相關文章