以基于Reactor模式的高并发EchoServer为例剖析muduo库框架

前言

      在讲解基于muduo库的高并发echo服务器之前,我们先来回顾一下我们一般编写基于Reactor模式的高并发服务器的基本流程。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第1张图片

        muduo的EchoServer其实也是基于上述流程,只不过进行了一定程度地封装,本质上仍是Reactor模式。muduo的example中的echo服务器代码展示的是只有一个reactor的服务器,muduo其实支持one loop per thread + 线程池的模式,也即multiple reactors + 业务线程池的模式,也就是服务器同时有多个IO线程,其中Acceptor所在的IO线程称为mainReactor,其他的IO线程称为subReactor,mainReactor主要负责处理监听描述符listenfd的上的事件,也就是负责处理客户端的连接请求。而subReactor主要负责已连接描述符connfd上的事件,也就是在和客户端建立好连接之后负责处理和客户端的具体通信。mainReactor+subReactor是由多个IO线程实现的,在muduo库当中,其通过在IO线程池中设置是让其工作在单IO线程模式还是多IO线程模式。而所谓的业务线程池主要是指计算线程池,其主要负责处理具体的业务逻辑,不涉及具体的IO操作。

        在认真阅读完一遍muduo库源码之后,我认为如果想基于muduo库编写一个单线程版的echo服务器,最需要掌握的一共有5个类,分别是TcpServer、EventLoop、Channel、Acceptor和TcpConnection。

  • TcpServer

        TcpServer对象一般运行在用户代码的主线程,它的生命周期应该和用户服务器程序的生命周期一致。注意,这里的用户不是指客户端,而是指编写服务器的程序员。TcpServer对象基本上是用户代码和Muduo库之间的总界面。它对内管理多个成员对象,如acceptor、连接列表、IO线程池等,对外为用户代码提供客户端连接建立、消息接收和发送的处理接口。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第2张图片

        首先我们来看一下TcpServer的数据成员,我们来看其中最主要的数据成员,这有助于我们理清整个muduo库的服务器框架。首先,我想介绍两个数据成员acceptor_和connections_,acceptor_即是一个指向Acceptor类对象的指针,很容易可知我们通过这个成员就可以处理客户端的连接请求,而且我们知道,acceptor_所在的IO线程就是mainReactor,一般情况下,我们就用一个IO线程处理用户的连接请求,也就是我们只需要一个Acceptor对象,因而可以看到,acceptor_是用unique_ptr智能指针管理的。客户的连接请求可以通过acceptor_来处理,那么一旦有客户连接,我们就需要另一个成员connections_了,因为通常情况下,连接的客户端可能不止一个,所以我们把它以一个map容器来表示。

        另外,我们知道服务器本身应该就位于一个IO线程中,因为你想想啊,服务器类中就有一个Acceptor类的指针,Acceptor类本身就是处理客户端连接请求的,必然涉及IO操作,Acceptor对象都属于一个IO线程了,TcpServer类作为它的主人,当然也应该在这个IO线程中,所以其有一个数据成员loop指针,指向EventLoop类对象,我们可以简单地把EventLoop类当做一个IO线程,创建了一个EventLoop对象就创建了一个IO线程,实际上,EventLoop是muduo网络库中的核心库,其重要性不言而喻,这个我们接下来再说。

        如果你想让服务器工作在多IO线程,即multiple reactors模式,我们也要关注一下threadPool_成员,TcpServer通过该成员去管理IO线程池。

  • EventLoop

        EventLoop类可以理解为一个IO线程类,创建一个EventLoop对象就相当于创建了一个IO线程,因此一个服务器必须要依托这个类才行。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第3张图片

        这里我想重点关注EventLoop的两个数据成员,一个是poller_,一个是activeChannels_。poller_是一个指向Poller类对象的unique_ptr智能指针,因此,每个EventLoop对象的poller_指针只能指向一个Poller对象。Poller类实际上一个虚基类,其被PollPoller和EPollPoller两个类继承,这也是muduo库中唯一使用面向对象编程风格的地方。因此,我们很容易知道,Poller其实是对多路IO转接的封装,相当于对poll以及epoll相关函数做了一层封装,因而通过poller指针我们可以实现IO多路转接,从而能够监听多个描述符的状态变化。实际上,我们经常调用的loop()函数,即loop循环,内部的底层就是在调用poll或者epoll_wait阻塞监听文件描述符的状态变化。

        另一个我想关注的成员是activeChannels_,也就是存储Poller监听到的有事件发生的通道,我们知道,在muduo库中,一个文件描述符会与一个通道Channel绑定,我们返回活动通道,其实是指与该活动通道绑定的文件描述符上状态发生了变化,有相应的事件发生了,那么为什么muduo库要将文件描述符fd与一个通道Channel绑定呢,其主要是为了能够为与其绑定的描述符注册读写事件,并且设置读写事件发生时的回调函数。另外,我们需要注意,一个Channel只能属于一个EventLoop,而一个EventLoop可以包含多个Channel。

  • Channel

        Channel类个人感觉就是一座桥梁,他为与其绑定的描述符注册读写事件,并且设置读写事件发生时的回调函数。当对应的事件发生时,我们只需要关注channel而不必去关注文件描述符fd就可以执行对应的函数。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第4张图片

        Channel类的数据成员我认为主要先关注loop_、fd_、events_、revents.这几个成员都很简单,分别是Channel所属的EventLoop对象的指针、与Channel绑定的文件描述符、文件描述符上所关注的事件和文件描述符上所发生的事件。

  • Acceptor

        Acceptor类的作用主要就是处理客户端的连接请求,其相当于封装了上述流程图中的前4个步骤,其socket、bind、listen以及初始化监控描述符集合,设置好关注事件。其中,第4个步骤在Acceptor类中特指为连接套接字listenfd设置好关注事件。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第5张图片

        Acceptor类的数据成员我想重点关注上图的前四个成员。loop_很显然了,主IO线程对应的EventLoop对象的指针,acceptSocket是对监听描述符listenfd的封装,可以简单理解为listenfd。acceptChannel_顾名思义,就是要与listenfd绑定的通道,我们知道,只有描述符与Channel绑定了,才能设置关注的事件与注册回调函数.设置了关注事件服务器才能通过poll或者epoll_wait感知到客户端有请求过来,注册回调函数服务器才能在客户端请求到来时执行相应的处理回调函数。第4个成员newConnectionCallback_,也就是客户端发出连接请求时服务器端执行的回调函数,务必注意newConnectionCallback_是服务器收到客户端的连接请求并接受后连接建立成功后主IO线程执行的回调函数,其通常是在该回调函数中创建一个TcpConnection对象(该对象会与一个EventLoop绑定,该EventLoop对象由Round-Robin轮询算法分配),并设置在该对象上的回调函数,例如客户端消息到来时的处理函数等,为客户端与服务器的通信做准备,因而这个回调函数是很重要的一步。我们一定要注意Acceptor类中newConnectionCallback_与下面要讲的TcpConnection类中的connectionCallback_的区别,connectionCallback_是在客户端与服务器建立连接之后,在TcpConnection所在的IO线程中所回调的函数,如果是多IO线程的话,其和主IO线程并不是一个IO线程,另外,TcpConnection类中的connectionCallback_个人感觉重要性不是很高,主要可以在里面打印一些客户端信息、表示连接建立之类的信息,个人感觉有时候并不是必须的。

  • TcpConnection

        TcpConnection主要是对已连接套接字的抽象,主要用来处理已经建立连接的客户端与服务器之间的通信。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第6张图片

         我认为我们需要关注一下TcpConnection的以下几个成员。首先,loop_不用说了,因为TcpConnection本身就是处理客户端与服务器的通信,所以必然涉及IO操作,所以其必然也要属于一个IO线程,因而需要执行一个EventLoop。如果是multiple reactor模式下,其所在线程就相当于subReactor。socket_是对已连接套接字connfd的封装,简单理解为已连接套接字connfd即可,有了已连接套接字,必然应该有与其绑定的channel_,注意到这两个成员都是unique_ptr管理的,因此很显然一个TcpConnection只能管理一个已连接套接字和通道。下面就是connectionCallback_了,这个在Acceptor类的那个地方已经介绍过了,其需要与Acceptor类的newConnectionCallback_区分,前者是在TcpConnection所在的IO线程中执行的,其有时不是很有必要存在。而后者是在主IO线程中执行的,是必须要存在的,因为需要通过这个回调创建TcpConnection对象,这样以后才能处理客户端与服务器的通信。下面的messageCallback_成员主要是用来处理客户逻辑的,相当于客户端发了数据过来,我们要对他在这个函数中进行处理。底下的一些***Callback_成员也很简单。另外,需要注意两个Buffer类成员,这是muduo专门在应用层设计的缓冲区,能够有效避免多次系统调用,并简化用户逻辑,使得用户只管收发数据,不用关心到底数据是一次性发送还是分几次发送。

