【基于one-loop-per-thread的高并发服务器】--- 项目介绍&&模块划分

Welcome to 9ilk's Code World

(๑•́ ₃ •̀๑) 个人主页: 9ilk

(๑•́ ₃ •̀๑) 文章专栏: 项目


为什么做这个项目

在学习Linux网络编程时,可以通过socket实现简单的通信服务器,但是这样的服务器,在高并发的场景下,即同时有多个客户端对服务器进行请求,此时是会出现瓶颈的,即使你内部封装了线程池也是可能出现瓶颈的,因为一条连接只能被一条线程处理,此时需要借助IO多路复用,同时我了解到了muduo库one loop peer thread的并发服务器设计,因此以此项目进行学习,旨在处理高并发场景下的网络请求。

项目技术栈

关键技术:

  • timerfd
  • eventfd
  • I/O多路复用 epoll
  • 正则库
  • C++ 11

开发环境:

  • Makefile
  • VsCode
  • Vim
  • Ubuntu 22.04

HTTP服务器

本项目实现的服务器支持协议扩展 , 其中我们最常用的就是HTTP协议,项目也会支持对该协议的扩展,HTTP协议是一种简单的"请求-响应"协议,它是一个运行在TCP协议上的应用层协议,因此HTTP服务器本质上是TCP服务器,只不过是在应用层基于HTTP协议格式进行数据的组织和解析来明确客户端的请求并完成业务处理,简单对实现HTTP服务器的总结:

  1. 搭建TCP服务器接收客户端请求。

  2. 以HTTP协议格式来解析请求数据,明确客户端的目的。

  3. 明确客户端请求后提供对应服务。

  4. 将服务/响应结果安装HTTP协议格式进行组织发送给客户端。

实现一个HTTP服务器的难点在于高性能,能够抗住高并发,这是本项目要解决的核心问题。

Reactor模型

Reactor模式是指将一个或多个输入同时传递给服务器进行请求处理时的事件驱动处理模式,服务端程序处理传入的多路请求,并将他们同步分派给请求对应的处理线程,因此Reactor模式也叫Dispatch模式,即靠事件驱动(I/O多路复用)运行,统一监听事件,然后把事件收上来分派给对应的线程

单Reactor单线程

该模型指的是在单线程中进行事件监控并处理。主要流程如下:

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

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

缺点 : 因为所有的事件监控以及业务处理都是在一个线程中完成的,无法有效利用CPU多核资源 ,很容易达到性能瓶颈

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

单Reactor多线程

该模型指的是在单个线程进行事件监控,然后将业务请求分发给线程池处理,主要流程如下:

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

优点 : 充分利用CPU多核资源,处理效率更高,降低了代码的耦合度,分离了I/O操作和业务处理。

缺点 : 多线程间的数据共享访问控制较为复杂,单个Reactor承担所有事件的监听和响应以及所有客户端的I/O操作,在单线程中运行**,高并发场景** (每一个时刻都有很多客户端连接 )下容易成为性能瓶颈,来不及进行新的客户端连接处理。

多Reactor多线程

基于单Reactor多线程的缺点进行考虑,即I/O时有新连接的到来而无法及时处理,因此将连接单独拎出来,让一个Reactor线程仅进行新链接的获取,让其他的Reactor线程进行I/O处理,这些IO Reactor拿到数据分发给业务线程池进行业务处理,因此多Reactor多线程也叫主从Reactor模型。主要流程如下:

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

优点 : 充分利用CPU多核资源并且可以进行合理分配,主从Reactor各司其职

注意:执行流不是越多越好,越多反而增加CPU切换调度的成本,因此在有些主从Reactor模式中将业务处理和IO处理放到从Reactor线程池里处理,获取数据处理完之后进行响应

项目定位

本项目要实现的是主从Reactor模型服务器,也就是Reactor线程仅仅监控监听描述符,获取新建连接,保证获取新连接的高效性,提高服务器的并发性能。主Reactor获取到新连接后分发给子Reactor进行通信事件监控,而子Reactor线程监控各自的描述符的读写事件进行数据读写以及业务处理。

OneThread One Loop的思想就是把所有的操作都放到一个线程中进行,一个线程对应一个事件处理的循环。本项目目前不提供业务工作线程池的实现,毕竟过多的执行流会增加CPU切换调度的成本,因此这里暂时不实现Worker线程池,可以等到未来迭代线程池版本或用户自己拓展。

