当前位置: 首页 > news >正文

【高并发服务器:前置知识】一、项目介绍 模块划分

文章目录

  • Ⅰ. 项目背景
    • 1、HTTP服务器
    • 2、Reactor模式
      • ① 单Reactor单线程模式:单I/O多路复用 + 业务处理
      • ② 单Reactor多线程模式:单I/O多路复用 + 业务线程池(负责业务处理)
      • ③ 多Reactor多线程模式:多I/O多路复用 + 业务线程池(负责业务处理)
    • 3、项目采用的模式 -- 主从`Reactor`模式服务器
  • Ⅱ. 模块划分
    • 1、**`SERVER`** 模块
      • Buffer模块
      • Socket模块
      • Channel模块
      • Connection模块
      • Acceptor模块
      • TimerQueue模块
      • Poller模块
      • EventLoop模块
      • TcpServer模块💥
    • 上述模块的大概流程总结💥

在这里插入图片描述

Ⅰ. 项目背景

1、HTTP服务器

​ 超文本传输协议 HTTP 我们都学过了,并且也做过类似简单的 HTTP 服务器,其实就是对在 TCP 服务器上对 HTTP 协议进行一个简单的请求-响应的过程,为此我们可以搭建一个属于我们自己的网站。

​ 但是这个项目的核心不是应用层,而是在应用层之下的一个高性能服务器框架,有了这个框架之后,我们可以在这个框架之上搭载多种应用层协议,而为了让项目效果更加明显一些,就搭载我们最常见的 HTTP 协议!

2、Reactor模式

Reactor 模式,是指通过一个或多个输入同时传递给服务器进行请求处理时的 事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式。

​ 简单理解就是使用 I/O多路复用 统一监听事件,收到事件后分发给处理进程或线程,是编写高性能网络服务器的必备技术之一。

​ 而 Reactor 模式又分为以下常见的几种:

① 单Reactor单线程模式:单I/O多路复用 + 业务处理

​ 整个服务器中只使用一个线程,并且采用 Reactor 模式,也就是说一个线程同时负责监听 IO事件/处理IO 事件/业务处理。

​ 优点很明显,就是 编码比较简单,不需要考虑线程间的同步、互斥问题等!缺点也很明显,一个线程处理所有的事情,是 很容易造成性能瓶颈问题 的,所以一般大型一点的服务器都不会采用这种方式!

​ 这种模式适用于客户端数量较少,并且业务处理比较简单快速的场景!
在这里插入图片描述

② 单Reactor多线程模式:单I/O多路复用 + 业务线程池(负责业务处理)

​ 一个服务器中存在一个采用 Reactor 模式的主线程,它负责 监听事件IO处理,而通过 IO 处理拿到数据之后,将这些数据交给 线程池 中的空闲线程去处理,这些线程也称为 业务线程

​ 这种模式的优点就是 利用了 CPU 的多核资源,因为现在大多数的硬件设备都是多核的,可以让多个线程并行执行,大大提高了效率,降低了代码的耦合度

​ 而缺点也是挺明显的,主线程需要同时负责 监听事件IO处理,既然涉及到了 IO处理,那自然就 不利于高并发的场景,因为每个时刻都有大量的客户端请求,如果 IO 处理不及时的话,会导致来不及监听新的客户端的请求。

在这里插入图片描述

③ 多Reactor多线程模式:多I/O多路复用 + 业务线程池(负责业务处理)

​ 基于上面的单 Reactor 多线程模式,要解决的问题就是在主线程上的 IO 问题,所以我们可以用多个 Reactor 来解决,以一个 Reactor 主线程为中心,主线程只负责监听新连接,并且一旦收到了新连接,主线程就将这些新连接派发给其它从属线程管理,从此这些新连接上的 IO 处理都由从属线程来解决。

​ 而从属线程进行 IO 处理之后,就将拿到的数据交给线程池的业务线程来处理,也就是说 从属线程负责的是对已有连接的监听和 IO 处理,而 业务处理由线程池中的业务线程负责

