注意:本项目需要先了解整个网络框架,学习epoll模型再来开启
背景
传统的解决方案
cpp
while (true) {
conn = accept(); // 阻塞
data = recv(conn); // 阻塞
process(data); // 处理
send(conn); // 阻塞
}
一个阻塞点卡住,整个程序卡住
并发靠线程堆出来
线程多 → 上下文切换炸裂
此时的并发量是靠线程堆出来的
而学习了epoll模型之后,我们可以利用epoll来通知,简单来说就是把IO事件中的等交给epoll
Reactor 模式: 是指通过⼀个或多个输⼊同时传递给服务器进⾏请求处理时的事件驱动处理模式。 服务端程序处理传⼊多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫Dispatcher 模式。简单理解就是使⽤ I/O 多路复⽤ 统⼀监听事件,收到事件后分发给处理进程或线程,是编写⾼性能网络服务器的必备技术之⼀。整个Reactor模式有三大核心组件
1️⃣ Reactor(反应堆)
👉 核心调度者
等待事件(epoll_wait)
拿到"哪些 fd 发生了什么事"
分发给对应的 Handler
muduo 里就是:EventLoop
2️⃣ Demultiplexer(事件分离器)
👉 底层 IO 多路复用
select / poll / epoll
负责告诉 Reactor:
哪些 fd 就绪了
muduo 里是:EpollPoller
3️⃣ Handler(事件处理器)
👉 真正干活的人
每个 fd 对应一个 handler
提供回调:
handleRead
handleWrite
handleClose
muduo 里是:Channel + TcpConnection
Reactor模式有很多类,以下说的Reactor指的就是反应堆(EventLoop)
单Reactor单线程:单I/O多路复⽤+业务处理:
- 通过IO多路复用模型进行客户端请求监控
- 触发事件后,进⾏事件处理
a. 如果是新建连接请求,则获取新建连接,并添加⾄多路复⽤模型进⾏事件监控。
b. 如果是数据通信请求,则进⾏对应数据处理(接收数据,处理数据,发送响应)。
优点: 所有操作均在同⼀线程中完成,思想流程较为简单,不涉及进程/线程间通信及资源争抢问题。
缺点: 无法有效利⽤CPU多核资源,很容易达到性能瓶颈。
适⽤场景: 适⽤于客⼾端数量较少,且处理速度较为快速的场景。(处理较慢或活跃连接较多,会导致串⾏处理的情况下,后处理的连接长时间得不到响应)。单Reactor多线程:单I/O多路复⽤+线程池(业务处理)
- Reactor线程通过I/O多路复⽤模型进⾏客⼾端请求监控
- 触发事件后,进⾏事件处理
a. 如果是新建连接请求,则获取新建连接,并添加⾄多路复⽤模型进⾏事件监控。
b. 如果是数据通信请求,则接收数据后分发给Worker线程池进⾏业务处理。
c. ⼯作线程处理完毕后,将响应交给Reactor线程进⾏数据响应
优点: 充分利⽤CPU多核资源
缺点: 多线程间的 数据共享访问控制较为复杂 ,单个Reactor 承担所有事件的监听和响应,在单线程中运⾏,⾼并发场景下容易成为性能瓶颈。多Reactor多线程:多I/O多路复⽤+线程池(业务处理)
- 在主Reactor中处理新连接请求事件,有新连接到来则分发到子Reactor中监控
- 在⼦Reactor中进⾏客⼾端通信监控,有事件触发,则接收数据分发给Worker线程池
- Worker线程池分配独⽴的线程进⾏具体的业务处理
a. ⼯作线程处理完毕后,将响应交给⼦Reactor线程进⾏数据响应
优点: 充分利⽤CPU多核资源,主从Reactor各司其职为什么要区分主从???
这涉及惊群效应,如果全部的reactor都监听同一个fd,全部苏醒但是只有一个能处理,其他的就会浪费,并且这涉及锁、线程的开销,不如设计成主从,主accept,从去处理每个客户端的通信问题,这样每个客户端还能但是占有某个线程,不涉及锁的开销
名词解释
muduo是陈硕老师开源的一个高并发网络框架
one thread one loop:一个线程只负责一个 EventLoop,一个 EventLoop 只在一个线程中运行
每个线程内部跑一个 事件循环(EventLoop) EventLoop 使用 I/O 多路复用(epoll / poll / select)所有 socket 的读写、回调都在所属 EventLoop 的线程里完成
那么此时就不需要加锁,避免锁的开销
优点:
几乎无锁
连接不跨线程
回调都在同一个线程执行
性能可预测
避免锁竞争
cache 友好
编程模型清晰
- "我只关心这个连接,不用担心并发"
非常适合网络服务器
- Redis / Nginx / muduo 都是类似思想
缺点:
单连接内不能阻塞
- 回调里 sleep = 整个 loop 卡死
CPU 密集任务不友好
- 需要额外线程池
负载不均问题
- 一个 loop 上"重连接"可能拖慢其他连接
适合什么场景?
✔ 高并发 TCP 服务器
✔ IO 密集型
✔ 请求处理快、非阻塞
❌ 长时间计算
❌ 同一连接内复杂阻塞逻辑
总结:
因为不需要加锁,这就是这个架构的核心优势,所有的连接、读写操作等再一个线程内完成,就注定了cpu中的cache命中率高,适合IO密集型,cpu密集不适合,也就是handler的处理部分如果事件很长(也就是每个可读事件可写事件)会导致epoll回不去,后续事件全部延迟,所有连接一起陪跑
⽬标定位:One Thread One Loop主从Reactor模型⾼并发服务器咱们要实现的是主从Reactor模型服务器,也就是主Reactor线程仅仅监控监听描述符,获取新建连接,保证获取新连接的⾼效性,提⾼服务器的并发性能。
主Reactor获取到新连接后分发给⼦Reactor进⾏通信事件监控。⽽⼦Reactor线程监控各⾃的描述符的读写事件进⾏数据读写以及业务处理。
One Thread One Loop的思想就是把所有的操作都放到⼀个线程中进⾏,⼀个线程对应⼀个事件处理的循环。
当前实现中,因为并不确定组件使⽤者的使⽤意向,因此并不提供业务层⼯作线程池的实现,只实现主从Reactor,⽽Worker⼯作线程池,可由组件库的使⽤者的需要⾃⾏决定是否使⽤和实现。
模块分析
功能模块划分:
基于以上的理解,我们要实现的是⼀个带有协议⽀持的Reactor模型⾼性能服务器,因此将整个项⽬的
实现划分为两个⼤的模块:
• SERVER模块:实现Reactor模型的TCP服务器;
• 协议模块:对当前的Reactor模型服务器提供应⽤层协议⽀持。
SERVER模块:
SERVER模块就是对所有的连接以及线程进⾏管理,让它们各司其职,在合适的时候做合适的事,最终完成⾼性能服务器组件的实现。
⽽具体的管理也分为三个⽅⾯:
• 监听连接管理:对监听连接进⾏管理。
• 通信连接管理:对通信连接进⾏管理。
• 超时连接管理:对超时连接进⾏管理。
基于以上的管理思想,将这个模块进⾏细致的划分⼜可以划分为以下多个⼦模块:
Buffer模块:
Buffer模块是⼀个缓冲区模块,⽤于实现通信中⽤⼾态的接收缓冲区和发送缓冲区功能
Socket模块:
Socket模块是对套接字操作封装的⼀个模块,主要实现的socket的各项操作。
Channel模块:
Channel模块是对⼀个描述符需要进⾏的IO事件管理的模块,实现对描述符可读,可写,错误...事件的管理操作,以及Poller模块对描述符进⾏IO事件监控就绪后,根据不同的事件,回调不同的处理函数功
能。
Connection模块
Connection模块是对Buffer模块,Socket模块,Channel模块的⼀个整体封装,实现了对⼀个通信套
接字的整体的管理,每⼀个进⾏数据通信的套接字(也就是accept获取到的新连接)都会使⽤
Connection进⾏管理。
• Connection模块内部包含有三个由组件使⽤者传⼊的回调函数:连接建⽴完成回调,事件回调,新数据回调,关闭回调。
• Connection模块内部包含有两个组件使⽤者提供的接⼝:数据发送接⼝,连接关闭接⼝
• Connection模块内部包含有两个⽤⼾态缓冲区:⽤⼾态接收缓冲区,⽤⼾态发送缓冲区
• Connection模块内部包含有⼀个Socket对象:完成描述符⾯向系统的IO操作
• Connection模块内部包含有⼀个Channel对象:完成描述符IO事件就绪的处理
具体处理流程如下:
- 实现向Channel提供可读,可写,错误等不同事件的IO事件回调函数,然后将Channel和对应的描述符添加到Poller事件监控中。
- 当描述符在Poller模块中就绪了IO可读事件,则调⽤描述符对应Channel中保存的读事件处理函数,进⾏数据读取,将socket接收缓冲区全部读取到Connection管理的⽤⼾态接收缓冲区中。然后调⽤由组件使⽤者传⼊的新数据到来回调函数进⾏处理。
- 组件使⽤者进⾏数据的业务处理完毕后,通过Connection向使⽤者提供的数据发送接⼝,将数据写⼊Connection的发送缓冲区中。
- 启动描述符在Poll模块中的IO写事件监控,就绪后,调⽤Channel中保存的写事件处理函数,将发送缓冲区中的数据通过Socket进⾏⾯向系统的实际数据发送。
Acceptor模块:
Acceptor模块是对Socket模块,Channel模块的⼀个整体封装,实现了对⼀个监听套接字的整体的管理。
• Acceptor模块内部包含有⼀个Socket对象:实现监听套接字的操作
• Acceptor模块内部包含有⼀个Channel对象:实现监听套接字IO事件就绪的处理
具体处理流程如下:- 实现向Channel提供可读事件的IO事件处理回调函数,函数的功能其实也就是获取新连接
- 为新连接构建⼀个Connection对象出来。
TimerQueue模块:
TimerQueue模块是实现固定时间定时任务的模块,可以理解就是要给定时任务管理器,向定时任务管
理器中添加⼀个任务,任务将在固定时间后被执⾏,同时也可以通过刷新定时任务来延迟任务的执行。
这个模块主要是对Connection对象的⽣命周期管理,对⾮活跃连接进⾏超时后的释放功能。
TimerQueue模块内部包含有⼀个timerfd:linux系统提供的定时器。
TimerQueue模块内部包含有⼀个Channel对象:实现对timerfd的IO时间就绪回调处理
Poller模块:
Poller模块是对epoll进⾏封装的⼀个模块,主要实现epoll的IO事件添加,修改,移除,获取活跃连接
功能。
EventLoop模块:
EventLoop模块可以理解就是我们上边所说的Reactor模块,它是对Poller模块,TimerQueue模块, Socket模块的⼀个整体封装,进⾏所有描述符的事件监控。
EventLoop模块必然是⼀个对象对应⼀个线程的模块,线程内部的⽬的就是运⾏EventLoop的启动函数。
EventLoop模块为了保证整个服务器的线程安全问题,因此要求使⽤者对于Connection的所有操作⼀ 定要在其对应的EventLoop线程内完成,不能在其他线程中进⾏(⽐如组件使⽤者使⽤Connection发 送数据,以及关闭连接这种操作)。
EventLoop模块保证⾃⼰内部所监控的所有描述符,都要是活跃连接,⾮活跃连接就要及时释放避免
资源浪费。
• EventLoop模块内部包含有⼀个eventfd:eventfd其实就是linux内核提供的⼀个事件fd,专⻔⽤于
事件通知。
• EventLoop模块内部包含有⼀个Poller对象:⽤于进⾏描述符的IO事件监控。
• EventLoop模块内部包含有⼀个TimerQueue对象:⽤于进⾏定时任务的管理。
• EventLoop模块内部包含有⼀个PendingTask队列:组件使⽤者将对Connection进⾏的所有操作,都加⼊到任务队列中,由EventLoop模块进⾏管理,并在EventLoop对应的线程中进⾏执⾏。
• 每⼀个Connection对象都会绑定到⼀个EventLoop上,这样能保证对这个连接的所有操作都是在 ⼀个线程中完成的。
具体操作流程:- 通过Poller模块对当前模块管理内的所有描述符进⾏IO事件监控,有描述符事件就绪后,通过描述符对应的Channel进⾏事件处理。
- 所有就绪的描述符IO事件处理完毕后,对任务队列中的所有操作顺序进⾏执⾏。
- 由于epoll的事件监控,有可能会因为没有事件到来⽽持续阻塞,导致任务队列中的任务不能及时得到执⾏,因此创建了eventfd,添加到Poller的事件监控中,⽤于实现每次向任务队列添加任务的时候,通过向eventfd写⼊数据来唤醒epoll的阻塞。
TcpServer模块:
这个模块是⼀个整体Tcp服务器模块的封装,内部封装了Acceptor模块,EventLoopThreadPool模 块
• TcpServer中包含有⼀个EventLoop对象:以备在超轻量使⽤场景中不需要EventLoop线程池,只需要在主线程中完成所有操作的情况。
• TcpServer模块内部包含有⼀个EventLoopThreadPool对象:其实就是EventLoop线程池,也就是⼦Reactor线程池
• TcpServer模块内部包含有⼀个Acceptor对象:⼀个TcpServer服务器,必然对应有⼀个监听套接
字,能够完成获取客⼾端新连接,并处理的任务。
• TcpServer模块内部包含有⼀个std::shared_ptr<Connection>的hash表:保存了所有的新建连接对应的Connection,注意,所有的Connection使⽤shared_ptr进⾏管理,这样能够保证在hash表中删除了Connection信息后,在shared_ptr计数器为0的情况下完成对Connection资源的释放操作。
具体操作流程如下:- 在实例化TcpServer对象过程中,完成BaseLoop的设置,Acceptor对象的实例化,以及EventLoop线程池的实例化,以及std::shared_ptr<Connection>的hash表的实例化。
- 为Acceptor对象设置回调函数:获取到新连接后,为新连接构建Connection对象,设置
Connection的各项回调,并使⽤shared_ptr进⾏管理,并添加到hash表中进⾏管理,并为
Connection选择⼀个EventLoop线程,为Connection添加⼀个定时销毁任务,为Connection添加事件监控,- 启动BaseLoop。
HTTP协议模块:
HTTP协议模块⽤于对⾼并发服务器模块进⾏协议⽀持,基于提供的协议⽀持能够更⽅便的完成指定协议服务器的搭建。而HTTP协议⽀持模块的实现,可以细分为以下⼏个模块。
Util模块:
这个模块是⼀个⼯具模块,主要提供HTTP协议模块所⽤到的⼀些⼯具函数,⽐如url编解码,⽂件读
写....等。
HttpRequest模块:
这个模块是HTTP请求数据模块,⽤于保存HTTP请求数据被解析后的各项请求元素信息。
HttpResponse模块:
这个模块是HTTP响应数据模块,⽤于业务处理后设置并保存HTTP响应数据的的各项元素信息,最终会被按照HTTP协议响应格式组织成为响应信息发送给客⼾端。
HttpContext模块:
这个模块是⼀个HTTP请求接收的上下⽂模块,主要是为了防⽌在⼀次接收的数据中,不是⼀个完整的 HTTP请求,则解析过程并未完成,⽆法进⾏完整的请求处理,需要在下次接收到新数据后继续根据上下⽂进⾏解析,最终得到⼀个HttpRequest请求信息对象,因此在请求数据的接收以及解析部分需要一个上下⽂来进⾏控制接收和处理节奏。
HttpServer模块:
这个模块是最终给组件使⽤者提供的HTTP服务器模块了,⽤于以简单的接⼝实现HTTP服务器的搭建。
HttpServer模块内部包含有⼀个TcpServer对象:TcpServer对象实现服务器的搭建
HttpServer模块内部包含有两个提供给TcpServer对象的接⼝:连接建⽴成功设置上下⽂接⼝,数据处理接⼝。
HttpServer模块内部包含有⼀个hash-map表存储请求与处理函数的映射表:组件使⽤者向
HttpServer设置哪些请求应该使⽤哪些函数进⾏处理,等TcpServer收到对应的请求就会使⽤对应的函 数进⾏处理。
EventLoop模块是整个的核心,是线程绑定的对象,是事件调度器核心职责:负责等待事件、分发事件、执行回调。(负责驱动 Poller 等待 IO 事件,并在事件就绪后分发给 Channel 执行回调。)
Channel模块是管理事件的核心,全部的事件的描述等问题需要通过这个类来操作
核心职责:fd 的事件描述 + 回调绑定对象
Poller模块是对epoll模型的封装
核心职责:向内核注册 fd、等待事件就绪、返回活跃的 Channel 列表
所以核心组件已经清晰:所以一个线程拥有EventLoop + Poller + 一组 Channel 共同构成一个 Reactor模型。来一个fd它需要通channel进行事件描述,然后把事件投递到EventLoop中,EventLoop驱动Poller进行监听,然后Poller监听到事件就绪,需要把事件投递到EventLoop中,EventLoop线程执行事件描述回调函数的回调
Connection模块:这个模块是承上启下,管理一个连接,对下对应reactor模型,对上对应TcpServer模块,一个connection管理一个链接,被创建的时候完成事件的描述,然后由上层调用established开启事件channel的监听,上层可以设置各种回调函数,比如当可读事件触发时,先把数据读上来,再把数据填写到回调函数中TcpServer模块:这个是对外提供服务的
组件的调用者需要写协议,然后设置协议和各种回调
比如你是http服务器,你需要写http的协议,然后设置回调函数,当来一个连接的时候acceptor就会接收上来,回调中就会把newfd返回出去给tcpserver,tcpserver就会创建一个新的connection,此时创建的过程当中又会调用httpserver设置的回调,去设置这个连接的协议
所以本质就是,不断的给外面提供回调函数设置接口,当可读事件来临的时候,除了把数据从底层读取的缓冲区,还要把数据返回给业务层
整个项目的难点在于回调函数+架构的理解
前置知识点学习
Linux系统,对进程线程有了解
Linux网络,对整个网络架构要有清晰认识
http
数据结构
c++的一些语法(包含智能指针等c++11特性)
所有的零碎知识点在对应的章节当中都涵盖
GitHub代码
JulyW-cf/Fcw_muduo: 这是一个仿陈硕老师的个人muduo网络库,感谢陈硕老师的开源代码
已经上传github,按需自取,感谢陈硕老师
