muduo net库学习笔记5——服务器监听类Acceptor、Tcp连接(优雅关闭连接)、TcpConnection的建立与关闭

先简单例子:

  1. 创建服务器(TcpServer)时,创建Acceptor,设置接收到客户端请求后执行的回调函数
  2. Acceptor创建监听套接字,将监听套接字绑定到一个Channel中,设置可读回调函数为AcceptorhandleRead
  3. 服务器启动,调用Acceptorlisten函数创建监听套接字,同时将Channel添加到Poller
  4. 有客户端请求连接,监听套接字可读,Channel被激活,调用可读回调函数handleRead
  5. 回调函数接收客户端请求,获得客户端套接字和地址,调用TcpServer提供的回调函数(newConnection)
  6. TcpServer的回调函数中创建 TcpConnection代表这个tcp连接,设置tcp连接各种回调函数(由用户提供给TcpServer)
  7. TcpServertcp连接所属线程调用TcpConnectionconnectEstablished
  8. connectEstablished开启对客户端套接字的Channel的可读监听,然后调用用户提供的回调函数

简单回顾一下Channel和TcpConnection的关系

Channel的回调函数就是根据被激活原因调用不同的回调函数, 这些回调函数是在TcpConnection创建时就被设置的

  • 每个TcpConnection对象代表一个tcp连接,所以每个TcpConnection中需要保存用于服务器/客户端通信的套接字,这个套接字就记录在Channel中
  • TcpConnection在创建之初就会为Channel设置回调函数,如果套接字可读/可写/错误/关闭等就会执行TcpConnection的函数
  • TcpConnection在确定连接已经建立后会向Poller注册自己的Channel
/* TcpConnection.cc */
  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));

ChannelhandleEvent就是根据fd激活事件的不同,调用不同的fd回调函数
Channel其实就是一个事件类,保存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);
  }
}

muduo net库学习笔记5——服务器监听类Acceptor、Tcp连接(优雅关闭连接)、TcpConnection的建立与关闭_第1张图片muduo net库学习笔记5——服务器监听类Acceptor、Tcp连接(优雅关闭连接)、TcpConnection的建立与关闭_第2张图片
handleEventWithGuard就是根据不同的激活原因调用不同的回调函数,这些回调函数都在TcpConnection

/*
 * 根据被激活事件的不同,调用不同的回调函数
 */
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;
}


开始了知识点密集

服务器监听类Acceptor

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


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连接建立(1取空闲fd,2取连接,3让fd负责这个连接

Acceptor类的定义

对TCP socket,bind,listen,accept的封装,将sockfd以Channel的形式注册到EventLoop的Poller中,检测到sockfd可读时接收套接字
主要就是将监听套接字变为可读的回调函数

//Acceptor.h文件
class Acceptor : noncopyable
{
 public:
  typedef std::function<void (int sockfd, const InetAddress&)> NewConnectionCallback;

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

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


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

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

  EventLoop* loop_;//事件驱动主循环
  Socket acceptSocket_;//封装socket的一些接口
  Channel acceptChannel_;//Channel,保存sockfd,被添加到Poller中,等待被激活
  /*
  当有客户端连接时首先内部接收连接,然后调用用户提供的回调函数
  客户端套接字和地址作为参数传入
  */
  NewConnectionCallback newConnectionCallback_;
  bool listenning_;
  int idleFd_;//优雅关闭连接
};

关于idleFd_,关于优雅关闭连接⭐

服务器启动时占用一个空闲文件描述符/dev/null,作用是解决文件描述符耗尽的情况
原理如下:
当服务器端文件描述符耗尽,当客户端再次请求连接,服务器端由于没有可用文件描述符,会返回-1,同时errno为EMFILE,意为描述符到达hard limit,无可用描述符,此时服务器端accept函数在获取一个空闲文件描述符时就已经失败,还没有从内核tcp连接队列中取出tcp连接
这会导致监听套接字一直可读,因为tcp连接队列中一直有客户端的连接请求

所以服务器在启动时打开一个空闲描述符/dev/null(文件描述符),先站着’坑‘,当出现上面情况,accept返回-1时,**服务器暂时关闭idleFd_让出’坑’,此时就会多出一个空闲描述符,然后再次调用accept接收客户端请求,并close接收后的客户端套接字,优雅的告诉客户端关闭连接,**然后再将’坑’占上
muduo net库学习笔记5——服务器监听类Acceptor、Tcp连接(优雅关闭连接)、TcpConnection的建立与关闭_第3张图片

Acceptor的实现

构造函数

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);

  acceptSocket_.setReuseAddr(true);
  /* 
   * setsockopt设置套接字选项SO_REUSEADDR,对于端口bind,如果这个地址/端口处于TIME_WAIT,也可bind成功
   * int flag = 1;
   * setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
   */
  acceptSocket_.setReusePort(reuseport);
    /*
   * setsockopt设置套接字选项SO_REUSEPORT,作用是对于多核cpu,允许在同一个对上运行多个相同服务器
   * 内核会采用负载均衡的的方式分配客户端的连接请求给某一个服务器
   */

  acceptSocket_.bindAddress(listenAddr);
 
  acceptChannel_.setReadCallback(
      std::bind(&Acceptor::handleRead, this));
      /* Channel设置读事件的回调函数,此时还没有开始监听这个Channel,需要调用Channel::enableReading() */
}

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

acceptChannnel_和listen()??

acceptChannel_用于保存用于监听的套接字,绑定回调函数,在合适的时机注册到Poller上(调用listen时)
void listen();//调用listen函数,转为监听套接字,同时将监听套接字添加到Poller中

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