​ 这样子做的好处,充分利用了 CPU 多核资源,并且 减轻了 Reactor 主线程的压力,因为主从 Reactor 线程各司其职,规避了上面那种模式的缺点!但是这种模式的缺点就是设计起来复杂,涉及到线程之间的同步、互斥等。

​ 要注意的是,执行流不是越多越好,因为执行流多了,CPU 的切换调度成本也自然就高了!
在这里插入图片描述

3、项目采用的模式 – 主从Reactor模式服务器

​ 因为上面这种方案虽然优秀,但是设计起来比较复杂,并且线程数量太多,也就是执行流太多的话其实也是有影响的!

​ 所以我们这里采用中立的方法,使用多Reactor多线程模式,让主 Reactor线程负责监听新连接的到来,然后派发给从属Reactor线程!但是不接入线程池,而是直接 让从属Reactor 线程负责IO处理和业务处理

​ 这种设计方案,称为 one-thread-one-loop 模式,也就是一个线程对应一个循环(循环无非就是将从属 Reactor 线程中的操作进行一个集合:IO 事件监控 + IO 操作 + 业务处理)。

​ 此外这种方案也称为:主从 Reactor 模式服务器。

在这里插入图片描述

​ 当前实现中,因为并不确定组件使用者的使用意向,因此并不提供业务层工作线程池的实现,只实现主从 Reactor,而 Worker 工作线程池,可由组件库的使用者的需要自行决定是否使用和实现。

Ⅱ. 模块划分

基于以上的理解,我们要实现的是一个带有协议支持的 Reactor 模型高性能服务器,因此将整个项目的实现划分为两个大的模块:

  • SERVER模块:实现 Reactor 模型的 TCP 服务器。
  • 协议模块:对当前的 Reactor 模型服务器提供应用层协议支持。

1、SERVER 模块

​ 服务器模块就是对所有的连接以及线程进行管理,让它们各司其职,在合适的时候做合适的事,最终完成高性能服务器组件的实现。而具体的管理也分为几个方面:

  • 监听连接管理:对监听连接进行管理,即获取一个新连接之后如何处理。
  • 通信连接管理:对通信连接进行管理,即连接产生的某个事件如何处理。
  • 超时连接管理:对超时连接进行管理,即非活跃超时的连接是否关闭如何处理。
  • 事件监控管理:即启动多少 EventLoop 线程等管理
  • 事件回调函数的设置:一个连接产生了一个事件,对于这个事件如何处理,只有组件使用者知道,因此一个事件的处理回调,一定是组件使用者设置给 TcpServer 的,然后由 TcpServer 设置给各个 Connection 连接。

​ 基于以上的管理思想,将这个模块进行细致的划分又可以划分为以下多个子模块:

Buffer模块

​ 顾名思义,就是一个缓冲区模块,它的作用就是 实现通信中用户态的接收缓冲区和发送缓冲区的功能。所以它需要提供两个接口,一个是往缓冲区中添加数据的接口,一个是从缓冲区中取出数据的接口!

​ 为什么需要有这个缓冲区模块❓❓❓

​ 我们前面写过简单的服务器,知道收发数据其实并不是直接往缓冲区调用接口就完事了,可能会有两种情况,一是当我们在从缓冲区中读取数据的时候,此时缓冲区虽然是空的,但是我们怎么知道当前报文就已经被读取完整了呢?还有情况就是当我们往缓冲区中写数据的时候,有可能缓冲区满了,此时接口返回,但是我们怎么知道数据就已经放到缓冲区中去了呢???

​ 单纯调用读写接口是没办法保证的,所以我们需要做处理,保证读写的是一个完整的报文,所以才有了缓冲区模块!

Socket模块

​ 该模块就是对我们基本的套接字操作进行一个封装,方便我们在使用的时候直接调用,减少重复的编程工作!

​ 我们可以封装出以下的接口:

  1. 创建套接字
  2. 绑定套接字信息
  3. 监听套接字
  4. 获取新连接
  5. 客户端发起请求
  6. 接收数据
  7. 发送数据
  8. 关闭套接字
  9. 将上面的几个接口再次封装出两个更方便的接口:
    • 创建一个服务端链接的接口
    • 创建一个客户端连接的接口
  10. 设置套接字选项 – 开启地址端口复用
  11. 设置套接字阻塞属性 – 设置为非阻塞

