C++项目:仿muduo库高并发服务器 - 实践

news/2025/9/27 12:37:45/文章来源:https://www.cnblogs.com/lxjshuju/p/19114967

文章目录

  • 前言
  • 一、项目核心目标与定位
  • 二、Reactor模型
  • 三、功能模块划分
    • 3.1 SERVER模块
      • 3.1.1 Buffer模块
      • 3.1.2 Socket模块
      • 3.1.3 Channel模块
      • 3.1.4 Connection模块
      • 3.1.5 Acceptor模块
      • 3.1.6 TimerQueue模块
      • 3.1.7 Poller模块
      • 3.1.8 EventLoop模块
      • 3.1.8 TcpServer模块
  • 四、HTTPServer模块划分
    • 4.1 Util模块
    • 4.2 HttpRequest模块
    • 4.3 HttpResponse模块
    • 4.4 HttpContext模块
    • 4.5 HttpServer模块


码云链节

前言

若需要一手资料,可以私信博主添加好友
本文将为大家介绍博主学习制作的 “仿muduo库实现高并发服务器” 项目的前瞻知识。核心内容围绕两方面展开:一是帮助大家对该项目建立整体认知,明确项目的核心方向与大致框架;二是清晰交代本项目的学习目标及所需具备的知识储备,为大家后续制定学习计划、高效推进学习提供参考。

需要说明的是,文中内容均来自博主在学习该项目过程中所用资料的汇总与润色,力求为大家呈现准确、实用的前置信息。
学习该项目前首先确保已经掌握Http协议、Tcp协议、I/O多路转接技术等


一、项目核心目标与定位

本次项目旨在仿muduo库One Thread One Loop(一线程一循环)式主从Reactor模型,实现一款高并发服务器组件,具体目标与核心定位如下:

1. 核心功能目标

2. 关键定位明确
需重点明确:本项目的核心产出是高并发服务器组件,而非包含具体业务逻辑的成品应用。组件聚焦于底层网络通信、并发调度、协议解析等通用能力的封装,不涉及实际业务内容。

二、Reactor模型

Reactor 模式,是服务器应对单客户端或多客户端并发请求时,采用的一种事件驱动式请求处理模式。其核心逻辑为:服务端程序先接收多路传入请求,再通过同步方式将这些请求分派至与请求一一对应的处理线程中执行处理。正因“分发请求”这一核心特性,Reactor 模式也被称为 Dispatcher 模式(分发者模式)。

若用更通俗的方式理解,Reactor模式的核心逻辑可概括为:借助I/O多路复用技术,对各类事件(如客户端连接请求、数据读写请求等)进行统一监听;一旦监听到事件触发,便将其精准分发给对应的处理进程或线程执行后续操作。
在这里插入图片描述

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

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

优点: 所有操作均在同⼀线程中完成,思想流程较为简单,不涉及进程/线程间通信及资源争抢问题。
缺点: 所有的事件监控及业务处理都在一个线程中处理,⽆法有效利用CPU多核资源,很容易达到性能瓶颈。
适⽤场景: 适⽤于客户端数量较少,且处理速度较为快速的场景。(处理较慢或活跃连接较多,会导致串行处理的情况下,后处理的连接⻓时间⽆法得到响应)

在这里插入图片描述
单Reactor多线程:单I/O多路复用+线程池(业务处理)

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

优点: 充分利⽤CPU多核资源,降低了代码耦合度
缺点: 多线程间的数据共享访问控制较为复杂,单个Reactor 承担所有事件的监听和响应,在单线程中运⾏,⾼并发场景下容易成为性能瓶颈(如:每时刻都有很多新客户端发起连接请求)。

单Reactor单线程模式中,Reactor线程需同时承担两项核心工作:一是监控各类请求事件(如客户端连接、数据到达等),二是直接对触发的请求进行处理。但实际场景中,请求处理往往需要消耗较长时间,这会导致Reactor线程被长期占用,无法及时响应新的事件,进而降低整体服务效率。
为解决这一问题,单Reactor多线程模式对职责进行了拆分优化:Reactor线程仅专注于事件监控与I/O数据操作——当检测到任务(如客户端请求)到来时,它只需通过I/O操作获取请求数据,随后便将完整的请求任务转交至专门的处理线程池;待线程池中的线程完成请求处理后,再由Reactor线程将处理结果转发回客户端。这种“I/O与业务处理分离”的设计,保证Reactor线程的响应及时性,也能通过线程池充分利用CPU资源,提升请求处理的并发能力。
但是再连接请求过多时,这种方法仍然存在性能瓶颈

在这里插入图片描述
多Reactor多线程:多I/O多路复用+线程池(业务处理)
解决当Reactor线程进行数据I/O时,无法响应连接请求

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

优点: 充分利用CPU多核资源,主从Reactor各司其职
在这里插入图片描述

在多Reactor模式(通常包含主Reactor与从属Reactor)的设计中,职责分工更为明确:首先由主Reactor线程负责监听并创建新的客户端连接,一旦完成连接建立,便会将该连接转交至从属Reactor;此后,该连接的所有请求事件(如数据读写触发的事件)均由对应的从属Reactor负责监控与检测。

当从属Reactor检测到请求到来时,存在两种常见的处理设计方向:

  1. 从属Reactor直接处理:由监控该连接的从属Reactor线程,直接完成请求的处理逻辑;
  2. 转交线程池处理:从属Reactor仅负责获取请求数据,再将具体的业务处理任务交付给专门的处理线程池执行。

不过需注意,后一种“从属Reactor+线程池”的设计虽能进一步拆分I/O操作与业务逻辑,但需重点衡量线程频繁切换带来的额外成本,而且还需要考虑资源资源竞争问题,锁资源消耗较大,实现也比较复杂。

