主从 Reactor 高并发服务器实战 (一):前置知识与功能目标

系列文章目录


文章目录

  • 系列文章目录
  • 前言
  • 一、项目认识
  • 二、知识背景
    • [2.1 HTTP服务器](#2.1 HTTP服务器)
    • [2.2 Reactor模型](#2.2 Reactor模型)
  • 三、选择的原因
    • [3.1 单 Reactor 单线程:单 I/O 多路复用 + 业务处理](#3.1 单 Reactor 单线程:单 I/O 多路复用 + 业务处理)
    • [3.2 单 Reactor 多线程:单 I/O 多路复用 + 线程池(业务处理)](#3.2 单 Reactor 多线程:单 I/O 多路复用 + 线程池(业务处理))
    • [3.3 多 Reactor 多线程:多 I/O 多路复用 + 线程池(业务处理)](#3.3 多 Reactor 多线程:多 I/O 多路复用 + 线程池(业务处理))
  • 四、项目解析
    • [4.1 SERVER模块](#4.1 SERVER模块)
    • [4.2 HTTP协议模块](#4.2 HTTP协议模块)
  • 总结

前言

  大家好,我是曦夜日长。经过这么长时间的学习,也是修得正果,正式来到我们的标准项目栏目,正如专栏所示,本系列实现:仿muduo库OneThreadOneLoop式主从Reactor模型实现⾼并发服务器 从零到有的开发。请跟着我的脚步,开始我们第一个项目的开发。


一、项目认识

  我们要开发的项目服务器,可以说是最经典的服务器模板,这里给出百度资料:

  主从 Reactor OneThreadOneLoop 模型至今仍广泛用于工业级高性能服务:C++ 生态 muduo、brpc,Java 生态 Netty 均采用同类架构,被应用在即时通讯 IM、游戏网关、流媒体服务器、IoT 设备网关、RPC 网关等海量长连接场景。

  当然我们开发的只是服务器底层,其中内部业务的实现还需要我们自己部署,作为主从Reactor,它有着这样的特性:主Reactor获取到新连接后分发给⼦Reactor进⾏通信事件监控。而子Reactor线程监控各⾃的描述符的读写事件进⾏数据读写以及业务处理。

  在谈及这样的Reactor模型,One Thread OneLoop常被重点标注,其的思想就是把所有的操作都放到⼀个线程中进行,⼀个线程对应⼀个事件处理的循环。

  借上面的图,我们来分析一下具体的流程:

  1. 客户端发起 TCP 连接请求
      多个客户端向服务器监听端口发起连接。
  2. 主 Reactor(mainReactor)只负责监听 accept 事件
      mainReactor 内部 epoll 监听 listenfd,捕获新连接事件 ;调用accept()拿到新客户端 socket 的 fd,完成三次握手,得到 TcpConnection。
  3. 新连接分发
      mainReactor 通过负载均衡策略(muduo 是轮询),将这个新连接 fd,投递到其中一个从属 Reactor (subReactor)。
  4. 从属 Reactor 接管连接,注册 IO 事件
      被选中的 subReactor(一个线程对应一个 EventLoop,OneThreadOneLoop),把这个新 fd 注册进自己内部的 epoll,后续这个连接永久绑定该 subReactor,不会切换线程。

自此:mainReactor 不再管这条连接,只继续等待下一个新客户端连接。

  1. 从属 Reactor 循环监控 IO 事件

    subReactor 的 epoll 持续等待事件:

      客户端发数据 → socket可读事件 触发;内核缓冲区可写入数据 → socket可写事件触发;连接断开 → 关闭事件。

  2. 事件回调处理

      subReactor 线程检测到可读事件:在当前 IO 线程内 执行读操作,从 socket 读取数据;直接在这个 IO 线程调用onMessage业务回调,解析数据包、执行业务逻辑;业务处理完成后,如果需要回复客户端,在当前线程执行 send 写数据。

  3. 连接关闭

      当检测到连接关闭事件,subReactor 在本线程清理该 TcpConnection 资源,从 epoll 移除 fd。


二、知识背景

  我们想实现这样一个项目,主要依靠的是我们之前学习的HTTP服务器和Reactor事件派发器,现在爆发鬼脑回想一下:

2.1 HTTP服务器

  HTTP(HyperTextTransferProtocol),超⽂本传输协议是应⽤层协议,是⼀种简单的请求-响应协议,但我们要明白的是它只是服务于应用层的数据传输方式,根本还是在于TCP为基础搭建的服务器上。

  因此可以说:HTTP服务器本质就是个TCP服务器 ,所以就需要socket->bind->listen->accept 这几个我们熟知的流程,在内部对数据HTTP协议封装、调用业务逻辑,当然在当前项目中的服务器本⾝并不存在业务。

2.2 Reactor模型

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

  又名Dispatcher 模式

  基础的Reactor模型的思路与基础的TcpServer的阻塞式IO不同,采用多路复用的方法,这两者的区别就是一直等和等多个,比如钓鱼:张三只带了一个鱼竿,钓鱼时只能等着鱼上钩,而多路复用则是李四带了多个鱼竿,等着只要有上钩就去处理。

  但这两种方法本质区别是等的区别,处理事件过程是相同的。Reactor基本思想是监控fd,如果fd就绪就告诉内核进而响应到用户,应用层处理后重新等待。

  这里就不讲述过多了,详细内容可以查看有关的博客我的没写:(


三、选择的原因

  那就有人要疑惑了,我之前不是实现了一个Reactor服务器吗,那为什么还有跟着你的项目写一个我写过的东西。其实不然,我们之前实现的只是一个单Reactor + 多线程的模式,它有着这样的一种缺陷:同一个连接 fd 上先后触发的 IO 事件会被封装为独立任务直接投递至全局线程池,线程池不保证任务完成顺序与事件入队顺序一致,若后提交的请求任务执行更快,会优先返回响应,造成同一连接上请求与响应顺序错乱,引发协议解析异常。

  因此,我们不可能使用这样一种具有严重处理缺陷的代码作为能够胜任高并发服务器开发的基础实现。那让我们认识一下Reactor的种类:

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

  这是最朴素的一种实现:整个服务器就一个线程,既负责监听事件,又负责干活。

工作流程:

  1. 通过 I/O 多路复用模型进行客户端请求监控。
  2. 触发事件后,进行事件处理:
    • a. 如果是新建连接请求,则获取新建连接,并添加至多路复用模型进行事件监控。
    • b. 如果是数据通信请求,则进行对应的数据处理(接收数据、处理数据、发送响应)。

优点: 所有操作均在同一线程中完成,思想流程较为简单,不涉及进程/线程间通信及资源争抢问题。

缺点: 无法有效利用 CPU 多核资源,很容易达到性能瓶颈。

适用场景: 适用于客户端数量较少、且处理速度较为快速的场景。(处理较慢或活跃连接较多时,会导致串行处理,后处理的连接长时间无法得到响应。)

  可以看到,它的"简单"是靠牺牲并发能力换来的------一旦某个业务处理耗时变长,整个服务器都会被它拖住,因为它根本没有第二个线程能接手。


3.2 单 Reactor 多线程:单 I/O 多路复用 + 线程池(业务处理)

  既然单线程扛不住,最直接的想法就是:把"监听"和"干活"拆开,干活交给线程池。这就是单 Reactor 多线程模式。

工作流程:

  1. Reactor 线程通过 I/O 多路复用模型进行客户端请求监控。
  2. 触发事件后,进行事件处理:
    • a. 如果是新建连接请求,则获取新建连接,并添加至多路复用模型进行事件监控。
    • b. 如果是数据通信请求,则接收数据后分发给 Worker 线程池进行业务处理。
    • c. 工作线程处理完毕后,将响应交给 Reactor 线程进行数据响应。

优点: 充分利用 CPU 多核资源。

缺点: 多线程间的数据共享访问控制较为复杂;且单个 Reactor 承担所有事件的监听和响应,在单线程中运行,高并发场景下容易成为性能瓶颈。

  注意第 2.c 步这个细节------响应最终还是要回到 Reactor 线程去发送 。这既带来了"各线程都要和 Reactor 打交道"的同步成本,也正是我们在上一节踩过的坑所在:业务任务一旦并发投递进线程池,同一连接上请求的处理与响应的先后顺序就没人保证了,顺序错乱、协议解析异常随之而来。


3.3 多 Reactor 多线程:多 I/O 多路复用 + 线程池(业务处理)

  单 Reactor 的瓶颈在于"所有事件的监听和响应都压在同一个线程上"。那思路就很清楚了:把 Reactor 本身也拆开,让它多干活 。这就是多 Reactor 多线程模式,也就是常说的主从 Reactor(Main-Sub Reactor)。

工作流程:

  1. 在主 Reactor 中处理新连接请求事件,有新连接到来则分发到子 Reactor 中监控。
  2. 在子 Reactor 中进行客户端通信监控,有事件触发,则接收数据分发给 Worker 线程池。
  3. Worker 线程池分配独立的线程进行具体的业务处理。
    • a. 工作线程处理完毕后,将响应交给子 Reactor 线程进行数据响应。

优点: 充分利用 CPU 多核资源,主从 Reactor 各司其职。

缺点: 实现复杂度最高------连接如何分发、线程间如何通信、生命周期如何管理,都需要自己设计好;代码量和对细节的把握要求也远高于前两种。

  与前两种模式相比,它最大的变化是:主 Reactor 只负责"接客",子 Reactor 负责"服务"。新连接被主 Reactor 接进来后,会被分配给某一个子 Reactor,此后这条连接上的所有 I/O 事件都由这个子 Reactor 处理。


  回到开头那个问题:为什么我们要在已经有一个单 Reactor + 多线程的服务器之后,再写一个主从 Reactor?答案就在 3.3 的分工结构里------同一个连接 fd 上的事件,从此由同一个子 Reactor 线程顺序处理。既不会出现"后提交的任务先返回"的乱序问题,又因为子 Reactor 可以有多个,把 CPU 多核真正用了起来;同时主 Reactor 只做分发、不做业务,保证它不会被慢业务拖住,能持续稳定地接收新连接。

  所以,主从 Reactor 不是"更花哨的写法",而是为了解决单 Reactor + 线程池在连接级顺序性和高并发扩展性上的双重缺陷------这也正是我们选择它的原因。

四、项目解析

  基于以上的理解,我们要实现的是⼀个带有协议⽀持的Reactor模型⾼性能服务器,因此将整个项⽬的实现划分为两个⼤的模块:

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

4.1 SERVER模块

  SERVER模块就是对所有的连接以及线程进⾏管理,让它们各司其职,在合适的时候做合适的事,最终完成⾼性能服务器组件的实现。并且这些管理可以详细分为:

  1. 监听连接管理:对监听连接进⾏管理。
  2. 通信连接管理:对通信连接进⾏管理。
  3. 超时连接管理:对超时连接进⾏管理。

  而这个大模块就可以分成多个小模块组装实现:

  • Buff模块: 缓冲区模块,⽤于实现通信中⽤⼾态的接收缓冲区和发送缓冲区功能
  • Socket模块: 负责服务器的系列操作
  • Channel模块: 对每个连接进行监控的模块,实现对描述符可读,可写,错误...事件的管理操作,以及Poller模块对描述符进⾏IO事件监控就绪后,根据不同的事件,回调不同的处理函数功能
  • Poller模块: 是对epoll模型的封装,实现对事件的添加、修改和删除等功能
  • Connection模块: 是对Buff、Socket、Channel模块的封装,使得可以管理所有的连接
  • Acceptor模块: Acceptor模块是对Socket模块,Channel模块的⼀个整体封装,实现了对⼀个监听套接字的整体的管理
  • TimerQueue模块: 是实现固定时间定时任务的模块,可以理解就是要给定时任务管理器向定时任务管理器中添加⼀个任务,任务将在固定时间后被执⾏,同时也可以通过刷新定时任务来延迟任务的执⾏
  • EventLoop模块: 可以理解就是我们上边所说的Reactor模块,它是对Poller模块TimerQueue模块,Socket模块的⼀个整体封装,进⾏所有描述符的事件监控
  • TcpServer模块: 这个模块是⼀个整体Tcp服务器模块的封装,内部封装了Acceptor模块EventLoopThreadPool模块

4.2 HTTP协议模块

  HTTP协议模块⽤于对⾼并发服务器模块进⾏协议⽀持,基于提供的协议⽀持能够更⽅便的完成指定协议服务器的搭建。⽽HTTP协议⽀持模块的实现,可以细分为以下⼏个模块。

  • Util模块: 这个模块是⼀个⼯具模块,主要提供HTTP协议模块所⽤到的⼀些⼯具函数,⽐如url编解码,⽂件读写...等。
  • HttpRequest模块: 这个模块是HTTP请求数据模块,⽤于保存HTTP请求数据被解析后的各项请求元素信息。
  • HttpResponse模块: 这个模块是HTTP响应数据模块,⽤于业务处理后设置并保存HTTP响应数据的的各项元素信息,最终会被按照HTTP协议响应格式组织成为响应信息发送给客⼾端。
  • HttpContext模块: 这个模块是⼀个HTTP请求接收的上下⽂模块,主要是为了防⽌在⼀次接收的数据中,不是⼀个完整的
  • HttpServer模块: 这个模块是最终给组件使⽤者提供的HTTP服务器模块了,⽤于以简单的接⼝实现HTT服务器的搭建。

  在之后具体的模块开发时,会对每个模块的具体实现做出规划,并解释功能与意义,以便我们更好的梳理项目结构。


总结

  我们的项目前提就到这里了,下期我们将学习一些项目中需要实现的小功能模块,正式开启我们的项目实现,感谢您的收看,让我们下期再见。

相关推荐
2601_965619692 小时前
历史解说视频怎么做:青铜器瓷器文物画面自动对齐的完整教程
运维·nginx·音视频
云和数据.ChenGuang2 小时前
AI 工程师相关就业岗位(细分,偏工程落地,由底层到上层)
服务器·开发语言·人工智能·python
LiaoWL1232 小时前
从零开始学 Linux(二):Shell 脚本入门与文本处理实战
linux·运维·服务器
.道阻且长.3 小时前
Linux软件包管理器
linux·运维·ubuntu·职场和发展·centos
元岳数字人小元3 小时前
一台终端兼顾接待与咨询,AI数字人对话赋能线下多场景服务
运维·人工智能·人机交互·交互·语音识别·源代码管理
djjjx.3 小时前
Linux进程篇(三):深度硬核!全面起底 Linux 进程状态变化与内核链表动态解绑
linux·运维·服务器
乔旭伟3 小时前
Linux 服务器环境搭建实战笔记:最小化安装、Docker Compose 编排与上线检查全流程
linux·服务器·笔记
燕落南枝3 小时前
HTML 学习:input 单选、复选与文件域详解
服务器·学习·html