muduo网络库学习:Tcp建立连接与断开连接

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

Acceptor

  对TCP socket, bind, listen, accept的封装 , 将sockfd以Channel的形式注册到EventLoop的Poller中.主要负责监听连接请求,调用listen() 接口时,通过Channel::enableReading() 把socket的描述符加到poller(I/O复用器)中。当有新连接到达时,先调用系统函数accept,再回调函数 newConnectionCallback_ 让TcpServer去创建连接。

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

void listen(): 用listen函数,转为监听套接字,同时将监听套接字添加到Poller中

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

  构造函数

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

void Acceptor::handleRead():

        当有客户端尝试连接服务器时,监听套接字变为可读,epoll_wait/poll返回, EventLoop处理激活队列中的Channel,调用对应的回调函数, 监听套接字的Channel的回调函数是handleRead(),用于接收客户端请求,如果设置了回调函数,那么就调用,参数是客户端套接字和地址/端口,否则就关闭连接。这个回调函数是TcpServer中的newConnection,用于创建一个TcpConnection连接.

        当有新连接到达时,先调用系统函数accept,再回调函数 newConnectionCallback_ 让TcpServer去创建连接。

  一个不好理解的变量是 idleFd_,它是一个文件描述符,这里是打开"/dev/null"文件后返回的描述符,用于解决服务器端描述符耗尽的情况。原理如下:

       当服务器端文件描述符耗尽,当客户端再次请求连接,服务器端由于没有可用文件描述符会返回-1,同时errno为EMFILE,意为描述符到达hard limit,无可用描述符,此时服务器端accept函数在获取一个空闲文件描述符时就已经失败,还没有从内核tcp连接队列中取出tcp连接这会导致监听套接字一直可读,因为tcp连接队列中一直有客户端的连接请求.  所以服务器在启动时打开一个空闲描述符/dev/null(文件描述符),先站着'坑‘,当出现上面情况accept返回-1时,服务器暂时关闭idleFd_让出'坑',此时就会多出一个空闲描述符,然后再次调用 accept接收客户端请求,然后close接收后的客户端套接字,优雅的告诉客户端关闭连接,然后再将'坑'占上

TcpConnection

  TcpConnection类主要负责封装一次TCP连接,向Channel类注册回调函数(可读、可写、可关闭、错误处理),将来当Channel类上的事件发生时,调用相应的回调函数进行数据收发或者错误处理。

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

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

  connectionCallback_: 连接建立后/关闭后的回调函数,通常是由用户提供给TcpServer,然后TcpServer提供给TcpConnection。

  messageCallback_:当tcp连接有消息通信时执行的回调函数,也是由用户提供

  closeCallback_:tcp连接关闭时调用的回调函数,由TcpServer设置,用于TcpServer将这个要关闭的TcpConnection从保存着所有TcpConnection的map中删除,这个回调函数和TcpConnection自己的handleClose不同,后者是提供给Channel的,函数中会使用到closeCallback_

  TcpConnection::handleRead,TcpConnection::handleWrite,TcpConnection::handleClose,TcpConnection::handleError

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

TcpServer

  构造函数

TcpServer::TcpServer(EventLoop* loop,
                     const InetAddress& listenAddr,
                     const string& nameArg,
                     Option option)
  : loop_(CHECK_NOTNULL(loop)),
    ipPort_(listenAddr.toIpPort()),
    name_(nameArg),
    acceptor_(new Acceptor(loop, listenAddr, option == kReusePort)),
    threadPool_(new EventLoopThreadPool(loop, name_)),
    connectionCallback_(defaultConnectionCallback),
    messageCallback_(defaultMessageCallback),
    nextConnId_(1)
{
  acceptor_->setNewConnectionCallback(
      std::bind(&TcpServer::newConnection, this, _1, _2));
}

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