本项目实现主从Reactor模型服务器的设计逻辑如下:

  1. 主Reactor线程:专注新连接高效获取
    主Reactor线程仅承担单一核心任务——监控监听描述符(负责接收客户端连接请求的文件描述符),一旦检测到新的连接请求,便快速完成连接建立。这种“单一职责”设计能最大程度减少主Reactor的任务干扰,确保新连接获取的高效性。

  2. 子Reactor线程:负责通信事件与业务处理
    当主Reactor完成新连接建立后,会将该连接对应的描述符分发给子Reactor。此后,子Reactor线程将全程负责监控自身管理的描述符:一旦检测到读写事件(如客户端发送数据、连接关闭等),便直接执行数据的读写操作,并同步完成对应的业务逻辑处理,不交由线程池处理。

  3. 核心设计思想:One Thread One Loop
    整个服务器的线程与事件处理逻辑,围绕“One Thread One Loop”(一线程一循环)思想构建:每个线程内部都运行一个独立的事件处理循环(Loop),线程内的所有操作(如事件监控、I/O读写、业务处理)均在该循环中完成,一个线程严格对应一个事件循环。

前置知识:
时间轮定时器

三、功能模块划分

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

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

3.1 SERVER模块

SERVER模块的核心功能是对所有连接及线程进行统筹管理,确保各组件在合适时机执行对应操作,最终实现高性能服务器组件。其管理范畴具体分为三个方向:

基于上述管理思想,SERVER模块可进行再次细化模块。

3.1.1 Buffer模块

  • 功能:用于实现通信套接字的用户态缓冲区
  • 意义:
    1. 防止接收到的数据不是一条完整的数据(提示:TCP面向字节流),因此对接收的数据进行缓冲
    2. 对客户端响应的数据,应该是在套接字可写的情况下进行发送
  • 功能设计:
    1. 向缓冲区中添加数据
    2. 从缓冲区中取出数据

buff模块的实现

3.1.2 Socket模块

Socket模块

  • 功能:对socket套接字的操作进行封装
  • 意义:在接下来的开发中,使我们对于套接字的各项操作更加简便,避免代码冗余
  • 功能设计:
    1. 创建套接字
    2. 绑定地址信息
    3. 开始监听
    4. 向服务器发起连接
    5. 获取新连接
    6. 接收数据
    7. 发送数据
    8. 关闭套接字
    9. 创建一个监听连接
    10. 创建一个客户端连接

3.1.3 Channel模块

  • 功能:对⼀个描述符需要进行的IO事件管理,实现对描述符可读,可写,错误…事件的管理操作,以及IO事件监控就绪后,根据不同的事件,回调不同的处理函数功能。
  • 意义:让描述符的监控事件在用户态更易维护,触发事件后的操作流程更清晰
  • 功能设计:
    1. 对监控事件的管理:
      • 描述符是否可读
      • 描述符是否可写
      • 对描述符监控可读
      • 对描述符监控可写
      • 解除可读事件监控
      • 解除可写事件监控
      • 解除所有事件监控
    2. 对监控事件触发后的处理:
      • 设置对于不同事件的回调处理函数,明确触发某个事件后该如何处理

Channel模块实现

3.1.4 Connection模块

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

  • 功能:
    1. 是对通信连接进行整体管理的模块,连接相关操作均通过它执行
    2. 处理连接事件,因不知使用者事件处理逻辑,提供事件回调函数由使用者设置
  • 意义:并非单独功能模块,而是对连接做管理的模块,增加了连接操作的灵活性
  • 功能设计:
    1. 基础操作:关闭连接、发送数据、协议切换(本质就是重新设置回调函数)、启动非活跃连接超时释放、取消非活跃连接超时释放
    2. 回调函数设置:连接建立完成回调、新数据接收成功回调、连接关闭回调、任意事件回调

本项目只是实现了一个组件,组件的使用者具体进行什么操作我们是不知到的,所以在组件内部提供回调函数,让使用者自行选择,如:使用者自行调用服务端提供的回调函数处理,发送给服务端的数据

connection模块

3.1.5 Acceptor模块

  • 功能:对监听套接字进行管理
  • 意义:
    • 获取新建连接描述符后,为通信连接封装Connection对象并设不同回调
    • 因自身不知连接事件处理逻辑,通信连接的Connection封装、事件回调设置由服务器模块负责
  • 功能设计:
    • 回调函数设置:新建连接获取成功的回调设置,由服务器指定

这意思是Acceptor模块提供一个回调函数,在连接建立成功后,由服务器调用该回调函数完成对connection对象中的回调函数设置?

  • Acceptor模块角色:它主要聚焦在“获取新连接”这个动作流程里,负责当检测到有新连接进来时,能触发一个“钩子”(也就是它提供的回调函数入口 )。但它本身不关心拿到新连接后,具体要怎么初始化Connection里的各类事件回调(比如连接建立完成后的数据处理回调、异常处理回调等 )。
  • 服务器模块职责:服务器模块更清楚整个业务场景下,拿到新连接后需要让这个连接具备哪些“事件响应能力”,所以由服务器去实现具体的回调逻辑,在Acceptor触发“新建连接成功”的回调时,服务器把这些逻辑“填”到Connection对象里,让连接后续能按照业务需求处理各类事件,这样分工也让模块职责更清晰,解耦了连接获取和连接事件逻辑初始化的过程。简单说就是 Acceptor 搭好“通知有新连接”的架子,服务器负责往架子里填“新连接后续咋干活”的具体内容 。

这时博主自己学习时产生的疑问,问的AI

3.1.6 TimerQueue模块

TimerQueue模块是实现固定时间定时任务的模块,可以理解就是要给定时任务管理器,向定时任务管理器中添加⼀个任务,任务将在固定时间后被执行,同时也可以通过刷新定时任务来延迟任务的执行。

  • 功能:定时任务模块,让任务能在指定时间后执行
  • 意义:组件内部用于释放非活跃连接(希望非活跃连接在N秒后被释放 )
  • 功能设计:
    1. 添加定时任务
    2. 刷新定时任务(使定时任务重新开始计时 )
    3. 取消定时任务

这个模块主要是对Connection对象的⽣命周期管理,对非活跃连接进行超时后的释放功能。

3.1.7 Poller模块

  • 功能:对任意描述符进行IO事件监控
  • 意义:封装epoll,简化描述符事件监控操作
  • 功能接口:
    1. 添加事件监控(针对Channel模块 )
    2. 修改事件监控
    3. 移除事件监控

通过对epoll的封装达到对多个描述符同时检测的目的

