【高并发服务器:前置知识】一、项目介绍 模块划分
文章目录
- Ⅰ. 项目背景
- 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模块
该模块就是对我们基本的套接字操作进行一个封装,方便我们在使用的时候直接调用,减少重复的编程工作!
我们可以封装出以下的接口:
- 创建套接字
- 绑定套接字信息
- 监听套接字
- 获取新连接
- 客户端发起请求
- 接收数据
- 发送数据
- 关闭套接字
- 将上面的几个接口再次封装出两个更方便的接口:
- 创建一个服务端链接的接口
- 创建一个客户端连接的接口
- 设置套接字选项 – 开启地址端口复用
- 设置套接字阻塞属性 – 设置为非阻塞
Channel模块
该模块是对一个描述符需要进行的 IO
事件管理的模块,实现对描述符可读,可写,错误………事件的管理操作,以及 Poller
模块对描述符进行 IO
事件监控就绪后,根据不同的事件,回调不同的处理函数功能。
之所以需要有该模块,是为了更方便的在编码的时候进行一个文件描述符对应事件的维护处理,比如说对一个文件描述符可写事件的设置,这个动作是很频繁的,我们可以将其封装成接口使用等等情况。
简单地说,就是 一个连接监听什么事件、触发什么事件都由该模块处理!
而功能设计我们分为两块:对文件描述符监控事件的管理、对文件描述符监控事件触发后的处理。
- 对文件描述符监控事件的管理:
- 判断描述符释放可读
- 判断描述符释放可写
- 设置描述符监控可读
- 设置描述符监控可写
- 解除可读事件的监控
- 解除可写事件的监控
- ……
- 对文件描述符监控事件出发后的处理:
- 设置对于不同事件的 回调处理函数,明确触发了某个事件之后应该如何处理。
Connection模块
该模块是对 Buffer
模块、Socket
模块、Channel
模块的一个整体封装,实现了对一个通信套接字的整体的管理,每一个进行数据通信的套接字(也就是 accept
获取到的新连接)都会使用 Connection
模块进行管理。
一个连接的任何事件该如何管理,其实是由使用者来决定的,但这对程序来说是未知的,所以我们 需要在 Connection
模块中提供事件回调的机制 供使用者设置,而这个模块存在的意义也就是 为了连接操作的灵活以及便捷性。因为应用层的协议如果改变了,我们只需要修改模块中回调的事件即可,而不需要再次去创建一些重复的接口!
下面是该模块所包含的内容:
-
有四个由组件使用者传入的回调函数:连接建立完成的回调、接收新数据成功后的回调、关闭连接的回调、产生任何事件进行的回调。
-
有五个组件由使用者提供的接口:
- 发送数据接口:就是将数据发送到
Buffer
对象中的发送缓冲区。 - 连接关闭接口
- 切换协议接口:这里的协议指的是应用层的协议,无非就是设置不同的使用者传入的回调函数罢了。
- 启动非活跃销毁接口
- 取消非活跃销毁接口
- 发送数据接口:就是将数据发送到
-
有一个
Buffer
对象:用户态接收缓冲区、用户态发送缓冲区,即Buffer
模块。 -
有一个
Socket
对象:完成描述符面向系统的IO
操作,即Socket
模块。 -
有一个
Channel
对象:完成描述符IO
事件就绪的处理,即Channel
模块。
Acceptor模块
该模块是对 Socket
模块(实现监听套接字的操作)、Channel
模块(实现监听套接字 IO
事件就绪以及就绪后的处理)的一个整体封装,实现了 对一个监听套接字的整体的管理。
具体处理流程如下:
- 实现向
Channel
模块提供可读事件的IO
事件处理回调函数,函数的功能其实也就是 获取新连接。 - 为新连接构建⼀个
Connection
对象。
需要注意的是,事件回调处理函数的设置是由服务器来指定的,该模块就是负责提供设置回调函数的接口!
TimerQueue模块
该模块的功能就是让一个任务可以在用户指定的时间之后执行。对应到我们的服务器中,主要是对 Connection
对象的生命周期管理,提供对非活跃连接进行超时后的释放功能,而不是一直占用着资源。
TimerQueue
模块内部包含有一个timerfd
:其实就是linux
系统提供的定时器。TimerQueue
模块内部包含有一个Channel
对象:实现对timerfd
的IO
时间就绪回调处理。
而接口设计这块,就是三个接口:
- 添加定时任务
- 取消定时任务
- 刷新定时任务(当连接活跃之后刷新,重新计时)
Poller模块
该模块是 对 epoll
的操作进行封装 的一个模块,主要实现 epoll
的 IO
事件添加、修改、移除、获取活跃连接的功能,而这些功能其实 和上面的 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
对象添加、刷新、取消定时任务!