模块划分

我们要实现的是一个带有协议支持的主从Reactor模型高性能服务器,因此整个项目的实现主要分为两个大板块:

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

Server模块

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

  • 监听连接管理 : 对监听连接进行管理。
  • 通信连接管理 : 对通信连接进行管理。
  • 超时连接管理 : 对超时连接进行管理。

Server模块又可以细致划分为如下几个子模块:

  • Buffer
  • Socket
  • Channel
  • Connection
  • Acceptor
  • TimeQueue
  • Poller
  • EventLoop
  • TcpServer

Buffer模块

考虑以下场景:

  1. 假设一个连接触发了读事件 , 此时我们需要从连接将数据读取上来 ,但有可能数据是不完整的,因此我们需要先将接收数据缓存起来 , 等下一次数据读取完整再进行业务处理。(接收缓冲区)

  2. 业务处理完不一定要立即发送出去,对方的接收缓冲区可能满了,调用send()会发生阻塞,这大大增加等待时间。对于客户端响应的数据,应该是在套接字可写 的情况下进行发送,所以需要此时将发送数据缓冲起来。(发送缓冲区

因此Buffer模块是一个缓冲区模块,用来实现通信中用户态的接收缓冲区和发送缓冲区功能,主要功能是从缓冲区中添加/取出数据。

Socket

Socket模块就是对socket套接字的一个封装模块,主要封装的是socket的各项操作,使程序中对于套接字的各项操作更加简便。封装的操作主要有:

  • 创建套接字
  • bind地址信息
  • 监听listen
  • connect向服务器发起连接
  • accept获取新连接
  • 接收数据
  • 发送数据
  • 关闭套接字
  • 集成功能:创建一个监听连接
  • 集成功能:创建一个客户端通信连接

Channel模块

为了对描述符的事件监控在用户态更容易维护以及触发事件后的操作流程更加清晰,channel模块是对一个描述符fd需要进行的IO事件管理的模块,实现对描述符可读、可写、错误等事件的管理操作,以及管理不同事件触发之后的回调。

(1)监控事件的管理

  • fd是否可读
  • fd是否可写
  • 对fd监控可读
  • 对fd监控可写
  • 解除可读事件监控
  • 解除可写事件监控
  • 解除所有事件监控

(2)对监控事件的处理:设置对于不同事件的回调处理函数,明确某个事件触发之后应怎么处理。

Connection模块

该模块主要是为了对通信连接进行一个整体管理,从而简化外界对连接的不同处理,增加连接操作的灵活性和便捷性。每一个进行数据通信的套接字(accept到的新链接)都会使用Connection进行管理,对一个连接的操作都通过这个模块进行,它是对Buufer、Socket、Channel模块的封装体现。

该模块主要功能是完成数据的发送,连接的关闭,以及它内部包含有三个由用户传入的回调函数(连接建立完成回调、连接有新数据回调、连接关闭回调、任何事件回调)。同时如果提供不同协议处理,也是设置不同事件的回调。连接有非活跃和活跃之分,该模块需要及时对非活跃连接进行释放,防止空耗服务器资源。

Acceptor模块

该模块就是对监听套接字的一个整体管理。当获取一个新建连接的描述符之后,就在这个模块为新链接封装一个Connection对象,然后设置不同回调函数。

TimeQueue模块

前面说到需要对非活跃连接在N秒后进行释放,因此需要一个实现固定时间定时任务的模块,相当于是一个定时任务管理器,完成一个定时任务的添加 ,同时如果一个非活跃连接在超时时间内有事件触发,此时超时是不需要释放它的,应该刷新定时任务。

总的来说,该模块提供的功能主要是:

  1. 添加定时任务

  2. 刷新定时任务

  3. 取消定时任务

Connection对应的是通信连接的管理

Acceptor对应的是监听连接的管理

TimeQueue对应的是超时连接的管理

Poller模块

该模块是对epoll进行封装的一个模块,使得对描述符进行事件监控的操作更加简单,主要实现epoll的是IO事件监控的添加、修改、移除等。

EventLoop模块

EventLoop模块可以理解是Reactor模块,是对Poller、TimeQueue、Socket模块的整体封装,进行所有描述符的事件监控。为了保证服务器的线程安全问题,要求使用者对于Connection的所有操作一定要在其对应的EventLoop线程内完成,因此每个Connection对象都会绑定到一个EventLoop上。

  • EventLoop模块内部需要包含eventfd,它是linux内核提供的一个事件fd,专门用于事件通知。
  • EventLoop模块内部需要包含PendingTask队列:组件使用者对Connection进行的所有操作都加入到任务队列中,由EventLoop模块进行管理,并在EventLoop对应的线程中执行。

TcpServer模块

这个模块是一个整体TCP服务器模块的封装,是提供给用户用于搭建一个高性能服务器的模块,让组件使用者可以更加轻便地完成服务器搭建。

  • TcpServer模块内部有一个主EventLoop对象,以备在在超轻量使用场景中不需要EventLoop线程池,只需要在主线程中完成所有操作的情况。
  • TcpServer模块内部包含有一个EventLoopThreadPool对象,其实就是EventLoop线程池,也就是子Reactor线程池。
  • TcpServer模块内部包含有一个Acceptor对象:一个TcpServer服务器,必然对应有一个监听套接
    字,能够完成获取客户端新连接,并处理的任务。
  • TcpServer模块内部包含有一个std::shared_ptr<Connection>的hash表,保存了所有的新建连接对应的Connection,注意,所有的Connection使用shared_ptr进行管理这样能够保证在hash表中删除了Connection信息后,在shared_ptr计数器为0的情况下完成对Connection资源的释放操作。

功能设计:

  1. 对于监听连接的管理 --- 获取一个新连接之后如何处理,由TcpServer模块设置
  2. 对于通信连接的管理 --- 连接产生某个事件如何处理,由TcpServer模块设置
  3. 对于超时连接的管理 --- 连接非活跃超时,是否关闭,由TcpServer模块设置
  4. 对于事件监控的管理 --- 启动多少线程,有多少个EventLoop(子Reactor),由TcpServer设置。
  5. 事件回调函数设置(用户 来设置给TcpServer,一个连接产生一个事件,对于这个事件如何处理只有组件使用者知道,因此一个事件的处理回调一定是组件使用者设置给 TcpServer,它再设置给各个Connection连接)

模块关系图

  1. 通信连接管理模块关系图

Connection模块当中是包含有如下的三个模块的,分别是Buffer缓冲区模块,Socket套接字操作模块,Channel描述符事件管理模块,接下来来看看Connection模块是如何借助下面的这三个模块来实现它的基本功能的:

我们直接看内部描述符事件操作的接口,Connection模块可以通过Socket接收数据和发送数据,所以Connection模块必然是要和Socket模块进行紧密联系的,而这里采用的是多路转接的方案,所以就意味着会对所有的描述符进行监听,那此时就需要Channel模块对于所有事件进行管理的工作了,Channel模块当中包含有对于可读和可写事件的回调,所以在Connection模块的Socket接收和发送数据,本质上是通过Channel模块的回调进行设置的,当监听的描述符满足要求之后,就会调用Channel模块自定义设置的回调函数,进行可读和可写事件的回调,进而在Connection模块就可以通过Socket接收和发送数据了,这样就解释清楚了Connection模块是如何通过Socket接收和发送数据的

每一个Connection对象内部都会被提前设置好各个事件回调,例如有连接建立完成回调,新数据到来的回调,任意事件的回调,关闭连接的回调,这些回调都是由用户设置给TcpServer,TcpServer再设置给Connection的,这也算是Connection对象内部的接口

而在Connection模块当中还会存在有关闭套接字移除事件监控和刷新活跃度的功能,这两个功能本质上是和Socket功能是一样的,它的底层都是借助了Channel模块,当触发了挂断和错误事件回调的时候,就会促使Connection模块执行对应的方案,如果使用者设置了非活跃连接销毁的方案,也会在事件触发后刷新统计时间,至此这就把Connection模块和Channel模块联系在一起了。

那下面来看Socket套接字模块,其实Socket套接字模块本身就和上面有十分紧密的关系,在底层进行监听的本质,其实就是监听对应的Socket的相关信息到底有没有就绪,当Socket套接字监听到有信息就绪的时候,就会被多路转接的相关接口监听到,进而促使到Channel模块进行函数回调,促使Connection模块执行对应的策略,所以Socket模块也就解释清楚了。

Buffer模块是缓冲区模块,这就意味着只要涉及到接收和发送数据的操作,都是和Buffer模块是紧密相关的,Socket接收到的数据放到接收缓冲区中,而发送数据是放到发送缓冲中,也就是说Buffer缓冲区是和Socket模块紧密相关的,而正是有了Socket模块才会有多路转接提醒上层可以进行后续操作了,此时就会调用到Channel模块进行调用用户设置的回调,进而到达Connection模块的各种数据的处理

至此,站在Connection模块的层面,不关心底层的逻辑,已经可以把内部的这些事件的接口都理清楚了,而对于暴露在外的接口来说,也只是底层的这些内部接口的封装,比如所谓关闭连接,发送数据,切换协议,启动和取消非活跃销毁这些操作,未来其本质就是借助的是内部对于描述符事件操作接口的描述。

(2)监听连接管理模块关系图

对于监听套接字来说,对他监控可读事件,一旦有事件触发,说明有新链接到来,此时需要为这个新链接进行初始化。主要流程是多路转接对监听fd进行可读事件监控,但事件触发说明可以获取新链接,此时channel模块就会调用一下回调来进行新链接的获取,获取新链接之后就需要初始化,设置一些回调等。

(3)事件监控管理模块关系图

先看Poller模块,这个模块主要做的是对于一个文件描述符进行各项事件的监控操作,包含有添加,修改,移除事件监控的操作,而每一个Poller管理的描述符又会和Channel模块产生联系,因为Channel模块本身就是用来对于每一个描述符可能包含的事件做出的管理,当管理的描述符内部触发了某种事件,那么就会相应的调用这些事件内部的一些回调函数,这是提前就被设置好的内容。

再看TimeQueue模块,它主要是用来完成对一些非活跃连接超时销毁的任务,这块会在Connection模块添加这样的定时任务。

对于Connection模块来说,Channel模块的意义就是设置了各种回调,这些回调都是会回调指向原来的Connection模块的,所以Channel对象的回调函数回调的位置就到了Connection模块,这样就把EventLoop、Channel、Connection模块都联系在了一起。


总的模块间关系梳理:

TcpServer监听连接一旦触发可读事件获取新连接,然后对新连接进行初始化,即为新连接创建了一个Connection对象,并给Connection对象设置一系列回调函数,接下来通过Channel为这个连接添加事件监控。Channel一旦触发一个事件,比如触发可读事件,就通过socket接收数据,这些回调一个个设置好就开始监控。添加监控通过EventLoop提供的接口添加事件监控,即调用EventLoop中的Poller来添加事件监控。添加监控是对fd添加监控,一旦事件触发(比如可读事件),则调用Channel的可读事件回调,这是Connection设置的,接收数据之后,调用TcpServer设置的(其实应该说是用户->TcpServer->Connection)新数据接收后的回调来进行数据的业务处理。在Accept初始化连接的时候,不仅仅初始化了一个Connection, 还添加了一个定时任务,即非活跃超时销毁

相关推荐
DO_Community7 小时前
裸金属 vs. 虚拟化 GPU 服务器:AI 训练与推理应该怎么选
运维·服务器·人工智能·llm·大语言模型
徐子元竟然被占了!!7 小时前
Linux的df和du
linux·运维·服务器
星哥说事7 小时前
NAS/SAN存储:NFS/iSCSI/FC 存储协议与应用场景
运维
科技峰行者7 小时前
华为发布Atlas 900 DeepGreen AI服务器:单机柜100PF算力重构AI训练基础设施
服务器·人工智能·华为·aigc·gpu算力
Mr. Cao code7 小时前
实战:Docker构建Haproxy负载均衡镜像
linux·运维·ubuntu·docker·容器·负载均衡
SimonKing7 小时前
Spring Boot还能这样玩?同时监听多个端口的黑科技
java·后端·程序员
@木辛梓7 小时前
Linux 线程
linux·开发语言·c++
门前灯7 小时前
Linux系统之pkg-config 命令详解
linux·运维·服务器·pkg-config
fyakm7 小时前
中间件的前世今生:起源与发展历程
中间件