事件处理函数(回调函数) handleRead()用户接收客户端请求

当监听套接字可读时,调用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;
    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.
    
    if (errno == EMFILE)//这就是资源耗尽了, 用idleFd_ 了
    {
      ::close(idleFd_);
      idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
      ::close(idleFd_);
      idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
    }
  }
}

newConnectionCallback_TcpServerTcpConnection之间

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

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

muduo net库学习笔记5——服务器监听类Acceptor、Tcp连接(优雅关闭连接)、TcpConnection的建立与关闭_第4张图片
但是重要的就:

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

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

typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;

}  // namespace net
}  // namespace muduo

TcpConnection的构造函数

设置当fd就绪时调用的回调函数,Channel代表一个对fd事件的监听,主要是为Channel提供各种回调函数

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;
            
  socket_->setKeepAlive(true);
  /*
   * 设置KEEP-ALIVE属性,如果客户端很久没有和服务器通讯,tcp会自动判断客户端是否还处于连接(类似心跳包)
   * 
   * int setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &sockopt, static_cast(sizeof(sockopt)));
   */
}

调用的是ChannelhanleEvent, 上文复习过了ChannelhanleEvent函数调用handleEventWithGuard,根据不同激活原因调用不同回调函数


TcpServer创建完TcpConnection后,会设置各种回调,调用TcpConnectionconnectEstablished函数,主要用于将Channel添加到Poller中,同时调用用户提供的连接建立成功后的回调函数

//TcpServer.cc
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
  loop_->assertInLoopThread();
  EventLoop* ioLoop = threadPool_->getNextLoop();//从事件驱动线程池中取出一个线程给TcpConnection
  
  char buf[64];
  snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
  ++nextConnId_;
  string connName = name_ + buf;

  LOG_INFO << "TcpServer::newConnection [" << name_
           << "] - new connection [" << connName
           << "] from " << peerAddr.toIpPort();
  InetAddress localAddr(sockets::getLocalAddr(sockfd));
  // FIXME poll with zero timeout to double confirm the new connection
  // FIXME use make_shared if necessary
  
  TcpConnectionPtr conn(new TcpConnection(ioLoop,
                                          connName,
                                          sockfd,
                                          localAddr,
                                          peerAddr));//创建一个新的TcpConnection代表一个Tcp连接
  connections_[connName] = conn;//添加到所有tcp连接的map中, 键是tcp连接特有的名字
  //服务器名+客户端<地址,端口>

//为tcp连接设置回调函数(由用户提供)
  conn->setConnectionCallback(connectionCallback_);
  conn->setMessageCallback(messageCallback_);
  conn->setWriteCompleteCallback(writeCompleteCallback_);



  conn->setCloseCallback(
      std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
/* 
   * 关闭回调函数,由TcpServer设置,作用是将这个关闭的TcpConnection从map中删除
   * 当poll返回后,发现被激活的原因是EPOLLHUP,此时需要关闭tcp连接
   * 调用Channel的CloseCallback,进而调用TcpConnection的handleClose,进而调用removeConnection
   */



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

muduo net库学习笔记5——服务器监听类Acceptor、Tcp连接(优雅关闭连接)、TcpConnection的建立与关闭_第5张图片

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. TcpServertcp连接所属线程调用TcpConnectionconnectEstablished
  8. connectEstablished开启对客户端套接字的Channel的可读监听,然后调用用户提供的回调函数
void TcpConnection::connectEstablished()
{
  loop_->assertInLoopThread();
  assert(state_ == kConnecting);
  setState(kConnected);
  channel_->tie(shared_from_this());//Channel中对TcpConnection的弱引用在这里设置 
  channel_->enableReading();//设置对可读事件的监听,同时将Channel添加到Poller中 

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

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


有连接的建立就有连接的关闭,当客户端主动关闭(调用close)时,服务器端对应的Channel被激活,激活原因为EPOLLHUP,表示连接已关闭,此时会调用TcpConnection的回调函数handleClose,在这个函数中,TcpConnection处理执行各种关闭动作,包括

  1. ChannelPoller中移除
  2. 调用TcpServer提供的关闭回调函数,将自己从TcpServertcp连接map中移除
  3. 调用客户提供的关闭回调函数(如果有的话)
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();

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

muduo net库学习笔记5——服务器监听类Acceptor、Tcp连接(优雅关闭连接)、TcpConnection的建立与关闭_第6张图片

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

TcpConnection所在的事件驱动循环所在的线程执行删除工作, 因为需要操作TcpServer::connections_,就需要传TcpServerthis指针到TcpConnection所在线程, 会导致将TcpServer暴露给TcpConnection线程,也不具有线程安全性

  • TcpConnection所在线程:在创建时从事件驱动循环线程池中选择的某个事件驱动循环线程
  • TcpServer所在线程:事件驱动循环线程池所在线程,不在线程池中
  1. 调用这个函数的线程是TcpConnection所在线程,因为它被激活,然后调用回调函数,都是在自己线程执行的
  2. removeConnection的调用者TcpServerthis指针如今在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();
  ioLoop->queueInLoop(
      std::bind(&TcpConnection::connectDestroyed, conn));
}

muduo net库学习笔记5——服务器监听类Acceptor、Tcp连接(优雅关闭连接)、TcpConnection的建立与关闭_第7张图片
muduo net库学习笔记5——服务器监听类Acceptor、Tcp连接(优雅关闭连接)、TcpConnection的建立与关闭_第8张图片
感谢https://blog.csdn.net/sinat_35261315/article/details/78343266

你可能感兴趣的:(muduo源码,muduo,net库,muduo源码解析)