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


文章目录

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

码云链节

前言

若需要一手资料,可以私信博主添加好友

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

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


一、项目核心目标与定位

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

1. 核心功能目标

  • 基于经典的主从Reactor模型 ,采用muduo库标志性的One Thread One Loop线程模型,保障服务器的高并发处理能力;
  • 打造可复用的高并发服务器组件:开发者基于该组件,能以简洁、高效的方式快速搭建高性能基础服务器,无需重复实现底层网络通信与并发控制逻辑;
  • 提供应用层协议扩展支持 :组件内置不同应用层协议适配能力(当前为便于演示,优先集成HTTP协议组件),支持开发者快速基于组件搭建高性能应用服务器(如HTTP服务器)。

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协议模块所用的工具函数

  • 功能:实现一些工具接口,具体包括读取文件内容、向文件写入内容、URL编码、URL解码、提供HTTP状态码&描述信息、根据文件后缀名获取mime。
  • 意义:在协议支持模块中,当需要某些零碎功能时,便于使用。
cpp 复制代码
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;
    }
    //根据文件后缀名获取文件的mime
    static std::string ExtMime(const std::string&filename){
        
        int pos=filename.find_last_of('.');
        if(pos==std::string::npos){
            return "application/octet-stream";//二进制流数据,表示不确定类型的文件
        }
        //根据扩展名获取mime
        auto 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(涉及请求方法、资源路径、查询字符串)、协议版本、头部字段、正文。
  • 接口:提供头部字段的插入和获取、查询字符串的插入和获取功能。
cpp 复制代码
//存储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正则进行解析后所提取的数据,比如提取资源路径中的数字等。

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

    • 实现
      • 已接收并处理的请求信息。
      • 接收并处理请求数据:包含接收请求行、解析请求行、接收头部、解析头部、接收正文。
    • 接口
      • 返回解析完毕的请求信息。
      • 返回响应状态码。
      • 返回接收解析状态。
cpp 复制代码
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 Long
                    return false;
                }
                // 缓冲区不足一行,等待数据到来
                return true;
            }
            // 接收到数据了,但是头部过长
            if (line.size() > MAX_LINE){
                _recv_statu = RECV_HTTP_ERR;
                _resp_statu = 414; // URI Too Long
                return 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(用于进行缓冲区数据解析处理)等回调相关接口,以及请求的路由查找(包含静态资源请求查找和处理、功能性请求的查找和处理)、组织响应进行回复等功能接口。
cpp 复制代码
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或HEAD
        if(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 默认为静态资源请求
        //如果不是静态请求也不是动态请求设置状态405
        if(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 Allowed
        return;
    }
    //初始化上下文数据
    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.通过上下文对缓冲区数据进行解析,得到HttpRequest
            content->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;
};
相关推荐
峰顶听歌的鲸鱼3 小时前
27.Linux swap交换空间管理
linux·运维·服务器·笔记·学习方法
凭栏落花侧3 小时前
NETSTAT命令详解
运维·服务器·网络
Absinthe_苦艾酒3 小时前
golang基础语法(五)切片
开发语言·算法·golang
轩情吖3 小时前
Qt常用控件之QWidget(三)
开发语言·c++·qt·控件·cursor·qwidget·windowopacity
深思慎考3 小时前
LinuxC++项目开发日志——基于正倒排索引的boost搜索引擎(1——项目框架)
linux·c++·搜索引擎
Yilena3 小时前
跟进 JDK25:将虚拟线程安全引入生产的权衡与实战
java·开发语言·虚拟线程·结构化并发·jdk25
liu****3 小时前
负载均衡式的在线OJ项目编写(二)
c++·负载均衡·个人开发
_bong3 小时前
python的高阶函数
开发语言·python
MSTcheng.3 小时前
【C++】如何搞定 C++ 内存管理?
开发语言·c++·内存管理