服务器监听类Acceptor及Tcp连接TcpConnection的建立与关闭(fd耗尽的解决方案)

https://blog.csdn.net/sinat_35261315/article/details/78343266 

通常服务器在处理客户端连接请求时,为了不阻塞在accept函数上,会将监听套接字注册到io复用函数中,当客户端请求连接时,监听套接字变为可读,随后在回调函数调用accept接收客户端连接。muduo将这一部分封装成了Acceptor类,用于执行接收客户端请求的任务。

类的定义如下,主要就是监听套接字变为可读的回调函数

class EventLoop;
class InetAddress;

///
/// Acceptor of incoming TCP connections.
///
/* 
 * 对TCP socket, bind, listen, accept的封装 
 * 将sockfd以Channel的形式注册到EventLoop的Poller中,检测到sockfd可读时,接收客户端
 */
class Acceptor : noncopyable
{
 public:
  typedef std::function<void (int sockfd, const InetAddress&)> NewConnectionCallback;

  Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport);
  ~Acceptor();

  /* 由服务器TcpServer设置的回调函数,在接收完客户端请求后执行,用于创建TcpConnection */
  void setNewConnectionCallback(const NewConnectionCallback& cb)
  { newConnectionCallback_ = cb; }

  bool listenning() const { return listenning_; }
  /* 调用listen函数,转为监听套接字,同时将监听套接字添加到Poller中 */
  void listen();

 private:
  /* 回调函数,当有客户端请求连接时执行(监听套接字变为可读) */
  void handleRead();

  /* 事件驱动主循环 */
  EventLoop* loop_;
  /* 封装socket的一些接口 */
  Socket acceptSocket_;
  /* Channel,保存着sockfd,被添加到Poller中,等待被激活 */
  Channel acceptChannel_;
  /* 
   * 当有客户端连接时首先内部接收连接,然后调用的用户提供的回调函数
   * 客户端套接字和地址作为参数传入
   */
  NewConnectionCallback newConnectionCallback_;
  bool listenning_;
  /* 
   * Tcp连接建立的流程
   *    1.服务器调用socket,bind,listen开启监听套接字监听客户端请求
   *    2.客户端调用socket,connect连接到服务器
   *    3.第一次握手客户端发送SYN请求分节(数据序列号)
   *    4.服务器接收SYN后保存在本地然后发送自己的SYN分节(数据序列号)和ACK确认分节告知客户端已收到
   *      同时开启第二次握手
   *    5.客户端接收到服务器的SYN分节和ACK确认分节后保存在本地然后发送ACK确认分节告知服务器已收到
   *      此时第二次握手完成,客户端connect返回
   *      此时,tcp连接已经建立完成,客户端tcp状态转为ESTABLISHED,而在服务器端,新建的连接保存在内核tcp
   *      连接的队列中,此时服务器端监听套接字变为可读,等待服务器调用accept函数取出这个连接
   *    6.服务器接收到客户端发来的ACK确认分节,服务器端调用accept尝试找到一个空闲的文件描述符,然后
   *      从内核tcp连接队列中取出第一个tcp连接,分配这个文件描述符用于这个tcp连接
   *      此时服务器端tcp转为ESTABLISHED,三次握手完成,tcp连接建立
   *      
   * 服务器启动时占用的一个空闲文件描述符,/dev/null,作用是解决文件描述符耗尽的情况
   * 原理如下:
   *    当服务器端文件描述符耗尽,当客户端再次请求连接,服务器端由于没有可用文件描述符
   *        会返回-1,同时errno为EMFILE,意为描述符到达hard limit,无可用描述符,此时服务器端
   *        accept函数在获取一个空闲文件描述符时就已经失败,还没有从内核tcp连接队列中取出tcp连接
   *        这会导致监听套接字一直可读,因为tcp连接队列中一直有客户端的连接请求
   *        
   *    所以服务器在启动时打开一个空闲描述符/dev/null(文件描述符),先站着'坑‘,当出现上面
   *        情况accept返回-1时,服务器暂时关闭idleFd_让出'坑',此时就会多出一个空闲描述符
   *        然后再次调用accept接收客户端请求,然后close接收后的客户端套接字,优雅的告诉
   *        客户端关闭连接,然后再将'坑'占上
   */
  int idleFd_;

};