3.1.8 EventLoop模块

  • 功能:
    1. 事件监控管理模块,是“one thread one loop”里的loop、reactor
    2. 一个模块对应一个线程
  • 意义:
    1. 负责服务器所有事件
    2. 每个Connection连接绑定一个EventLoop模块和线程,连接操作需在对应线程执行
  • 思想:
    1. 监控所有连接事件,事件触发后调用回调函数处理
    2. 连接操作放到EventLoop线程执行
  • 功能设计:
    1. 连接操作任务入队
    2. 定时任务增、刷、删

3.1.8 TcpServer模块

  • 功能:
    1. 整合所有子模块,供用户搭建高性能服务器
    2. 管理监听连接(新连接处理逻辑由Server设)
    3. 管理通信连接(连接事件处理逻辑由Server设)
    4. 管理超时连接(非活跃超时关闭逻辑由Server设)
    5. 管理事件监控(线程数、EventLoop数量由Server设)
    6. 设置事件回调函数(事件到来时该如何处理,由使用者设给TcpServer,再设给Connection )
  • 意义:让组件使用者更轻便搭建服务器

在这里插入图片描述

当有连接到来时,执行逻辑可总结为以下流程:

  1. Acceptor 模块触发:TcpServer 中的 Acceptor 模块监测到有新连接请求(可读事件等触发 ),代表新连接建立流程启动,获取新建连接的描述符。
  2. 创建 Connection 对象:利用获取到的连接描述符,封装创建 Connection 对象,用于后续对该连接的管理,同时会为其设置相关回调(如连接建立、数据收发等回调 ),并关联到 EventLoop 进行事件监控 。
  3. EventLoop 关联监控:将 Connection 对应的描述符相关事件(如可读、可写等 ),通过 Channel 模块注册到 EventLoop 中,由 EventLoop 依托 Poller 模块对这些 IO 事件进行监控管理 。
  4. 事件循环与处理:后续连接在生命周期内产生的各类事件(数据接收、可写、异常、关闭等 ),会被 EventLoop 检测到,触发 Channel 中设置的回调函数,进而执行 Connection 中或开发者自定义的对应逻辑(如数据解析、业务处理、资源清理等 ),实现对连接事件的响应和处理 。

至此,SERVER模块的组成部分已全部介绍完毕。接下来将进入SERVER模块功能的代码实现环节。 需要说明的是,为保证文章内容的连贯性,协议模块的组成相关内容将安排在后面。建议您先学习完SERVER模块的代码实现部分,再继续阅读协议模块的内容,以更好地理解整体逻辑衔接。

四、HTTPServer模块划分

由于应用层协议较多我们不可能全部实现,这里我们选择提供一种较为常见的协议支持:HTTP。用于对高并发服务器模块进行协议支持,基于其协议支持能更方便搭建指定协议服务器。

4.1 Util模块

工具模块,提供HTTP协议模块所用的工具函数

