muduo库的简化仿写
知识要求
C++11特性:多线程,智能指针
Linux:网络通信,多路复用
为什么写这个项目
在学校学了这些知识,想要提高自己的能力,就在网上找的相关的开源项目,通过原码阅读、仿写的⽅式提升多线程⽹络编程能⼒。
整体计划
1.高性能服务器框架 10
2.支持应用层协议:HTTP 5
3.通过自定制协议实现RPC通信框架 (自定制协议:在网络通信中别人不知道我的格式) 5
框架设计
Reactor模型
别名:反应堆模型(直译),事件驱动模型(原理),事件循环模型(实现机制)。
就是epoll
核心特性
被动反应:不像主动轮询,而是等待事件发生然后反应
链式反应:⼀个事件可能触发⼀系列后续处理
能量集中:少量线程处理大量连接,像反应堆高效产⽣能量
控制中心:像核反应堆的控制系统,统⼀调度所有事件
传统阻塞模型:主动询问
每个客户端连接主动询问:"你有数据吗?"
Reactor模型:被动反应
事件发生时自动被唤醒
reactor模型的演化过程
单reactor模型

优点:极其简单
缺点:一个线程里既要获取新连接,又要处理数据
单reactor+线程池模型

把 业务处理 单独拿出来用 线程池 处理。
缺点:依旧存在单线程IO的性能瓶颈。只有一个线程 处理所有网络 IO 事件
主从reactor模型

主从 Reactor 模型将连接接收 与读写事件处理分离,主 Reactor 只负责 accept 新连接并分派,多个从 Reactor 分担网络 IO 与协议解析,耗时业务交由独立业务线程池,充分利用多核 CPU、消除单线程瓶颈与单点故障。
Proactor模型
别名:前摄器模型、主动器模型

核心思想
应用程序发起异步I/O操作,然后继续执行其他任务,当I/O操作完成时,操作系统会通知应用程序,应用程序再处理相应的业务逻辑。
Reactor:"当I/O就绪时通知我,我来做读写操作"
Proactor:"我告诉你我要读写什么,你帮我做完后通知我结果。
核心特点
异步I/O:应⽤程序调⽤异步I/O操作,由操作系统负责执⾏I/O,应⽤程序不阻塞等待。
完成通知:I/O操作完成后,操作系统主动通知应⽤程序。
分离关注点:应⽤程序的I/O操作和事件处理分离,由操作系统负责I/O操作,应⽤程序负责业务处 理。
I/O操作
1.发起IO操作
2.等待IO就绪
3.拷贝数据
4.IO调用返回
异步IO
进程发起IO调用,IO等待+数据拷贝过程由系统完成,IO完成后通知进程
概要设计
Reactor模型实现主要分为三⼤模块:reactor、handler,逻辑整合,在这三⼤模块中,⼜细分了其他 的⼦模块进⾏细节实现。
模块设计
reactor:负责事件循环监控及分发功能(事件监控)
Poller描述符监控模块:负责描述符的事件监控与事件分发。
Eventloop事件循环模块:负责实现Poller事件循环监控 + 任务池实现连接的线程安全操作。
- Weakup事件循环唤醒模块:负责Poller的阻塞唤醒事件描述符的读事件处理(muduo中没 有)
LoopThread模块:负责实现Eventloop与对应线程thread封装在⼀起。
LoopThreadPool模块:负责实现池化LoopThread功能
handler:负责描述符的监控事件描述以及事件处理(事件处理)
Channel事件处理器模块:负责描述描述符的监控事件以及事件处理
TimerQueue定时器模块:负责实现定时器中定时器描述符的读事件处理
- Timestamp时间操作模块:负责简单的系统⽇期时间操作
- Timer定时任务模块:负责描述⼀个定时任务(过期时间,是否重复,过期回调,...)
- TimerId模块:与定时任务Timer对应的定时任务ID(唯⼀序号,定时任务指针)
Acceptor监听对象模块:负责监听套接字的读事件处理(新连接)
Connection连接对象模块:负责I/O套接字的读写事件处理。(IO处理)
- Buffer模块:负责实现发送/接收数据缓冲区
- Sockets模块:负责套接字的各项基础操作
逻辑整合
TcpServer服务器模块:负责基于以上模块整合实现服务端
TcpClient客⼾端模块:负责基于以上模块整合实现客户端
模块关系图


详细设计
channel
fd:要监控的描述符
event:要监控的事件
revent:实际就绪的事件
readcallback:读事件回调函数
writecallback:写事件回调函数
errorcallback:错误时间回调函数
closecallback:连接关闭事件回调函数
功能:如果要对哪个描述符进行事件监控,就为其构造一个channel对象。这个channel对象就描述了监控了哪个描述符的哪个事件。然后将这些信息交给Poller(epoll)进行实际的事件监控。
poller
epoll封装
updataChannel
removeChannel
wait
eventLoop
epoll_wait:事件监控
执行跨线程任务
weakuoChannel(fd,this)构造一个channel对象,里面有fd,eventloop*loop。eventloop*loop指向的就是eventloop对象。enableReading开始监控,调用loop->updateChannel(this),指向eventloop的updateChannel。然后把updateChannel加到poller里面进行监控。
是事件循环类。
功能:
1.针对Poller进行二次封装实现channel的事件监控。
2.解决后期连接 线程安全问题 ------任务池。
3.定时任务的操作。
定时器的设计
Timestamp类:时间戳管理类
Timerld:定时任务ID。唯一标识一个定时任务对象
Timer类:定时任务类,封装了一个回调函数对象
TimerQueue类:定时任务管理类,
定时器实现原理:
1.能够让系统通知进程自身,现在超时了。(告诉系统一个固定的时间点,让系统在这个时间点通知进程)