一个不好理解的变量是idleFd_;,它是一个文件描述符,这里是打开"/dev/null"文件后返回的描述符,用于解决服务器端描述符耗尽的情况。 
如果当服务器文件描述符耗尽后,服务器端accept还没等从tcp连接队列中取出连接请求就已经失败返回了,此时内核tcp队列中一直有客户端请求,内核会一直通知监听套接字,导致监听套接字一直处于可读,在下次直接poll函数时会直接返回。 
解决的办法就是在服务器刚启动时就预先占用一个文件描述符,通常可以是打开一个文件,这里是"/dev/null"。此时服务器就有一个空闲的文件描述符了,当出现上述情况无法取得tcp连接队列中的请求时,先关闭这个文件让出一个文件描述符,此时调用accept函数再次接收,由于已经有一个空闲的文件描述符了,
accept会正常返回,将连接请求从tcp队列中取出,然后优雅的关闭这个tcp连接(调用close函数),最后再打开"/dev/null"这个文件把”坑“占住。

成员函数的实现也有比较重点的地方,首先是构造函数

Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
  : loop_(loop),
    acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())),
    acceptChannel_(loop, acceptSocket_.fd()),
    listenning_(false),
    idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))
{
  assert(idleFd_ >= 0);
  /* 
   * setsockopt设置套接字选项SO_REUSEADDR,对于端口bind,如果这个地址/端口处于TIME_WAIT,也可bind成功
   * int flag = 1;
   * setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
   */
  acceptSocket_.setReuseAddr(true);
  /*
   * setsockopt设置套接字选项SO_REUSEPORT,作用是对于多核cpu,允许在同一个<ip, port>对上运行多个相同服务器
   * 内核会采用负载均衡的的方式分配客户端的连接请求给某一个服务器
   */
  acceptSocket_.setReusePort(reuseport);
  acceptSocket_.bindAddress(listenAddr);
  /* Channel设置读事件的回调函数,此时还没有开始监听这个Channel,需要调用Channel::enableReading() */
  acceptChannel_.setReadCallback(
      std::bind(&Acceptor::handleRead, this));
}


构造函数中为用于监听的套接字设置了SO_REUSEPORT和SO_REUSEADDR属性,一个是端口重用,一个是地址重用。

SO_REUSEPORT,端口重用,可以使用还处于TIME_WAIT状态的端口
SO_REUSEADDR,地址重用,服务器可以同时建立多个用于监听的socket,每个socket绑定的地址端口都相同,内核会采用负载均衡的方法将每个将每个客户端请求分配给某一个socket,可以很大程序的提高并发性,充分利用CPU资源
acceptChannel_用于保存这个用于监听的套接字,绑定回调函数,在合适的时机注册到Poller上(调用listen时)

void Acceptor::listen()
{
  loop_->assertInLoopThread();
  listenning_ = true;
  acceptSocket_.listen();
  /* 
   * 开始监听Channel,也就是设置fd关心的事件(EPOLLIN/EPOLLOUT等),然后添加到Poller中 
   * Poller中保存着所有注册到EventLoop中的Channel
   */
  acceptChannel_.enableReading();
}


比较重要的是事件处理函数,当监听套接字可读时,调用accept接收客户端请求,如果描述符耗尽,释放idleFd_重新accept,然后关闭,再占用idleFd_

/*
 * 当有客户端尝试连接服务器时,监听套接字变为可读,epoll_wait/poll返回
 * EventLoop处理激活队列中的Channel,调用对应的回调函数
 * 监听套接字的Channel的回调函数是handleRead(),用于接收客户端请求
 */
void Acceptor::handleRead()
{
  loop_->assertInLoopThread();
  InetAddress peerAddr;
  //FIXME loop until no more
  int connfd = acceptSocket_.accept(&peerAddr);
  if (connfd >= 0)
  {
    // string hostport = peerAddr.toIpPort();
    // LOG_TRACE << "Accepts of " << hostport;

    /* 
     * 如果设置了回调函数,那么就调用,参数是客户端套接字和地址/端口
     * 否则就关闭连接,因为并没有要处理客户端的意思
     * 
     * 这个回调函数是TcpServer中的newConnection,用于创建一个TcpConnection连接
     */
    if (newConnectionCallback_)
    {
      newConnectionCallback_(connfd, peerAddr);
    }
    else
    {
      sockets::close(connfd);
    }
  }
  else
  {
    LOG_SYSERR << "in Acceptor::handleRead";
    // Read the section named "The special problem of
    // accept()ing when you can't" in libev's doc.
    // By Marc Lehmann, author of libev.
    // 
    /* 解决服务器端描述符耗尽的情况,原因见.h文件 */
    if (errno == EMFILE)
    {
      ::close(idleFd_);
      idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
      ::close(idleFd_);
      idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
    }
  }
}


