小言_互联网的博客

Muduo网络库整体架构与分阶段解析

617人阅读  评论(0)


Muduo是一个基于Reactor模式的C++网络库。它采用非阻塞I/O模型,采用one loop per thread + thread pool架构实现,基于事件驱动和回调。我们不仅可以通过Muduo来学习linux服务端多线程编程,其采用的现代C++编程技术也是非常值得借鉴的。


Muduo网络库的Reactor模式

Reactor释义“反应堆”,是一种事件驱动机制。和普通函数调用的不同之处在于:应用程序不是主动的调用某个API完成处理,而是恰恰相反,Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。

Reactor,换个名词“non-blocking IO + IO multiplexing”,意思就显而易见了。Reactor模式用非阻塞IO+poll(epoll)函数来处理并发,程序的基本结构是一个事件循环,以事件驱动和事件回调的方式实现业务逻辑

Reactor模式的基础是事件驱动,事件源可以有多个,并且会并发地产生事件。Reactor模式的核心是一个事件分发器多个事件处理器,多个事件源向事件分发器发送事件,并要求事件分发器响应,Reactor模式的设计难点也是在事件分发器,它必须能够有条不紊地把响应时间分派到合适的事件处理器中,保证事件处理的最小延迟。事件处理器主要是负责处理事件的业务逻辑,这是关系到具体事件的核心,因此和事件分发器不一样,它并不太具有一般性。

Reactor模式的特点可以很自然地应用到C/S架构中。在C/S架构的应用程序中,多个客户端会同时向服务端发送request请求。服务端接收请求,并根据请求内容处理请求,最后向客户端发送请求结果。这里,客户端就相当于事件源,服务端由事件分发器和事件处理器组成。分发器的任务主要是解析请求和将解析后的请求发送到具体的事件处理器中

从技术的层面来说,怎么把“事件”这个概念放到“请求”上,也就是怎么样使得请求到来可以触发事件,对于技术层面,Linux的解决方案是:epoll,select等。而设计层面,muduo提供了较好的解决方案。

Muduo的基础设施是epoll,并在此基础上实现了one-thread-one-loop和thread-pool设计方案。也就是将事件处理器设置成线程池,每个线程对应一个事件处理器;因为事件处理器主要处理的是I/O事件,而且每个事件处理器可能会处理一个连接上的多个I/O事件,而不是处理完一个事件后直接断开,因此muduo选择每个事件处理器一个event-loop。这样,连接建立后,对于这条连接上的所有事件全权由它的事件处理器在event-loop中处理。

我们可以根据上面的Reactor架构图,简单地绘制出Muduo的架构图:

如上图所示,客户端首先和服务端建立连接,如图橙色线所示。建立连接之后将这个连接分发到具体的EventLoopThread中(所有的EventLoopThread由server中的一个EventLoopThreadPool线程池管理),这部分主要由Server中的Acceptor完成。后续Client就不再和Acceptor发生关系了。因此可以看出,建立连接之后,client直接和EventLoopThread关联,不再经过Acceptor。


整体框架说明

Muduo 是基于 Reactor 模式的网络库,其核心是个事件循环 EventLoop,用于响应计时器和I/O事件。Muduo采用基于对象(object based)而非面向对象(object oriented)的设计风格,其事件回调接口多以 boost::function + boost::bind 表达,用户在使用muduo的时候不需要继承其中的class。

学习一个网络库需要了解下面的内容:

  • 网络库是怎样bind、listen和accept的
  • epoll模型的封装以及是怎样调度读写事件的
  • accept描述符是怎样被注册到epoll模型中的


首先从TcpServer谈起,有一个TcpServer,它管理一个 Acceptor(监听socket套接字) 和 ConnectionMap(管理TCP连接),用一个ip加端口号 listenAddr 和 一个loop与TCPserver绑定起来,并为它设置新连接回调为 TcpServer::newConnection,然调用TcpServer::start() 开启线程池,并调用Acceptor::listen()监听新连接,那么当TcpConnection到达时,就会回调newConnection(),它会首先从EventLoopThreadPool中获取一个loop(通过round-robin轮询方式),然后创建一个TcpConnection 对象 conn,并且加入ConnectionMap中,然后为这个conn设置连接回调和消息回调,最后绑定回调函数connectEstablished()。TcpServer所处的线程只用来接受新连接,新连接用其他的EventLoop来执行I/O,每个EventLoop在loop()函数中调用Poller的poll()函数来更新pollfds,再调用fillActiveChannels()遍历pollfds,根据有活动事件的fd找到对应的channel,并将Channel保存到activeChannels,然后遍历activeChannels并执行它的handleEvent()函数,该函数通过revents的值判断具体执行哪个回调。


分阶段解析

1号虚线框:

  • 一是完成的描述符的创建和bind操作;
  • 二是注册了回调函数。

ConnectionCallback和MessageCallback是暴露给外界使用的

ConnectionCallback在请求成功(::accept)后调用;MessageCallback在处理具体请求时调用。

net库对外封装为TcpServer类提供了两个可供外界实现的回调函数接口:ConnectionCallback和MessageCallback,在TcpServer的构造函数中初始化了Acceptor和EventLoopThreadPool,Acceptor中创建了socket同时进行了bind;将socket放在了acceptChannel中,在acceptChannel中注册了Acceptor::handleRead函数;绑定了TcpServer::newConnection函数。


2号虚线框:

  • 一是完成socket的listen操作
  • 二是将socket注册到epoll模型中

TcpServer通过start函数调用了EventLoop的runLoop方法。

runLoop中执行了Acceptor::listen函数,在此函数中完成了socket的listen操作和注册到epoll模型的操作。


3号虚线框:

2号虚线框已经把listen的socket注册到epoll中,当有客户端连接请求时会触发epoll模型。3号虚线框描述的就是此时的操作。3号虚线框把accept成功的socket放到了TcpConnection中,并按照轮询方式把TcpConnection的socket注册到不同的EventLoop中。

当有客户端发起链接时,触发acceptChannel_中注册的Acceptor::handleRead函数,而Acceptor::handleRead中继续调用了Acceptor中注册的TcpServer::newConnection。

在TcpServer::newConnection中,进行了socket的accept操作,并生成了新的TcpConnection。

在runInLoop中调用了TcpConnection::connectEstablished方法,将socket注册到EventLoopThreadPool中的EventLoop中,并调用了在TcpServer中注册的ConnectionCallback函数。


4号虚线框:

4号虚线框epoll模型开始等待外界发送请求,这时会触发channel_的handRead方法,在handRead中读取了请求,然后调用了TcpServer中注册的messageCallback_方法

messageCallback_方法中不仅包含处理请求的逻辑,还必须考虑怎样返回结果,其中一种可选方式是调用TcpConnection的send方法发送结果。

框架:https://www.2cto.com/net/201609/544547.html


转载:https://blog.csdn.net/ZYZMZM_/article/details/99261631
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场