文章目录

Ⅰ. 项目背景
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
对象添加、刷新、取消定时任务!