对文件描述符耗尽的处理比较重要,以前没怎么接触过

回调函数中调用的newConnectionCallback_函数是在Acceptor创建之初由TcpServer设置的(TcpServer表示服务器,内有一个监听类Acceptor),这个函数主要用于初始化一个TcpConnection,一个TcpConnection对象代表着一个tcp连接

TcpConnection的定义主要都是写set*函数,成员变量比较多,但是重要的就

事件驱动循环loop_
用于tcp通信的socket_
用于监听sockfd的channel_
输入输出缓冲区inputBuffer_/outputBuffer_
由TcpServer提供的各种回调函数
 

/* 事件驱动循环 */
  EventLoop* loop_;
  /* 每个tcp连接有一个独一无二的名字,建立连接时由TcpServer传入 */
  const string name_;
  StateE state_;  // FIXME: use atomic variable
  bool reading_;
  // we don't expose those classes to client.
  /* 用于tcp连接的套接字,以及用于监听套接字的Channel */
  std::unique_ptr<Socket> socket_;
  std::unique_ptr<Channel> channel_;
  /* 本地<地址,端口>,客户端<地址,端口>,由TcpServer传入 */
  const InetAddress localAddr_;
  const InetAddress peerAddr_;
  /* 连接建立后/关闭后的回调函数,通常是由用户提供给TcpServer,然后TcpServer提供给TcpConnection */
  ConnectionCallback connectionCallback_;
  /* 当tcp连接有消息通信时执行的回调函数,也是由用户提供 */
  MessageCallback messageCallback_;
  /* 
   * 写入tcp缓冲区之后的回调函数
   * 通常是tcp缓冲区满然后添加到应用层缓冲区后,由应用层缓冲区写入内核tcp缓冲区
   * 后执行,一般用户不关系这部分
   */
  WriteCompleteCallback writeCompleteCallback_;
  /* 高水位回调,设定缓冲区接收大小,如果应用层缓冲区堆积的数据大于某个给定值时调用 */
  HighWaterMarkCallback highWaterMarkCallback_;
  /* 
   * tcp连接关闭时调用的回调函数,由TcpServer设置,用于TcpServer将这个要关闭的TcpConnection从
   * 保存着所有TcpConnection的map中删除
   * 这个回调函数和TcpConnection自己的handleClose不同,后者是提供给Channel的,函数中会使用到
   * closeCallback_
   */
  CloseCallback closeCallback_;
  /* 高水位值 */
  size_t highWaterMark_;
  /* 输入输出缓冲区 */
  Buffer inputBuffer_;
  Buffer outputBuffer_; // FIXME: use list<Buffer> as output buffer.
  boost::any context_;
  // FIXME: creationTime_, lastReceiveTime_
  //        bytesReceived_, bytesSent_


首先是构造函数的实现,主要是为Channel提供各种回调函数

/* 
 * 构造函数,设置当fd就绪时调用的回调函数
 * Channel代表一个对fd事件的监听
 */
TcpConnection::TcpConnection(EventLoop* loop,
                             const string& nameArg,
                             int sockfd,
                             const InetAddress& localAddr,
                             const InetAddress& peerAddr)
  : loop_(CHECK_NOTNULL(loop)),
    name_(nameArg),
    state_(kConnecting),
    reading_(true),
    socket_(new Socket(sockfd)),
    channel_(new Channel(loop, sockfd)),
    localAddr_(localAddr),
    peerAddr_(peerAddr),
    highWaterMark_(64*1024*1024)
{
  /* 设置各种回调函数 */
  channel_->setReadCallback(
      std::bind(&TcpConnection::handleRead, this, _1));
  channel_->setWriteCallback(
      std::bind(&TcpConnection::handleWrite, this));
  channel_->setCloseCallback(
      std::bind(&TcpConnection::handleClose, this));
  channel_->setErrorCallback(
      std::bind(&TcpConnection::handleError, this));
  LOG_DEBUG << "TcpConnection::ctor[" <<  name_ << "] at " << this
            << " fd=" << sockfd;
  /*
   * 设置KEEP-ALIVE属性,如果客户端很久没有和服务器通讯,tcp会自动判断客户端是否还处于连接(类似心跳包)
   * 
   * int setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &sockopt, static_cast<socklen_t>(sizeof(sockopt)));
   */
  socket_->setKeepAlive(true);
}