Channel模块

​ 该模块是对一个描述符需要进行的 IO 事件管理的模块,实现对描述符可读,可写,错误………事件的管理操作,以及 Poller 模块对描述符进行 IO 事件监控就绪后,根据不同的事件,回调不同的处理函数功能。

​ 之所以需要有该模块,是为了更方便的在编码的时候进行一个文件描述符对应事件的维护处理,比如说对一个文件描述符可写事件的设置,这个动作是很频繁的,我们可以将其封装成接口使用等等情况。

​ 简单地说,就是 一个连接监听什么事件、触发什么事件都由该模块处理

​ 而功能设计我们分为两块:对文件描述符监控事件的管理、对文件描述符监控事件触发后的处理。

  • 对文件描述符监控事件的管理:
    • 判断描述符释放可读
    • 判断描述符释放可写
    • 设置描述符监控可读
    • 设置描述符监控可写
    • 解除可读事件的监控
    • 解除可写事件的监控
    • ……
  • 对文件描述符监控事件出发后的处理:
    • 设置对于不同事件的 回调处理函数,明确触发了某个事件之后应该如何处理。

Connection模块

​ 该模块是对 Buffer 模块、Socket 模块、Channel 模块的一个整体封装,实现了对一个通信套接字的整体的管理,每一个进行数据通信的套接字(也就是 accept 获取到的新连接)都会使用 Connection 模块进行管理。

​ 一个连接的任何事件该如何管理,其实是由使用者来决定的,但这对程序来说是未知的,所以我们 需要在 Connection 模块中提供事件回调的机制 供使用者设置,而这个模块存在的意义也就是 为了连接操作的灵活以及便捷性。因为应用层的协议如果改变了,我们只需要修改模块中回调的事件即可,而不需要再次去创建一些重复的接口!

​ 下面是该模块所包含的内容:

  • 有四个由组件使用者传入的回调函数:连接建立完成的回调、接收新数据成功后的回调、关闭连接的回调、产生任何事件进行的回调。

  • 有五个组件由使用者提供的接口:

    • 发送数据接口:就是将数据发送到 Buffer 对象中的发送缓冲区。
    • 连接关闭接口
    • 切换协议接口:这里的协议指的是应用层的协议,无非就是设置不同的使用者传入的回调函数罢了。
    • 启动非活跃销毁接口
    • 取消非活跃销毁接口
  • 有一个 Buffer 对象:用户态接收缓冲区、用户态发送缓冲区,即 Buffer 模块。

  • 有一个 Socket 对象:完成描述符面向系统的 IO 操作,即 Socket 模块。

  • 有一个 Channel 对象:完成描述符 IO 事件就绪的处理,即 Channel 模块。

在这里插入图片描述

Acceptor模块

​ 该模块是对 Socket 模块(实现监听套接字的操作)、Channel 模块(实现监听套接字 IO 事件就绪以及就绪后的处理)的一个整体封装,实现了 对一个监听套接字的整体的管理

​ 具体处理流程如下:

  1. 实现向 Channel 模块提供可读事件的 IO 事件处理回调函数,函数的功能其实也就是 获取新连接
  2. 为新连接构建⼀个 Connection 对象。

​ 需要注意的是,事件回调处理函数的设置是由服务器来指定的,该模块就是负责提供设置回调函数的接口

在这里插入图片描述

TimerQueue模块

​ 该模块的功能就是让一个任务可以在用户指定的时间之后执行。对应到我们的服务器中,主要是对 Connection 对象的生命周期管理,提供对非活跃连接进行超时后的释放功能,而不是一直占用着资源。

  • TimerQueue 模块内部包含有一个 timerfd:其实就是 linux 系统提供的定时器。
  • TimerQueue 模块内部包含有一个 Channel 对象:实现对 timerfdIO 时间就绪回调处理。

