muduo之​​React​​or(反应堆)模式

最近在看muduo的reactor模式,从中学到了很多东西,我打算把我学到的知识整理一下分享出来,对自己来说也是一种知识总结,对他人来说也是一种学习指南吧。

Reactor模式总述

我打算从后往前,从整体到细节的方式来讲述这一部分的内容,所以第一节就讲一下Reactor概述。

反应堆设计模式(Reactor pattern)是一种为处理并发服务请求,并将请求提交到一个或者多个服务处理程序的事件设计模式。当客户端请求抵达后,服务处理程序使用多路分配策略,由一个非阻塞的线程来接收所有的请求,然后派发这些请求至相关的工作线程进行处理。

在muduo中只要使用到了这样几个类:EventLoop,Poller,Channel,具体细节放在后面再讲,先看一下总体的流程图
C++ muduo网络库 学习笔记之:Reactor框架总览_网络

首先创建一个Eventloop,在里面执行事件循环,每一个Eventloop通过scpoed_ptr来间接持有Poller。Poller是IO multiplexing(多路IO转接)的封装,可实现poll(2)和epoll(4)两种IO multiplexing机制。Poller调用poll(2)获得当前活动的IO事件,把它填入到Channel中。Channel负责一个文件描述符(file description)的IO事件分发,把不同事件分发给不同的回调函数。

以上就是Muduo中Reactor模式的大概了。

Tcp网络编程的本质无非就是处理三个半事件,即:

  1. 连接的简历
  2. 连接的断开:包括主动断开和被动断开
  3. 消息到达,文件描述符可读
  4. 消息发送完毕,这算半个事件。

现在我们具体分析一下这三个半事件的框架实现(本篇博客只是梳理框架,先懂理论才有资格谈实践嘛,具体实现请查看我的其他博文)

连接的建立

在我们单纯使用linux的API,编写一个简单的Tcp服务器时,建立一个新的连接通常需要四步:

  • 步骤1. socket() // 调用socket函数建立监听socket
  • 步骤2. bind() // 绑定地址和端口
  • 步骤3. listen() // 开始监听端口
  • 步骤4. accept() // 返回新建立连接的fd

我们接下来分析下,这四个步骤在muduo中都是何时进行的:
先看一下流程图:

C++ muduo网络库 学习笔记之:Reactor框架总览_网络_02
前面三个类都是Reactor模式的标准框架。当TcpServer server(&loop, listenAddr);构造完成时候,监听socket也就建立好了,并且已经绑定到对应的地址和端口了,即步骤1和步骤2在这时候就完成了。server.start()里面主要完成了两个工作,在sokect(注意这里的socket是指linsten socket 只负责客户端的连接)上面启动linsten()函数,也就是步骤3。将socket的可读事件注册到EventLoop中。当socket可读的时候便触发Channel::handleEvent()进而调用Acceptor::handleRead()。这时Acceptor干了三件事,完

  1. 成accept()也就是步骤4,
  2. 2.回调TcpServer::newConnetion(),后者会创建TcpConnection对象
  3. 将已连接的socket的可读事件注册到EventLoop;
    这里还有一个需要注意的点,创建的TcpConnnection对象是个shared_ptr,该对象会被保存在TcpServer的connections中。这样才能保证引用计数大于0,对象不被释放。至此,一个新的连接已完全建立好,其可读事件也已注册到EventLoop中了。

消息的读取

上节说到,当有新连接的时候,会将新的socket的可读事件注册到EventLoop中。假如客户端向服务器发送消息,就会触发socket的可读事件,该可读事件的消息处理回调函数同样的会在EventLoop::loop()中被调用。

当Socket发生可读事件的时候,通过Channel的HandleEvent()调用Tcpconnection::handleread()。(请向上看Reactor模式图),在后者中从socket中读取数据,并放入Buffer(缓冲区)成员变量 inputbuffer中,再调用messagecallback,执行业务逻辑
C++ muduo网络库 学习笔记之:Reactor框架总览_客户端_03
顺便提一下 如果读取的数据长度为0 即代表客户端主动关闭连接,关闭连接放在后面讲。

messagecallback是Tcpserver创建的时候用户传入的参数(在这个函数中完成用户自己的业务逻辑,我们可以在里面实现消息的编解码、消息的分发等工作等等),通过Tcpserver渗透到TcpConnection 并在此调用。

消息的发送

发送数据比接收数据要难,因为发送数据是主动的,接收数据是被动的。首先需要注意的是线程安全问题, 对于消息的读写必须都在EventLoop的同一个线程(通常称为IO线程)中进行。muduo通过Eventloop的runinloop函数来切换IO线程进行消息发送的工作,暂时不讲,暂时先知道这个概念就行,我的另外一篇博客有专门讲解这个巧妙的方法。

在sentInloop中,做了这样几件事情:

  1. 如果OutputBuffer为空,则直接向socket写入数据
  2. 否则在OutputBuffer继续Append数据(注意,如果缓冲区满了怎么办?)
  3. 将socket的可写事件注册到eventLoop中,当socket可写时,Channel就会调用TcpConnection::handleWrite(),继续发送OutputBuffer中的数据,数据全部发送完毕后应该立即停止观察可写事件,避免Busy loop。

连接的断开

我们看下muduo对于连接的断开是怎么处理的。 连接的断开分为被动断开和主动断开。muduo只有一种关闭连接的方式:被动关闭。即对方先关闭连接,本地Read返回0,触发关闭逻辑。
C++ muduo网络库 学习笔记之:Reactor框架总览_开发语言_04
socket有可读事件,read的字节数为0,触发Tcpconnect::handleClose()在此函数中先清除Channel中的观察事件,然后回调TcpServer的RemoveConn(),在这个函数中删除原来建立的TcpConnection对象,最后调用此对象的析构函数,close(fd);思考一下,为什么TcpServer要去EventLoop里面转一大圈而不是直接调用TcpConnection的connectDestroyed()?