以Echo服务器剖析muduo库框架

        好了,前面的大致的总结也差不多了,这里我们先以echo服务器为例展示基于muduo库的单IO线程的并发服务器的执行流程,该IO线程既充当IO线程的角色,又充当计算线程的角色(实际上也可以任务这里没有计算线程,因为其并没有对读到的数据进行计算处理,只是简单的回射过去而已)。上代码啦!

echo.cc

#include "examples/simple/echo/echo.h"

#include "muduo/base/Logging.h"

using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;

// using namespace muduo;
// using namespace muduo::net;

EchoServer::EchoServer(muduo::net::EventLoop* loop,
                       const muduo::net::InetAddress& listenAddr)
  : server_(loop, listenAddr, "EchoServer")
{
  server_.setConnectionCallback(
      std::bind(&EchoServer::onConnection, this, _1));
  server_.setMessageCallback(
      std::bind(&EchoServer::onMessage, this, _1, _2, _3));
}

void EchoServer::start()
{
  server_.start();
}

void EchoServer::onConnection(const muduo::net::TcpConnectionPtr& conn)
{
  LOG_INFO << "EchoServer - " << conn->peerAddress().toIpPort() << " -> "
           << conn->localAddress().toIpPort() << " is "
           << (conn->connected() ? "UP" : "DOWN");
}

