muduo记录1:一次典型的muduo based Tcp服务器建立连接的过程

一.用户创建Server实例

用户在main()函数里初始化一个EventLoop *对象,这就是baseloop,生命周期等于即将创建的整个服务器的生命周期。

所谓EventLoop,直译事件循环,做的事情主要也是在一个while里面不停地执行epoll_wait(),处理各种各样的事件,EventLoop分为两种,一个是用户主动创建的loop,一般称为mainloop或者baseloop,只管监听和建立新连接,建立好的连接扔给ioloop处理,所以另外一种就是这个ioloop或者说subloop,专门处理已建立连接套接字的读写 关闭 错误等。

class EventLoop;
EventLoop loop;

用这个baseloop构造一个TcpServer对象

TcpServer::TcpServer(EventLoop *loop, const InetAddress &listenaddr, const std::string &namearg, Option option)
:loop_(CheckLoopNotNull(loop))
,ipPort_(listenaddr.toIpPort())
,name_(namearg)
,acceptor_(new Acceptor(loop, listenaddr,option == kReusePort))
,threadPool_(new EventLoopThreadPool(loop, name_))
,connectionCallback_()
,messageCallback_()
,nextConnId_(1)
,started_(0)
{
//当有新用户连接时,会执行TcpNreconnctionCallback
	acceptor_->setNewConnectionCallback(
	std::bind(&TcpServer::newConnection, this, std::placeholders::_1, 		std::placeholders::_2));
}

在第一步中我们已经创建了唯一的baseloop_,利用一个指向baseloop_的指针作为参数,在堆上申请空间初始化Acceptor对象acceptor_和线程池对象threadpool_,这两个都是unique_ptr<>对象。nextConnId初始化为1

紧接bind newConnection回调方法给acceptor_。

接着仅仅需要baseloop_指针和name_就可以构造出一个EventLoopThreadPool实例。

baseloop加上必需的用户定义的回调函数和监听的地址端口后,server.start()可以启动

二.TcpServer第一次启动

void TcpServer::start()
{
    if (started_++ == 0) //防止一个tcpserver对象被start多次
    {
        threadPool_->start(threadInitCallback_); //启动底层的线程池
        loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));
    }
}

调用server.start(),在函数里server只干两件事,那就是调用threadPool_->start(threadInitCallback_)启动线程池和启动mainloop的loop循环,也就是分别让ioloop和mainloop工作起来;

void EventLoopThreadPool::start(const ThreadInitCallback &cb)
{
    started_ = true;

    for (int i = 0; i < numThreads_; ++i) {
        char buf[name_.size() + 32];
        snprintf(buf, sizeof buf, "%s%d", name_.c_str(), i);
        EventLoopThread *t = new EventLoopThread(cb, buf);
        threads_.push_back(std::unique_ptr<EventLoopThread>(t));
        //底层创建线程,绑定一个新的loop并返回该loop的地址
        loops_.push_back(t->startloop());
    }

    //整个服务端只有一个线程在运行baseloop
    if (numThreads_ == 0 && cb) {
        cb(baseLoop_);
    }
}

1.threadPool_->start( )會根据numThreads_(subloop个数),在堆上循环创建相应数量的线程(muduo中所有的线程都是EventLoopThread,内部封裝了Thread對象),同样用unique_ptr包装。

接下來执行這個EventLoopThread類startloop(),这个函数负责启动底层的线程(底層的綫程庫函數用到的執行函數是上層傳下去的回調,缺省一次創建一个EventLoop/subloop并且开始执行loop循环),并且返回这个EventLoopThread刚刚创建的EventLoop的地址,將其放入threadPool的loops_数组成员变量里记录下来,现在这个线程池里应该有了规定数量的工作线程和subloop,分別用一個vector來保存。