而接口设计这块,就是三个接口:

  • 添加定时任务
  • 取消定时任务
  • 刷新定时任务(当连接活跃之后刷新,重新计时)

Poller模块

​ 该模块是 epoll 的操作进行封装 的一个模块,主要实现 epollIO 事件添加、修改、移除、获取活跃连接的功能,而这些功能其实 和上面的 Channel 模块是相关联的,因为 Channel 模块管理的就是文件描述符的事件!

EventLoop模块

​ 该模块可以理解就是我们上边所说的 Reactor 模块,它是对 Poller 模块、TimerQueue 模块、Socket 模块的一个整体封装,进行所有描述符的事件监控。所以 EventLoop 模块必然是一个对象对应一个线程,线程内部的目的就是运行 EventLoop 的启动函数。

EventLoop 模块为了保证整个服务器的线程安全问题,因此要求使用者对于 Connection 模块的所有操作一定要在其对应的 EventLoop 线程内完成,即 每一个 Connection 对象都会绑定到一个 EventLoop 线程上,不能在其他线程中进行

​ 比如组件使用者使用 Connection 模块发送数据,以及关闭连接这种操作,涉及到了线程安全问题,所以要统一放在一个 EventLoop 线程内完成。所以说 对于连接的所有操作,都需要放到 EventLoop 线程中执行

​ 此外,EventLoop 模块保证自己内部所监控的所有描述符都必须是活跃连接,而非活跃连接就要及时释放避免资源浪费。

下面是该模块内部需要包含的内容:

  • ⼀个 Poller 对象:用于进行描述符的 IO 事件监控操作。
  • ⼀个 TimerQueue 对象:用于进行定时任务的管理。
  • ⼀个 eventfd 文件描述符:这个描述符其实就是 linux 内核提供的⼀个事件 fd,专门用于事件通知。
  • ⼀个 PendingTask 任务队列:组件使用者对 Connection 模块进行的所有操作,都要加入到任务队列中,由 EventLoop 模块进行管理,并在 EventLoop 模块对应的线程中执行。这样子做的原因是一个 EventLoop 对象中是有多个 Connection 连接对象的,所以需要 让任务同步的执行

而该模块需要具备的功能设计如下:

  • 添加连接操作任务到任务队列中的接口
  • 定时任务的添加、删除、刷新
  • 监控时间的添加、删除、修改

在这里插入图片描述

TcpServer模块💥

​ 该模块的功能就是对前边所有子模块的整合模块,是提供给用户用于搭建一个高性能服务器的模块,目的就是为了让组件使用者可以更加轻便的完成一个服务器的搭建。

​ 其内部包括:

  • 一个 EventLoop 对象:

    • 这个对象是以备在超轻量使用场景中不需要 EventLoop 线程池,而只需要在主线程中完成所有操作的情况。
  • 一 个 EventLoopThreadPool 对象:

    • 其实就是 EventLoop 线程池,也就是子 Reactor 线程池。
  • 一个 Acceptor 对象:

    • 作为一个 TcpServer 服务器必然对应有一个监听套接字,能够完成获取客户端新连接,并处理任务。
  • 一个 std::shared_ptr<Connection> 类型的哈希表:

    • 这个哈希表保存了所有的新建连接对应的 Connection 对象,注意,所有的 Connection 使用智能指针 shared_ptr 进行管理,这样能够保证在 hash 表中删除了 Connection 信息后,在 shared_ptr 计数器为 0 的情况下完成对 Connection 资源的释放操作,也就是利用了 RAII 思想!
  • 功能设计:

    • 对于监听连接的管理:获取一个新连接之后如何处理,由 Server 模块设置。
    • 对于通信连接的管理:连接产生的某个事件如何处理,由 Server 模块设置。
    • 对于超时连接的管理:连接非活跃超时是否关闭,由 Server 模块设置。
    • 对于事件监控的管理:启动多少个线程,有多少个 EventLoop,由 Server 模块设置。
    • 事件回调函数的设置:一个连接产生了一个事件,对于这个事件如何处理,只有组件使用者知道,因此一个事件的处理回调,一定是组件使用者,设置给 TcpServer,然后由 TcpServer 模块设置给各个 Connection 连接。