timerfd本质原理:
本质是内核的一个8B计数器+计时功能,当我们给描述符设置了一个超时/间隔时间,内核在每隔一定的间隔时间后,给这个计数器+1,这时候,定时器描述符就会触发可读事件(这个事件就是超时通知),超时之后,我们需要将计数器中的计数归零。
2.能够快速的找出超时任务,针对这些任务执行一下
定时任务管理 实现思想:
1.时间轮思想

1.定义一个时间轮(环形数组/链表,每个结点都是一个链表,该链表中存放的都是定时任务)
2.定义一个刻度指针,刻度指针指向哪个结点,那个结点链表中的任务就都是过期任务
3.通过定时器,让刻度指针移动起来
优点:不用调整堆结构
缺点:需要不断触发事件
2.小根堆思想
小根堆:根顶总是最小节点
定义一个以时间为比较关键字的小根堆,堆顶任务就是最接近超时任务。
实现过程:
1.获取堆顶节点的时间戳,定时定时器超时时间
2.当定时器超时的时候,意味着堆顶节点必然超时
3.当超时时,不断从堆顶取出节点,直到堆顶节点时间大于当前系统时间(没有超时)
优点:不会出现空跑
muduo库中的定时器的实现:小根堆思想。
实现所用的数据结构:红黑树
节点结构:pair<timestamp , timer>


左叶子节点就是最接近超时的节点。
定时器的实现:
1.使用timerfd实现定时器超时通知
2.使用红黑树进行超市任务管理(map<pair<timestamp , timer*>>),在收到超时通知后快速获取过期任务。
3.实现细节:
1.定义一个定时任务池,保存所有的定时任务。
2.取出任务池中最小节点的超时时间,设置为定时器的国企通知时间
3.当收到通知,则从定时任务池中取出所有的过期任务,然后进行处理。
4.针对所有的过期任务,判断是否是循环任务,如果是且没有被标记为取消,则重新添加到定时任务池中;如果时但是被标记为取消,则直接释放任务;如果不是则直接释放任务。
取消定时任务的细节:
(1)如果任务在定时任务池中,意味着必然不是正在执行的过期任务。
(2)如果任务不在定时任务池中,这个任务必然在过期池中,正在被执行
为什么要有定时器
1.几乎在所有的网络通信库中,都会配备有定时器功能,因为很多时候我们搭建了服务器,都需要丢客户端连接设置超时断开时间。
2.任务池加入的任务是什么:任务就是一个回调函数对象+任务管理信息
3.循环任务都可能存放在哪些池子中:(1)任务都在红黑树定时任务池中(2)任务过期的时候,会从定时任务池中取出,放到临时的过期池中处理,处理完毕后重新放回定时任务池。
Acceptor
监听事件的处理
TcpConnection
通信事件的处理
readv分块接收操作
核心关键点:
为什么muduo库性能在大块数据的pingpong测试中比libevent高了近3倍?
因为muduo中使用readv分块接收,一次IO取出了socket缓冲区所有数据;而libevent是循环每次接收4096字节数据。
核心关键点:
muduo库中,epoll监控时事件触发模式使用的是水平触发,而不是边缘触发。
水平触发:只要socket接收缓冲区中的数据大于高水位标记(1B)就会触发事件
边缘触发:只有新数据到来的时候才会触发事件,强制要求程序员在一次事件触发中,处理完所有数据;减少了数据触发数量。
因为muduo数据接受操作使用分块接收,一次性就取出了所有数据,没必要使用边缘触发。水平出发逻辑也更加简单。
逻辑关系
fd挂载到epoll,找到Channel里的事件的回调函数,然后执行。
Acceptor(监听管理器)里有个handleRead来获取新连接。Channel里的回调函数就是handleRead。
获取到的新连接会创建一个TcpConnection,里面封装一个Channel对象,来设置一系列的回调函数。
TimerQueue里有个定时器描述符,也会封装一个Channel对象,执行定时任务。
请求流程
1.服务器启动,Main Reactor 开始监听端口。
2.客户端发起连接,Acceptor 读取事件,accept 得到新 fd。
3.Main Reactor 轮询选择一个 Sub Reactor。
4.创建 TcpConnection 对象,绑定到对应 Sub Reactor。
5.数据到达时,epoll 通知,Channel 调用读回调。
6.数据读到 Buffer,触发用户的 MessageCallback。
7.用户 send 数据时,先写入输出缓冲区,开启写事件,可写时自动发送。
8.连接关闭时,自动清理资源、回调、移除监听。