void EchoServer::onMessage(const muduo::net::TcpConnectionPtr& conn,
                           muduo::net::Buffer* buf,
                           muduo::Timestamp time)
{
  muduo::string msg(buf->retrieveAllAsString());
  LOG_INFO << conn->name() << " echo " << msg.size() << " bytes, "
           << "data received at " << time.toString();
  conn->send(msg);
}

echo.h

#ifndef MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
#define MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H

#include "muduo/net/TcpServer.h"

// RFC 862
class EchoServer
{
 public:
  EchoServer(muduo::net::EventLoop* loop,
             const muduo::net::InetAddress& listenAddr);

  void start();  // calls server_.start();

 private:
  void onConnection(const muduo::net::TcpConnectionPtr& conn);

  void onMessage(const muduo::net::TcpConnectionPtr& conn,
                 muduo::net::Buffer* buf,
                 muduo::Timestamp time);

  muduo::net::TcpServer server_;
};

#endif  // MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H

main.c

#include "examples/simple/echo/echo.h"

#include "muduo/base/Logging.h"
#include "muduo/net/EventLoop.h"

#include 

// using namespace muduo;
// using namespace muduo::net;

int main()
{
  LOG_INFO << "pid = " << getpid();
  muduo::net::EventLoop loop;
  muduo::net::InetAddress listenAddr(2007);
  EchoServer server(&loop, listenAddr);
  server.start();
  loop.loop();
}

        从大的框架来讲,我们还是应该从TcpServer类来剖析。首先,主函数中,显示创建了EventLoop对象以及一个服务器端的地址结构,InetAddress类前文并没有提及,因为它就是一个对地址结构的封装类,没什么好讲的。为什么要先定义这两个对象呢?当然是TcpServer类要用到啊!刚刚不是说了,要从TcpServer入手剖析。我们看到,main函数接下来构造了一个EchoServer 对象,而EchoServer 其实只有一个成员,也就是TcpServer类对象server_,所以EchoServer 的构造函数其实是调用了TcpServer的构造函数,那我们就先来看看TcpServer的构造函数吧!

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第7张图片

        我们看到,刚刚创建的loop和listenAddr都作为实参传递给了TcpServer的构造函数,构造函数的后两个参数并不太重要,一个表示服务器名,一个表示端口复用选项,对程序整体框架不是太重要,就先不剖析了。我们看到构造函数先创建了一个Acceptor类对象,注意Acceptor所传递的loop也是TcpServer的loop,从这边也可以看出TcpServer所在的IO线程和Acceptor所在的IO线程是同一个。接下来我们有必要看一下Acceptor类的构造函数又干啥了。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第8张图片

         很显然,这里创建了监听套接字listenfd(acceptSocket_只是对它的封装而已),然后将套接字与通道绑定,同时设置了通道的读回调函数,也就是listenfd上有读事件的回调函数。应当注意到,此时还没有设置listenfd关注读事件。 

        继续回到TcpServer的构造函数,接下来构造了一个EventLoopThreadPool的IO线程池对象,这样接下来我们只要使用server_. setThreadNum(N)就可以使服务器工作于多IO线程模式(这里server_为TcpServer类对象,N为subReactor个数),关于多IO线程我准备在以后的文章再说,这里先不过多讨论。目前,由于我们没有设置,服务器是工作在单IO线程模式下的。

        下面的connectionCallback_与messageCallback_是设置的TcpConnection的响应的回调函数,也就是连接建立好之后TcpConnection所在的IO线程执行的回调函数。这点可以从后面的TcpServer::newConnection函数得知,过会再说。

        下面在构造函数的主体内设置了Acceptor的newConnectionCallback_,也就是主IO线程有客户端连接请求建立好之后执行的回调。好了,TcpServer的构造函数我们分析结束了,下面继续EchoServer的构造函数。

        EchoServer构造函数在初始化server_之后,又重新设置了连接回调函数和消息处理回调函数,也就是修改了上文中原先在TcpServer类的构造函数中默认的connectionCallback_与messageCallback_。这样客户端有数据发送给服务器时,TcpConnection回调的是我们在EchoServer中设置的回调。

        回到main函数,EchoServer终于构造好了,下面执行server.start(),其实质上执行的是TcpServer的start函数。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第9张图片

        一开始,我们会先开启IO线程池,其实质上是创建了多个IO线程并将其添加到容器中,以便供以后轮询算法为每个连接分配IO线程。如果我们之前没有设置多IO线程,那么只会使当前的IO线程执行loop循环前的回调函数threadInitCallback_。然后立即去执行Acceptor的listen函数,至于runInLoop其也是muduo库设计的一大亮点,其可以实现跨线程调用,我准备在另一篇文章中讨论它。好了,我们来看看Acceptor::listen函数吧!

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第10张图片

        可以看到,这个地方真正地关注了listenfd的可读事件,相当于真正的将listenfd调用底层的epoll_ctl将它挂到了红黑树上(因为默认采用的是epoll)。

        也就是说,至此我们完成了之前流程图的前4个步骤,这4个步骤其实都是Acceptor类干的活。接下来,可以开启loop循环执行epoll_wait了。果然,main函数中loop.loop()开启了事件循环。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第11张图片

        我们发现,loop里面调用了poll函数进行事件监听,其底层默认调用的是epoll_wait,当有客户端发起连接请求时,由于我们之前已经开启了listenfd的读事件,所以listenfd对应的通道就会被添加到activeChannels_中,然后我们就会调用对应通道的处理函数。我们知道,监听套接字的listenfd是与AcceptChannel_绑定的,这在Acceptor的构造函数中就可看出。因而会调用AcceptChannel_所注册的读回调,AcceptChannel_的读回调也是在Acceptor的构造函数中被设置,可以看到是Acceptor::handleRead函数,我们来看看这个函数。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第12张图片

        其实,不用想我们也知道这里面肯定应该accept,然后accept之后会获得已连接套接字connfd,然后针对这个已连接套接字,我们需要进行相应的处理,好让他可以和服务器通信。果不其然,handleRead里确实就干了这些事,当它获得已连接描述符connfd时,其调用了newConnectionCallback_,也就是主IO线程调用的新连接回调函数。newConnectionCallback_在TcpServer的构造函数中就设置好了,设置的是TcpServer::newConnection。其实,不用想也知道,在这个回调函数中肯定会为这个已连接套接字分配一个TcpConnection对象,然后设置上面的回调函数。我们来看看。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第13张图片

        果不其然,首先,先使用Round-Robin算法获取一个IO线程,接下来新建一个TcpConnection对象,并将该对象与分配的IO线程绑定以及已连接套接字绑定,接下来就是设置TcpConnection上的各种回调函数了。然后,立即到TcpConnection对象所在的IO线程中执行TcpConnection::connectEstablished函数,我们来看看这个函数干了啥。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第14张图片

           其实它就是执行了我们注册的connectionCallback_函数,也就是EchoServer::onConnection,这个函数是怎么一步步注册到这边的呢?我们再来总结一下,首先,我们调用下面语句进行注册

  server_.setConnectionCallback(
      std::bind(&EchoServer::onConnection, this, _1));

        该语句其实调用的是TcpServer的setConnectionCallback函数

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第15张图片

        上图是TcpServer的setConnectionCallback函数,connectionCallback_是TcpServer的数据成员。因而现在,TcpServer的connectionCallback_被我们设置为EchoServer::onConnection,而我们在TcpServer::newConnection调用了

  conn->setConnectionCallback(connectionCallback_);

        因而TcpConnection才能执行我们注册的函数,应当注意到,TcpServer和TcpConnection都有数据成员connectionCallback_,其都是指TcpConnection中所要执行的连接处理函数。

        另外,我们注意到TcpConnection::connectEstablished函数中,有一句

 channel_->enableReading();//注册Channel的可读事件,底层会把fd正式挂到poll或者epoll上,此时客户端和服务器就正式地可以通信了

         也就是说,与已连接套接字绑定的通道现在也已经注册了可读事件。因此,假如客户端发送数据,那么epoll_wait必然监听到,该通道就会被添加到活跃通道中,这时就会执行channel上注册的读回调,读回调是在TcpConnection的构造函数中被注册为TcpConnection::handleRead()。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第16张图片

        我们来看看TcpConnection::handleRead

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第17张图片

        发现了什么,执行的不就是我们在EchoServer构造函数注册的messageCallback_吗,我们发现,EchoServer的messageCallback_就是EchoServer::onMessage,其内部就是将收到的数据又发了出去,所以被称为回射服务器,好了,差不多这就是比较完整的muduo库中单IO线程的高并发的echo服务器的执行流程了。

    下面以一张流程图大致表示一下EchoServer的运行流程。

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架_第18张图片

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(muduo)