上述模块的大概流程总结💥

​ 首先就是在 TcpServer 中,Acceptor 对象一旦收到了新连接请求,就通过获取新连接接口以及新连接初始化回调函数,为这个新连接创建一个 Connection 对象,如下图所示:

在这里插入图片描述

​ 为新连接创建了 Connection 对象之后,TcpServer 会将连接建立完成后的回调、新数据接收后的回调、任意事件触发后的回调、关闭连接之后的回调这些内容设置到 Connection 对象中。

​ 而在 Connection 对象中也是有一个 Channel 对象的,所以该 Connection 对象就可以通过其事件的触发来调用 Connection 对象内部的事件操作接口,如下图所示:

在这里插入图片描述

​ 此时需要将该 Channel 对象添加到事件监控中,因为当前 Channel 对象只有回调函数的设置,而没有被监控!所以我们需要 EventLoop 对象调用添加事件监控接口来将 Channel 对象添加到监控事件中,又因为 EventLoop 对象中包含了 Poller 对象,所以就相当于间接调用了 Poller 对象中的添加事件监控接口来讲 Channel 对象添加到监控事件中,所以它们俩就产生了间接关联!

​ 并且在 Poller 对象和 Channel 对象之间,其实最重要的就是一个文件描述符 fd,因为 Poller 对象就是将 Channel 对象的 fd 进行添加到监控事件中的!

​ 在此之后只要 Channel 对象上的事件被监控到触发了,就会调用其对应的回调函数,也就是 Connection 对象内部的事件操作接口,这些接口会去调用 TcpServer 曾经设置进来的回调事件去处理!

在这里插入图片描述

​ 而 TimerQueue 超时任务模块则负责给 Connection 对象添加、刷新、取消定时任务!

在这里插入图片描述

http://www.dtcms.com/a/427049.html

相关文章:

  • 数据结构入门 (五):约束即是力量 —— 深入理解栈
  • 如何搭建网站的结构丰台专业网站建设公司
  • Web渗透之一句话木马
  • 网站建设如何选择服务器百度做公司网站多少钱
  • Vscode+CMake编译时出现中文乱码
  • 38、spark读取hudi报错:java.io.NotSerializableException: org.apache.hadoop.fs.Path
  • 三年级上册语文快乐读书吧读书笔记+知识点(格林童话、安徒生童话、稻草人)+三年级语文快乐读书吧笔记汇总+完整电子版可下载打印
  • 迅为Hi3516CV610开发板强劲内核-海思Hi3516CV610核心板
  • 网站开发可以当程序员wordpress 怎么迁移
  • babelfish for postgresql 分析--babelfishpg_tds--doing
  • 手机网站排行榜焦作专业网站建设费用
  • 小程序开发:开启定制化custom-tab-bar但不生效问题,以及使用NutUI-React Taro的安装和使用
  • 避坑指南:关于文件夹加密软件(以“文件夹加密超级大师”为例)卸载前的正确操作流程
  • 用矩阵实现元素绕不定点旋转
  • Web UI自动化测试学习系列5--基础知识1--常用元素定位1
  • 大模型-扩散模型(Diffusion Model)原理讲解(2)
  • 一文讲解反射、注解
  • 学习日报 20250930|优惠券事务处理模块
  • 【Nest.js】模块之间依赖关系,以及导出导入链的完整性
  • MyBatis —— 多表操作和注解开发
  • 自动化脚本的自动化执行实践
  • 有颜二维码 1.0.5| 告别单调,一键生成有颜色的二维码
  • 信创浪潮下的国产组态软件——紫金桥RealSCADA
  • 做网站新闻移动动态网络规划设计师资料及视频教程
  • 机器学习之三大学习范式:监督学习、无监督学习、强化学习
  • 18002.机器人电机姿态控制
  • mysql语句基本操作之select查询
  • 做mp3链接的网站宁波专业seo外包
  • Spring Boot 集成 EHCache 缓存解决方案
  • Spring Boot 缓存与验证码生成