EventLoop *EventLoopThread::startloop()
{
    thread_.start(); //启动底层的新线程

    EventLoop *loop = nullptr;
    {
        std::unique_lock<std::mutex> lock(mutex_);
        while (!loop_)
        {
            cond_.wait(lock);
        }
        loop = loop_;
    }
    return loop_;
}

2.创建完了所有的ioloop并且让他们开始loop循环后,mainloop才会调用runInLoop,执行acceptor_的listen()方法,绑定server的地址端口并且开启监听。runInLoop是EventLoop的成员函数,检查当前loop是否在创建它的线程内部,是的话立即执行,否则调用queueInLoop()把bind的函数放入ioLoop自己的一个任务队列。这个loop_ 就是主线程中的loop,判断结果一定在当前线程里面,所以会直接执行listen() 。

void EventLoop::runInLoop(Functor cb)
{
    //在当前的loop线程中执行cb,直接执行
    if (isInLoopThread())
    {
        cb();
    }
    //在非当前loop线程中执行cb,需要唤醒loop所在线程执行cb
    else
    {
        queueInLoop(cb);
    }
}

现在有了线程池,有了acceptor,搭配上用户自己设置的相关回调操作,主事件循环可以开启了,main调用loop.loop(),server可以接收连接请求了

三.Acceptor发生的事,需要引入Channel了

此时,acceptor_建立,并且通过内部的EventLoop成员表示为Server所有,读回调bind了handleRead(),开始监听。但是muduo是基于IO多路复用(其实就是epoll)事件驱动的。新连接,可读,可写都应该通过epoll来完成。

所以,这里的listen()开始后,新连接来了不会直接accept系统调用,而是通过另外一个类Channel来进行事件到来的时的回调和分派任务。

Channel是单独用来注册和相应事件的类

Channel是单独用来创建和相应事件的类,陈硕老师在书中提到一个channel对象并不拥有fd,它是Acceptor,Connector,EventLoop,TimerQueue,TcpConnection的成员,生命期由它们管理。

Channel起到了非常重要的串联各个模块的作用,成员变量如下

  static const int kNoneEvent;
  static const int kReadEvent;
  static const int kWriteEvent;

  EventLoop *loop_; //事件循环
  const int fd_;    // fd,poller监听的对象
  int events_;      // 注册fd感兴趣的事件
  int revents_;     // poller返回的具体事件
  int index_; // 表示这个channel在poller中的状态:未添加过 已添加 需要删除

  std::weak_ptr<void> tie_;
  bool tied_;

  //因为channel通道里面能够获知fd里具体发生的事件,
  //所以它负责调用具体事件的回调操作
  ReadEventCallback readcallback_;
  EventCallback writecallback_;
  EventCallback closecallback_;
  EventCallback errorcallback_;

可以看到,Channel类中同样有一个EventLoop指针(怎么哪都有你),假设现在创建了一个channel的实例,我们就有了一个套接字(可能是监听套接字/也可能是已连接套接字),channel对这个套接字的关注点,这个套接字发生的事件,index_是channel在poller中的状态,后面会提到。上面三个静态常量定义了channel对fd_的态度:不关心,关心读,关心写

channel中有许多回调函数,这些函数都是从上游的acceptor_和connector来的,这些回调函数定义了对channel关注点的修改,关注事件发生时的处理方法。其实,Channel有三种,acceptChannelconnectedChannel,wakeupChannel,先说前两种

acceptor_有一个成员变量acceptChannel,顾名思义这个channel专门用来接收新连接,所以通过Acceptor类的listen()就调用了Channel类的enableReading方法,把关注点设置成可读事件。同理,TcpConnection类中也有成员变量是Channel类对象,TcpConnection表示已连接的套接字,是从acceptor那里的新连接变来的,具体怎么实现呢