回调函数的设置对应了Channel的hanleEvent函数中根据不同激活原因调用不同回调函数(handleEvent调用handleEventWithGuard)。 
另外,handleEvent中的tie_是对TcpConnection的弱引用(在后面设置),因为回调函数都是TcpConnection的,所以在调用之前需要确保TcpConnection没有被销毁,所以将tie_提升为shared_ptr判断TcpConnection是否还存在,之后再调用TcpConnection的一系列回调函数

/*
 * 根据fd激活事件的不同,调用不同的fd的回调函数
 */
void Channel::handleEvent(Timestamp receiveTime)
{
  /* 
   * RAII,对象管理资源
   * weak_ptr使用lock提升成shared_ptr,此时引用计数加一
   * 函数返回,栈空间对象销毁,提升的shared_ptr guard销毁,引用计数减一
   */
  std::shared_ptr<void> guard;
  if (tied_)
  {
    guard = tie_.lock();
    if (guard)
    {
      handleEventWithGuard(receiveTime);
    }
  }
  else
  {
    handleEventWithGuard(receiveTime);
  }
}

/*
 * 根据被激活事件的不同,调用不同的回调函数
 */
void Channel::handleEventWithGuard(Timestamp receiveTime)
{
  eventHandling_ = true;
  LOG_TRACE << reventsToString();
  if ((revents_ & POLLHUP) && !(revents_ & POLLIN))
  {
    if (logHup_)
    {
      LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLHUP";
    }
    if (closeCallback_) closeCallback_();
  }

  if (revents_ & POLLNVAL)
  {
    LOG_WARN << "fd = " << fd_ << " 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;
}


当TcpServer创建完TcpConnection后,会设置各种会调用书,然后调用TcpConnection的connectEstablished函数,主要用于将Channel添加到Poller中,同时调用用户提供的连接建立成功后的回调函数 
TcpServer创建并设置TcpConnection的部分,可以看到,TcpServer会将用户提供的所有回调函数都传给TcpConnection,然后执行TcpConnection的connectEstablished函数,这个函数的执行要放到它所属的那个事件驱动循环线程做,不要阻塞TcpServer线程(这个地方不是为了线程安全性考虑,因为TcpConnection本身就是在TcpServer线程创建的,暴露给TcpServer线程很正常,而且TcpServer中也记录着所有创建的TcpConnection,这里的主要目的是不阻塞TcpServer线程,让它继续监听客户端请求)
 

 /* ... */

  /* 从事件驱动线程池中取出一个线程给TcpConnection */
  EventLoop* ioLoop = threadPool_->getNextLoop();

  /* ... */

  /* 创建一个新的TcpConnection代表一个Tcp连接 */
  TcpConnectionPtr conn(new TcpConnection(ioLoop,
                                          connName,
                                          sockfd,
                                          localAddr,
                                          peerAddr));
  /* 添加到所有tcp连接的map中,键是tcp连接独特的名字(服务器名+客户端<地址,端口>) */
  connections_[connName] = conn;
  /* 为tcp连接设置回调函数(由用户提供) */
  conn->setConnectionCallback(connectionCallback_);
  conn->setMessageCallback(messageCallback_);
  conn->setWriteCompleteCallback(writeCompleteCallback_);
  /* 
   * 关闭回调函数,由TcpServer设置,作用是将这个关闭的TcpConnection从map中删除
   * 当poll返回后,发现被激活的原因是EPOLLHUP,此时需要关闭tcp连接
   * 调用Channel的CloseCallback,进而调用TcpConnection的handleClose,进而调用removeConnection
   */
  conn->setCloseCallback(
      std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe

  /* 
   * 连接建立后,调用TcpConnection连接建立成功的回调函数,这个函数会调用用户提供的回调函数
   * 1.新建的TcpConnection所在事件循环是在事件循环线程池中的某个线程
   * 2.所以TcpConnection也就属于它所在的事件驱动循环所在的那个线程
   * 3.调用TcpConnection的函数时也就应该在自己所在线程调用
   * 4.所以需要调用runInLoop在自己的那个事件驱动循环所在线程调用这个函数
   */
  ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));