class Util{
public:
//分割字符串,以sep作为分割标志分割src,将分割后的字符串存入arry
static size_t Split(const std::string &src,const std::string &sep,std::vector<std::string>*arry){int offset=0;//分割起始偏移量while(offset<src.size()){int pos=src.find(sep,offset);if(pos==std::string::npos){//添加最后一个字符arry->push_back(src.substr(offset));return arry->size();}if(pos==offset){//分割标志相连,不包含有效字符串offset=pos+sep.size();continue;}arry->push_back(src.substr(offset,pos-offset));offset=pos+sep.size();}return arry->size();}//读取文件的所有内容,将读取的内容放到一个Buffer中static bool ReadFile(const std::string &filename, std::string *buf) {//ERR_LOG("%s,%ld",filename.c_str(),filename.size());std::ifstream ifs(filename, std::ios::binary);if (ifs.is_open() == false) {printf("OPEN %s FILE FAILED!!", filename.c_str());return false;}size_t fsize = 0;ifs.seekg(0, ifs.end);//跳转读写位置到末尾fsize = ifs.tellg();  //获取当前读写位置相对于起始位置的偏移量,从末尾偏移刚好就是文件大小ifs.seekg(0, ifs.beg);//跳转到起始位置buf->resize(fsize); //开辟文件大小的空间ifs.read(&(*buf)[0], fsize);if (ifs.good() == false) {printf("READ %s FILE FAILED!!", filename.c_str());ifs.close();return false;}//ERR_LOG("%ld",buf->size());ifs.close();return true;}//向文件写入数据static bool WriteFile(const std::string &filename, const std::string &buf) {//ERR_LOG("0000000    %ld",buf.size());std::ofstream ofs(filename, std::ios::binary | std::ios::trunc);if (ofs.is_open() == false) {printf("OPEN %s FILE FAILED!!", filename.c_str());return false;}ofs.write(buf.c_str(), buf.size());if (ofs.good() == false) {ERR_LOG("WRITE %s FILE FAILED!", filename.c_str());ofs.close();return false;}ofs.close();return true;}//URL编码//编码目的:避免URL中资源路径与查询字符串里的特殊字符和HTTP请求中的特殊字符产生歧义。//编码格式:把特殊字符的ASCII值,转换成两个十六进制字符,前缀为`%`,例如`C++`编码后为`C%2B%2B`。//不编码字符:依据RFC3986文档,`.`、`-`、`_`、`~`以及字母、数字属于绝对不编码的字符。//空格编码特殊规定:在w3C标准里,查询字符串中的空格需要编码为`+`,解码时`+`转换为空格。static std::string UrlEncode(const std::string&str,bool convert_space_to_plus){std::string res;for(auto c:str){if(c=='.'||c=='-'||c=='_'||c=='~'||isalnum(c)){res+=c;continue;}if(c==' '&&convert_space_to_plus){res+='+';continue;}char tmp[4]={0};snprintf(tmp,4,"%%%02X",c);res+=tmp;}return res;}static char HEXTOI(char c){if(c>='0'&&c<='9'){return c-'0';}if(c>='a'&&c<='z'){return c-'a'+10;}if(c>='A'&&c<='Z'){return c-'A'+10;}return -1;}//URL解码static std::string UrlDecode(const std::string&str,bool convert_space_to_plus){std::string res;int n=str.size();for(int i=0;i<n;i++){if(str[i]=='+'&&convert_space_to_plus){res+=' ';continue;}if(str[i]=='%'){char v1=HEXTOI(str[i+1]);char v2=HEXTOI(str[i+2]);char v=v1*16+v2;res+=v;i+=2;continue;}res+=str[i];}return res;}//获取响应状态码的描述信息static std::string StatuDesc(int statu){auto it=_statu_msg.find(statu);if(it==_statu_msg.end()){return "Unknow";}return it->second;}//根据文件后缀名获取文件的mimestatic std::string ExtMime(const std::string&filename){int pos=filename.find_last_of('.');if(pos==std::string::npos){return "application/octet-stream";//二进制流数据,表示不确定类型的文件}//根据扩展名获取mimeauto it=_mime_msg.find(filename.substr(pos));if(it==_mime_msg.end()){return "application/octet-stream";}return it->second;}//判断一个文件是否是目录static bool IsDirectory(const std::string&filename){struct stat st;//用于存储文件或目录的相关信息int ret=stat(filename.c_str(),&st);if(ret<0){ERR_LOG("STAT FAIL!");return false;}return S_ISDIR(st.st_mode);//检查 stat 结构体中的 st_mode 字段是否表示一个目录}//判断一个文件是否是普通文件static bool IsRegular(const std::string&filename){struct stat st;//用于存储文件或目录的相关信息int ret=stat(filename.c_str(),&st);if(ret<0){ERR_LOG("STAT FAIL!");return false;}return S_ISREG(st.st_mode);//检查 stat 结构体中的 st_mode 字段是否表示一个目录}//http请求的资源路径有效性判断//http请求的资源路径有效性判断// /index.html --- 前边的/叫做相对根目录 映射的是某个服务器上的子目录// 想表达的意思就是,客户端只能请求相对根目录中的资源,其他地方的资源都不予理会static bool ValidPath(const std::string&path) {//思想:按照/进行路径分割,根据有多少个计算目录深度std::vector<std::string> subdir;Split(path,"/",&subdir);int level=0;for(auto& sdir:subdir){if(sdir==".."){level--;if(level<0)return false;continue;}level++;}return true;}};

4.2 HttpRequest模块

HTTP请求数据模块,保存HTTP请求数据解析后的各项请求元素信息

  • 功能:存储HTTP请求信息,具体是接收到一个数据后,按照HTTP请求格式进行解析,将得到的各个关键要素放到HttpRequest中。
  • 意义:让HTTP请求的分析更加简单。
  • 要素:包含URL(涉及请求方法、资源路径、查询字符串)、协议版本、头部字段、正文。
  • 接口:提供头部字段的插入和获取、查询字符串的插入和获取功能。
//存储http请求信息
class HttpRequest
{
public:
HttpRequest():_version("HTTP/1.1"){}
// 重新设置请求信息
void ReSetRequest(){
_method.clear();
_path.clear();
_version="HTTP/1.1";
_body.clear();
std::smatch smatch;
_smatch.swap(smatch);
_headers.clear();
_params.clear();
}
//设置解析出来的头部字段
void SetHeader(const std::string&key,const std::string&val){
_headers.insert({key,val});
}
//指定头部字段是否存在
bool HasHeader(const std::string&key){
auto it=_headers.find(key);
if(it==_headers.end()){
return false;
}
return true;
}
//获取指定字段的val
std::string GetHeader(const std::string&key){
auto it=_headers.find(key);
if(it==_headers.end()){
return "";
}
return it->second;
}
//设置解析出来的查询字符串
void SetParam(const std::string&key,const std::string&val){
_params.insert({key,val});
}
//判断指定查询字符串是否存在
bool HasParam(const std::string&key){
auto it=_params.find(key);
if(it==_params.end()){
return false;
}
return true;
}
//获取指定查询字符串
std::string GetParam(const std::string&key){
auto it=_params.find(key);
if(it==_params.end()){
return "";
}
return it->second;
}
//获取正文大小
size_t ContentLength(){
//从头部字段中获取
if(!_headers.count("Content-Length")){
return 0;
}
std::string clen=_headers["Content-Length"];
return std::stol(clen);
}
//判断该是否是短连接
//没有Connection字段或者说Connection字段的是close都是短连接
bool Close(){
if(HasHeader("Connection")&&_headers["Connection"]=="keep-alive"){
return false;
}
return true;
}
public:
std::string _method;//请求方法
std::string _path;//资源路径
std::string _version;//协议版本
std::string _body;//请求正文
std::smatch _smatch;//资源路径的正则提取数据
std::unordered_map<std::string,std::string> _headers;//头部字段std::unordered_map<std::string,std::string> _params;//查询字符串};

4.3 HttpResponse模块

HTTP响应数据模块,业务处理后设置并保存HTTP响应数据的各项元素信息,最终按HTTP协议响应格式组织成响应信息发送给客户端。

  • 功能:存储HTTP响应信息;在进行业务处理的同时,让使用者向HttpResponse中填充响应要素,完毕后,将其组织成为HTTP响应格式的数据,发送给客户端。
  • 意义:让HTTP响应的过程变得简单。
  • 要素:包含协议版本、响应状态码、状态码描述信息、头部字段、正文。
  • 接口:提供头部字段的插入和获取、长连接&短链接的设置与判断、正文的设置功能。

std::smatch:保存首行使用regex正则进行解析后所提取的数据,比如提取资源路径中的数字等。

//HttpResponse:存储响应信息
class HttpResponse{
public:
HttpResponse():_statu(200),_redirect_flag(false)
{}
HttpResponse(int statu):_statu(statu),_redirect_flag(false)
{}
//重新设置响应信息
void ReSetResponse(){
_statu=200;
_redirect_flag=false;
_body.clear();
_redirect_url.clear();
_headers.clear();
}
//设置头部
void SetHeader(const std::string&key,const std::string&val){
_headers.insert({key,val});
}
//指定头部字段是否存在
bool HasHeader(const std::string&key){
auto it=_headers.find(key);
if(it==_headers.end()){
return false;
}
return true;
}
//获取指定字段的val
std::string GetHeader(const std::string&key){
auto it=_headers.find(key);
if(it==_headers.end()){
return "";
}
return it->second;
}
//设置正文信息
void SetContent(const std::string&body,const std::string&type){
_body=body;
SetHeader("Content-Type",type);
}
//设置重定向路径,302:临时重定向
void SetRedirect(const std::string&url,int statu=302){
_redirect_url=url;
_statu=statu;
_redirect_flag=true;
}
//判断该是否是短连接
//没有Connection字段或者说Connection字段的是close都是短连接
bool Close(){
if(HasHeader("Connection")&&_headers["Connection"]=="keep-alive"){
return false;
}
return true;
}
public:
int _statu;//响应状态码
bool _redirect_flag;//是否重定向标志
std::string _body;//响应正文
std::string _redirect_url;//重回定向路径
std::unordered_map<std::string,std::string> _headers;//响应头部};

4.4 HttpContext模块

HTTP请求接收的上下文模块,防止一次接收的数据不是完整HTTP请求导致解析未完成,需在下次接收新数据后根据上下文继续解析,最终得到完整HttpRequest请求信息对象,用于控制请求数据的接收及解析节奏。

  • 请求接收上下文模块

    • 功能:记录HTTP请求的接收和处理进度。
    • 意义:因可能接收的不是完整HTTP请求数据,请求处理需多次收数据后完成,所以每次处理要记录进度,以便下次从当前进度继续。
    • 要素
      • 接收状态:包括接收请求行(当前处于接收并处理请求行的阶段)、接收请求头部(表示请求头部的接收还没有完毕)、接收正文(表示还有正文没有接收完毕)、接收数据完毕(接收完毕,可对请求进行处理的阶段)、接收处理请求出错。
      • 响应状态码:在请求接收并处理中,可能出现解析出错、访问资源不对、没有权限等不同问题,对应错误的响应状态码不同。
  • 接收并处理请求数据及接口相关

    • 实现
      • 已接收并处理的请求信息。
      • 接收并处理请求数据:包含接收请求行、解析请求行、接收头部、解析头部、接收正文。
    • 接口
      • 返回解析完毕的请求信息。
      • 返回响应状态码。
      • 返回接收解析状态。
typedef enum{
RECV_HTTP_ERR,
RECV_HTTP_LINE,
RECV_HTTP_HEAD,
RECV_HTTP_BODY,
RECV_HTTP_OVER
}HttpRecvStatu;
//存储http请求解析的信息
#define MAX_LINE 8192
class HttpContent{
private:
//接收请求行
bool RecvHttpLine(Buffer*buf){
//判断请求处理阶段是否为首行处理
if(_recv_statu!=RECV_HTTP_LINE)return false;
//从接收缓冲区中获取请求行
std::string line = buf->GetLineAndPop();
if(line.size()==0){
//未接收到,判断缓冲区数据是否很长且没有‘/n’
if(buf->ReadAbleSize()>MAX_LINE){
_recv_statu=RECV_HTTP_ERR;
_resp_statu=414;//URI Too Long
return false;
}
//缓冲区不足一行,等待数据到来
return true;
}
//接收到数据了,但是请求行过长
if(line.size()>MAX_LINE){
_recv_statu=RECV_HTTP_ERR;
_resp_statu=414;//URI Too Long
return false;
}
bool ret=ParseHttpLine(line);
if(ret==false){
return false;
}
//首行处理完毕进入头部处理阶段
_recv_statu=RECV_HTTP_HEAD;
return true;
}
//解析Http请求头部
bool ParseHttpLine(const std::string &line){
// http请求行格式:GET/bitejiuyeke/login?user=xiaoming&pass=123123 HTTP/1.1\r\n
std::smatch matchs; // 存储提取数据
// 匹配任意字符串并提取GET|DELETE|PUT|POST|HEAD
//[^?]:表示匹配非?字符  *,表示匹配零次或多次
//\\?匹配普通字符?,在正则表达式中?表示匹配0次或1次因此需要\转译
//(HTTP/1\\.[01]):匹配以HTTP/1.开始后边为0或1的字符串
//std::regex::icase进行匹配时不区分大小写
//?:表示它所处括号内容只匹配不捕获?表示匹配0次或1次
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
//解析得到的结果
// 0 :GET/bitejiuyeke/login?user=xiaoming&pass=123123 HTTP/1.1
// 1 :GET
// 2 :/bitejiuyeke/login
// 3 :user=xiaoming&pass=123123
// 4 :HTTP/1.1
bool ret = regex_match(line, matchs, e);
if (!ret){
_recv_statu=RECV_HTTP_ERR;
_resp_statu=400;
ERR_LOG("REGEX PARSE HTTP LINE FAIL!");
return false;
}
// std::cout << matchs[0] << std::endl;
// std::cout << matchs[1] << std::endl;
// std::cout << matchs[2] << std::endl;
// std::cout << matchs[3] << std::endl;
// std::cout << matchs[4] << std::endl;
_request._method=matchs[1];
//:: 表示引用全局接口
std::transform(_request._method.begin(),_request._method.end(),_request._method.begin(),::toupper);
_request._path=Util::UrlDecode(matchs[2],true);
_request._version=matchs[4];
std::string query_string=matchs[3];
std::vector<std::string> query_string_arry;Util::Split(query_string,"&",&query_string_arry);for(auto &str:query_string_arry){int pos=str.find('=');if(pos==std::string::npos){_recv_statu = RECV_HTTP_ERR;_resp_statu = 400;ERR_LOG("REGEX PARSE HTTP LINE FAIL!");return false;}std::string key=str.substr(0,pos);std::string val=str.substr(pos+1);_request.SetParam(key,val);}return true;}//接收http请求头部bool RecvHttpHead(Buffer*buf){//判断请求处理阶段是否为头部处理if(_recv_statu!=RECV_HTTP_HEAD)return false;while(1){// 从接收缓冲区中获取请求头部std::string line = buf->GetLineAndPop();if (line.size() == 0){// 未接收到,判断缓冲区数据是否很长且没有‘/n’if (buf->ReadAbleSize() > MAX_LINE){_recv_statu = RECV_HTTP_ERR;_resp_statu = 414; // URI Too Longreturn false;}// 缓冲区不足一行,等待数据到来return true;}// 接收到数据了,但是头部过长if (line.size() > MAX_LINE){_recv_statu = RECV_HTTP_ERR;_resp_statu = 414; // URI Too Longreturn false;}//判断头部是否处理完毕if(line=="\n"||line=="\r\n"){//处理完毕进入正文处理阶段_recv_statu=RECV_HTTP_BODY;return true;}//进入头部解析int ret=ParseHttpHead(line);if(ret==false){return false;}}_recv_statu=RECV_HTTP_BODY;return true;}//解析Http头部bool ParseHttpHead(std::string&line){//key: val\r\n....if(line.back()=='\n') line.pop_back();if(line.back()=='\r') line.pop_back();int pos = line.find(": ");if (pos == std::string::npos){_recv_statu = RECV_HTTP_ERR;_resp_statu = 400;ERR_LOG("PARSEHTTPHEAD FAIL!");return false;}std::string key = line.substr(0, pos);std::string val = line.substr(pos +2);_request.SetHeader(key, val);return true;}//接收请求正文bool RecvHttpBody(Buffer*buf){if(_recv_statu!=RECV_HTTP_BODY)return false;//获取请求正文大小int content_length=_request.ContentLength();if(content_length==0){//请求没有正文_recv_statu=RECV_HTTP_OVER;return true;}//计算实际还需获取的长度int real_length=content_length-_request._body.size();//判断缓冲区的数据能否提供完整的正文if(buf->ReadAbleSize()>=real_length){//获取正文_request._body.append(buf->ReadPosition(),real_length);_recv_statu=RECV_HTTP_OVER;buf->MoveReadOffset(real_length);return true;}//缓冲区正文不完整_request._body.append(buf->ReadPosition(), buf->ReadAbleSize());buf->MoveReadOffset(buf->ReadAbleSize());return true;}public:HttpContent():_resp_statu(200),_recv_statu(RECV_HTTP_LINE){}//获取响应状态码int RespStatu(){return _resp_statu;}//获取当前解析得到的请求信息HttpRequest& Request(){return _request;}//获取当前请求接收状态HttpRecvStatu RecvStatu(){return _recv_statu;}//接收并解析http请求void HttpRecvRequest(Buffer*buf){switch(_recv_statu){case RECV_HTTP_LINE: RecvHttpLine(buf);case RECV_HTTP_HEAD: RecvHttpHead(buf);case RECV_HTTP_BODY: RecvHttpBody(buf);};//std::cout<<_request._method<<" "<<_request._path<<" ";return;}void ReSetRecvContent(){_resp_statu=200;_recv_statu=RECV_HTTP_LINE;_request.ReSetRequest();}public:int _resp_statu;//http响应状态HttpRecvStatu _recv_statu;//当前http接收状态HttpRequest _request;//当前已解析得到的http请求};

4.5 HttpServer模块

这个模块综合了前面几个模块的功能,实现起来比较复杂
给组件使用者提供的HTTP服务器模块,以简单接口实现HTTP服务器搭建。内部包含一个TcpServer对象(TcpServer对象实现服务器的搭建);包含两个提供给TcpServer对象的接口:连接建立成功设置上下文接口、数据处理接口;还包含一个hash - map表存储请求与处理函数的映射表

  • 请求路由表设计目的:记录针对具体请求应使用哪个函数进行业务处理的映射关系。
  • 工作流程:服务器收到请求后,在请求路由表中查找是否有对应请求的处理函数,若有则执行该函数。
  • 核心逻辑:“什么请求,怎么处理”由用户设定,服务器收到请求只需执行对应函数。
  • 好处:用户只需实现业务处理函数,并将请求与处理函数的映射关系添加到服务器;服务器只需负责接收数据、解析数据、查找路由表映射关系、执行业务处理函数。

所需要素

  1. 请求路由映射表:涵盖 GET、POST、PUT、DELETE 请求的路由映射表,用于记录对应请求方法的处理函数映射关系,以处理功能性请求。
  2. 静态资源相对根目录:用于实现静态资源(如 html、图片等实体文件)请求的处理。
  3. 高性能 TCP 服务器:负责连接的 IO 操作。

服务器处理流程

  • 从 socket 接收数据,放到接收缓冲区。

  • 调用 OnMessage 回调函数进行业务处理。

  • 解析请求,得到包含所有请求要素的 HttpRequest 结构。

  • 进行请求路由查找,确定对应请求的处理方法:

    • 静态资源请求:读取静态资源文件数据,填充到 HttpResponse 结构中。
    • 功能性请求:在请求路由映射表中查找处理函数,找到则执行该函数,进行具体业务处理并填充 HttpResponse 结构。
  1. 对静态资源请求或功能性请求处理完毕后,得到填充了响应信息的 HttpResponse 对象,组织成 HTTP 格式的响应并进行发送。

相关接口

  1. 可进行添加请求 - 处理函数映射信息(支持 GET、POST、PUT、DELETE 等请求类型)、设置静态资源根目录、设置是否启动超时连接关闭、设置线程池中线程数量、启动服务器等操作。
  2. 还有 OnConnected(用于给 TcpServer 设置协议上下文)、OnMessage(用于进行缓冲区数据解析处理)等回调相关接口,以及请求的路由查找(包含静态资源请求查找和处理、功能性请求的查找和处理)、组织响应进行回复等功能接口。
class HttpServer{
using Handler=std::function<void(const HttpRequest&,HttpResponse*)>;
using Handlers=std::vector<std::pair<std::regex,Handler>>;private://将HttpReponse中的数据组织成http格式并返回void WriteReponse(const PtrConnection&conn,HttpRequest&req,HttpResponse&rsp){//构建头部if(req.Close()==true){rsp.SetHeader("Connection","close");}else{rsp.SetHeader("Connection","keep-alive");}if(!rsp._body.empty()&&rsp.HasHeader("Content-Length")==false){rsp.SetHeader("Content-Length",std::to_string(rsp._body.size()));}if(!rsp._body.empty()&&rsp.HasHeader("Content-Type")==false){rsp.SetHeader("Content-Type","application/octet-stream");}if(rsp._redirect_flag){rsp.SetHeader("Location",rsp._redirect_url);}//将rsp的数据组织为http响应std::stringstream rsp_str;rsp_str<<req._version<<" "<<std::to_string(rsp._statu)<<" "<<Util::StatuDesc(rsp._statu)<<"\r\n";for(auto &it:rsp._headers){rsp_str<<it.first<<": "<<it.second<<"\r\n";}rsp_str<<"\r\n";rsp_str << rsp._body;//发送响应std::cout << "已发送响应: " << rsp_str.str() << std::endl;conn->Send(rsp_str.str().c_str(),rsp_str.str().size());return;}//静态资源的处理void FileHandler(const HttpRequest&req,HttpResponse* rsp){std::string buf;int ret=Util::ReadFile(req._path,&buf);if(ret==false){return ;}rsp->_body=buf;std::string mime=Util::ExtMime(req._path);rsp->SetHeader("Content-Type",mime);return ;}//功能性请求的分发void Dispatcher(HttpRequest&req,HttpResponse* rsp,const Handlers&handlers){// 在对应请求方法的路由表中,查找是否含有对应资源请求的处理函数,有则调用,没有则返回 404。// 思想:路由表存储正则表达式与处理函数的键值对,使用正则表达式对请求的资源路径进行正则匹配,匹配成功就用对应函数处理。// 示例:/numbers/(\d+) 可匹配 /numbers/12345。for(auto&handler:handlers){std::regex re=handler.first;bool ret=std::regex_match(req._path,req._smatch,re);if(ret==false){continue;}handler.second(req,rsp);return;}rsp->_statu=404;}//判断请求是否为静态资源请求bool IsFileHandler(HttpRequest&req){//保证设置了静态资源根目录if(static_basedir.empty()){return false;}//请求方法必须是GET或HEADif(req._method!="GET"&&req._method!="HEAD"){return false;}//请求资源路径必须合法if(Util::ValidPath(req._path)==false){return false;}//请求资源必须存在,且是一个合法路径// 检测请求路径中是否包含根目录前缀:判断req._path是否以服务器根目录前缀(如/wwwroot/)开头。// 拼接正确路径:用处理后的相对路径与服务器根目录拼接,得到实际路径。std::string req_path=static_basedir+req._path;std::cout <<  "资源路径是:"  << req_path << "xxx"<< std::endl;if(req_path.back()=='/'){req_path+="index.html";}std::cout <<  "资源路径是:"  << req_path << "xxx"<< std::endl;if(Util::IsRegular(req_path)!=true){return false;}req._path=req_path;return true;}//路由表查询划分void Route(HttpRequest&req,HttpResponse* rsp){//对请求进行分辨//GET HEAD 默认为静态资源请求//如果不是静态请求也不是动态请求设置状态405if(IsFileHandler(req)){//静态资源请求FileHandler(req,rsp);return;}if(req._method=="GET"||req._method=="HEAD"){Dispatcher(req,rsp,_get_route);return;}if(req._method=="POST"){ERR_LOG("POST ....");Dispatcher(req,rsp,_post_route);return;}if(req._method=="PUT"){Dispatcher(req,rsp,_put_route);return;}if(req._method=="DELETE"){Dispatcher(req,rsp,_delete_route);return;}rsp->_statu=405;// Method Not Allowedreturn;}//初始化上下文数据void OnConnected(const PtrConnection&conn){conn->SetContext(HttpContent());}//对请求解析并处理void OnMessage(const PtrConnection&conn,Buffer*buf){while(1){// 1.获取上下文HttpContent *content = conn->GetContext()->get<HttpContent>();// 2.通过上下文对缓冲区数据进行解析,得到HttpRequestcontent->HttpRecvRequest(buf);HttpResponse rsp(content->_resp_statu);HttpRequest &req = content->Request();if(content->_resp_statu>=400){ERR_LOG("HTTPRECVREQUEST FAIL!");// 缓冲区数据解析失败,构建错误信息ErrorHandler(req,&rsp);buf->MoveReadOffset(buf->ReadAbleSize());// 返回响应WriteReponse(conn, req, rsp);content->ReSetRecvContent();conn->Shutdown();}if (content->_recv_statu != RECV_HTTP_OVER){return;}// 3.请求路由+业务处理Route(req, &rsp);// 4.对HttpResponse进行业务发送WriteReponse(conn, req, rsp);// 5.重置上下文content->ReSetRecvContent();// 6.判断长短连接if (rsp.Close() == true){ // 短连接直接关闭conn->Shutdown();return;}}}void ErrorHandler(const HttpRequest &req, HttpResponse *rsp) {// 组织一个错误展示页面std::string body;body += "<html>";body += "<head>";body += "<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>";body += "</head>";body += "<body>";body += "<h1>";body += std::to_string(rsp->_statu);body += " ";body += Util::StatuDesc(rsp->_statu);body += "</h1>";body += "</body>";body += "</html>";// 将页面数据当作响应正文放入rsp中rsp->SetContent(body, "text/html");}public:HttpServer(int port,int timeout=DEFALT_TIMEOUT):_server(port){_server.EnableInactiveRelease(timeout);//http服务默认启动超时销毁_server.SetConnectedCallback(std::bind(&HttpServer::OnConnected,this,std::placeholders::_1));_server.SetMessageCallback(std::bind(&HttpServer::OnMessage,this,std::placeholders::_1,std::placeholders::_2));}//设置路由表// using Handler=std::function<void(const HttpRequest&,HttpResponse*)>;// using Handlers=std::vector<std::pair<std::regex,Handler>>;void Get(const std::string&pattern,const Handler&handler){_get_route.push_back(std::make_pair(std::regex(pattern),handler));}void Post(const std::string&pattern,const Handler&handler){_post_route.push_back(std::make_pair(std::regex(pattern),handler));}void Put(const std::string&pattern,const Handler&handler){_put_route.push_back(std::make_pair(std::regex(pattern),handler));}void Delete(const std::string&pattern,const Handler&handler){_delete_route.push_back(std::make_pair(std::regex(pattern),handler));}//设置线程数量void SetThreadCount(int count){_server.SetThreadCount(count);}//设置静态资源根目录void SetBasedir(const std::string&path){assert(Util::IsDirectory(path));static_basedir=path;}//启动服务器void Listen(){_server.Start();}private:std::string static_basedir;//静态资源根目录Handlers _get_route;//GET的请求路由映射表Handlers _post_route;//POST的请求路由映射表Handlers _put_route;//put的请求路由映射表Handlers _delete_route;//delete的请求路由映射表TcpServer _server;};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/919466.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

完整教程:zk管理kafkakafka-broker通信

完整教程:zk管理kafka&kafka-broker通信pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", &qu…

域泛化DomainBed的评价指标含义解释

DomainBed是域泛化领域的公认框架,其统一了输入输出以及相关细节处理,使得泛化性能比较更加公平公正,但是庞大的框架使其理解十分困难,今天首先介绍其评价指标,即Selection字段。结果展示 +------------+--------…

JUC: 线程锁

1 面试题复盘如何理解多线程,如何处理并发,线程池有哪些核心参数?Java加锁有哪几种锁?synchronized原理是什么?为什么可重入?如何获取对象的锁?JVM对原生锁做了哪些优化?什么是锁清除和锁粗化?乐观锁是什么?…

手机网站是怎么制作的wordpress好玩插件

1.新建Android应用&#xff0c;确定应用包名 2.注册高德开放平台&#xff0c;打开控制台页面&#xff0c;应用管理&#xff0c;我的应用&#xff0c;创建新应用 3.添加Key 4.获取SHA1码 找到Android Studio自带的keytool 将其拖到cmd中&#xff0c;输入命令 -v -list -keystor…

网站在线咨询模块东营市招投标信息网

&#x1f389;博主首页&#xff1a; 有趣的中国人 &#x1f389;专栏首页&#xff1a; Linux &#x1f389;其它专栏&#xff1a; C初阶 | C进阶 | 初阶数据结构 小伙伴们大家好&#xff0c;本片文章将会讲解Linux中项目自动化构建工具make/makefile的相关内容。 如果看到最后…

dede网站地图怎么做lamp网站开发 pdf

为什么80%的码农都做不了架构师&#xff1f;>>> 介绍 在本系列的第一篇文章中&#xff0c;安装了Node.js、Ignite的Node.js瘦客户端包&#xff0c;并且测试了一个示例应用。在本文中&#xff0c;可以看一下Ignite在处理其它数据源&#xff08;比如关系数据库&#…

InteractiveCommunication Problems

/偏向于前者。CSP 初赛塞了两个交互,有点慌。

JSON 框架混用避坑指南:FastJSON vs Jackson

`com.alibaba.fastjson.JSON.parseObject()` 方法无法识别 Jackson 的 `@JsonProperty` 注解,导致字段映射失败。 核心矛盾:FastJSON 无法识别 Jackson 的 @JsonProperty 注解目录一、问题定位二、框架对比表三、典…

实用指南:网络通信协议全解析:HTTP/UDP/TCP核心要点

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

企业级大数据技术栈:基于Hadoop+Spark的全球经济指标分析与可视化环境实践

企业级大数据技术栈:基于Hadoop+Spark的全球经济指标分析与可视化环境实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-famil…

网站制作的相关术语西安专业做网站建

连接MySQL数据库时常见故障问题的分析与解决 初学的mysql网友好象经常会碰到mysql无法连接的错误。特开贴收集这样问题的现象和原因。 先自己扔块砖头出来。 归纳如下&#xff1a; 故障现象 : 无法连接 mysql 错误信息1 &#xff1a;ERROR 1045 (28000): Access deni…

若邻接矩阵是三角矩阵,则存在拓扑序列;反之则不一定成立

目录1. 命题回顾2. 前半句:邻接矩阵是三角矩阵 ⇒ 存在拓扑序列2.1 邻接矩阵是上三角矩阵的情况2.2 邻接矩阵是下三角矩阵的情况3. 后半句:反之则不一定成立4. 最终判断1. 命题回顾若邻接矩阵是三角矩阵,则存在拓扑…

Gateway-断言 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

macOS 多 Java 版本管理(jenv 方案)

目录一、目标二、查看已安装的 JDK三、使用 jenv 管理 Java 版本1. 安装 jenv2. 配置 Shell 环境3. 添加已安装的 JDK4. 查看可用版本5. 切换 Java 版本6. 验证版本四、常见问题1. 权限问题2. Shell 配置文件选择错误五…

龙口网站制作价格衡阳网站建设技术外包

操作&#xff1a; 是时机函数&#xff0c;在页面加载前&#xff0c;可以在这两个函数里面做一些事情&#xff0c; 比如发送异步请求。 类似过滤器&#xff0c;或者拦截器。1. axios安装 安装报错&#xff0c;多装几遍&#xff0c;或者用cnpm安装 npm install axios -s npm in…

怎么提高网站关键字排名网站怎么做360免费优化

在数字化浪潮席卷全球的今天&#xff0c;跨境电商业务蓬勃发展&#xff0c;成为推动国际贸易增长的重要引擎。亚马逊&#xff0c;作为全球最大的电商平台之一&#xff0c;以其独特的平台特点和全球化布局&#xff0c;为卖家和买家提供了便捷、高效的交易环境&#xff0c;成为众…

广州搜索seo网站优化建设银行网站字体

免责声明: 本文旨在提供有关特定漏洞的深入信息,帮助用户充分了解潜在的安全风险。发布此信息的目的在于提升网络安全意识和推动技术进步,未经授权访问系统、网络或应用程序,可能会导致法律责任或严重后果。因此,作者不对读者基于本文内容所采取的任何行为承担责任。读者在…

AI 落地教育智慧招生:从 “热线占线” 到 “724 小时精准应答” 的实践分享

AI 落地教育智慧招生:从 “热线占线” 到 “724 小时精准应答” 的实践分享在教育招生季,家长对 “报名时间”“学区范围”“学校特色” 的咨询需求集中爆发,而传统招生咨询模式往往陷入 “家长急、老师累、效率低”…