【C++仿Muduo库 #1】基本了解
📃个人主页:island1314
🔥个人专栏:Linux—登神长阶
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
- 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》
🔥 目录
- 一、实现目标
- 二、HTTP 服务器
- 三、Reactor 模型
- 概念
- 分类
- 1. 单 Reator 单线程:单 I/O 多路复用 + 业务处理
- 2. 单 Reator 多线程:单 I/O 多路复用 + 线程池(业务处理)
- 3. 多 Reator 多线程:多 I/O 多路复用 + 线程池(业务处理)
- 四、功能模块划分
- 1. Buffer 模块
- 2. Socket 模块
- 3. Channel 模块
- 4. Connection 模块
- 5. Acceptor 模块
- 6. TimeQueue 模块
- 7. Poller 模块
- 8. EventLoop 模块
- 9. TcpServer 模块
- 10. 模块关系图
一、实现目标
仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器:
- 通过咱们实现的高并发服务器组件,可以简洁快速的完成一个高性能的服务器搭建。
- 并通过组件内提供的不同应用层协议支持,也可以快速完成一个高性能应用服务器的搭建(当前为了便于项目的演示,项目中提供HTTP协议组件的支持)
- 注意:要明确的是咱们实现的是一个高并发服务器组件,因此当前的项目中并不包含实际的业务内容。
二、HTTP 服务器
概念
🔥 HTTP(Hyper Text Transfer Protocol)超文本传输协议是应用层协议,是一种简单的 请求—响应协议 (客户端根据自己的需要向服务器发送请求,服务器针对请求提供服务,完毕后通信结束)。协议细节已经讲过,这里不再赘述(【Linux网络#7】:应用层协议 HTTP (超文本传输协议)_)
- 但是需要注意的是HTTP协议是一个运行在TCP协议之上的应用层协议,这一点本质上是告诉我们,HTTP服务器其实就是个TCP服务器,只不过在应用层基于HTTP协议格式进行数据的组织和解析来明确客户端的请求并完成业务处理。
因此实现HTTP服务器简单理解,只需要以下几步即可
- 搭建一个TCP服务器,接收客户端请求。
- 以HTTP协议格式进行解析请求数据,明确客户端目的。
- 明确客户端请求目的后提供对应服务。
- 将服务结果 HTTP 协议格式进行组织,发送给客户端
注意:实现一个HTTP服务器很简单,但是实现一个高性能的服务器并不简单
这个单元中主要的是将讲解基于 Reactor 模式的高性能服务器实现。当然准确来说,由于要实现的服务器本身并不存在业务,咱们要实现的应该算是一个高性能服务器基础库,是一个基础组件
三、Reactor 模型
概念
Reactor 模式:是指通过一个或多个输入同时传递给服务器进行请求处理时的事件驱动处理模式
- 服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式。
简单理解就是使用 I/O 多路复用 统一监听事件,收到事件后分发给处理进程或线程,是编写高性能网络服务器的必备技术之一
分类
1. 单 Reator 单线程:单 I/O 多路复用 + 业务处理
- 通过 IO 多路复用模型进行客户端请求监控
- 触发事件后,进行事件处理
- 如果是新建连接请求,则获取新建连接,并添加至多路复用模型进行事件监控
- 如果是数据通信请求,则进行对应数据处理(接收数据,处理数据,发送响应)
优点:所有操作均在同一线程中完成(串行化),思想流程较为简单,编码流程也较为简单,无需进程/线程间通信及资源争抢问题,以及安全问题
缺点:无法有效利用CPU多核资源(因为所有事件监控和业务处理都在同一个线程中完成),很容易达到性能瓶颈。
适用场景:适用于客户端数量较少,且处理速度较为快速的场景。(处理较慢或活跃连接较多,会导致串行处理的情况下,后处理的连接长时间无法得到响应)。
2. 单 Reator 多线程:单 I/O 多路复用 + 线程池(业务处理)
- Reactor 线程通过 IO 多路复用模型进行客户端请求监控
- 触发事件后,进行事件处理
- 如果是新建连接请求,则获取新建连接,并添加至多路复用模型进行事件监控。
- 如果是数据通信请求,!则接收数据后分发给 Worker 线程池进行业务处理。
- 工作线程处理完毕后,将响应交给 Reactor 线程进行数据响应
- 优点:充分利用CPU多核资源,处理效率高,降低了代码耦合度
- 缺点:多线程间的数据共享访问控制较为复杂,单个Reactor承担所有事件的监听和响应,在单线程中运行,高并发场景下容易成为性能瓶颈(因为每个时刻都会同时有很多客户端进行连接,来不及对新的客户端进行连接处理)
3. 多 Reator 多线程:多 I/O 多路复用 + 线程池(业务处理)
多 Reactor 多线程模式:基于单Reactor多线程的缺点考虑,如果 IO 的时候有连接到来但是无法处理,然后就把连接单独拿出来因此让一个 Reactor 线程仅仅进行新连接的处理,让其他的 Reactor 线程进行 IO 处理, Reactor 线程拿到数据分发给业务线程池进行业务处理。因此多Reactor多线程模式也叫做 主从 Reactor 模型
- 在主 Reactor 中处理新连接请求事件,有新连接到来则分发到子 Reactor 中监控(新连接事件监控)
- 在子 Reactor 中进行客户端通信监控,有事件触发,则接收数据分发给 Worker 线程池(IO 事件监控)
- Worker 线程池分配独立的线程进行具体的 业务处理
- 工作线程处理完毕后,将响应交给子Reactor线程进行数据响应
- 优点:充分利用CPU多核资源,进行合理分配,主从Reactor各司其职
- 缺点:需要理解的是,执行流并不是越多越好,因为执行流多了,会增加 cpu 切换调度的成本
四、功能模块划分
1. Buffer 模块
🏷 Buffer 模块是一个缓冲区模块,用于实现通信中用户态的接收缓冲区和发送缓冲区功能
2. Socket 模块
🏒 Socket 模块是对套接字操作封装的一个模块,主要实现的socket的各项操作。
3. Channel 模块
🐙 Channel 模块是对一个描述符需要进行的 IO 事件管理的模块,实现对描述符可读,可写,错误…事件的管理操作,以及 Poller 模块对描述符进行 IO 事件监控就绪后,根据不同的事件,回调不同的处理函数功能
4. Connection 模块
📫 Connection 模块是对Buffer模块,Socket模块,Channel模块的一个整体封装。实现了对一个通信套接字的整体的管理,每一个进行数据通信的套接字(也就是accept获取到的新连接)都会使用 Connection 进行管理
- Connection 模块内部包含有三个由组件使用者传入的回调函数:连接建立完成回调,事件回调,新数据回调,关闭回调。
- Connection 模块内部包含有两个组件使用者提供的接口:数据发送接口,连接关闭接
- Connection 模块内部包含有两个用户态缓冲区:用户态接收缓冲区,用户态发送缓冲
- Connection 模块内部包含有一个Socket对象:完成描述符面向系统的IO操作
- Connection 模块内部包含有一个Channel对象:完成描述符IO 事件就绪的处理
具体处理流程如下:
- 实现向 Channel 提供可读,可写,错误等不同事件的10事件回调函数,然后将Channel和对应的描述符添加到 Poller 事件监控中。
- 当描述符在 Poller 模块中就绪了 IO 可读事件,则调用描述符对应 Channel 中保存的读事件处理函数,进行数据读取,将 socket 接收缓冲区全部读取到 Connection 管理的用户态接收缓冲区中。然后调用由组件使用者传入的新数据到来回调函数进行处理。
- 组件使用者进行数据的业务处理完毕后,通过 Connection 向使用者提供的数据发送接口,将数据写入 Connection 的发送缓冲区中。
- 启动描述符在 Poll 模块中的 IO 写事件监控,就绪后,调用 Channel 中保存的写事件处理函数,将发送缓冲区中的数据通过 Socket 进行面向系统的实际数据发送。
5. Acceptor 模块
🖌 Acceptor 模块是对 Socket 模块,Channel模块的一个整体封装,实现了对一个监听套接字的整体的管理。
- Acceptor 模块内部包含有一个 Socket 对象:实现监听套接字的操作
- Acceptor 模块内部包含有一个 Channel 对象:实现监听套接字 IO 事件就绪的处理
具体处理流程如下:
- 实现向 Channel 提供可读事件的 IO 事件处理回调函数,函数的功能其实也就是获取新连接
- 为新连接构建一个 Connection 对象出来。
6. TimeQueue 模块
🍨 TimerQueue 模块是实现固定时间定时任务的块,可以理解就是要给定时任务管理器,向定时任务管理器中添加一个任务,任务将在固定时间后被执行,同时也可以通过刷新定时任务来延迟任务的执行
作用:这个模块主要是对 Connection 对象的生命周期管理,对非活跃连接进行超时后的释放功能。
- TimerQueue 模块内部包含有一个 timerfd: linux 系统提供的定时器。
- TimerQueue 模块内部包含有一个 Channel 对象:实现对 timerfd 的I0时间就绪回调处理
7. Poller 模块
🕋 Poller 模块是对 epoll 进行封装的一个模块,主要实现 epoll 的 IO 事件添加,修改,移除,获取活跃连接功能
8. EventLoop 模块
🍦 EventLoop 模块可以理解就是我们上边所说的 Reactor模块,它是对Poller模块,TimerOueue模块,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 的阻塞
9. TcpServer 模块
这个模块是一个整体Tcp服务器模块的封装,内部封装了 Acceptor 模块,EventLoopThreadPool 模块
- TcpServer 中包含有一个 EventLoop 对象:以备在超轻量使用场景中不需要 EventLoop 线程池,只需要在主线程中完成所有操作的情况。
- TcpServer 模块内部包含有一个 EventLoopThreadPool对象:其实就是 EventLoop 线程池,也就是子Reactor线程池
- TcpServer 模块内部包含有一个Acceptor对象:一个 TcpServer 服务器,必然对应有一个监听套接字,能够完成获取客户端新连接,并处理的任务。
具体操作流程如下:
- 在实例化 TcpServer 对象过程中,完成BaseLoop的设置,Acceptor对象的实例化,以及EventLoop 线程池的实例化,以及 std::shared_ptr 的hash表的实例化。
- 为 Acceptor 对象设置回调函数:获取到新连接后,为新连接构建 Connection 对象,设置Connection 的各项回调,并使用 shared ptr 进行管理,并添加到hash表中进行管理,并为Connection 选择一个 EventLoop 线程,为 Connection 添加一个定时销毁任务,为Connection添加事件监控
- 启动 BaseLoop
10. 模块关系图
- EventLoop 模块为了保证整个服务器的线程安全问题,因此要求使用者对于 Connection 的所有操作一定要在其对应的 EventLoop 线程内完成,不能在其他线程中进行(比如组件使用者使用 Connection 发送数据,以及关闭连接这种操作)。
- 但是需要注意的是HTTP协议是一个运行在TCP协议之上的应用层协议,这一点本质上是告诉我们,HTTP服务器其实就是个TCP服务器,只不过在应用层基于HTTP协议格式进行数据的组织和解析来明确客户端的请求并完成业务处理。