通过server里面的一个回调,就是TcpServer构造时为acceptor_绑定的newConnection来完成。当Client端请求到来时,mainloop里的poller返回可读事件,于是acceptorChannel根据读事件执行handleRead()成员函数,这个成员函数里面就有server类为acceptor传入的newConnection回调。函数来到server的成员函数中,通过轮询选择线程池里的一个ioloop,建立连接产生一个connfd,用connfd,*ioloop,peeraddr构造一个TcpConnection 智能指针对象,TcpConnection对象构造时会生成一个成员变量channel,同时把对已连接套接字的读写 关闭 错误处理回调函数传给channel,之后连接期间便由这个channel来负责事件响应。用智能指针包装这个连接对象,把用户设置的相关回调传给它,最后ioloop执行runInLoop,里面的函数是TcpConnection::connectEstablished(),让channel关心读事件。

思考:这里的runInLoop会因为不在创建线程而转而queueInLoop吗?
两种情况:1.单线程,只有mainloop是没有竞态的,runInLoop立即执行回调;
2.多线程环境,那这个ioloop必然是线程池里面的一个,而当前的函数是mainloop的成员函数,所以一定不在ioloop的本来线程。runInLoop判断出来跨线程了,就把任务塞进对应的队列,然后唤醒它。

void TcpServer::newConnection(int sockfd, const InetAddress &peerAddr)
{
    //轮询算法,选择一个subloop,来管理subloop
    EventLoop *ioLoop = threadPool_->getNextLoop();
    char buf[64] = {0};
    snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
    ++nextConnId_;
    std::string connName = name_ + buf;

    LOG_INFO << "TcpServer::newConnection " << name_.c_str() << " - new Connection" << connName.c_str() << "from"
             << peerAddr.toIpPort().c_str();

    sockaddr_in local;
    ::bzero(&local, sizeof local);
    socklen_t addrlen = sizeof local;
    if (::getsockname(sockfd, (sockaddr *)&local, &addrlen) < 0)
    {
        LOG_ERROR << "sockets::getlocalAddr";
    }
    //通过sockfd获取其绑定的ip地址和port信息
    InetAddress localAddr(local);

    //根据连接成功的sockfd,创建TcpConnetion连接对象
    TcpConnectionPtr conn(new TcpConnection(ioLoop, connName, sockfd, localAddr, peerAddr));

    connections_[connName] = conn;

    /*下面的回调都是用户设置给TcpServer
     *    TcpServer      =》   TcpConnection
     *   TcpConnection   =》   Channel
     *     Channel       =》   Poller
     *      Poller       =》   notify channel调用回调
     */
    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->setWriteCompleteCallback(writeCompleteCallback_);
    //设置了如何关闭连接的 回调      当conn执行shutdown
    conn->setCloseCallback(std::bind(&TcpServer::removeConnection, this, std::placeholders::_1));

    //直接调用TcpConnection ::connectEstablished
    ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}

以上就是muduo服务器初始化和建立连接的大致过程,其中涉及大量的回调操作。Channel的设计思想,包装成性质不一样的三类Channel,分别去完成不同的任务。

阶段总结:

1.EventLoop是事件循环,遵循one loop per thread。每个EventLoop内部都持有一个poller,一个wakeupChannel(wakeupChannel一直关注读事件,往里面写一个东西,假设epoll_wait工作在阻塞状态,下次poll()一定会为此返回,自然会唤醒它起来做事)EventLoop都是线程栈上的对象,线程起起来就去loop了,server开着就在loop,生命周期跟server一样。

2.EventLoop和Poller,Channel具有层级关系。EventLoop不关心poller怎么工作,不关心channel如何更新,设置好回调操作以后,一切由事件和IO多路复用驱动。channel只用管好自己的注册,更新,需要的时候通过loop指针向上请求调用EventLoop的方法,EventLoop调用poller的方法,间接地设置自己的关注点,事件到来调用相应回调使用EventLoop的方法处理事件。

这是第二次写相关的笔记,用于阶段性总结。说的不对的地方请多指教,以上

posted on 2021-07-27 16:52  minuet  阅读(111)  评论(0)    收藏  举报

导航