connectEstablished函数如下

/* 
 * 1.创建服务器(TcpServer)时,创建Acceptor,设置接收到客户端请求后执行的回调函数
 * 2.Acceptor创建监听套接字,将监听套接字绑定到一个Channel中,设置可读回调函数为Acceptor的handleRead
 * 3.服务器启动,调用Acceptor的listen函数创建监听套接字,同时将Channel添加到Poller中
 * 4.有客户端请求连接,监听套接字可读,Channel被激活,调用可读回调函数(handleRead)
 * 5.回调函数接收客户端请求,获得客户端套接字和地址,调用TcpServer提供的回调函数(newConnection)
 * 6.TcpServer的回调函数中创建TcpConnection代表这个tcp连接,设置tcp连接各种回调函数(由用户提供给TcpServer)
 * 7.TcpServer让tcp连接所属线程调用TcpConnection的connectEstablished
 * 8.connectEstablished开启对客户端套接字的Channel的可读监听,然后调用用户提供的回调函数
 */
void TcpConnection::connectEstablished()
{
  loop_->assertInLoopThread();
  assert(state_ == kConnecting);
  setState(kConnected);
  /* Channel中对TcpConnection的弱引用在这里设置 */
  channel_->tie(shared_from_this());
  /* 设置对可读事件的监听,同时将Channel添加到Poller中 */
  channel_->enableReading();

  /* 用户提供的回调函数,在连接建立成功后调用 */
  connectionCallback_(shared_from_this());
}


至此tcp连接建立完成,在用户提供的回调函数中,传入的参数便是这个TcpConnection的shared_ptr,用户可以使用TcpConnection::send操作向客户端发送消息(放到后面)

有连接的建立就有连接的关闭,当客户端主动关闭(调用close)时,服务器端对应的Channel被激活,激活原因为EPOLLHUP,表示连接已关闭,此时会调用TcpConnection的回调函数handleClose,在这个函数中,TcpConnection处理执行各种关闭动作,包括将Channel从Poller中移除
调用TcpServer提供的关闭回调函数,将自己从TcpServer的tcp连接map中移除
调用客户提供的关闭回调函数(如果有的话)

void TcpConnection::handleClose()
{
  loop_->assertInLoopThread();
  LOG_TRACE << "fd = " << channel_->fd() << " state = " << stateToString();
  assert(state_ == kConnected || state_ == kDisconnecting);
  // we don't close fd, leave it to dtor, so we can find leaks easily.
  setState(kDisconnected);
  channel_->disableAll();

  /* 此时当前的TcpConnection的引用计数为2,一个是guardThis,另一个在TcpServer的connections_中 */
  TcpConnectionPtr guardThis(shared_from_this());
  connectionCallback_(guardThis);
  // must be the last line
  /* 
   * closeCallback返回后,TcpServer的connections_(tcp连接map)已经将TcpConnection删除,引用计数变为1
   * 此时如果函数返回,guardThis也会被销毁,引用计数变为0,这个TcpConnection就会被销毁
   * 所以在TcpServer::removeConnectionInLoop使用bind将TcpConnection生命期延长,引用计数加一,变为2
   * 就算guardThis销毁,引用计数仍然有1个
   * 等到调用完connectDestroyed后,bind绑定的TcpConnection也会被销毁,引用计数为0,TcpConnection析构
   */
  closeCallback_(guardThis);
}


connectionCallback_是由用户提供的,连接建立/关闭时调用,主要调用closeCallback_函数(由TcpServer)提供 
这个函数主要就存在线程不安全的问题,原因就是此时的线程是TcpConnection所在线程 
函数执行顺序为: 
EventLoop::loop->Poller::poll->Channel::handleEvent->TcpConnection::handleClose->TcpServer::removeConnection 
此时就将TcpServer暴露给其他线程,导致线程不安全的问题 
为了减轻线程不安全带来的危险,尽量将线程不安全的函数缩短,muduo中使用runInLoop直接将要调用的函数放到自己线程执行,转换到线程安全,所以这部分只有这一条语句是线程不安全的