TcpServer创建并设置TcpConnection

  void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)

        TcpServer会将用户提供的所有回调函数都传给TcpConnection,然后执行TcpConnection的connectEstablished函数,这个函数的执行要放到它所属的那个事件驱动循环线程做,不要阻塞TcpServer线程(这个地方不是为了线程安全性考虑,因为TcpConnection本身就是在TcpServer线程创建的,暴露给TcpServer线程很正常,而且TcpServer中也记录着所有创建的TcpConnection,这里的主要目的是不阻塞TcpServer线程,让它继续监听客户端请求)

  创建TcpConnection

TcpConnectionPtr conn(new TcpConnection(ioLoop,
                                          connName,
                                          sockfd,
                                          localAddr,
                                          peerAddr));
   /* 添加到所有tcp连接的map中,键是tcp连接独特的名字(服务器名+客户端<地址,端口>) */

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连接建立。

Muduo Tcp建立过程

 

image

 

 

  1. 创建服务器(TcpServer)时,创建Acceptor,设置接收到客户端请求后执行的回调函数
            acceptor_->setNewConnectionCallback(
                        std::bind(&TcpServer::newConnection, this, _1, _2));
  2. Acceptor创建监听套接字,将监听套接字绑定到一个Channel中,设置可读回调函数为Acceptor的handleRead(这个回调函数是TcpServer::newConnection,用于创建一个TcpConnection连接)
  3. 服务器启动,调用Acceptor的listen函数创建监听套接字,同时将Channel添加到Poller中
  4. 有客户端请求连接,监听套接字可读,Channel被激活,调用可读回调函数(handleRead)
  5. 回调函数接收客户端请求,获得客户端套接字和地址,调用TcpServer提供的回调函数(TcpServer::newConnection)
  6. TcpServer的回调函数(TcpServer::newConnection)中创建TcpConnection代表这个tcp连接,设置tcp连接各种回调函数(由用户提供给TcpServer)
  7. TcpServer让tcp连接所属线程调用TcpConnection的connectEstablished
  8. TcpConnection::connectEstablished开启对客户端套接字的Channel的可读监听,然后调用用户提供的回调函数(连接建立后/关闭后的回调函数,通常是由用户提供给TcpServer,然后TcpServer提供给TcpConnection)

  TcpServer::newConnection

  TcpServer创建完TcpConnection --> 设置各种回调函数(第6步设置)
                            conn->setConnectionCallback(connectionCallback_);(第8步调用回调函数)
                            conn->setMessageCallback(messageCallback_);
                            conn->setWriteCompleteCallback(writeCompleteCallback_);
                调用TcpConnection的connectEstablished函数
                主要用于将Channel添加到Poller中,同时调用用户提供的连接建立成功后的回调函数 。

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

 Muduo的Tcp断开连接

  muduo只有一种关闭连接的方式:被动关闭。即对方先关闭连接,本地read(2)返回0,触发关闭逻辑。如果有必要也可以给TcpConnection新增forceClose()成员函数,用于主动关闭连接,实现很简单,调用handleClose()即可。函数调用的流程如下图,其中的“X”表示TcpConnection通常会在此时析构。

 

  当客户端主动关闭(调用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);
}

  断开连接,函数执行顺序为:
  EventLoop::loop->Poller::poll->Channel::handleEvent->TcpConnection::handleClose->TcpServer::removeConnection 

 

TcpConnection::handleClose 执行过程

1、--》TcpConnection::connectionCallback_--》TcpServer::connectionCallback_--》TcpServer::defaultConnectionCallback
2、--》TcpConnection::closeCallback_--》TcpServer::removeConnection

  TcpConnection::handleClose()中connectionCallback_是由用户提供的,连接建立/关闭时调用。主要调用TcpServer::closeCallback_函数。这个函数主要就存在线程不安全的问题,原因就是此时的线程是TcpConnection所在线程 .

        此时就将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函数如下

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而言,它的引用计数在

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

  所以如果在第7步不使用std::bind增加TcpConnection生命期的话,TcpConnection可能在handleClose函数返回后就销毁了,根本不能执行TcpConnection::connectDestroyed函数

 

Linux多线程服务端编程:使用muduo C++网络库

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

 

posted on 2019-05-02 22:45  flysong  阅读(825)  评论(0编辑  收藏  举报

导航