void TcpServer::removeConnection(const TcpConnectionPtr& conn)
{
  // FIXME: unsafe
  /* 
   * 在TcpConnection所在的事件驱动循环所在的线程执行删除工作
   * 因为需要操作TcpServer::connections_,就需要传TcpServer的this指针到TcpConnection所在线程
   * 会导致将TcpServer暴露给TcpConnection线程,也不具有线程安全性
   * 
   * TcpConnection所在线程:在创建时从事件驱动循环线程池中选择的某个事件驱动循环线程
   * TcpServer所在线程:事件驱动循环线程池所在线程,不在线程池中
   * 
   * 1.调用这个函数的线程是TcpConnection所在线程,因为它被激活,然后调用回调函数,都是在自己线程执行的
   * 2.而removeConnection的调用者TcpServer的this指针如今在TcpConnection所在线程
   * 3.如果这个线程把this指针delele了,或者改了什么东西,那么TcpServer所在线程就会出错
   * 4.所以不安全
   * 
   * 为什么不在TcpServer所在线程执行以满足线程安全性(TcpConnection就是由TcpServer所在线程创建的)
   *    1.只有TcpConnection自己知道自己什么时候需要关闭,TcpServer哪里会知道
   *    2.一旦需要关闭,就必定需要将自己从TcpServer的connections_中移除,还是暴露了TcpServer
   *    3.这里仅仅让一条语句变为线程不安全的,然后直接用TcpServer所在线程调用删除操作转为线程安全
   */
  loop_->runInLoop(std::bind(&TcpServer::removeConnectionInLoop, this, conn));
}


removeConnectionInLoop函数如下

/* 
 * 这个函数是线程安全的,因为是由TcpServer所在事件驱动循环调用的
 */
void TcpServer::removeConnectionInLoop(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* ioLoop = conn->getLoop();

  /* 
   * 为什么不能用runInLoop, why? 
   */

  /* 
   * std::bind绑定函数指针,注意是值绑定,也就是说conn会复制一份到bind上
   * 这就会延长TcpConnection生命期,否则
   * 1.此时对于TcpConnection的引用计数为2,参数一个,connections_中一个
   * 2.connections_删除掉TcpConnection后,引用计数为1
   * 3.removeConnectionInLoop返回,上层函数handleClose返回,引用计数为0,会被析构
   * 4.bind会值绑定,conn复制一份,TcpConnection引用计数加1,就不会导致TcpConnection被析构
   */
  ioLoop->queueInLoop(
      std::bind(&TcpConnection::connectDestroyed, conn));
}


比较重要的地方是TcpConnection生命期的问题,注释中也有提及。因为muduo中对象使用智能指针shared_ptr存储的,所以只有当shard_ptr的引用计数为0时才会析构它保存的对象。对于TcpConnection而言,它的引用计数在

建立之初保存在TcpServer的connections_中,这个connections_是一个map,键是字符串类型,值就是shared_ptr<TcpConnection>类型,所以建立之初,TcpConnection对象的引用计数为1
创建TcpConnection后,TcpConnection为内部用于监听文件描述符sockfd的Channel传递保存着自己指针的shared_ptr,但是Channel中的智能指针是以weak_ptr的形式存在的(tie_),不增加引用计数,所以此时仍然是1
在TcpConnection处于连接创建的过程中未有shared_ptr的创建和销毁,所以仍然是1
客户端主动关闭连接后,服务器端的TcpConnection在handleClose函数中又创建了一个shared_ptr引用自身的局部变量,此时引用计数加一,变为2
紧接着调用TcpServer::removeConnectionInLoop函数,因为传入的参数是引用类型,不存在赋值,所以引用计数仍为2
在removeConnectionInLoop函数中,TcpServer将TcpConnection从自己的connections_(保存所有tcp连接的map)中删除,此时引用计数减一,变为1
TcpServer通过std::bind绑定函数指针,将TcpConnection::connectDestroyed函数和TcpConnection对象绑定并放到EventLoop中等待调用,因为std::bind只能是赋值操作,所以引用计数加一,变为2
返回到handleClose函数中,函数返回,局部变量销毁,引用计数减一,变为1
EventLoop从poll中返回,执行TcpConnection::connectDestroyed函数,做最后的清理工作,函数返回,绑定到这个函数上的TcpConnection对象指针也跟着销毁,引用计数减一,变为0
开始调用TcpConnection析构函数,TcpConnection销毁
所以如果在第7步不使用std::bind增加TcpConnection生命期的话,TcpConnection可能在handleClose函数返回后就销毁了,根本不能执行TcpConnection::connectDestroyed函数
 

猜你喜欢

转载自blog.csdn.net/songchuwang1868/article/details/89448321