C++ 仿 muduo 库实现高并发服务器

目录

项目定位

[学习 http 协议](#学习 http 协议)

[Reactor 模式](#Reactor 模式)

模块划分

前置知识

[timerfd 的使用](#timerfd 的使用)

基于时间轮思想的超时释放机制

正则表达式的使用

[通⽤类型 any 类型的实现](#通⽤类型 any 类型的实现)

[eventfd 的使用](#eventfd 的使用)

项目实现

简易的日志打印宏

[SERVER 模块](#SERVER 模块)

[Buffer 模块](#Buffer 模块)

[Socket 模块](#Socket 模块)

[Channel 模块](#Channel 模块)

[Poller 模块](#Poller 模块)

[EventLoop 模块](#EventLoop 模块)

[TimerQueue 模块](#TimerQueue 模块)

[Connection 模块](#Connection 模块)

[Accepter 模块](#Accepter 模块)

[LoopThread 模块](#LoopThread 模块)

[LoopThreadPool 模块](#LoopThreadPool 模块)

[TcpServer 模块](#TcpServer 模块)

[HTTP 协议模块](#HTTP 协议模块)

[Util 模块](#Util 模块)

[HttpRequest 模块](#HttpRequest 模块)

[HttpResponse 模块](#HttpResponse 模块)

[HttpContext 模块](#HttpContext 模块)

[HttpServer 模块](#HttpServer 模块)

整体测试

基础测试

长连接测试

连接超时测试

错误请求测试

业务处理超时测试

同时多条请求测试

大文件传输测试


项目定位

咱们要实现的是⼀个⾼并发服务器组件,因此当前的项⽬中并不包含实际的业务内容。代码量大概 2000 多行。

学习 http 协议

HTTP(超文本传输协议)是互联网上应用最广泛的应用层协议,是Web数据通信的基础。它定义了客户端(如浏览器)与服务器之间请求和响应的通信格式与规则。

【计算机网络】应用层协议(序列化与反序列化/HTTP/HTTPS)-CSDN博客

Reactor 模式

Reactor 模式,是指通过⼀个或多个输入同时传递给服务器进行请求处理时的事件驱动处理模式 。可以简单理解为谁给我发送了数据、谁触发了我的事件,我就处理谁。 服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫 Dispatcher 模式 。 简单理解就是使用I/O 多路复用统⼀监听事件,收到事件后分发给处理进程或线程,是编写高性能网络服务器的必备技术之一。

【Linux】网络高级 IO-CSDN博客

Reactor 模式的分类

单 Reactor 单线程:

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

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

  • 优点:充分利⽤CPU多核资源
  • 缺点:多线程间的数据共享访问控制较为复杂,单个Reactor承担所有事件的监听和响应,在单线程中 运⾏,⾼并发场景下容易成为性能瓶颈。

多 Reactor 多线程(主从线程模式):多I/O多路复用+业务线程池

这是本项目采用的 Reactor 模式

  • 在主Reactor中处理新连接请求事件,有新连接到来则分发到⼦Reactor中监控
  • 在⼦Reactor中进⾏客⼾端通信监控,有事件触发,则接收数据分发给Worker线程池
  • Worker线程池分配独⽴的线程进⾏具体的业务处理
  • ⼯作线程处理完毕后,将响应交给⼦Reactor线程进⾏数据响应
  • 优点:充分利⽤CPU多核资源,主从Reactor各司其职

当前实现中,因为并不确定组件使⽤者的使⽤意向,因此并不提供业务层⼯作线程池的实现,只实现主从Reactor,⽽Worker⼯作线程池,可由组件库的使⽤者的需要⾃⾏决定是否使⽤和实现。

模块划分

下面总体介绍本项目的所有模块,通过了解这些模块之间的关系,可以对本项目的具体实现有大致的认识。基于以上的理解,我们要实现的是**⼀个带有协议⽀持的基于 Reactor 模型的⾼性能服务器**,因此将整个项⽬的实现划分为两个⼤的模块:

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

SERVER模块

SERVER模块就是对所有的连接以及线程进⾏管理,让它们各司其职,在合适的时候做合适的事,最终完成⾼性能服务器组件的实现。 ⽽具体的管理也分为三个⽅⾯:

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

基于以上的管理思想,将这个模块进⾏细致的划分⼜可以划分为以下多个⼦模块:

  • Buffer 模块

作用:Buffer模块是⼀个缓冲区模块,⽤于实现通信中⽤⼾态的接收缓冲区和发送缓冲区功

存在的意义:1、如果接收到不完整的数据,要将不完整的数据缓存下来。2、如果发送被阻塞,要将待发送的数据缓存下来。

功能设计:1、向缓冲区添加数据的功能。2、从缓冲区读取数据的功能


  • Socket 模块

作用:对套接字操作进行封装

存在的意义:简化项目中对套接字的操作

功能设计:创建套接字、绑定端口号、设置为监听状态、发起/获取连接、接收/发送数据、关闭套接字。除了以上功能,为了更加简便,我们可以组合以上功能,形成一些集成的功能:创建一个监听套接字


  • Channel 模块

作用:对一个描述符进行监控事件管理(包括要监听哪些事件、事件触发后的回调函数)

存在的意义:对于描述符的监控事件在用户态更容易维护。

功能设计

  1. 对监控事件的管理:监控描述符是否可读/可写、设置/解除对描述符的可读/可写事件的监控
  2. 回调函数:定义不同事件的回调函数

  • Connection 模块

作用:Connection模块是对 Buffer 模块,Socket 模块,Channel 模块的⼀个整体封装,实现了对⼀个通信套接字的整体的管理,每⼀个进⾏数据通信的套接字(也就是accept获取到的新连接)都会使⽤ Connection 进⾏管理。在这个模块中,提供了由组件使用者传递的设置处理事件就绪的回调函数接口。

存在的意义:增加组件使用者对连接管理的灵活性和便捷性

功能设计

  1. 提供一系列设置回调函数的接口:连接建⽴完成的回调,有新数据成功接收后的回调,连接关闭后的回调,产生任何事件的回调
  2. 提供管理连接的功能:关闭连接、发送数据、协议切换、启动/关闭非活跃连接超时释放。

  • Acceptor 模块

作用:Acceptor 模块是对 Socket 模块,Channel 模块的⼀个整体封装,实现了对⼀个监听套接字的整体的管理。

存在的意义:获取新连接的描述符后,需要把该描述符封装到一个 connection 对象中,设置各种不同的回调函数

功能设计:对外提供新连接获取成功的回调函数的设置接口


  • TimerQueue 模块

作用:定时任务模块,让一个任务可以在指定的时间后执行

存在的意义:比如设置一个让非活跃的连接在 30 秒后被关闭的定时任务

功能设计:添加定时任务,刷新定时任务(连接活跃时应该重置它的非活跃时间),取消定时任务。


  • Poller 模块

作用:对 epoll 操作的封装

存在的意义:让对描述符进行事件监控的操作更加简单

功能设计:添加/修改/移除对描述符的事件监控


  • EventLoop 模块

作用:承担 Reactor 的角色。对 Poller 模块,TimerQueue 模块, Socket 模块的⼀个整体封装,进⾏所有描述符的事件监控。

存在的意义:EventLoop 模块为了保证整个服务器的线程安全问题,因此要求组件使⽤者对于 Connection 的所有操作⼀定要在其对应的 EventLoop 线程内完成,不能在其他线程中进⾏(⽐如组件使⽤者使⽤ Connection 发送数据,以及关闭连接这种操作)。

功能设计:内有任务队列,有将对连接的操作任务添加到任务队列、定时任务的添加/刷新/取消的功能


  • TcpServer 模块

作用:对上面所有的模块的封装。提供给组件使用者用于搭建高性能服务器的模块。

存在的意义:让组件使用者可以更加方便快捷的搭建一个高性能服务器

功能设计:对于监听/通信/超时/连接的管理、对事件监控的管理、事件回调函数的设置

HTTP 协议模块

HTTP协议模块⽤于对⾼并发服务器模块进⾏协议⽀持,基于提供的协议⽀持能够更⽅便的完成指定协 议服务器的搭建。⽽HTTP协议⽀持模块的实现,可以细分为以下⼏个模块。

  • Util 模块

这个模块是⼀个⼯具模块,主要提供 HTTP 协议模块所⽤到的⼀些比较零散的⼯具函数,⽐如 url 编解码,⽂件读写等等。

  • HttpRequest 模块

这个模块是 HTTP 请求数据模块,⽤于保存HTTP请求数据被解析后的各项请求元素信息。它存在的意义是让 HTTP 请求的分析更加简单。

  • HttpResponse 模块

这个模块是 HTTP 响应数据模块,⽤于业务处理后设置并保存 HTTP 响应数据的的各项元素信息,最终会被按照 HTTP 协议响应格式组织成为响应信息发送给客⼾端。

  • HttpContext 模块

这个模块是⼀个 HTTP 请求接收的上下⽂模块,主要是为了防止有可能出现接收的数据并不是一条完整的 HTTP 请求数据,也就是请求的处理需要在多次收到数据后才能处理完成,因此在每次处理的时候,就需要将处理进度记录起来,以便于下次从当前进度继续向下处理。具体的做法是设置接收状态,比如目前处于接收起始行/头部字段/正文阶段,或者接收完毕阶段,或者接收出错,要根据不同的出错原因(比如解析出错、访问的资源不对、无权限)设置相应的状态码

  • HttpServer 模块

这个模块是最终给组件使⽤者提供的 HTTP 服务器模块了,⽤于以简单的接⼝实现 HTTP 服务器的搭建。 HttpServer 模块内部包含有⼀个 TcpServer 对象,TcpServer 对象实现服务器的搭建 HttpServer 模块内部包含有两个提供给 TcpServer 对象的接⼝:连接建⽴成功设置上下⽂接⼝,数据处理接⼝。 HttpServer 模块内部包含有⼀个 hash-map 表存储请求与处理函数的映射表:组件使⽤者向 HttpServer 设置哪些请求应该使⽤哪些函数进⾏处理,等 TcpServer 收到对应的请求就会使⽤对应的函数进⾏处理。

前置知识

在正式编写项目代码之前,要了解以下知识:

timerfd 的使用

在当前的⾼并发服务器中,我们不得不考虑⼀个问题,那就是连接的超时关闭问题。我们需要避免⼀个连接⻓时间不通信,空耗资源的情况。 这时候我们就需要设置⼀个定时任务,定时的将超时过期的连接进⾏释放。

timerfd 是 Linux 系统提供的一个非常实用的定时器接口,它的核心思想是将定时器与文件描述符关联起来 。这使得定时器可以像操作文件一样,被 selectpollepoll 等 I/O 多路复用机制统一管理,非常适合集成到事件驱动型的程序中。使用 timerfd 要包含头文件:#include<sys/timerfd.h>

创建定时器

cpp 复制代码
int timerfd_create(int clockid, int flags);

clockid:时钟源选择

这个参数决定了定时器基于哪种时间来源进行计时。最常用的选项有:

时钟ID 说明 使用场景
CLOCK_REALTIME 系统实时时间,代表自1970年1月1日以来的秒数。 与墙上时间对齐的任务(如定时在每天凌晨3点执行)。注意:如果系统时间被手动调整(如用 date 命令),定时器可能会产生意外的跳跃或延迟。
CLOCK_MONOTONIC 单调递增时间,从系统启动后开始计时,不受系统时间跳变影响 绝大多数定时任务的首选。适用于周期性心跳检测、超时控制等。

flags:行为控制标志

这个参数用于控制文件描述符的属性,可以设置为 0 (阻塞)或通过按位或(|)组合以下选项:

标志 说明
TFD_NONBLOCK 将返回的文件描述符设置为非阻塞模式。 • 如果未设置,调用 read() 读取到期事件时,若无事件则会阻塞等待。 • 如果设置了,read() 在无事件时会立即返回 -1,并设置 errnoEAGAIN

返回值

  • 成功 :返回一个非负整数 文件描述符,用于后续的 timerfd_settimetimerfd_gettime 操作。

  • 失败 :返回 -1,并设置 errno 以指示具体错误。

启动定时器

cpp 复制代码
int timerfd_settime(int fd, int flags,
                    const struct itimerspec *new_value,
                    struct itimerspec *old_value);
  • fdtimerfd_create() 返回的定时器文件描述符
  • **flags:**时间解释标志,默认为 0 ,使用相对时间(系统启动后开始计时)
  • new_value:新的定时器设置(输入型参数)
cpp 复制代码
struct itimerspec {
    struct timespec it_interval;  /* 首次超时时间之后的超时时间 */
    struct timespec it_value;     /* 首次超时时间 */
};

struct timespec {
    time_t tv_sec;                /* 秒 */
    long   tv_nsec;               /* 纳秒 */
};

// 如果 it_value 的秒和纳秒均为 0,表示停止定时器(取消所有到期事件)。
// 如果 it_interval 的秒和纳秒均为 0,表示单次定时器(首次到期后不再重复)。
// 如果 it_interval 不为 0,表示周期性定时器(首次到期后,每隔 it_interval 时间重复触发)。
  • **old_value:**旧的定时器设置(输出型参数)

如果该指针不为 NULL,内核会将当前的定时器设置(在应用新的设置之前)保存到这个结构体中。这可以用于获取定时器还剩余多少时间到期,或者在修改定时器后恢复之前的设置。

如果不关心旧值,可以传入 NULL

  • 返回值

成功 :返回 0失败 :返回 -1,并设置 errno

启动定时器后,定时器会在每次超时时,⾃动给定时器文件描述符中写⼊ 8 字节的数据,表⽰在上⼀次读取数据到当前读取数据期间超时了多少次。

基于时间轮思想的超时释放机制

基于 timerfd,我们可以实现每隔 3s,检测⼀下哪些连接超时了,然后将超时的连接释放掉。但这样做存在⼀个很⼤的问题,每次超时都要将所有的连接遍历⼀遍,如果有上万个连接,效率⽆疑是较为低下的。

利用小根堆

这时候⼤家就会想到,我们可以针对所有的连接,根据每个连接最近⼀次通信的系统时间建⽴⼀个⼩根堆,这样只需要每次针对堆顶部分的连接逐个释放,直到没有超时的连接为⽌,这样也可以⼤⼤提⾼处理的效率。

利用时间轮

上述⽅法可以实现定时任务,但是这⾥给⼤家介绍另⼀种⽅案:时间轮。时间轮的思想来源于钟表,如果我们定了⼀个3点钟的闹铃,则当时针⾛到3的时候,就代表时间到了。 同样的道理,如果我们定义了⼀个数组,并且有⼀个指针 tick,指向数组起始位置, tick 每秒钟向后⾛动⼀步,⾛到哪⾥,则代表哪⾥的任务该被执⾏了,那么如果我们想要定⼀个 3s 后的任务,则只需要将任务添加到 tick+3 位置,则每秒⾛⼀步,三秒钟后 tick ⾛到对应位置,这时候执⾏对应位置的任务即可。 但是,同⼀时间可能会有⼤批量的定时任务,因此我们可以给数组对应位置下拉⼀个数组,这样就可以在同⼀个时刻上添加多个定时任务了,其实就是创建一个二维数组, tick 的步长就是这个二维数组的一维数组的大小。

当然,上述操作也有⼀些缺陷,⽐如我们如果要定义⼀个 60s 后的任务,则需要将二维数组的元素个数设置为 60 个一维数组才可以,如果设置⼀⼩时后的定时任务,则需要定义 3600 个元素的数组,这样⽆疑是⽐较⿇烦的。 因此,可以采⽤多层级的时间轮 ,有秒针轮,分针轮,时针轮 。因为当前我们的应⽤中,倒是不⽤设计的这么⿇烦,因为我们的定时任务通常设置在 30s 以内,所以简单的单层时间轮就够⽤了。

然而,我们还需要考虑一个问题:当前的设计是在时间到达后主动 执行定时任务来释放连接。那么,能否让定时任务在时间到达后自动 执行呢?这时我们想到了一个机制------类的析构函数

类的析构函数会在对象被释放时自动执行。如果我们把一个定时任务作为某个类的析构函数中的操作,那么这个定时任务就会在对象被释放时自动执行。但仅仅为了实现这个目的而专门设计一个额外的任务类,似乎有些得不偿失。

这里我们还需要考虑另一个问题:假设一个连接建立成功后,我们为其设置了一个 30 秒后超时销毁的任务。但在第 10 秒时,该连接进行了一次通信,那么它应该在第 40 秒时才被关闭。这意味着我们需要让第 30 秒的任务失效。如何实现这一点呢?

这时,我们可以利用智能指针 shared_ptr 的特性。shared_ptr 内部维护一个引用计数器 ,只有当计数变为 0 时,对象才会被真正释放。如果连接在第 10 秒进行了通信,我们可以继续向定时任务中添加一个 30 秒后(即第 40 秒)的任务类对象的 shared_ptr。此时,两个 shared_ptr 指向同一个对象,引用计数变为 2。当第 30 秒的定时任务被释放时,引用计数减 1 变为 1,并不为 0,因此不会执行实际的析构函数。这就相当于第 30 秒的任务失效了。只有在第 40 秒时,这个任务才会被真正释放。

具体实现:

timerwheel.cpp:

我们先实现定时任务类 TimerTask

cpp 复制代码
using Task_func = std::function<void()>;
class TimerTask
{
private:
    uint64_t _id;         // 定时任务的 id,要求唯一,以便找到该定时任务
    uint32_t _timeout;    // 定时器的超时时间
    Task_func _task_cb;    // 定时任务的回调函数
public:

    // 定时任务的 id 由外部统一分配
    TimerTask(uint64_t id, uint32_t timeout, Task_func task_cb):
    _id(id), _timeout(timeout), _task_cb(task_cb)
    {};

    ~TimerTask(){_task_cb();}
};

接下来再实现时间轮类 TimerWheel,在实现时间轮类的同时,反过来完善定时任务类

cpp 复制代码
using Task_func = std::function<void()>;
using Release_func = std::function<void()>;
class TimerTask
{
private:
    uint64_t _id;              // 定时任务的 id,要求唯一,以便找到该定时任务
    uint32_t _timeout;         // 定时器的超时时间
    Task_func _task_cb;        // 定时任务的回调函数
    Release_func _release_cb;  // 用于释放TimerWheel中_task_map保存的定时任务信息
    bool _canceled;            // 定时任务是否被取消
public:     

    // 定时任务的 id 由外部统一分配
    TimerTask(uint64_t id, uint32_t timeout, Task_func task_cb):
    _id(id), _timeout(timeout), _task_cb(task_cb),_canceled(false)
    {};
    void SetRelease(Release_func release_cb) {_release_cb = release_cb;}
    void Cancel() {_canceled = true;}
    uint32_t GetTimeout() {return _timeout;}
    ~TimerTask()
    {
        if(_canceled == false) _task_cb();
        _release_cb();
    }
};

using Task_ptr = std::shared_ptr<TimerTask>;
using Task_wptr = std::weak_ptr<TimerTask>;
class TimerWheel
{
private:
    
    int _tick;       // 秒针,走到哪里就执行哪里的定时任务
    int _capacity;   // 轮盘的容量,也就是最大的超时时间
    
    std::vector<std::vector<Task_ptr>> _wheel; // 时间轮
    
    // 每个定时任务id对应一个 weak_ptr指向定时任务,在刷新定时任务时,
    // 要根据weak_ptr给定时任务添加 shared_ptr
    std::unordered_map<uint64_t, Task_wptr> _task_map;

    void RemoveTask(uint64_t id) // 删除定时任务
    {
        auto it = _task_map.find(id);
        if(it != _task_map.end())
        {
            _task_map.erase(it);
        }
    }
public:
    TimerWheel():_tick(0),_capacity(60),_wheel(_capacity){}

    // 添加定时任务
    void AddTask(uint64_t id, uint32_t timeout, const Task_func& task_cb)
    {
        // 创建定时任务对象
        Task_ptr task = std::make_shared<TimerTask>(id, timeout, task_cb);

        // 设置定时任务释放函数
        task->SetRelease(std::bind(&TimerWheel::RemoveTask,this,id));

        // 将定时任务添加到_task_map和时间轮中
        _task_map[id] = Task_wptr(task);
        _wheel[(_tick + task->GetTimeout()) % _capacity].push_back(task);
    }

    // 刷新定时任务
    void RefreshTask(uint64_t id)                                         
    {
        auto it = _task_map.find(id);
        if(it != _task_map.end())
        {
            // lock:获取weak_ptr对应的shared_ptr
            Task_ptr task = it->second.lock();
            // 现在又有一个智能指针指向了该定时任务

            _wheel[(_tick + task->GetTimeout()) % _capacity].push_back(task);
        }
    }


    // 取消定时任务
    void CancelTask(uint64_t id)
    {
        auto it = _task_map.find(id);
        if(it != _task_map.end())
        {
            it->second.lock()->Cancel();
        }
    }

    
    // 这个函数应该每秒钟被执行一次
    void Run() 
    {
        _tick = (_tick + 1) % _capacity; // 秒针走一格
        _wheel[_tick].clear(); 
    }
};

测试:

cpp 复制代码
int main()
{
    int time = 0;
    TimerWheel tw;
    tw.AddTask(1, 5, [](){std::cout << "hello world" << std::endl;});
    std::cout << "已添加定时任务" << std::endl;

    // 测试刷新任务功能
    for(int i = 0; i < 3; i++)
    {
        std::cout << "---------------------------" << time++ << 's' << std::endl; 
        // 打印一次代表一秒过去了
        tw.RefreshTask(1); // 刷新定时任务
        tw.Run();          // 秒针走一格
        sleep(1);          // 秒针每秒走一格
    }

    // 测试运行定时任务功能
    for(int i = 0; i < 10; i++)
    {
        std::cout << "---------------------------" << time++ << 's' << std::endl; 
        // 打印一次代表一秒过去了
        tw.Run();
        sleep(1);
    }

    // 测试取消定时任务功能
    tw.AddTask(2, 5, [](){std::cout << "hello world" << std::endl;});
    for(int i = 0; i < 2; i++)
    {
        std::cout << "---------------------------" << time++ << 's' << std::endl; 
        tw.Run();
        sleep(1);
    }

    tw.CancelTask(2);
    std::cout << "已取消定时任务" << std::endl;
    for(int i = 0; i < 7; i++)
    {
        std::cout << "---------------------------" << time++ << 's' << std::endl; 
        tw.Run();
        sleep(1);
    }

    return 0;
}

输出结果:

bash 复制代码
[hxh@VM-16-12-centos High-concurrency server]$ g++ timerwheel.cpp -o test
[hxh@VM-16-12-centos High-concurrency server]$ ./test
已添加定时任务
---------------------------0s
---------------------------1s
---------------------------2s
---------------------------3s
---------------------------4s
---------------------------5s
---------------------------6s
hello world
---------------------------7s
---------------------------8s
---------------------------9s
---------------------------10s
---------------------------11s
---------------------------12s
---------------------------13s
---------------------------14s
已取消定时任务
---------------------------15s
---------------------------16s
---------------------------17s
---------------------------18s
---------------------------19s
---------------------------20s
---------------------------21s
[hxh@VM-16-12-centos High-concurrency server]$ 

timerfd 与 TimerWheel 的结合

上面的 TimerWheel 类并没有实现完成,还有一个问题:必须有一个定时器来"驱动 " TimerWheel 的 Run 函数,即每秒钟执行一次该函数,这时我们就要把上面介绍的 timerfd 与 TimerWheel 结合起来了。定时器"驱动" TimerWheel 的 Run 函数涉及到事件触发,这要等到部分实现完 EventLoop 模块后再来完成该结合。

正则表达式的使用

正则表达式(regularexpression)描述了⼀种字符串匹配的模式(pattern),可以⽤来检查⼀个串是否含有某种⼦串、将匹配的⼦串替换或者从某个串中取出符合某个条件的⼦串等。 正则表达式的使⽤,可以使得HTTP请求的解析更加简单(这⾥指的是程序员的⼯作变得的简单,这并不代表处理效率会变⾼,实际上效率上是低于直接的字符串处理的),使我们实现的HTTP组件库使⽤起来更加灵活。

核心组件(3 个主要类)

类型 说明
std::regex 表示正则表达式对象,需指定语法标志(如 ECMAScript、basic、extended、awk、grep)
std::smatch / std::cmatch 存储匹配结果(smatch 用于 std::stringcmatch 用于 C 风格字符串)
std::regex_iterator / std::regex_token_iterator 用于迭代搜索多个匹配结果

常用操作函数

函数 用途
std::regex_match 完全匹配:整个字符串是否与正则完全匹配
std::regex_search 部分匹配:在字符串中搜索第一个匹配的子串
std::regex_replace 替换匹配:用指定格式替换匹配到的内容

std::regex_match:

cpp 复制代码
// 1. 最简单形式:判断是否完全匹配
bool regex_match(const std::string& s, const std::regex& e);

// 2. 带匹配结果:可提取捕获组
bool regex_match(const std::string& s, std::smatch& m, const std::regex& e);

常用正则语法(ECMAScript 模式)

语法 含义
. 任意字符(除 \n、\r)
\d 数字 [0-9]
\w 单词字符 [A-Za-z0-9_]
\s 空白字符
* 匹配 0 次或多次,比如 .* 表示匹配任意字符(除 \n、\r)0 次或多次
+ 匹配 1 次或多次
? 匹配 0 次或 1 次
{n} 匹配恰好 n 次
{n,m} 匹配 n 到 m 次
() 捕获组(如果匹配,存储到 smatch 或 cmatch)
(?:) 非捕获组
[] 字符类
\b 单词边界
^ 行首
$ 行尾
` `

注意 :在普通字符串中,反斜杠需要转义,如 \\d;推荐使用 原始字符串字面量 R"()",可避免双反斜杠。

使用示例

cpp 复制代码
#include <iostream>
#include <regex>
#include <string>

int main()
{
    std::string str = "/number/1234"; // 原始字符串

    std::regex reg("/number/(\\d+)"); // 正则表达式
    // 匹配规则:匹配以"/number/" 开头的,后面跟着一个或多个数字的子串
    // 如果匹配,则存储匹配的数字子串

    std::smatch result;               // 存储匹配结果

    if (std::regex_match(str, result, reg))
    {
        std::cout << "匹配成功!" << std::endl;
        for(auto& s : result) std::cout << s << std::endl;
    }

    return 0;
}

输出:

bash 复制代码
[hxh@VM-16-12-centos High-concurrency server]$ g++ regex.cpp -o test
[hxh@VM-16-12-centos High-concurrency server]$ ./test
匹配成功!
/number/1234
1234
[hxh@VM-16-12-centos High-concurrency server]$ 

使用正则表达式解析 http 请求消息的起始行

cpp 复制代码
#include <iostream>
#include <regex>
#include <string>

int main()
{
    std::string str = "GET /helloworld/login?user=xiaoming&password=123456 HTTP/1.1\r\n";
    std::regex reg("(GET|POST|HEAD|DELETE|PUT) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");
    std::smatch results;

    // 匹配规则解释:
    // () : 表示如果匹配成功,则将匹配结果保存在results中
    // (GET|POST|HEAD|DELETE|PUT) : 解析HTTP请求方法,匹配括号内任意一个字符
    // ([^?]*) : 解析资源路径,[^?] 匹配非问号任意字符,* 匹配零个或多个
    // (?:\\?(.*))?  : 解析参数,(?:...) 匹配但不存储, \\? 匹配问号,. 匹配任意字符,* 匹配零个或多个,? 匹配零个或一个(可能没有参数),直到遇到空格
    // (HTTP/1\\.[01]) : 解析HTTP版本,\\. 匹配点号,\\[01] 匹配0或1
    // (?:\n|\r\n)? : 解析换行符,(?:...) 匹配但不存储,\n|\r\n 匹配换行符(有些客户端可能是 \n) ? 匹配零个或一个
    
    if (std::regex_match(str, results, reg))
    {
        std::cout << "匹配成功" << std::endl;
        for(auto& s : results) std::cout << s << std::endl;
    }

    return 0;
}

通⽤类型 any 类型的实现

  • 每⼀个Connection对连接进⾏管理,最终都不可避免需要涉及到应⽤层协议的处理,接收缓冲区可能接收到一个不完整 的数据,也可能接收到一个半或多个半 的数据,因此在 Connection 中需要设置协议处理的上下⽂来控制处理节奏 。但是应⽤层协议千千万,为了降低耦合度,这个协议接收解析上下⽂就不能有明显的协议倾向,它可以是任意协议的上下⽂信息,因此就需要⼀个通⽤的类型来保存各种不同的数据结构。
  • 在C语⾔中,通⽤类型可以使⽤ void* 来管理,但是在 C++ 中,boost 库和 C++17 给我们提供了通⽤类型 any,如果考虑增加代码的移植性,尽量减少第三⽅库的依赖,则可以使⽤ C++17 特性中的 any,或者⾃⼰来实现。⽽这个any通⽤类型类的实现其实并不复杂,以下是简单的模拟实现。

模拟实现 any 类

我们模拟实现的 any 类的设计思想:any 类内定义了一个父类、模板化的子类和一个父类指针。当 any 类要保存数据的时候,先用子类模板实例化后保存该数据,再用父类指针指向子类。当 any 类要保存其他数据的时候,先释放掉父类指针,再重复上面的步骤。

cpp 复制代码
#include <iostream>
#include <typeinfo>
#include <cassert>

class Any
{
private:
    class holder
    {
    public:
        virtual ~holder() {};
        virtual const std::type_info& type() = 0;
        virtual holder* clone() = 0;
    };

    template<typename T>
    class placeholder : public holder
    {
    public:
        placeholder(const T& val) : _val(val) {}
        virtual ~placeholder() {}
        virtual const std::type_info& type() { return typeid(T); }
        virtual holder* clone() { return new placeholder(_val); }
    public:
        T _val;
    };

    holder* _content;
public:
    public:
    Any(): _content(nullptr) {}

    template<typename T>
    Any(const T& val): _content(new placeholder<T>(val)) {}

    Any(const Any& other): _content(other._content ? other._content->clone() : nullptr) {}

    ~Any() { if(_content) delete _content;}

    // 移动构造函数
    Any(Any&& other) noexcept : _content(other._content) {other._content = nullptr;}

    // 移动赋值运算符
    Any& operator=(Any&& other) noexcept {
        if (this != &other) {
            delete _content;
            _content = other._content;
            other._content = nullptr;
        }
        return *this;
    }

    // 拷贝赋值运算符(正确的版本)
    Any& operator=(const Any& other) {
        if (this != &other) {
            delete _content;
            _content = (other._content ? other._content->clone() : nullptr);
        }
        return *this;
    }

    // 从任意类型赋值
    template<typename T>
    Any& operator=(const T& val) {
        Any(val).swap(*this);
        return *this;
    }

    Any& swap(Any& other) {
        std::swap(_content, other._content);
        return *this;
    }

    template<typename T>
    T* get() const {
        if(_content == nullptr) {
            ERR_LOG("_content == nullptr");
            return nullptr;
        }
        
        if(typeid(T) != _content->type()) {
            ERR_LOG("type mismatch: requested %s, actual %s", 
                    typeid(T).name(), _content->type().name());
            return nullptr;
        }
        
        return &((placeholder<T>*)_content)->_val;
    }
};

测试存储任意类型数据的功能:

cpp 复制代码
int main()
{
    Any a;
    
    a = 10;
    std::cout << *(a.get<int>()) << std::endl;

    a = std::string("abcde");
    std::cout << *(a.get<std::string>()) << std::endl;

    a = 123.4;
    std::cout << *(a.get<double>()) << std::endl;

    return 0;
}

输出:

bash 复制代码
[hxh@VM-16-12-centos High_concurrency_server]$ ./test
10
abcde
123.4
[hxh@VM-16-12-centos High_concurrency_server]$ 

测试是否存在内存泄漏问题:

bash 复制代码
int main()
{
    Any a;
    test t;
    a = t;

    return 0;
}

输出:

bash 复制代码
[hxh@VM-16-12-centos High_concurrency_server]$ ./test
构造
拷贝构造
析构
析构
[hxh@VM-16-12-centos High_concurrency_server]$ 

eventfd 的使用

eventfd 是 Linux 系统提供的一个轻量级且高效的线程或进程间事件通知机制。你可以把它想象成一个内核维护的、可通过文件描述符访问的64位无符号整数计数器。通过向这个描述符写入和读取数据,就能实现事件的发送与接收

创建:eventfd()

使用 eventfd() 函数创建一个事件文件描述符。

cpp 复制代码
#include <sys/eventfd.h>

int efd = eventfd(unsigned int initval, int flags);
  • initval:计数器的初始值。

  • flags:用于控制行为,常用标志如下:

标志 作用 说明
EFD_CLOEXEC exec时自动关闭 推荐始终加上,防止描述符被子进程继承。
EFD_NONBLOCK 非阻塞模式 若计数器为0,read会立即返回EAGAIN错误,而不是阻塞。
EFD_SEMAPHORE 信号量模式 启用后,每次 read 只会将计数器减 1,并返回 1;否则,read 会读取当前计数器值并将其清零。

读写:read() / write()

eventfd 的文件描述符支持标准的 readwrite 系统调用,但操作的数据大小必须为 8 字节sizeof(uint64_t)

通知(写入) :向计数器增加一个值。

cpp 复制代码
uint64_t val = 1;
ssize_t ret = write(efd, &val, sizeof(val)); // 将计数器增加 1

多次写入会使计数器累加

等待与接收(读取)

  • 默认模式read 会读取计数器的当前值,并将其重置为 0

  • 信号量模式 (EFD_SEMAPHORE)read 会读取值 1,并将计数器减 1,直到减为 0

cpp 复制代码
uint64_t val = 0;
ssize_t ret = read(efd, &val, sizeof(val)); // 获取事件

清理:close()

与普通文件描述符一样,使用完毕后调用 close(efd) 来释放资源

使用示例

eventfd.cpp:

cpp 复制代码
#include <stdio.h>
#include <sys/eventfd.h>
#include <unistd.h>

int main()
{
    int efd = eventfd(0,EFD_CLOEXEC | EFD_NONBLOCK);
    if(efd < 0)
    {
        perror("eventfd failed!!!");
        return -1;
    }

    uint64_t val = 1;
    write(efd,&val,sizeof(uint64_t));
    // 此时eventfd的读事件就绪
    write(efd,&val,sizeof(uint64_t));
    write(efd,&val,sizeof(uint64_t));

    uint64_t res = 0;
    read(efd,&res,sizeof(uint64_t));
    // 在默认模式下,read 会读取计数器的当前值,并将其重置为 0,此时读事件变为未就绪
    printf("%ld\n",res);
    return 0;
}

输出:

cpp 复制代码
[hxh@VM-16-12-centos example]$ gcc eventfd.cpp -o test
[hxh@VM-16-12-centos example]$ ./test
3
[hxh@VM-16-12-centos example]$ 

与本项目的关系

我们使用 eventfd 主要是在 EventLoop 模块中防止 epoll_wait 长时间阻塞而导致任务队列的任务迟迟没有被执行。我们在向任务队列添加一个任务后,向 eventfd 写入一个数据,触发 eventfd 的读事件,此时因为有事件就绪,epoll_wait 就会返回,从而执行任务队列的任务。

项目实现

接下来我们就要正式编写项目代码了,我们所有的项目代码都要写在同一个头文件:server.hpp里。在正式编写项目代码之前,为了方便我们之后的调试,先编写一个简易的日志打印宏。

简易的日志打印宏

cpp 复制代码
#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL DBG //全局日志等级,控制哪些日志要打印
#define LOG(level,format,...) do\
{\
    if(level < LOG_LEVEL) break;\
    time_t t = time(NULL);\
    struct tm *ltm = localtime(&t);\
    char tmp[32] = {0};\
    strftime(tmp,31,"%H:%M:%S",ltm);\
    fprintf(stdout,"[%s %s:%d] " format "\n",tmp,__FILE__,__LINE__,##__VA_ARGS__);\
}while(0)

#define INF_LOG(format,...) LOG(LNF,format,##__VA_ARGS__)
#define DBG_LOG(format,...) LOG(DBG,format,##__VA_ARGS__)
#define ERR_LOG(format,...) LOG(ERR,format,##__VA_ARGS__)

说明:下面编写的模块并不是独立的,在编写一个模块时,可能有些功能要等到实现下一个模块后再补充完善

SERVER 模块

Buffer 模块

  • 提供的功能:⽤于实现⽤⼾态缓冲区,提供数据存储,取出等功能。
  • 实现思想:采用 vector<char> 存储数据(使用 string 会被 \0 干扰),read 指针标记当前读取位置,write 指针标记当前写入位置。读取数据时,从 read 指针标记处开始读取,读取完毕后,先不整体移动数据,等到写入时,如果整体移动数据后腾出的空间够这次写入所需的空间,再整体移动数据,同时更新 read 和 write 指针;如果不够这次写入所需的空间,直接对 vector<char> 扩容。

Buffer 类的总体框架

复制代码
#include <vector>
#include <cstdint>
#include <cassert>
#include <string>
#include <cstring>

#define BUFFER_DEFAULT_SIZE 1024

// for debug
#include <iostream>

class Buffer
{
private:
    std::vector<char> _buffer;  // 缓冲区
    uint64_t _read_idx;         // 当前读取位置
    uint64_t _write_idx;        // 当前写入位置
public:
    Buffer():_buffer(BUFFER_DEFAULT_SIZE),_read_idx(0),_write_idx(0) {}
    
    // 获取缓冲区起始地址
    char* Begin();
    
    // 获取当前写入位置
    char* Read_Pos();

    // 获取当前读取位置
    char* Write_Pos();

    // 获取缓冲区尾部空闲空间大小
    uint64_t Tail_Idle_size();
    
    // 获取缓冲区头部空闲空间大小
    uint64_t Head_Idle_size();

    // 获取可读数据大小
    uint64_t Readable_Size();

    // 将读偏移向后移动
    void Move_Read_Idx(uint64_t len);
   
    // 将写偏移向后移动
    void Move_Write_Idx(uint64_t len);
    
    // 确保可写空间足够
    void Ensure_Write_Space(uint64_t len);
   
    // 写入数据
    void Write(void* data, uint64_t len);
   
    // 专门为写入string设计的接口
    void Write_String(const std::string& data);
   
    // 专门为写入其他Buffer对象数据的接口
    void Write_Buffer(Buffer& data);
   
    // 读取数据
    void Read(void* buf,uint64_t len);
   
    // 专门为作为string读取而设计的接口
    std::string Read_As_String(uint64_t len);
   
    // 寻找换行符,专门为http协议设计的接口
    char* Find_CRLF();
   
    // 获取一行数据,专门为http协议设计的接口
    std::string Get_Line();
  
    // 清空缓冲区
    void Clear();
};

具体实现

cpp 复制代码
#include <vector>
#include <cstdint>
#include <cassert>
#include <string>
#include <cstring>

#define BUFFER_DEFAULT_SIZE 1024

// for debug
#include <iostream>

class Buffer
{
private:
    std::vector<char> _buffer;  // 缓冲区
    uint64_t _read_idx;         // 当前读取位置
    uint64_t _write_idx;        // 当前写入位置
public:
    Buffer():_buffer(BUFFER_DEFAULT_SIZE),_read_idx(0),_write_idx(0) {}
    // 获取缓冲区起始地址
    char* Begin() { return &(*_buffer.begin()); }

    // 获取当前写入位置
    char* Read_Pos() { return Begin() + _read_idx; }

    // 获取当前读取位置
    char* Write_Pos() { return Begin() + _write_idx; }

    // 获取缓冲区尾部空闲空间大小
    uint64_t Tail_Idle_size() { return _buffer.size() - _write_idx; }

    // 获取缓冲区头部空闲空间大小
    uint64_t Head_Idle_size() { return _read_idx; }

    // 获取可读数据大小
    uint64_t Readable_Size() { return _write_idx - _read_idx; }

   // 将读偏移向后移动
    void Move_Read_Idx(uint64_t len)
    {
        if(len == 0) return;
        assert(len <= Readable_Size());
        _read_idx += len;
    }

    // 将写偏移向后移动
    void Move_Write_Idx(uint64_t len)
    {
        if(len == 0) return;
        assert(len <= Tail_Idle_size());
        _write_idx += len;
    }

    // 确保可写空间足够
    void Ensure_Write_Space(uint64_t len)
    {
        if(Tail_Idle_size() >= len) return;
        if(Head_Idle_size() + Tail_Idle_size() >= len) 
        {
            // 将数据总体移动
            // 先记录可读数据大小
            uint64_t Readable_size = Readable_Size();
            std::copy(Read_Pos(),Read_Pos() + Readable_size,Begin());
            // 更新读取和写入位置
            _read_idx = 0;
            _write_idx = Readable_size;
            return;
        }

        // 直接扩容,不总体移动
        _buffer.resize(_write_idx + len);
    }

    // 写入数据
    void Write(void* data, uint64_t len)
    {
        if(len == 0) return;
        
        // 确保有足够的空间
        Ensure_Write_Space(len);

        // 写入数据
        const char* d = (const char*)data;
        std::copy(d,d + len,Write_Pos());

        // 更新写入位置
        Move_Write_Idx(len);
    }

    // 专门为写入string设计的接口
    void Write_String(const std::string& data)
    {
        Write((void*)data.c_str(),data.size());
    }

    // 专门为写入其他Buffer对象数据的接口
    void Write_Buffer(Buffer& data)
    {
        Write(data.Read_Pos(),data.Readable_Size());
    }

    // 读取数据
    void Read(void* buf,uint64_t len)
    {
        if(len == 0) return;
        
        // 确保读取不会越界
        assert(len <= Readable_Size());

        // 读取数据
        std::copy(Read_Pos(),Read_Pos() + len,(char*)buf);

        // 更新读取位置
        Move_Read_Idx(len);
    }

    // 专门为作为string读取而设计的接口
    std::string Read_As_String(uint64_t len)
    {
        if(len == 0) return "";
        
        // 确保读取不会越界
        assert(len <= Readable_Size());

        // 创建data临时对象
        std::string data;
        data.resize(len);

        // 读取数据
        Read(&data[0],len);

        // 返回
        return data;
    }

    // 寻找换行符,专门为http协议设计的接口
    char* Find_CRLF()
    {
        void* res = memchr(Read_Pos(),'\n',Readable_Size());
        return (char*)res;
    }

    // 获取一行数据,专门为http协议设计的接口
    std::string Get_Line()
    {
        // 寻找换行符,其实就是寻找 \r\n
        char* pos = Find_CRLF();
        if(pos == nullptr) return "";
        
        // 获取一行数据,包括 \r\n
        return Read_As_String(pos - Read_Pos() + 1);
    }

    // 清空缓冲区
    void Clear()
    {
        _read_idx = 0;
        _write_idx = 0;
    }
};

测试:

复制代码
#include "server.hpp"
#include <iostream>

int main()
{
    Buffer buf;
    std::string str = "Hello!!!";

    std::cout << "以Write_String和Read_As_String测试buffer的读写功能"<< std::endl;

    std::cout << "开始向buf写入Hello!!!" << std::endl;
    buf.Write_String(str);
    
    std::cout << "写入结果:" << std::endl;
    std::cout << "read_pos: " << buf.Read_Pos() - buf.Begin() << std::endl;
    std::cout << "write_pos: " << buf.Write_Pos() - buf.Begin() <<std::endl;
    std::cout << "readable_size: " << buf.Readable_Size() << std::endl;

    std::cout << "开始读取" << std::endl;
    std:: string res;
    res = buf.Read_As_String(str.size());
    std::cout << "res 读取结果: " << res << std::endl;
    std::cout << "read_pos: " << buf.Read_Pos() - buf.Begin() << std::endl;
    std::cout << "write_pos: " << buf.Write_Pos() - buf.Begin() <<std::endl;
    std::cout << "readable_size: " << buf.Readable_Size() << std::endl;

    std::cout << std::endl;
    std::cout << std::endl;

    std::cout << "测试buffer中写入其他buffer数据的功能" << std::endl;
    Buffer buf_1;
    std::cout << "开始向buf_1写入Hello!!!" << std::endl;

    buf_1.Write_String(str);

    std::cout << "开始向buf写入buf_1" << std::endl;
    buf.Write_Buffer(buf_1);
    std::cout << "写入结果:" << std::endl;
    std::cout << "read_pos: " << buf.Read_Pos() - buf.Begin() << std::endl;
    std::cout << "write_pos: " << buf.Write_Pos() - buf.Begin() <<std::endl;
    std::cout << "readable_size: " << buf.Readable_Size() << std::endl;

    std::cout << std::endl;
    std::cout << std::endl;

    std::cout << "测试向buffer中写入大量数据,buffer的扩容功能" <<std::endl;
    Buffer buf_2;
    std::cout << "开始向buf_2写入300个Hello!!1\\n,即2700字节的数据" << std::endl;
    for(int i = 1; i <= 300; i++)
    {
        std::string str = "Hello!!" + std::to_string(i) + '\n'; 
        buf_2.Write_String(str);
    }
    std::cout << "写入完成,写入结果:" << std::endl;
    std::cout << "read_pos: " << buf_2.Read_Pos() - buf_2.Begin() << std::endl;
    std::cout << "write_pos: " << buf_2.Write_Pos() - buf_2.Begin() <<std::endl;
    std::cout << "readable_size: " << buf_2.Readable_Size() << std::endl;

    // std::cout << "开始读取:" << std::endl;
    // std::string tmp;
    // tmp = buf_2.Read_As_String(buf_2.Readable_Size());
    // std::cout << tmp << " " << tmp.size() <<std::endl;

    std::cout << "把读取注释掉,现在测试Get_line接口" << std::endl;
    while(buf_2.Readable_Size() > 0)
    {
        std::string tmp = buf_2.Get_Line();
        std::cout << tmp << std::endl;
    }

    return 0;
}

Socket 模块

该模块其实就是对套接字操作的简单封装,为了便于我们进行套接字操作。

总体框架

cpp 复制代码
#define MAX_LISTEN 1024

class Socket
{
private:
    int _sock_fd;
public:
    Socket():_sock_fd(-1){}
    Socket(int fd):_sock_fd(fd){}
    ~Socket() { Close(); }
    int FD() { return _sock_fd; }

    // 创建套接字
    bool Create();

    // 绑定ip和端口号
    bool Bind(const std::string& ip,uint16_t port);
    
    // 设置为监听状态
    bool Listen(int backlog = MAX_LISTEN);
   
    // 向服务器发起连接
    bool Connection(const std::string& ip,uint16_t port);
    
    // 获取新连接
    int Accept();    

    // 接收数据
    ssize_t Recv(void* buf,size_t len,int flag = 0);
   
    // 非阻塞接收数据
    ssize_t Non_Block_Recv(void* buf,size_t len);
    
    // 发送数据
    ssize_t Send(const void* buf,size_t len,int flag = 0);   

    // 非阻塞发送数据
    ssize_t Non_Block_Send(void* buf,size_t len);
    
    // 关闭套接字
    void Close();
   
    // 组合功能,直接创建一个服务端连接
    bool Create_Server(uint16_t port,const std::string& ip = "0.0.0.0",bool is_non_block = false);
   
    // 组合功能,直接创建一个客户端连接
    bool Create_client(uint16_t port,const std::string& ip = "0.0.0.0");
   
    // 设置套接字选项 - 开启地址端口重用
    void Reuse_Address();
  
    // 设置套接字为非阻塞
    void Set_Non_Block();
   
};

具体实现

cpp 复制代码
#define MAX_LISTEN 1024

class Socket
{
private:
    int _sock_fd;
public:
    Socket():_sock_fd(-1){}
    Socket(int fd):_sock_fd(fd){}
    ~Socket() { Close(); }
    int FD() { return _sock_fd; }

    // 创建套接字
    bool Create()
    {
        _sock_fd = socket(AF_INET,SOCK_STREAM,0);
        if(_sock_fd < 0)
        {
            ERR_LOG("socket create error!!!");
            return false;
        }
        return true;
    }

    // 绑定ip和端口号
    bool Bind(const std::string& ip,uint16_t port)
    {
        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(sockaddr_in);

        int ret = bind(_sock_fd,(const sockaddr*)&local,len);
        if(ret < 0)
        {
            ERR_LOG("socket bind error!!!");
            return false;
        }

        return true;
    }

    // 设置为监听状态
    bool Listen(int backlog = MAX_LISTEN)
    {
        int ret = listen(_sock_fd,MAX_LISTEN);
        if(ret < 0)
        {
            ERR_LOG("socket listen error!!!");
            return false;
        }

        return true;
    }

    // 向服务器发起连接
    bool Connection(const std::string& ip,uint16_t port)
    {
        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(sockaddr_in);

        int ret = connect(_sock_fd,(const sockaddr*)&local,len);
        if(ret < 0)
        {
            ERR_LOG("connect server error!!!");
            return false;
        }

        return true;
    }

    // 获取新连接
    int Accept()
    {
        int ret = accept(_sock_fd,nullptr,nullptr);
        if(ret < 0)
        {
            ERR_LOG("accept error!!!");
            return -1;
        }
        
        return ret;
    }

    // 接收数据
    ssize_t Recv(void* buf,size_t len,int flag = 0)
    {
        int n = recv(_sock_fd,buf,n,flag);
        if(n <= 0) // 没有数据、连接断开或出错
        {
            if(errno == EAGAIN || errno == EINTR)
            {
                // 没有数据或被信号中断,不算出错
                return 0;
            }
            
            return -1; // 出错
        }

        return n; // 实际接收的数据长度
    }

    // 非阻塞接收数据
    ssize_t Non_Block_Recv(void* buf,size_t len)
    {
        return Recv(buf,len,MSG_DONTWAIT);
    }

    // 发送数据
    ssize_t Send(const void* buf,size_t len,int flag = 0)
    {
        if(len == 0) return 0;
        
        int n = send(_sock_fd,buf,len,flag);
        if(n < 0)
        {
            if(errno == EAGAIN || errno == EINTR)
            {
                // 发送缓冲区已满或被信号中断,不算出错
                return 0;
            }

            ERR_LOG("send error!!!");
            return -1;
        }

        return n; // 实际发送的数据的长度
    }

    // 非阻塞发送数据
    ssize_t Non_Block_Send(void* buf,size_t len)
    {
        return Send(buf,len,MSG_DONTWAIT);
    }

    // 关闭套接字
    void Close()
    {
        if(_sock_fd != -1) 
        {
            close(_sock_fd);
            _sock_fd = -1;
        }
    }

    // 组合功能,直接创建一个服务端连接
    bool Create_Server(uint16_t port,const std::string& ip = "0.0.0.0",bool is_non_block = false)
    {
        if(Create() == false) return false;
        if(is_non_block) Set_Non_Block();
        if(Bind(ip,port) == false) return false;
        if(Listen() == false) return false;
        Reuse_Address();

        return true; 
    }

    // 组合功能,直接创建一个客户端连接
    bool Create_client(uint16_t port,const std::string& ip = "0.0.0.0")
    {
        if(Create() == false) return false;
        if(Connection(ip,port) == false) return false;
        return true;
    }

    // 设置套接字选项 - 开启地址端口重用
    void Reuse_Address()
    {
        int val = 1;
        setsockopt(_sock_fd,SOL_SOCKET,SO_REUSEADDR,(void*)&val,sizeof(val));
        val = 1;
        setsockopt(_sock_fd,SOL_SOCKET,SO_REUSEPORT,(void*)&val,sizeof(val));
    }

    // 设置套接字为非阻塞
    void Set_Non_Block()
    {
        int flag = fcntl(_sock_fd,F_GETFL,0);
        fcntl(_sock_fd,F_SETFL,flag | O_NONBLOCK); 
    }
};

测试:

测试服务端收发数据功能:test_ser.cc:

cpp 复制代码
int main()
{
    Socket lis_sock;
    lis_sock.Create_Server(8080);

    while(1)
    {
        int newfd = lis_sock.Accept();
        if(newfd < 0) continue;

        Socket cli_sock(newfd);
        char buf[1024];
        int n = cli_sock.Recv(buf,1023);
        if(n < 0)
        {
            cli_sock.Close();
            continue;
        }

        cli_sock.Send(buf,n);
        cli_sock.Close();
    }

    lis_sock.Close();
    return 0;
}

测试客户端收发数据的功能:test_cli.cc:

cpp 复制代码
int main()
{
    Socket cli_sock;
    cli_sock.Create_client(8080,"127.0.0.1");
    std::string str = "Hello!!!";
    cli_sock.Send(str.c_str(),str.size());
    char buf[1024];
    int n = cli_sock.Recv(buf,1023);
    buf[n] = '\0';
    DBG_LOG("%s",buf);
    return 0;
}

Channel 模块

该模块对描述符的事件进行管理,包括开启/关闭描述符的读/写/错误/任意事件的监控、把事件监控从 epoll 的红黑树移除、管理所有事件的回调函数等等。

注意:下面的 Channel 模块还没有完善,只是一个半成品,等一下在实现完 Poller 类之后,要与Poller 类进行整合,到时候会新增成员。要等到实现完 EventLoop 模块后才能彻底完善。

cpp 复制代码
class Channel
{
private:
    int _fd;  // 对_fd描述符做事件监控
    Poller* _poller;

    uint32_t _events;   // 当前需要监控的事件
    uint32_t _revents;  // 当前连接触发的事件
    using EventCallBack = std::function<void()>;  // 声明回调函数类型

    // 回调函数
    EventCallBack _read_callback;    // 可读事件被触发的回调函数
    EventCallBack _write_callback;   // 可写事件被触发的回调函数
    EventCallBack _error_callback;   // 错误事件被触发的回调函数
    EventCallBack _close_callback;   // 断连事件被触发的回调函数
    EventCallBack _event_callback;   // 任意事件被触发的回调函数

public:
    Channel(Poller* poller,int fd):_fd(fd),_events(0),_revents(0),_poller(poller){}

    int FD() { return _fd; }

    // 获取当前需要监控的事件
    uint32_t Events() { return _events; }

    // 设置当前连接触发的事件,该函数由Poller调用
    void Set_Revents(uint32_t revents) { _revents = revents; }

    // 设置回调函数
    void Set_Read_Callback(const EventCallBack& cb)     { _read_callback = cb; }
    void Set_Write_Callback(const EventCallBack& cb)    { _write_callback = cb; }
    void Set_Error_Callback(const EventCallBack& cb)    { _error_callback = cb; }
    void Set_Close_Callback(const EventCallBack& cb)    { _close_callback = cb; }
    void Set_Event_Callback(const EventCallBack& cb)    { _event_callback = cb; }

    // 管理事件
    // 当前是否监控了可读事件
    bool Is_Read_Monitored() { return _events & EPOLLIN; }  

    // 当前是否监控了可写事件       
    bool Is_Write_Monitored() { return _events & EPOLLOUT; }

    // 在 Poller 中更新当前 channel 要监控的事件
    void Update(); // 只需声明,在 Poller 类之后实现

    // 在 Poller 中移除当前 channel 监控的事件
    void Remove(); // 只需声明,在 Poller 类之后实现

    // 启动读事件监控,自动调用 poller 的Update将channel添加到poller中(首次)
    // 或者更新(往后),下面的事件监控管理监控同理
    void Enable_Read() { _events |= EPOLLIN; Update(); }

    // 启动写事件监控      
    void Enable_Write() { _events |= EPOLLOUT; Update(); } 

    // 关闭读事件监控    
    void Disable_Read() { _events &= ~EPOLLIN; Update(); }

    // 关闭写事件监控  
    void Disable_Write() { _events &= ~EPOLLOUT; Update(); }

    // 关闭所有事件监控     
    void Disable_All() { _events = 0; Update(); }

    // 触发事件,调用事件处理函数        
    void Handle_Event()
    {
        if((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI))
        {
            if(_event_callback) _event_callback();
            if(_read_callback) _read_callback();
        }
            
        // 有可能会释放连接的操作事件,一次只处理一个
        if(_revents & EPOLLOUT)
        {
            if(_event_callback) _event_callback();
            if(_write_callback) _write_callback();
        }
        else if(_revents & EPOLLERR)
            {
                // 一旦出错,就会释放连接,此时不能调用_event_callback
                // 所以_event_callback要在_error_callback之前调用
                if(_event_callback) _event_callback();
                if(_error_callback) _error_callback();
            }
            
        else if(_revents & EPOLLHUP)
            {
                // 与上面同理,_event_callback要在_close_callback之前调用
                if(_event_callback) _event_callback();
                if(_close_callback) _close_callback();
            }
            
    }
};

Poller 模块

  • 该模块是对 epoll 操作的封装,与 Socket 模块是对套接字操作的封装相同,该模块是为了让 epoll 的操作更加简便。
  • 该模块要实现的功能:添加/修改描述符的事件监控(添加/修改用同一个接口,有则修改,没有则添加)、移除描述符的事件监控、开始监控获取就绪的描述符。
  • 封装思想:有一个 epoll 的操作句柄(即 epoll 的 fd)、有一个类型为 epoll_event 的结构体数组、有一个 unordered_map 用来保存 fd 对应的 Channel 对象,当 fd 就绪,对外界返回对应的 Channel 对象。
  • 与 Channel 模块的整合:该模块要与 Channel 模块整合

基本框架

cpp 复制代码
#define MAX_EPOLL_EVENT 1024
class Poller
{
private:
    int _epoll_fd;
    struct epoll_event _evs[MAX_EPOLL_EVENT];
    std::unordered_map<int,Channel> _Channels;
private:
    // 下面是直接对 epoll 进行操作的接口

    // 添加/修改描述符的事件监控
    void Update(Channel* channel,int op);
    
    bool Is_Exist(Channel* channel);
   
public:
    Poller()  

    // 添加/修改描述符的事件监控
    void Updata_Event(Channel* channel);

    // 移除描述符的事件监控
    void Remove_Event(Channel* channel);
    
    // 开始监控获取就绪的描述符
    // active: 输出型参数,给外界返回就绪的fd对应的channel
    void Run(std::vector<Channel*>* active);
   
};

与 Channel 模块的整合

在 Channel 模块,每个 Channel 对象都要包含一个 Poller 指针,Channel 对象更新时,要依赖该指针来设置 Channel 在 Poller 的更新。

cpp 复制代码
class Poller; 
// Channel 模块与 Poller 模块的整合
// 虽然提前声明了 Poller 类,但是编译器现在并不知道 Poller 类中有哪些成员
// 下面 channel 的成员函数Update:void Update() { _poller->Updata_Event(this); }
// 编译器现在并不认识Updata_Event,所以下面 channel 的成员函数 Update 要在
// 实现 Poller 类之后定义
// 在类中只需声明,Remove 同理。

class Channel // Channel 新增的成员
{
private:
     Poller* _poller;
public:
     // 在 Poller 中更新当前 channel 要监控的事件
    void Update(); // 只需声明,在 Poller 类之后实现

    // 在 Poller 中移除当前 channel 监控的事件
    void Remove(); // 只需声明,在 Poller 类之后实现
    // ...

}

class Poller
{
    // ...
}

// 完善 Channel 类的实现,整合 Channel 类与 Poller 类
void Channel::Update() { _poller->Updata_Event(this); }
void Channel::Remove() { _poller->Remove_Event(this); }

Poller 类完整实现

cpp 复制代码
#define MAX_EPOLL_EVENT 1024
class Poller
{
private:
    int _epoll_fd;
    struct epoll_event _evs[MAX_EPOLL_EVENT];
    std::unordered_map<int,Channel*> _Channels;
private:
    // 下面是直接对 epoll 进行操作的接口

    // 添加/修改描述符的事件监控
    void Update(Channel* channel,int op)
    {
        int fd = channel->FD();
        struct epoll_event events;
        events.data.fd = fd;
        events.events = channel->Events();

        int ret = epoll_ctl(_epoll_fd,op,fd,&events);
        if(ret < 0)
        {
            ERR_LOG("epoll_ctl failed!!!,error string : %s",strerror(errno));
            abort();
        }
    }
    bool Is_Exist(Channel* channel)
    {
        auto it = _Channels.find(channel->FD());
        if(it == _Channels.end()) return false;

        return true;
    }
public:
    Poller():_epoll_fd(epoll_create(1))
    {
        if(_epoll_fd < 0)
        {
            ERR_LOG("epoll_create failed");
            abort();
        }
    }

    // 添加/修改描述符的事件监控
    void Updata_Event(Channel* channel)
    {
        // 先判断是否存在
        if(Is_Exist(channel))
        {
            // 存在则修改
            Update(channel,EPOLL_CTL_MOD);
            return;
        }

        // 不存在则添加
        _Channels.insert(std::make_pair(channel->FD(),channel));
        Update(channel,EPOLL_CTL_ADD);
    }

    // 移除描述符的事件监控
    void Remove_Event(Channel* channel)
    {
        if(Is_Exist(channel)) _Channels.erase(channel->FD());
        Update(channel,EPOLL_CTL_DEL);
    }

    // 开始监控获取就绪的描述符
    // active: 输出型参数,给外界返回就绪的fd对应的channel
    void Run(std::vector<Channel*>* active)
    {
        int nfds = epoll_wait(_epoll_fd,_evs,MAX_EPOLL_EVENT,-1);
        if(nfds < 0)
        {
            // epoll_wait 被信号打断
            if(errno == EINTR) return;

            // 真的出错了
            ERR_LOG("epoll_wait error,error str:%s",strerror(errno));
            abort();
        }

        // 向 active 添加就绪的fd对应的channel
        for(int i = 0; i < nfds; i++)
        {
            auto it = _Channels.find(_evs[i].data.fd);
            assert(it != _Channels.end());

            // 设置channel中就绪的事件
            it->second->Set_Revents(_evs[i].events);
            active->push_back(it->second);
        }
    }
};

Poller 模块和 Channel 模块的联合调试

服务器测试代码:test_ser.cc:

cpp 复制代码
#include "../source/server.hpp"

void Close_Handler(Channel* channel)
{
    std::cout << "close fd : " << channel->FD() << std::endl; 
    channel->Remove();
    delete channel;
}

void Error_Handler(Channel* channel)
{
    return Close_Handler(channel);
}

void Sender(Channel* channel)
{
    int fd = channel->FD();
    const char* str = "今天天气不错!!!";
    int n = send(fd,str,strlen(str),0);
    if(n < 0) return Error_Handler(channel);

    std::cout << "n: " << n << std::endl;
    channel->Disable_Write();
}

void Reader(Channel* channel)
{
    int fd = channel->FD();
    char buf[1024] = {0};
    int n = recv(fd,buf,1023,0);
    if(n < 0) return Error_Handler(channel);
    buf[n] = '\0';
    std::cout << "client say: " << buf << std::endl;
    channel->Enable_Write();
}

void Event_Handler()
{
    std::cout << "有新事件发生!!!"  << std::endl;
}

void Acceptor(Poller* poller,Socket* lis_sock)
{
    // 获取新连接
    int newfd = lis_sock->Accept();
    if(newfd < 0) return;

    // 为新连接创建 channel 对象
    Channel* channel = new Channel(poller,newfd);

    // 并且设置好回调函数
    channel->Set_Read_Callback(std::bind(Reader,channel));
    channel->Set_Write_Callback(std::bind(Sender,channel));
    channel->Set_Error_Callback(std::bind(Error_Handler,channel));
    channel->Set_Close_Callback(std::bind(Close_Handler,channel));
    channel->Set_Event_Callback(std::bind(Event_Handler));

    // 开启新连接的读事件监控->等待客户发送数据
    channel->Enable_Read();
}

int main()
{
    Poller poller;
    Socket lis_sock;
    lis_sock.Create_Server(8080);
    Channel channel(&poller,lis_sock.FD());

    // 为监听套接字设置回调函数->有新连接到来如何处理
    channel.Set_Read_Callback(std::bind(Acceptor,&poller,&lis_sock));

    // 开启监听套接字的读事件监控->开始监听 
    channel.Enable_Read();

    while(1)
    {
        std::vector<Channel*> actives;

        // 获取活跃连接
        poller.Run(&actives);

        // 处理事件
        for(auto i : actives)
        {
            // 自动判断哪些事件就绪,自动调用对应回调
            i->Handle_Event();
        }
    }

    lis_sock.Close();
    return 0;
}

客户端测试代码:test_cli.cc:

cpp 复制代码
#include "../source/server.hpp"

int main()
{
    Socket cli_sock;
    cli_sock.Create_client(8080,"127.0.0.1");

    while(1)
    {
        std::string str = "Hello!!!";
        cli_sock.Send(str.c_str(),str.size());
        char buf[1024] = {0};
        int n = cli_sock.Recv(buf,1023);
        std::cout << "n: " << n << std::endl;
        buf[n] = '\0';
        DBG_LOG("%s",buf);
        sleep(1);
    }
    
    return 0;
}

EventLoop 模块

  • EventLoop 模块是进行事件监控,以及事件处理的模块,这个模块与线程是一一对应关联的。
  • 监控了一个连接,而这个连接一旦就绪,就要进行事件处理。但是如果这个描述符在多个线程中都触发了事件,就会存在线程安全问题。因此我们需要将一个连接的事件监控,以及连接事件处理,以及其他操作都放在同一个线程中进行,即 EventLoop 对应的线程中进行
  • 如何保证一个连接的所有操作都在 eventloop 对应的线程中?解决方案:给 eventloop 模块中,添加一个任务队列,并且对连接的所有操作,都进行一次封装,对连接的操作并不直接执行,而是当作任务添加到任务队列中

EventLoop 处理流程:

  1. 在线程中对描述符进行事件监控

  2. 有描述符就绪则对描述符进行事件处理:判断将要执行的任务是否处于当前线程中,如果是则执行,如果不是则添加到任务队列中。

  3. 所有的就绪事件处理完了,这时候再去将任务队列中的所有任务一一执行

这样能够保证对于连接的所有操作,都是在一个线程中进行的,不涉及线程安全问题。但是对于任务队列的操作有线程安全的问题,我们只需要对任务队列进行加锁,而不必给成千上万的连接都各自加一把锁。

注意:下面的 EventLoop 模块没有包含 TimerWheel 模块,等到实现 TimerWheel 模块之后,再将这两个模块融合到一起并调试。

总体框架

cpp 复制代码
class EventLoop
{
private:
    using Func_ptr = std::function<void()>;     // 声明任务队列的任务指针类型
    std::thread::id _thread_id;                 // 当前EventLoop所对应的线程的线程id
    int _eventfd;                               // eventfd唤醒IO事件监控有可能导致的阻塞
    std::unique_ptr<Channel> _eventfd_channel;  // 管理 eventfd 的 channel
    Poller _poller;                             // 进行所有描述符的事件监控
    std::vector<Func_ptr> _tasks;               // 任务队列
    std::mutex _mutex;                          // 保证任务队列的线程安全
public:
    // 执行任务队列的所有任务
    void Run_All_Task();
    
    // 创建 eventfd
    static int Create_Eventfd();
    

    // 读取 eventfd 中的数据->数据无意义,仅仅为了处理eventfd的读事件就绪
    void Read_Eventfd();
    

    // 向 eventfd 中写入数据->触发读事件->唤醒epoll_wait的阻塞
    void Weakup_EventLoop();
   
public:
    // 注意初始化列表初始化的顺序
    // _eventfd 一定要在 _eventfd_channel 之前初始化
    EventLoop();

    // 启动 EventLoop
    void Start();
    

    // 判断当前线程是否是 EventLoop 对应的线程
    bool Is_Cur_Loop_Tid();
    
    // 判断 cb 指向的任务是否处于当前线程中,如果是则执行,不是则添加到任务队列
    void Run_In_Loop(const Func_ptr& cb);
   

    // 将任务添加到任务队列中
    void Add_Task(const Func_ptr& cb);

    // 添加/修改描述符的事件监控
    void Update_Event(Channel* channel); 
    // 移除描述符的事件监控
    void Remove_Event(Channel* channel);
};

具体实现:

cpp 复制代码
class EventLoop
{
private:
    using Func_ptr = std::function<void()>;     // 声明任务队列的任务指针类型
    std::thread::id _thread_id;                 // 当前EventLoop所对应的线程的线程id
    int _eventfd;                               // eventfd唤醒IO事件监控有可能导致的阻塞
    std::unique_ptr<Channel> _eventfd_channel;  // 管理 eventfd 的 channel
    Poller _poller;                             // 进行所有描述符的事件监控
    std::vector<Func_ptr> _tasks;               // 任务队列
    std::mutex _mutex;                          // 保证任务队列的线程安全
public:
    // 执行任务队列的所有任务
    void Run_All_Task()
    {
        std::vector<Func_ptr> _tasks_to_do;
        {
            std::unique_lock<std::mutex> _lock(_mutex);
            // std::unique_lock 是 C++11 引入的互斥量包装器
            // 以下是临界区代码

            // 直接把 _tasks 的内容交换到 _tasks_to_do
            _tasks.swap(_tasks_to_do);

            // std::unique_lock:离开作用域 _mutex 自动解锁
        }

        // 将 _tasks_to_do 的任务依次执行
        for(auto& to_do : _tasks_to_do) to_do();
    }

    // 创建 eventfd
    static int Create_Eventfd()
    {
        int efd = eventfd(0,EFD_CLOEXEC | EFD_NONBLOCK);
        if(efd < 0)
        {
            ERR_LOG("create eventfd failed!!!");
            abort();
        }

        return efd;
    }

    // 读取 eventfd 中的数据->数据无意义,仅仅为了处理eventfd的读事件就绪
    void Read_Eventfd()
    {
        uint64_t res = 0;
        int ret = read(_eventfd,&res,sizeof(uint64_t));
        if(ret < 0)
        {
            if(errno == EINTR || errno == EAGAIN) return;

            // 真的出错了
            ERR_LOG("read eventfd failed!!!");
            abort();
        }
    }

    // 向 eventfd 中写入数据->触发读事件->唤醒epoll_wait的阻塞
    void Weakup_EventLoop()
    {
        uint64_t val = 1;
        int ret = write(_eventfd,&val,sizeof(uint64_t));
        if(ret < 0)
        {
            if(errno == EINTR) return;

            // 真的出错了
            ERR_LOG("write eventfd failed!!!");
            abort();
        }
    }
public:
    // 注意初始化列表初始化的顺序
    // _eventfd 一定要在 _eventfd_channel 之前初始化
    EventLoop():_thread_id(std::this_thread::get_id()),
                _eventfd(Create_Eventfd()),
                _eventfd_channel(new Channel(&_poller,_eventfd)) // 注意内容泄漏问题
    {
        // 设置管理 eventfd 的 channel
        // 设置 eventfd 读事件回调
        _eventfd_channel->Set_Read_Callback(std::bind(&EventLoop::Read_Eventfd,this));

        // 启动 eventfd 的读事件监控
        _eventfd_channel->Enable_Read();
    }

    // 启动 EventLoop
    void Start()
    {
        while(true)
        {
            // 第一步:事件监控,获取活跃连接
            std::vector<Channel*> acctives;
            _poller.Run(&acctives);

            // 第二步:事件处理
            for(auto& channel : acctives)
            {
                // 具体触发了什么事件,EventLoop 模块不关心
                // 在 Handle_Event 内部判断
                channel->Handle_Event();
            }

            // 第三步:执行任务
            Run_All_Task();
        }
    }

    // 判断当前线程是否是 EventLoop 对应的线程
    bool Is_Cur_Loop_Tid()
    {
        return (_thread_id == std::this_thread::get_id());
    }

    // 判断当前线程是否是 EventLoop 对应的线程,不是的话终止程序
    void Assert_In_Loop()
    {
        assert(_thread_id == std::this_thread::get_id());
    }

    // 判断 cb 指向的任务是否处于当前线程中,如果是则执行,不是则添加到任务队列
    void Run_In_Loop(const Func_ptr& cb)
    {
        // cb 指向的任务处于当前线程中,直接执行,不会有线程安全问题
        if(Is_Cur_Loop_Tid) return cb();

        // 否则添加到任务队列
        Add_Task(cb);
    }

    // 将任务添加到任务队列中
    void Add_Task(const Func_ptr& cb)
    {
        {
            std::unique_lock<std::mutex> _lock(_mutex);
            _tasks.push_back(cb);

            // 唤醒有可能因为没有事件就绪,而导致的epoll_wait阻塞
            // 执行 Weakup_EventLoop,向 eventfd 写入一个数据
            // 触发 eventfd 的读事件就绪,就解除了epoll_wait的阻塞状态
            // 这样做是为了防止epoll_wait阻塞而导致 _tasks 的任务迟迟没有被执行

            Weakup_EventLoop();
        }
    }

    // 添加/修改描述符的事件监控
    void Update_Event(Channel* channel) { return _poller.Updata_Event(channel); }

    // 移除描述符的事件监控
    void Remove_Event(Channel* channel) { return _poller.Remove_Event(channel); }
};

修改完善 Channel 类的实现

之前为了方便 Channel 类与 Poller 类的联合调试,Channel 类使用的 poller 指针来指向一个 poller 实例以更新 poller 里的对应 channel。其实在本项目的最终实现中,Channel 是使用 EventLoop 指针来更新 poller 里的对应 channel 的。

cpp 复制代码
class EventLoop;
// Channel 模块与 EventLoop 模块的整合
// 虽然提前声明了 EventLoop 类,但是编译器现在并不知道 >Update_Event 类中有哪些成员
// 下面 channel 的成员函数Update:void Update() { _loop->Update_Event(this); }
// 编译器现在并不知道EventLoop有Update_Event成员,所以下面 channel 的成员函数 Update 要在实现 EventLoop 类之后定义
// 在类中只需声明,Remove 同理。

class Channel
{
private:
     EventLoop* _loop;
public:
    // 在 EventLoop 中更新当前 channel 要监控的事件
    void Update(); // 只需声明,在 EventLoop 类之后实现

    // 在 EventLoop 中移除当前 channel 监控的事件
    void Remove(); // 只需声明,在 EventLoop 类之后实现
    // ...

}

class Poller
{
    // ...
}

class EventLoop
{
    // ...
}

// 完善 Channel 类的实现,整合 Channel 类与 EventLoop 类
void Channel::Update() { _loop->Updata_Event(this); }
void Channel::Remove() { _loop->Remove_Event(this); }

测试:

服务器测试代码:test_ser.cc:

cpp 复制代码
#include "../source/server.hpp"

void Close_Handler(Channel* channel)
{
    std::cout << "close fd : " << channel->FD() << std::endl; 
    channel->Remove();
    delete channel;
}

void Error_Handler(Channel* channel)
{
    return Close_Handler(channel);
}

void Sender(Channel* channel)
{
    int fd = channel->FD();
    const char* str = "今天天气不错!!!";
    int n = send(fd,str,strlen(str),0);
    if(n < 0) return Error_Handler(channel);

    std::cout << "n: " << n << std::endl;
    channel->Disable_Write();
}

void Reader(Channel* channel)
{
    int fd = channel->FD();
    char buf[1024] = {0};
    int n = recv(fd,buf,1023,0);
    if(n < 0) return Error_Handler(channel);
    buf[n] = '\0';
    std::cout << "client say: " << buf << std::endl;
    channel->Enable_Write();
}

void Event_Handler()
{
    std::cout << "有新事件发生!!!"  << std::endl;
}

void Acceptor(EventLoop* loop,Socket* lis_sock)
{
    // 获取新连接
    int newfd = lis_sock->Accept();
    if(newfd < 0) return;

    // 为新连接创建 channel 对象
    Channel* channel = new Channel(loop,newfd);

    // 并且设置好回调函数
    channel->Set_Read_Callback(std::bind(Reader,channel));
    channel->Set_Write_Callback(std::bind(Sender,channel));
    channel->Set_Error_Callback(std::bind(Error_Handler,channel));
    channel->Set_Close_Callback(std::bind(Close_Handler,channel));
    channel->Set_Event_Callback(std::bind(Event_Handler));

    // 开启新连接的读事件监控->等待客户发送数据
    channel->Enable_Read();
}

int main()
{
    EventLoop loop;
    Socket lis_sock;
    lis_sock.Create_Server(8080);
    Channel channel(&loop,lis_sock.FD());

    // 为监听套接字设置回调函数->有新连接到来如何处理
    channel.Set_Read_Callback(std::bind(Acceptor,&loop,&lis_sock));

    // 开启监听套接字的读事件监控->开始监听 
    channel.Enable_Read();

    while(1)
    {
        loop.Start();
    }

    lis_sock.Close();
    return 0;
}

客户端测试代码:test_cli.cc 与上面 Poller 模块的 test_cli.cc 相同

TimerQueue 模块

上面的 EventLoop 模块其实还没有实现完成,它的内部其实还要包含一个 TimerQueue 子模块,来管理服务器的定时任务。在前置知识部分,我们说明了 timerfd 的使用基于时间轮思想的超时释放机制 即定时任务类 TimerTask 和时间轮类 TimerWheel 的实现。我们说过,必须有一个定时器来"驱动" TimerWheel 的 Run 函数,即每秒钟执行一次该函数,即要把上面介绍的 timerfd 与 TimerWheel 结合起来。timerfd 要受到 EventLoop 的事件监控,timerfd 每秒钟触发一次可读事件,而 timerfd 的读事件处理就是执行 TimerWheel 的 Run 函数。

将 TimerWheel、timerfd、EventLoop 融合调整后的 TimerWheel 类:( TimerTask没有改变**)**

cpp 复制代码
class TimerWheel
{
private:
    
    int _tick;       // 秒针,走到哪里就执行哪里的定时任务
    int _capacity;   // 轮盘的容量,也就是最大的超时时间
    std::vector<std::vector<Task_ptr>> _wheel; // 时间轮
    
    // 每个定时任务 id 对应一个 weak_ptr 指向定时任务,在刷新定时任务时,
    // 要根据 weak_ptr 给定时任务添加 shared_ptr
    std::unordered_map<uint64_t, Task_wptr> _task_map;

    EventLoop* _loop;   // 监控定时器读事件的 EventLoop
    int _timer_fd;      // 定时器的fd

    // 管理 _timer_fd 的 channel,
    // _timer_channel 设置了 _timer_fd 的读回调->执行下面的 Ontime 函数
    std::unique_ptr<Channel> _timer_channel;

    void RemoveTask(uint64_t id) // 删除定时任务
    {
        auto it = _task_map.find(id);
        if(it != _task_map.end())
        {
            _task_map.erase(it);
        }
    }

    // 创建 timerfd
    static int Create_Timerfd()
    {
        int timerfd = timerfd_create(CLOCK_MONOTONIC,0);
        if(timerfd < 0)
        {
            ERR_LOG("create timerfd failed!!!");
            abort();
        }

        // 设置 itimerspec 结构体
        itimerspec itime;
        itime.it_value.tv_sec = 1;       // 首次超时时间 1s
        itime.it_value.tv_nsec = 0;      // 防止随机数
        itime.it_interval.tv_sec = 1;    // 之后的超时时间 1s
        itime.it_interval.tv_nsec = 0;   // 防止随机数

        int ret = timerfd_settime(timerfd,0,&itime,nullptr);
        if(ret < 0)
        {
            ERR_LOG("timerfd_settime failed!!!");
            abort();
        }

        return timerfd;
    }

    int Read_Timerfd()
    {
        uint64_t times;
        int ret = read(_timer_fd,&times,sizeof(uint64_t));
        if(ret < 0)
        {
            ERR_LOG("read timerfd failed!!!");
            abort();
        }

        // 有可能描述符的事件处理花费时间较长(大于1s),定时器描述符事件已经超时很多次了
        return times;
    }

    // 这个函数应该每秒钟被执行一次
    void Run()
    {
        _tick = (_tick + 1) % _capacity; // 秒针走一格
        _wheel[_tick].clear(); 
    }

    // 这个函数应该每秒钟被执行一次,但是由于要处理其他连接的请求,
    // 导致该函数可能几秒钟才被执行一次,所以要获取timerfd的超时次数(即过去了几秒钟)
    // 而下面的 Run 函数就该执行timerfd的超时次数次
    void On_time()
    {
        // 读取 timerfd,获取超时次数
        int times = Read_Timerfd();

        // 执行定时任务
        for(int i = 0; i < times; i++) Run();
    }

    void AddTask_In_Loop(uint64_t id, uint32_t timeout, const Task_func& task_cb) 
    {
        // 创建定时任务对象
        Task_ptr task = std::make_shared<TimerTask>(id, timeout, task_cb);

        // 设置定时任务释放函数
        task->SetRelease(std::bind(&TimerWheel::RemoveTask,this,id));

        // 将定时任务添加到 _task_map 和时间轮中
        _task_map[id] = Task_wptr(task);
        _wheel[(_tick + task->GetTimeout()) % _capacity].push_back(task);
    }

    void RefreshTask_In_Loop(uint64_t id)                                         
    {
        auto it = _task_map.find(id);
        if(it != _task_map.end())
        {
            Task_ptr task = it->second.lock(); // lock:获取 weak_ptr 对应的 shared_ptr
            _wheel[(_tick + task->GetTimeout()) % _capacity].push_back(task);
        }
    }

    void CancelTask_In_Loop(uint64_t id)
    {
        auto it = _task_map.find(id);
        if(it != _task_map.end())
        {
            it->second.lock()->Cancel();
        }
    }

public:
    TimerWheel(EventLoop* loop):_tick(0),_capacity(60),_wheel(_capacity),
                 _loop(loop),_timer_fd(Create_Timerfd()),_timer_channel(new Channel(_loop,_timer_fd))
    {
        // 设置 timerfd 的可读事件回调
        _timer_channel->Set_Read_Callback(std::bind(TimerWheel::On_time,this));

        // 开启 timerfd 的读事件监控
        _timer_channel->Enable_Read();
    }

    /*
    由于添加/刷新/取消定时任务涉及到对时间轮 _wheel 的增删查改,可能有线程安全问题
    解决方案:如果不想加锁降低效率,可以将添加/刷新/取消定时任务都放到 TimerWheel 所依赖的 EventLoop 线程执行
    */
    
    // 由于下面的接口使用了 EventLoop 的接口,而 EventLoop 是在 TimerWheel 之后实现的
    // 为了防止编译报错,下面的接口在 EventLoop 之后实现,现在只需要声明

    // 添加定时任务
    void AddTask(uint64_t id, uint32_t timeout, const Task_func& task_cb);

    // 刷新定时任务
    void RefreshTask(uint64_t id);
   
    // 取消定时任务
    void CancelTask(uint64_t id);
};

class EventLoop
{
    // ...
}

// 完善 TimerWheel 类的实现
// 添加定时任务
void TimerWheel::AddTask(uint64_t id, uint32_t timeout, const Task_func& task_cb)
{
    _loop->Run_In_Loop(std::bind(TimerWheel::AddTask_In_Loop,this,id, timeout, task_cb));
}

// 刷新定时任务
void TimerWheel::RefreshTask(uint64_t id)
{
    _loop->Run_In_Loop(std::bind(TimerWheel::RefreshTask_In_Loop,this,id));
}

// 取消定时任务
void TimerWheel::CancelTask(uint64_t id)
{
    _loop->Run_In_Loop(std::bind(TimerWheel::CancelTask_In_Loop,this,id));
}

在 EventLoop 模块中融合 TimerWheel 模块:在上面实现的 EventLoop 模块的基础之上,添加定时任务模块和相关接口。

cpp 复制代码
class EventLoop
{
private:
    TimerWheel _timer_wheel;                    // 定时任务模块
public:
    EventLoop():_thread_id(std::this_thread::get_id()),
                _eventfd(Create_Eventfd()),
                _eventfd_channel(new Channel(this,_eventfd)),
                _timer_wheel(this)
    {}

    // 定时任务模块相关接口

    // 添加定时任务
    void Add_Timer_Task(uint64_t id, uint32_t timeout, const Task_func& task_cb)
    {
        return _timer_wheel.AddTask(id,timeout,task_cb);
    }

    // 刷新定时任务
    void Refresh_Timer_Tsak(uint64_t id)
    {
        return _timer_wheel.RefreshTask(id);
    }

    // 取消定时任务
    void Cancel_Timer_Task(uint64_t id)
    {
        return _timer_wheel.CancelTask(id);
    }

    // 判断是否存在某个定时任务
    bool Is_Timer_Task_Exist(uint64_t id)
    {
        return _timer_wheel.Is_Task_Exist(id);
    }
};

EventLoop 和 TimerWheel 的联合调试:

服务器测试代码:test_ser.cc:

cpp 复制代码
#include "../source/server.hpp"

void Close_Handler(Channel* channel)
{
    std::cout << "close fd : " << channel->FD() << std::endl; 
    channel->Remove();
    delete channel;
}

void Error_Handler(Channel* channel)
{
    return Close_Handler(channel);
}

void Sender(Channel* channel)
{
    int fd = channel->FD();
    const char* str = "今天天气不错!!!";
    int n = send(fd,str,strlen(str),0);
    if(n < 0) return Error_Handler(channel);

    //std::cout << "n: " << n << std::endl;
    channel->Disable_Write();
}

void Reader(Channel* channel)
{
    int fd = channel->FD();
    char buf[1024] = {0};
    int n = recv(fd,buf,1023,0);
    if(n < 0) return Error_Handler(channel);
    buf[n] = '\0';
    std::cout << "client say: " << buf << std::endl;
    channel->Enable_Write();
}

void Event_Handler(EventLoop* loop,uint64_t timerid)
{
    loop->Refresh_Timer_Tsak(timerid);
    std::cout << "有新事件发生,超时时间已刷新"  << std::endl;
}

void Acceptor(EventLoop* loop,Socket* lis_sock)
{
    // 获取新连接
    int newfd = lis_sock->Accept();
    if(newfd < 0) return;

    // 为新连接创建 channel 对象
    Channel* channel = new Channel(loop,newfd);

    // 超时销毁任务的 id
    uint64_t timerid = rand() % 10000;

    // 并且设置好回调函数
    channel->Set_Read_Callback(std::bind(Reader,channel));
    channel->Set_Write_Callback(std::bind(Sender,channel));
    channel->Set_Error_Callback(std::bind(Error_Handler,channel));
    channel->Set_Close_Callback(std::bind(Close_Handler,channel));

    // 连接触发任意事件,都要刷新连接的超时时间
    channel->Set_Event_Callback(std::bind(Event_Handler,loop,timerid));

    // 非活跃连接的超时释放任务
    // 注意:开始超时释放任务,必须在启动读时间监控之前
    loop->Add_Timer_Task(timerid,10,std::bind(Close_Handler,channel));

    // 开启新连接的读事件监控->等待客户发送数据
    channel->Enable_Read();
}

int main()
{
    // 随机数种子,分配定时任务id
    srand(time(nullptr));

    EventLoop loop;
    Socket lis_sock;
    lis_sock.Create_Server(8080);
    Channel channel(&loop,lis_sock.FD());

    // 为监听套接字设置回调函数->有新连接到来如何处理
    channel.Set_Read_Callback(std::bind(Acceptor,&loop,&lis_sock));

    // 开启监听套接字的读事件监控->开始监听 
    channel.Enable_Read();

    while(1)
    {
        loop.Start();
    }

    lis_sock.Close();
    return 0;
}

客户端测试代码:test_cli.cc:

cpp 复制代码
#include "../source/server.hpp"

int main()
{
    Socket cli_sock;
    cli_sock.Create_client(8080,"127.0.0.1");

    // 每隔一秒发送一次 Hello!!!
    for(int i = 0; i < 5; i++)
    {
        std::string str = "Hello!!!";
        cli_sock.Send(str.c_str(),str.size());
        char buf[1024] = {0};
        int n = cli_sock.Recv(buf,1023);
        //std::cout << "n: " << n << std::endl;
        buf[n] = '\0';
        DBG_LOG("%s",buf);
        sleep(1);
    }

    // 故意死循环,测试超时是否会释放
    while(1) sleep(1);
    
    return 0;
}

server 输出:

bash 复制代码
[hxh@VM-16-12-centos test]$ ./server
[21:25:43 test_ser.cc:38] 有新事件发生,超时时间已刷新
[21:25:43 test_ser.cc:31] client say: Hello!!!
[21:25:43 test_ser.cc:38] 有新事件发生,超时时间已刷新
[21:25:44 test_ser.cc:38] 有新事件发生,超时时间已刷新
[21:25:44 test_ser.cc:31] client say: Hello!!!
[21:25:44 test_ser.cc:38] 有新事件发生,超时时间已刷新
[21:25:45 test_ser.cc:38] 有新事件发生,超时时间已刷新
[21:25:45 test_ser.cc:31] client say: Hello!!!
[21:25:45 test_ser.cc:38] 有新事件发生,超时时间已刷新
[21:25:46 test_ser.cc:38] 有新事件发生,超时时间已刷新
[21:25:46 test_ser.cc:31] client say: Hello!!!
[21:25:46 test_ser.cc:38] 有新事件发生,超时时间已刷新
[21:25:47 test_ser.cc:38] 有新事件发生,超时时间已刷新
[21:25:47 test_ser.cc:31] client say: Hello!!!
[21:25:47 test_ser.cc:38] 有新事件发生,超时时间已刷新
[21:25:56 test_ser.cc:5] close fd: 7
^C
[hxh@VM-16-12-centos test]$ 

Connection 模块

  • 该模块存在的目的:

对连接进行全方位的管理,对通信连接的所有操作都是通过这个模块提供的功能完成。

  • 该模块要管理的内容:
  1. 套接字的管理,能够进行套接字的操作

  2. 连接事件的管理,可读,可写,错误,挂断,任意

  3. 缓冲区的管理,便于socket数据的接收和发送

  4. 协议上下文的管理,记录请求数据的处理过程

  5. 回调函数的管理:这里的回调函数是指一个连接在不同的状态应该干的事情,而不是一个连接触发了某个事件应该干的事情 因为连接接收到数据之后该如何处理,需要由用户决定,因此必须有业务处理回调函数

    一个连接建立成功后,该如何处理,由用户决定,因此必须有连接建立成功的回调函数

    一个连接关闭前,该如何处理,由用户决定,因此必须由关闭连接回调函数。

    任意事件的产生,有没有某些处理,由用户决定,因此必须有任意事件的回调函数

  • 提供的功能:
  1. 发送数据 --- 给用户提供的发送数据接口,但并不是真正的发送接口,而只是把数据放到发送缓冲区,然后启动写事件监控,等到发送缓冲区触发写事件后再发送,如果贸然使用 send 发送,如果 socket 套接字缓冲区已满,程序就会阻塞,降低了效率
  2. 关闭连接 --- 给用户提供的关闭连接接口,但不是一旦调用就立刻关闭,应该在实际关闭连接之前,看看输入输出缓冲区是否还有数据待处理,我们将枚举出连接的所有状态,只有连接处于合适的状态,才能关闭连接。
  3. 启动非活跃连接的超时销毁功能
  4. 取消非活跃连接的超时销毁功能(长连接)
  5. 协议切换功能 --- 一个连接接收数据后如何进行业务处理,取决于上下文,以及数据的业务处理回调函数
  • 考虑以下场景:

对连接进行操作的时候,操作的连接已经被释放,导致内存访问错误,最终程序崩溃

  • 解决方案:

使用智能指针shared_ptr对Connection对象进行管理,这样就能保证任意一个地方对Connection对象进行操作的时候,保存了一份shared_ptr,因此就算其他地方进行释放操作,也只是对shared_ptr的计数器-1,而不会导致Connection的实际释放

基本框架

cpp 复制代码
typedef enum {CONNECTING,CONNECTED,DISCONNECTING,DISCONNECTED} ConnStatu;
/*
    连接状态:
    CONNECTING:连接正在建立中
    CONNECTED:连接建立完成,可以通信
    DISCONNECTING:连接正在关闭,处理发送/接收缓冲区的剩余数据
    DISCONNECTED:连接已关闭,无法通信
*/
class Connection;
using ConnectionPtr = std::shared_ptr<Connection>;
class Connection : public std::enable_shared_from_this<Connection>
{
private:
    uint64_t _conn_id;  // 连接的唯一 ID,便于连接的管理和查找
    uint64_t _timer_id; // 定时器的唯一 ID,为了简化这里就使用连接的 ID 充当
    int _socket_fd;     // 连接的文件描述符
    bool _is_inactive_release_enabled; // 是否启动了非活跃连接释放功能,默认为 false
    EventLoop* _loop;                  // 连接所关联的EventLoop
    ConnStatu _statu;                  // 连接的状态
    Socket _socket;                    // 连接的套接字
    Channel _channel;                  // 连接的事件管理
    Buffer _in_buffer;                 // 输入缓冲区,存放从socket读取到的数据
    Buffer _out_buffer;                // 输出缓冲区,存放要给客户发送的数据
    Any _context;                      // 处理请求的上下文

    // 下面四个回调函数是组件使用者传递的
    using ConnectedCallBack = std::function<void(const ConnectionPtr&)>;
    using MessageCallBack = std::function<void(const ConnectionPtr&,Buffer*)>;
    using ClosedCallBack = std::function<void(const ConnectionPtr&)>;
    using AnyEventCallBack = std::function<void(const ConnectionPtr&)>;
    ConnectedCallBack _connected_callback;    // 连接建立完成的回调
    MessageCallBack _message_callback;        // 业务处理回调->如何处理接收到的数据
    ClosedCallBack _closed_callback;          // 关闭连接回调
    AnyEventCallBack _any_event_callback;     // 任意事件回调

    // 下面这个回调函数用来释放服务器管理的所有 connections 中该
    // connection 的信息
    ClosedCallBack _server_closed_callback;
private:
    void Establish_In_Loop();

    // 将数据放到 _out_buffer,然后启动读事件监控
    void Send_In_Loop(Buffer buf);

    void Close_In_Loop();
   
    // 实际的释放接口
    void Disconnection_In_Loop();

    // 启动非活跃连接超时销毁任务
    void Enable_Inactive_Release_In_Loop(const int sec);
    
    // 取消非活跃连接超时销毁任务
    void Cancel_Inactive_Release_In_Loop();

    void Protocol_Switch_In_Loop(const Any& context,
                                 const ConnectedCallBack& connected_callback,
                                 const MessageCallBack& message_callback,
                                 const ClosedCallBack& closed_callback,
                                 const AnyEventCallBack& any_event_callback);

    /*五个Channel的事件回调函数*/
    // 描述符触发可读事件后调用的函数,接收Socket数据放到_in_buffer中,然后调用 message_callback
    void Reader();

    // 描述符触发可写事件后调用的函数,将_out_buffer的数据进行发送
    void Sender();
  
    // 描述符触发出错事件后调用的函数
    void Error_Handler();
    
    // 描述符触发任意事件后调用的函数
    void Any_Event_Handler();
   
public:
    Connection(uint64_t conn_id,EventLoop* loop,int socket_fd);
    ~Connection();

    /*下面九个函数是成员访问函数*/
    // 获取套接字 fd
    int FD();

    // 获取连接 ID
    int ID();

    // 访问 _statu,检查是否处于 CONNECTED 状态
    bool Is_Connected();

    // 设置上下文,连接建立完成时进行调用
    void Set_Context(const Any& context);

    // 获取上下文,返回的是指针
    Any* Get_Context();

    // 设置回调指针
    void Set_Connected_Callback(const ConnectedCallBack& cb);
    void Set_Message_Callback(const MessageCallBack& cb);    
    void Set_Closed_Callback(const ClosedCallBack& cb);       
    void Set_Any_Event_Callback(const AnyEventCallBack& cb);  
    void Set_Server_Closed_Callback(const ClosedCallBack cb);

    // 连接建立完成之后,进行 channel 回调设置,启动读监控,调用 closed_callback
    void Establish();
   
    // 发送数据,将数据放到 _out_buffer,然后启动读事件监控
    void Send(const char* data,size_t len);

    // 关闭连接,关闭之前要处理发送/接收缓冲区的剩余数据
    void Close();

    // 启动非活跃连接销毁
    void Enable_Inactive_Release(const int sec);

    // 关闭非活跃建立销毁
    void Cancel_Inactive_Release();

    // 切换协议->重置上下文以及阶段性回调处理函数,这个接口是非线程安全的
    // 这个接口如果要执行,必须在EventLoop线程中立刻执行,不能添加到任务队列
    // 防止新事件触发,这时协议还没有切换完成,新数据用旧协议处理的情况
    void Protocol_Switch(const Any& context,
                         const ConnectedCallBack& connected_callback,
                         const MessageCallBack& message_callback,
                         const ClosedCallBack& closed_callback,
                         const AnyEventCallBack& any_event_callback);
    void Disconnection();
};

具体实现

cpp 复制代码
typedef enum {CONNECTING,CONNECTED,DISCONNECTING,DISCONNECTED} ConnStatu;
/*
    连接状态:
    CONNECTING:连接正在建立中
    CONNECTED:连接建立完成,可以通信
    DISCONNECTING:连接正在关闭,处理发送/接收缓冲区的剩余数据
    DISCONNECTED:连接已关闭,无法通信
*/
class Connection;
using ConnectionPtr = std::shared_ptr<Connection>;
class Connection : public std::enable_shared_from_this<Connection>
{
private:
    uint64_t _conn_id;  // 连接的唯一 ID,便于连接的管理和查找
    uint64_t _timer_id; // 定时器的唯一 ID,为了简化这里就使用连接的 ID 充当
    int _socket_fd;     // 连接的文件描述符
    bool _is_inactive_release_enabled; // 是否启动了非活跃连接释放功能,默认为 false
    EventLoop* _loop;                  // 连接所关联的EventLoop
    ConnStatu _statu;                  // 连接的状态
    Socket _socket;                    // 连接的套接字
    Channel _channel;                  // 连接的事件管理
    Buffer _in_buffer;                 // 输入缓冲区,存放从socket读取到的数据
    Buffer _out_buffer;                // 输出缓冲区,存放要给客户发送的数据
    Any _context;                      // 处理请求的上下文

    // 下面四个回调函数是组件使用者传递的,由TcpServer模块设置的
    using ConnectedCallBack = std::function<void(const ConnectionPtr&)>;
    using MessageCallBack = std::function<void(const ConnectionPtr&,Buffer*)>;
    using ClosedCallBack = std::function<void(const ConnectionPtr&)>;
    using AnyEventCallBack = std::function<void(const ConnectionPtr&)>;
    ConnectedCallBack _connected_callback;    // 连接建立完成回调
    MessageCallBack _message_callback;        // 业务处理回调->如何处理接收到的数据
    ClosedCallBack _closed_callback;          // 关闭连接回调
    AnyEventCallBack _any_event_callback;     // 任意事件回调

    // 下面这个回调函数用来释放服务器管理的所有 connections 中该
    // connection 的信息
    ClosedCallBack _server_closed_callback;
private:
    void Establish_In_Loop()
    {
        assert(_statu == CONNECTING);

        // 1、更新状态为 CONNECTED 已建立连接状态
        _statu = CONNECTED;

        // 2、启动connection的读事件监控
        // 注意,一旦启动connection的读事件监控,可能立刻触发读事件刷新定时销毁任务
        // 所以在启动读事件之前,必须先通过构造函数创建定时销毁任务
        _channel.Enable_Read();

        // 3、调用组件使用者传递的连接建立完成回调
        if(_connected_callback) _connected_callback(shared_from_this());
    }

    // 将数据放到 _out_buffer,然后启动读事件监控
    void Send_In_Loop(Buffer& buf)
    {
        if(_statu != CONNECTED) return;

        _out_buffer.Write_Buffer(buf);
        if(!_channel.Is_Write_Monitored()) _channel.Enable_Write();
    }

    void Close_In_Loop()
    {
        // 更新状态为 DISCONNECTING 半关闭状态
        _statu = DISCONNECTING;

        // 处理输入输出缓冲区的剩余数据
        if(_in_buffer.Readable_Size() > 0)
        {
            if(_message_callback) _message_callback(shared_from_this(),&_in_buffer);
        }
        if(_out_buffer.Readable_Size() > 0)
        {
            // 启动读事件监控就行,读事件触发调用 Reader
            if(!_channel.Is_Write_Monitored()) _channel.Enable_Write();
        }
        if(_out_buffer.Readable_Size() == 0) Disconnection();
    }

    // 实际的释放接口
    void Disconnection_In_Loop()
    {
        // 1、更新状态为 DISCONNECTED 已关闭状态
        _statu = DISCONNECTED;

        // 2、移除连接的事件监控
        _channel.Remove();

        // 3、关闭套接字
        _socket.Close();

        // 4、取消定时销毁任务(如果有)
        if(_loop->Is_Timer_Task_Exist(_conn_id)) 
        {
            Cancel_Inactive_Release_In_Loop();
        }

        // 5、调用关闭回调函数
        // 先调用组件使用者传递的关闭回调函数
        if(_closed_callback) _closed_callback(shared_from_this());
        // 再调用服务器连接关闭回调函数
        _server_closed_callback(shared_from_this());
    }

    // 启动非活跃连接超时销毁任务
    void Enable_Inactive_Release_In_Loop(const int sec) 
    {
        // 1、将判断标志 is_inactive_release_enabled 置为 true
        _is_inactive_release_enabled = true;

        // 2、创建定时任务:存在则刷新,不存在则创建
        if(_loop->Is_Timer_Task_Exist(_conn_id)) _loop->Refresh_Timer_Tsak(_conn_id);
        else _loop->Add_Timer_Task(_conn_id,sec,std::bind(&Connection::Disconnection,this));
    }

    // 取消非活跃连接超时销毁任务
    void Cancel_Inactive_Release_In_Loop()
    {
        _is_inactive_release_enabled = false;
        if(_loop->Is_Timer_Task_Exist(_conn_id)) _loop->Cancel_Timer_Task(_conn_id);
    }

    void Protocol_Switch_In_Loop(const Any& context,
                                 const ConnectedCallBack& connected_callback,
                                 const MessageCallBack& message_callback,
                                 const ClosedCallBack& closed_callback,
                                 const AnyEventCallBack& any_event_callback)
    {
        _context = context;
        _connected_callback = connected_callback;
        _message_callback = message_callback;
        _closed_callback = closed_callback;
        _any_event_callback = any_event_callback;
    }

    /*五个Channel的事件回调函数*/
    // 描述符触发可读事件后调用的函数,接收Socket数据放到_in_buffer中,然后调用 message_callback
    void Reader()
    {
        //读取Socket数据
        char buf[65536];
        ssize_t ret = _socket.Non_Block_Recv(buf,65535);
        if(ret == 0) return; // 非阻塞读取,0 表示没有读取到数据
        else if(ret < 0)
        {
            // 出错了,不会立刻关闭连接,要处理发送/接收缓冲区的剩余数据
            return Close_In_Loop();
        }

        // 将数据放到_in_buffer中,然后调用 message_callback
        _in_buffer.Write(buf,ret);

        if(_in_buffer.Readable_Size() > 0)
        {
            return _message_callback(shared_from_this(),&_in_buffer);
        }
    }

    // 描述符触发可写事件后调用的函数,将_out_buffer的数据进行发送
    void Sender()
    {
        // _out_buffer 的数据就是要发送的数据
        ssize_t ret = _socket.Non_Block_Send(_out_buffer.Read_Pos(),_out_buffer.Readable_Size());
        if(ret < 0)
        {
            // 发送出错,要关闭连接了,在关闭连接之前,处理接收缓冲区剩余的数据
            if(_in_buffer.Readable_Size() > 0) 
            {
                _message_callback(shared_from_this(),&_in_buffer);
            }
            return Disconnection(); // 真正的关闭连接
        }

        // 发送成功,不要忘记将读指针向后移动
        _out_buffer.Move_Read_Idx(ret);

        if(_out_buffer.Readable_Size() == 0)
        {
            // 没有数据要发送了
            // 关闭写事件监控,防止一直触发
            _channel.Disable_Write();

            // 如果当前连接处于 DISCONNECTING 待关闭状态,现在可以直接关闭
            if(_statu == DISCONNECTING) return Disconnection();
        }
    }

    // 描述符触发挂断事件后调用的函数
    void Close_Handler()
    {
        // 处理接收缓冲区剩余的数据
        if(_in_buffer.Readable_Size() > 0) 
        {
            _message_callback(shared_from_this(),&_in_buffer);
        }
        return Disconnection();
    }

    // 描述符触发出错事件后调用的函数
    void Error_Handler()
    {
        // 处理接收缓冲区剩余的数据
        if(_in_buffer.Readable_Size() > 0) 
        {
            _message_callback(shared_from_this(),&_in_buffer);
        }
        return Disconnection();
    }

    // 描述符触发任意事件后调用的函数
    void Any_Event_Handler()
    {
        // 刷新连接的活跃度->延迟定时销毁任务
        if(_is_inactive_release_enabled) _loop->Refresh_Timer_Tsak(_conn_id);

        // 调用组件使用者传递的任意事件回调函数
        if(_any_event_callback) _any_event_callback(shared_from_this());
    }
public:
    Connection(uint64_t conn_id,EventLoop* loop,int socket_fd)
    :_conn_id(conn_id),_socket_fd(socket_fd),_is_inactive_release_enabled(false),
    _loop(loop),_statu(CONNECTING),_socket(socket_fd),_channel(loop,socket_fd)
    {
        // 初始化回调函数
        _channel.Set_Close_Callback(std::bind(&Connection::Close_Handler,this));
        _channel.Set_Error_Callback(std::bind(&Connection::Error_Handler,this));
        _channel.Set_Event_Callback(std::bind(&Connection::Any_Event_Handler,this));
        _channel.Set_Read_Callback(std::bind(&Connection::Reader,this));
        _channel.Set_Write_Callback(std::bind(&Connection::Sender,this));
    }
    ~Connection() 
    {
        // 检查是否存在内存泄漏
        DBG_LOG("release connection: %p", this);
    }

    /*下面九个函数是成员访问函数*/
    // 获取套接字 fd
    int FD() { return _socket_fd; }

    // 获取连接 ID
    int ID() { return _conn_id; }

    // 访问 _statu,检查是否处于 CONNECTED 状态
    bool Is_Connected() { return (_statu == CONNECTED); }

    // 设置上下文,连接建立完成时进行调用
    void Set_Context(const Any& context) { _context = std::move(context); }

    // 获取上下文,返回的是指针
    Any* Get_Context() { return &_context; }

    // 设置回调指针
    void Set_Connected_Callback(const ConnectedCallBack& cb) { _connected_callback = cb; }
    void Set_Message_Callback(const MessageCallBack& cb)     { _message_callback = cb; }
    void Set_Closed_Callback(const ClosedCallBack& cb)       { _closed_callback = cb; }
    void Set_Any_Event_Callback(const AnyEventCallBack& cb)  { _any_event_callback = cb; }
    void Set_Server_Closed_Callback(const ClosedCallBack cb) { _server_closed_callback = cb; }

    // 连接建立完成之后,进行 channel 回调设置,启动读监控,调用 closed_callback
    void Establish()
    {
        _loop->Run_In_Loop(std::bind(&Connection::Establish_In_Loop,this));
    }

    // 发送数据,将数据放到 _out_buffer,然后启动读事件监控
    void Send(const char* data,size_t len)
    {
        // 外界传入的 data 可能是临时空间,现在可能只是把发送操作添加到了任务队列
        // 发送操作没有被执行,等到执行的时候,临时空间可能被销毁了
        // 因此我们要将 data 的数据自行保存起来
        Buffer buf;
        buf.Write(data,len);
        _loop->Run_In_Loop(std::bind(&Connection::Send_In_Loop,this,std::move(buf)));
    }

    // 关闭连接,关闭之前要处理发送/接收缓冲区的剩余数据
    void Close()
    {
        _loop->Run_In_Loop(std::bind(&Connection::Close_In_Loop,this));
    }

    // 启动非活跃连接销毁
    void Enable_Inactive_Release(const int sec)
    {
        _loop->Run_In_Loop(std::bind(&Connection::Enable_Inactive_Release_In_Loop,this,sec));
    }

    // 关闭非活跃建立销毁
    void Cancel_Inactive_Release()
    {
        _loop->Run_In_Loop(std::bind(&Connection::Cancel_Inactive_Release_In_Loop,this));
    }

    // 切换协议->重置上下文以及阶段性回调处理函数,这个接口是非线程安全的
    // 这个接口如果要执行,必须在EventLoop线程中立刻执行,不能添加到任务队列
    // 防止新事件触发,这时协议还没有切换完成,新数据用旧协议处理的情况
    void Protocol_Switch(const Any& context,
                         const ConnectedCallBack& connected_callback,
                         const MessageCallBack& message_callback,
                         const ClosedCallBack& closed_callback,
                         const AnyEventCallBack& any_event_callback)
    {
        _loop->Assert_In_Loop();
        _loop->Run_In_Loop(std::bind(&Connection::Protocol_Switch_In_Loop,this,context,
        connected_callback,message_callback,closed_callback,any_event_callback));
    }

    void Disconnection()
    {
        // 将连接的释放操作直接添加到任务队列中,而不是 Run_In_Loop,是为了防止
        // 处理其他连接太久而导致拖累其他链接,如果其他链接被拖累,也要先处理
        // 链接的请求再释放连接。
        _loop->Add_Task(std::bind(&Connection::Disconnection_In_Loop,this));
    }
};

测试

服务器测试代码

cpp 复制代码
// 管理所有的 connection
std::unordered_map<uint64_t,ConnectionPtr> conns;

// 从 1 开始分配的 conn_id
uint64_t id = 1;

// 业务回调函数
// Message_Callback->如何处理接收到的数据
// 这里的处理方式:接收后回显给客户
void Recv_And_Echo(const ConnectionPtr& conn,Buffer* buf)
{
    DBG_LOG("client say: %s",buf->Read_Pos());
    buf->Move_Read_Idx(buf->Readable_Size());
    std::string echo_str = "Hello client!!!";
    conn->Send(echo_str.c_str(),echo_str.size());

    //conn->Close();
}

// Connected_Callback->连接建立完成后应该怎么做
// 这里打印 connection 的地址
void Print_Addr(const ConnectionPtr& conn)
{
    DBG_LOG("new connection establish success!!!,addr: %p",conn.get());
}

// Closed_Callback->连接断开/关闭后该怎么做
// 这里移除 conns 中连接的信息
void Distroy_Conn(const ConnectionPtr& conn)
{
    conns.erase(conn->ID());
}

void Acceptor(EventLoop* loop,Socket* lis_sock)
{
    // 获取新连接
    int newfd = lis_sock->Accept();
    if(newfd < 0) return;

    // 为新连接创建 connection 对象
    ConnectionPtr conn(new Connection(id,loop,newfd));

    // 并且设置好业务回调函数
    conn->Set_Server_Closed_Callback(std::bind(Distroy_Conn,std::placeholders::_1));
    conn->Set_Connected_Callback(std::bind(Print_Addr,std::placeholders::_1));
    conn->Set_Message_Callback(std::bind(Recv_And_Echo,std::placeholders::_1,std::placeholders::_2));

    // 非活跃连接的超时释放任务
    // 注意:开始超时释放任务,必须在启动读事件监控之前
    conn->Enable_Inactive_Release(15);

    // 开启新连接的读事件监控->等待客户发送数据
    conn->Establish();

    // 将新连接添加到 conns 管理起来
    conns.insert(std::make_pair(id,conn));

    //  更新下一个要分配的 conn id
    id++;
}

int main()
{
    // 随机数种子,分配定时任务id
    srand(time(nullptr));

    EventLoop loop;
    Socket lis_sock;
    lis_sock.Create_Server(8080);
    Channel channel(&loop,lis_sock.FD());

    // 为监听套接字设置回调函数->有新连接到来如何处理
    channel.Set_Read_Callback(std::bind(Acceptor,&loop,&lis_sock));

    // 开启监听套接字的读事件监控->开始监听 
    channel.Enable_Read();

    while(1)
    {
        loop.Start();
    }

    lis_sock.Close();
    return 0;
}

客户端测试代码与 TimerQueue 模块相同

Accepter 模块

功能:对监听套接字进行管理

管理的基本流程

  1. 创建一个监听套接字
  2. 启动读事件监控
  3. 读事件触发后,获取新连接
  4. 调用由 TcpServer 模块传递的新连接获取成功后的回调函数,回调函数会为新连接创建Connection进行管理。

因为 Acceptor 模块只进行监听连接的管理,因此获取到新连接的描述符之后,对于新连接描述符如何处理其实并不关心,对于新连接如何处理,应该是 TcpServer 模块来管理的

cpp 复制代码
class Acceptor
{
private:
    Socket _lis_socket;    // 监听套接字
    EventLoop* _loop;      // 监听套接字的事件监控
    Channel _channel;      // 监听套接字的事件管理
    
    using AcceptorCallBack = std::function<void(int)>;
    AcceptorCallBack _accept_callback;  // 获取新连接后该如何处理,由 TcpServer 模块传递

    int Create_Server(int port)
    {
        bool ret = _lis_socket.Create_Server(port);
        assert(ret == true);

        return _lis_socket.FD();
    }
private:
    // 监听套接字的读事件回调处理函数
    void Accept()
    {
        int newfd = _lis_socket.Accept();
        if(newfd < 0) return;

        if(_accept_callback) _accept_callback(newfd);
    }
public:
    Acceptor(EventLoop* loop,int port):_lis_socket(Create_Server(port)),
    _loop(loop),_channel(loop,_lis_socket.FD())
    {
        _channel.Set_Read_Callback(std::bind(&Acceptor::Accept,this));
        // _channel.Enable_Read(); error!!!
        // 不能在构造函数启动读事件监控,如果启动读事件监控后立即有新连接到来
        // 而现在 _accept_callback 还没有设置
        // 后果:新连接得不到处理,并且没有释放(内存泄漏)
    }
    void Set_Accept_CallBack(AcceptorCallBack cb) { _accept_callback = cb; }
    void Listen() {  _channel.Enable_Read(); }
};

测试

服务器代码测试

cpp 复制代码
// 管理所有的 connection
std::unordered_map<uint64_t,ConnectionPtr> conns;

// 从 1 开始分配的 conn_id
uint64_t id = 1;

EventLoop loop;

// 业务回调函数
// Message_Callback->如何处理接收到的数据
// 这里的处理方式:接收后回显给客户
void Recv_And_Echo(const ConnectionPtr& conn,Buffer* buf)
{
    DBG_LOG("client say: %s",buf->Read_Pos());
    buf->Move_Read_Idx(buf->Readable_Size());
    std::string echo_str = "Hello client!!!";
    conn->Send(echo_str.c_str(),echo_str.size());

    //conn->Close();
}

// Connected_Callback->连接建立完成后应该怎么做
// 这里打印 connection 的地址
void Print_Addr(const ConnectionPtr& conn)
{
    DBG_LOG("new connection establish success!!!,addr: %p",conn.get());
}

// Closed_Callback->连接断开/关闭后该怎么做
// 这里移除 conns 中连接的信息
void Distroy_Conn(const ConnectionPtr& conn)
{
    conns.erase(conn->ID());
}

void Handle_New_Conn(int newfd)
{
    // 为新连接创建 connection 对象
    ConnectionPtr conn(new Connection(id,&loop,newfd));

    // 并且设置好业务回调函数
    conn->Set_Server_Closed_Callback(std::bind(Distroy_Conn,std::placeholders::_1));
    conn->Set_Connected_Callback(std::bind(Print_Addr,std::placeholders::_1));
    conn->Set_Message_Callback(std::bind(Recv_And_Echo,std::placeholders::_1,std::placeholders::_2));

    // 非活跃连接的超时释放任务
    // 注意:开始超时释放任务,必须在启动读事件监控之前
    conn->Enable_Inactive_Release(15);

    // 开启新连接的读事件监控->等待客户发送数据
    conn->Establish();

    // 将新连接添加到 conns 管理起来
    conns.insert(std::make_pair(id,conn));

    //  更新下一个要分配的 conn id
    id++;
}

int main()
{
    // 随机数种子,分配定时任务id
    srand(time(nullptr));

    Acceptor acceptor(&loop,8080);
    acceptor.Set_Accept_CallBack(std::bind(Handle_New_Conn,std::placeholders::_1));
    acceptor.Listen();

    while(1)
    { 
        loop.Start();
    }
    return 0;
}

客户端测试代码与 TimerQueue 模块相同

LoopThread 模块

  • 功能:将 EventLoop 模块与线程关联起来,实现一个线程对应一个 EventLoop 模块
  • 要解决的问题:EventLoop 模块在实例化对象的时候,必须在对应线程的内部。EventLoop实例化对象时会设置自己的thread _id,如果我们先创建了多个 EventLoop对,然后创建了多个线程,再将各个线程的 id 重新设置给 EventLoop,会存在这样的问题:在构造 EventLoop 对象,到设置新的 thread_id 期间将是不可控的。因此我们必须先创建线程,然后在线程的入口函数中,去实例化 EventLoop 对象。具体的解决方法:LoopThread 模块定义了 EventLoop* 指针,而不是 EventLoop 对象,在创建的线程中去实例化 EventLoop 对象。

具体实现

cpp 复制代码
class LoopThread
{
private:
    // 互斥锁和条件变量在这里的作用:
    // 保证先初始化 _loop (_loop不为空)之后
    // 外界才能使用 Get_Loop 接口返回 _loop 指针
    // 否则要现在条件变量下等待 _loop 初始化完毕
    std::mutex _mutex;
    std::condition_variable _cond;

    EventLoop* _loop;    // 要在线程的入口函数内初始化
    std::thread _thread; // EventLoop 对应的线程
private:
    // 线程的入口函数
    void Thread_Entry()
    {
        EventLoop loop;
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _loop = &loop;
            _cond.notify_all();
        }
        loop.Start();
    }
public:
    // 创建线程,设置好线程入口函数
    LoopThread():_loop(nullptr),_thread(std::thread(&LoopThread::Thread_Entry,this)) {}

    // 返回当前线程对应的EventLoop指针
    EventLoop* Get_Loop()
    {
        EventLoop* loop = nullptr;
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _cond.wait(lock,[&](){ return _loop != nullptr; });
            loop = _loop;  
        }
        return loop;
    }
};

更新日志打印宏,使得它可以打印当前的线程ID:

cpp 复制代码
#define LOG(level,format,...) do\
{\
    if(level < LOG_LEVEL) break;\
    time_t t = time(NULL);\
    struct tm *ltm = localtime(&t);\
    char tmp[32] = {0};\
    strftime(tmp,31,"%H:%M:%S",ltm);\
    fprintf(stdout,"[%p %s %s:%d] " format "\n",(void*)pthread_self(),tmp,__FILE__,__LINE__,##__VA_ARGS__);\
}while(0)

测试:

服务器测试代码:

cpp 复制代码
#include "../source/server.hpp"

// 管理所有的 connection
std::unordered_map<uint64_t,ConnectionPtr> conns;

// 从 1 开始分配的 conn_id
uint64_t id = 1;

EventLoop base_loop; // 主线程对应的 loop ,即主Reactor -> 只负责获取新连接
std::vector<LoopThread> threads(2); // 线程池,即从属Reactor
int next_thread = 1; // 下一个分配的线程的下标


// 业务回调函数
// Message_Callback->如何处理接收到的数据
// 这里的处理方式:接收后回显给客户
void Recv_And_Echo(const ConnectionPtr& conn,Buffer* buf)
{
    DBG_LOG("client say: %s",buf->Read_Pos());
    buf->Move_Read_Idx(buf->Readable_Size());
    std::string echo_str = "Hello client!!!";
    conn->Send(echo_str.c_str(),echo_str.size());

    //conn->Close();
}

// Connected_Callback->连接建立完成后应该怎么做
// 这里打印 connection 的地址
void Print_Addr(const ConnectionPtr& conn)
{
    DBG_LOG("new connection establish success!!!,addr: %p",conn.get());
}

// Closed_Callback->连接断开/关闭后该怎么做
// 这里移除 conns 中连接的信息
void Distroy_Conn(const ConnectionPtr& conn)
{
    conns.erase(conn->ID());
}

void Handle_New_Conn(int newfd)
{
    // 为新连接创建 connection 对象
    ConnectionPtr conn(new Connection(id,threads[next_thread].Get_Loop(),newfd));
    next_thread = (next_thread + 1) % 2;

    // 并且设置好业务回调函数
    conn->Set_Server_Closed_Callback(std::bind(Distroy_Conn,std::placeholders::_1));
    conn->Set_Connected_Callback(std::bind(Print_Addr,std::placeholders::_1));
    conn->Set_Message_Callback(std::bind(Recv_And_Echo,std::placeholders::_1,std::placeholders::_2));

    // 非活跃连接的超时释放任务
    // 注意:开始超时释放任务,必须在启动读事件监控之前
    conn->Enable_Inactive_Release(15);

    // 开启新连接的读事件监控->等待客户发送数据
    conn->Establish();

    // 将新连接添加到 conns 管理起来
    conns.insert(std::make_pair(id,conn));

    //  更新下一个要分配的 conn id
    id++;

    DBG_LOG("Handle_New_Conn Success");
}

int main()
{
    // 随机数种子,分配定时任务id
    srand(time(nullptr));

    Acceptor acceptor(&base_loop,8080);
    acceptor.Set_Accept_CallBack(std::bind(Handle_New_Conn,std::placeholders::_1));
    acceptor.Listen();

    base_loop.Start();
    return 0;
}

客户端测试代码与 TimerQueue 模块相同

输出:

服务器输出:

bash 复制代码
[0x7f10d7162740 15:16:39 test_ser.cc:103] Handle_New_Conn Success
[0x7f10d58ce700 15:16:39 ../source/server.hpp:768] create timer tesk
[0x7f10d58ce700 15:16:39 test_ser.cc:69] new connection establish success!!!,addr: 0x245ea90
[0x7f10d58ce700 15:16:39 test_ser.cc:57] client say: Hello!!!
[0x7f10d58ce700 15:16:40 test_ser.cc:57] client say: Hello!!!
[0x7f10d58ce700 15:16:41 test_ser.cc:57] client say: Hello!!!
[0x7f10d58ce700 15:16:42 test_ser.cc:57] client say: Hello!!!
[0x7f10d58ce700 15:16:43 test_ser.cc:57] client say: Hello!!!
[0x7f10d58ce700 15:16:44 test_ser.cc:57] client say: Hello!!!
[0x7f10d58ce700 15:16:45 test_ser.cc:57] client say: Hello!!!
[0x7f10d58ce700 15:16:46 test_ser.cc:57] client say: Hello!!!
[0x7f10d58ce700 15:16:47 test_ser.cc:57] client say: Hello!!!
[0x7f10d58ce700 15:16:48 test_ser.cc:57] client say: Hello!!!
[0x7f10d58ce700 15:17:02 ../source/server.hpp:1287] release connection: 0x245ea90

LoopThreadPool 模块

作用:对所有的LoopThread进行管理及分配

要实现的功能:

  • 从属线程数量可配置 (0个或多个)
    注意:这里的数量是从属线程的数量,如果为0,也就是实现单 Reactor 服务器,一个线程既负责获取连接,也负责连接的处理。这使得我们的项目更加灵活,既可以搭建从属 Reactor 服务器,也可以搭建单 Reactor 服务器。
  • 对所有的线程进行管理,其实就是管理 0 个或多个 LoopThread 对象
  • 提供线程分配的功能
    当主线程获取了一个新连接,需要将新连接挂到从属线程上进行事件监控及处理。假设有 0 个从属线程,即单 Reactor 服务器,则直接分配给主线程的 EventLoop进行处理;假设有多个从属线程,则采用 RR 轮转思想,进行线程的分配

具体实现:

cpp 复制代码
class LoopThreadPool
{
private:
    int _sub_thread_count;              // 从属线程的数量
    int _next_loop_idx;                 // 下一个可以分配的loop的下标
    EventLoop* _base_loop;              // 主Reactor
    std::vector<LoopThread*> _threads;  // 从属线程池
    std::vector<EventLoop*>  _loops;    // 从属线程池的EventLoop
public:
    LoopThreadPool(EventLoop* base_loop):_sub_thread_count(0),_next_loop_idx(0),_base_loop(base_loop) {}

    // 设置从属线程的数量
    void Set_Sub_Thread_Count(int count) { _sub_thread_count = count; }

    // 创建所有的从属线程
    void Create_Sub_Thread() 
    {
        if(_sub_thread_count > 0)
        {
            _threads.resize(_sub_thread_count);
            _loops.resize(_sub_thread_count);
            for(int i = 0; i < _sub_thread_count; i++)
            {
                _threads[i] = new LoopThread();

                // 如果_threads[i]对应的EventLoop还没有实例化完成,这里会阻塞
                _loops[i] = _threads[i]->Get_Loop();
            }
        }
    }

    // 返回下一个可以分配的EventLoop
    EventLoop* Next_Loop()
    {
        if(_sub_thread_count == 0) return _base_loop;

        _next_loop_idx = (_next_loop_idx + 1) % _sub_thread_count;
        return _loops[_next_loop_idx];
    }
};

测试:

服务器测试代码:

cpp 复制代码
// 管理所有的 connection
std::unordered_map<uint64_t,ConnectionPtr> conns;

// 从 1 开始分配的 conn_id
uint64_t id = 1;

EventLoop base_loop; // 主线程对应的 loop ,即主Reactor -> 只负责获取新连接
LoopThreadPool* Loop_pool;

// 业务回调函数
// Message_Callback->如何处理接收到的数据
// 这里的处理方式:接收后回显给客户
void Recv_And_Echo(const ConnectionPtr& conn,Buffer* buf)
{
    DBG_LOG("client say: %s",buf->Read_Pos());
    buf->Move_Read_Idx(buf->Readable_Size());
    std::string echo_str = "Hello client!!!";
    conn->Send(echo_str.c_str(),echo_str.size());

    //conn->Close();
}

// Connected_Callback->连接建立完成后应该怎么做
// 这里打印 connection 的地址
void Print_Addr(const ConnectionPtr& conn)
{
    DBG_LOG("new connection establish success!!!,addr: %p",conn.get());
}

// Closed_Callback->连接断开/关闭后该怎么做
// 这里移除 conns 中连接的信息
void Distroy_Conn(const ConnectionPtr& conn)
{
    conns.erase(conn->ID());
}

void Handle_New_Conn(int newfd)
{
    // 为新连接创建 connection 对象
    ConnectionPtr conn(new Connection(id,Loop_pool->Next_Loop(),newfd));

    // 并且设置好业务回调函数
    conn->Set_Server_Closed_Callback(std::bind(Distroy_Conn,std::placeholders::_1));
    conn->Set_Connected_Callback(std::bind(Print_Addr,std::placeholders::_1));
    conn->Set_Message_Callback(std::bind(Recv_And_Echo,std::placeholders::_1,std::placeholders::_2));

    // 非活跃连接的超时释放任务
    // 注意:开始超时释放任务,必须在启动读事件监控之前
    conn->Enable_Inactive_Release(15);

    // 开启新连接的读事件监控->等待客户发送数据
    conn->Establish();

    // 将新连接添加到 conns 管理起来
    conns.insert(std::make_pair(id,conn));

    //  更新下一个要分配的 conn id
    id++;

    DBG_LOG("Handle_New_Conn Success");
}

int main()
{
    Loop_pool = new LoopThreadPool(&base_loop);
    Loop_pool->Set_Sub_Thread_Count(2);
    Loop_pool->Create_Sub_Thread();

    Acceptor acceptor(&base_loop,8080);
    acceptor.Set_Accept_CallBack(std::bind(Handle_New_Conn,std::placeholders::_1));
    acceptor.Listen();

    base_loop.Start();
    return 0;
}

客户端测试代码与 TimerQueue 模块相同

TcpServer 模块

该模块是对所有模块的整合,通过 TcpServer 模块实例化的对象,可以非常简单的完成一个服务器的搭建

  • 要管理的成员:
  1. Acceptor对象,创建一个监听套接字

  2. EventLoop对象,即 baseloop 对象,实现对监听套接字的事件监控

  3. std :: unordered_map<uint64_t, PtrConnection>_conns,实现对所有新建连接的管理

  4. LoopThreadPool 对象,创建loop线程池,对新建连接进行事件监控及处理

  • 要实现的功能:
  1. 设置从属线程的数量

  2. 启动服务器

  3. 设置各种回调函数(连接建立完成回调,消息回调,关闭回调,任意回调)

  4. 是否启动非活跃连接超时销毁功能

  5. 添加定时任务功能

  • 流程:
  1. 在 TcpServer 中实例化一个 Acceptor 对象,以及一个EventLoop对象(baseloop)

  2. 将 Acceptor 挂到 baseloop 上进行事件监控

  3. 一旦 Acceptor 对象就绪了可读事件,则执行读事件回调函数获取新建连接

  4. 对新连接,创建一个 Connection 对象进行管理

  5. 对连接对应的 Connection 设置功能回调(连接完成回调,消息回调,关闭回调,任意事件回调)

  6. 启动 Connection 的非活跃连接的超时销毁规则

  7. 将新连接对应的 Connection 挂到 LoopThreadPool 中的从属线程对应的 Eventloop 中进行事件监控

  8. 一旦 Connection 对应的连接就绪了可读事件,则执行读事件回调函数读取数据,读取完毕后调用 TcpServer 设置的消息回调进行业务处理

基本框架

cpp 复制代码
class TcpServer
{
private:
    uint64_t _next_id;                  // 自动增长的 ID
    int _port;                          // 服务器监听的端口号
    int _timeout;                       // 连接超时时间
    bool _enable_inactive_release;      // 是否启动非活跃连接销毁功能
    EventLoop _base_loop;               // 对监听套接字的事件监听
    Acceptor _acceptor;                  // 监听套接字
    LoopThreadPool _pool;               // 从属线程池

    // 管理的所有 connection 对象
    std::unordered_map<uint64_t,ConnectionPtr> _conns;

    // 由组件使用者传递的回调函数
    using ConnectedCallBack = std::function<void(const ConnectionPtr&)>;
    using MessageCallBack = std::function<void(const ConnectionPtr&,Buffer*)>;
    using ClosedCallBack = std::function<void(const ConnectionPtr&)>;
    using AnyEventCallBack = std::function<void(const ConnectionPtr&)>;
    ConnectedCallBack _connected_callback;    // 连接建立完成回调
    MessageCallBack _message_callback;        // 业务处理回调->如何处理接收到的数据
    ClosedCallBack _closed_callback;          // 关闭连接回调
    AnyEventCallBack _any_event_callback;     // 任意事件回调
private:
    void Run_After_In_Loop(const Task_func& task,int delay);

    // 为新连接创建 connection 对象进行管理
    void New_Connection(int fd);

    void Remove_Connection_In_Loop(const ConnectionPtr& conn);

    // 从 _conns 中移除 connection 对象的信息
    void Remove_Connection(const ConnectionPtr& conn);

public:
    TcpServer(int port);

    // 设置从属线程数量
    void Set_Sub_Thread_Count(int count);

    // 设置回调函数
    void Set_Connected_Callback(const ConnectedCallBack& cb);
    void Set_Message_Callback(const MessageCallBack& cb);     
    void Set_Closed_Callback(const ClosedCallBack& cb);      
    void Set_Any_Event_Callback(const AnyEventCallBack& cb);  

    // 启动非活跃连接销毁功能
    void Enable_Inactive_Release(int timeout);

    // 添加一个定时任务
    void Run_After(const Task_func& task,int delay);
   
    // 启动服务器
    void Start();
};

具体实现:

cpp 复制代码
class TcpServer
{
private:
    uint64_t _next_id;                  // 自动增长的 ID
    int _port;                          // 服务器监听的端口号
    int _timeout;                       // 连接超时时间
    bool _enable_inactive_release;      // 是否启动非活跃连接销毁功能
    EventLoop _base_loop;               // 对监听套接字的事件监听
    Acceptor _acceptor;                  // 监听套接字
    LoopThreadPool _pool;               // 从属线程池

    // 管理的所有 connection 对象
    std::unordered_map<uint64_t,ConnectionPtr> _conns;

    // 由组件使用者传递的回调函数
    using ConnectedCallBack = std::function<void(const ConnectionPtr&)>;
    using MessageCallBack = std::function<void(const ConnectionPtr&,Buffer*)>;
    using ClosedCallBack = std::function<void(const ConnectionPtr&)>;
    using AnyEventCallBack = std::function<void(const ConnectionPtr&)>;
    ConnectedCallBack _connected_callback;    // 连接建立完成回调
    MessageCallBack _message_callback;        // 业务处理回调->如何处理接收到的数据
    ClosedCallBack _closed_callback;          // 关闭连接回调
    AnyEventCallBack _any_event_callback;     // 任意事件回调
private:
    void Run_After_In_Loop(const Task_func& task,int delay)
    {
        _next_id++;
        _base_loop.Add_Timer_Task(_next_id,delay,task);
    }

    // 为新连接创建 connection 对象进行管理
    void New_Connection(int fd)
    {
        _next_id++;

        // 为新连接创建 connection 对象
        ConnectionPtr conn(new Connection(_next_id,_pool.Next_Loop(),fd));

        // 并且设置好业务回调函数
        conn->Set_Connected_Callback(_connected_callback);
        conn->Set_Message_Callback(_message_callback);
        conn->Set_Closed_Callback(_closed_callback);
        conn->Set_Any_Event_Callback(_any_event_callback);
        conn->Set_Server_Closed_Callback(std::bind(&TcpServer::Remove_Connection,this,std::placeholders::_1));


        // 非活跃连接的超时释放任务
        // 注意:开始超时释放任务,必须在启动读事件监控之前
        if(_enable_inactive_release) conn->Enable_Inactive_Release(_timeout);

        // 开启新连接的读事件监控->等待客户发送数据
        conn->Establish();

        // 将新连接添加到 conns 管理起来
        _conns.insert(std::make_pair(_next_id,conn));
    }

    void Remove_Connection_In_Loop(const ConnectionPtr& conn)
    {
        int id = conn->ID();
        auto it = _conns.find(id);
        if(it != _conns.end()) _conns.erase(id);
    }

    // 从 _conns 中移除 connection 对象的信息
    void Remove_Connection(const ConnectionPtr& conn)
    {
        _base_loop.Run_In_Loop(std::bind(&TcpServer::Remove_Connection_In_Loop,this,conn));
    }
public:
    TcpServer(int port):
    _next_id(0),_port(port),_enable_inactive_release(false),
    _acceptor(&_base_loop,port),
    _pool(&_base_loop)
    {
        // 设置 acceptor 的读回调函数->获取新连接
        _acceptor.Set_Accept_CallBack(std::bind(&TcpServer::New_Connection,this,std::placeholders::_1)); 

        // 将监听套接字挂到 _base_loop
        _acceptor.Listen();
    }

    // 设置从属线程数量
    void Set_Sub_Thread_Count(int count) { return _pool.Set_Sub_Thread_Count(count); }

    // 设置回调函数
    void Set_Connected_Callback(const ConnectedCallBack& cb) { _connected_callback = cb; }
    void Set_Message_Callback(const MessageCallBack& cb)     { _message_callback = cb;   }
    void Set_Closed_Callback(const ClosedCallBack& cb)       { _closed_callback = cb;    }
    void Set_Any_Event_Callback(const AnyEventCallBack& cb)  { _any_event_callback = cb; }

    // 启动非活跃连接销毁功能
    void Enable_Inactive_Release(int timeout)
    {
        _timeout = timeout;
        _enable_inactive_release = true;
    }

    // 添加一个定时任务
    void Run_After(const Task_func& task,int delay)
    {
        _base_loop.Run_In_Loop(std::bind(&TcpServer::Run_After_In_Loop,this,task,delay));
    }    

    // 启动服务器
    void Start() { _pool.Create_Sub_Thread(); _base_loop.Start(); }
};

测试:

服务器测试代码:

cpp 复制代码
void Print_Addr(const ConnectionPtr& conn)
{
    DBG_LOG("new connection establish success!!!,addr: %p",conn.get());
}

void Recv_And_Echo(const ConnectionPtr& conn,Buffer* buf)
{
    DBG_LOG("client say: %s",buf->Read_Pos());
    buf->Move_Read_Idx(buf->Readable_Size());
    std::string echo_str = "Hello client!!!";
    conn->Send(echo_str.c_str(),echo_str.size());

    //conn->Close();
}

void Distroy_Conn(const ConnectionPtr& conn)
{
    DBG_LOG("connection %p closed!!!",conn.get());
}

int main()
{
    TcpServer server(8080);
    server.Set_Sub_Thread_Count(2);
    server.Enable_Inactive_Release(10);
    server.Set_Connected_Callback(Print_Addr);
    server.Set_Message_Callback(Recv_And_Echo);
    server.Set_Closed_Callback(Distroy_Conn);
    server.Start();

    return 0;
}

客户端测试代码与 TimerQueue 模块相同

忽略 SIGPIPE 信号

在 server.hpp 里添加以下代码:

cpp 复制代码
class NetWork
{
public:
    NetWork()
    {
        signal(SIGPIPE,SIG_IGN);
        DBG_LOG("SIGPIPE ignored");
    }
};

static NetWork nw; 

基于 TcpServer 模块实现回显服务器 Echo_Server

echo.hpp:

cpp 复制代码
#include "../server.hpp"

class Echo_Server
{
private:
    TcpServer _server;
private:
    void Print_Addr(const ConnectionPtr& conn)
    {
        DBG_LOG("new connection establish success!!!,addr: %p",conn.get());
    }

    void Recv_And_Echo(const ConnectionPtr& conn,Buffer* buf)
    {
        DBG_LOG("client say: %s",buf->Read_Pos());
        conn->Send(buf->Read_Pos(),buf->Readable_Size());
        buf->Move_Read_Idx(buf->Readable_Size());
        //conn->Close();
    }

    void Distroy_Conn(const ConnectionPtr& conn)
    {
        DBG_LOG("connection %p closed!!!",conn.get());
    }
public:
    Echo_Server(int port):_server(port)
    {
        _server.Set_Sub_Thread_Count(2);
        _server.Enable_Inactive_Release(10);
        _server.Set_Connected_Callback(std::bind(&Echo_Server::Print_Addr,this,std::placeholders::_1));
        _server.Set_Message_Callback(std::bind(&Echo_Server::Recv_And_Echo,this,std::placeholders::_1,std::placeholders::_2));
        _server.Set_Closed_Callback(std::bind(&Echo_Server::Distroy_Conn,this,std::placeholders::_1));
    }

    void start() { return _server.Start(); }
};

mian.cc:

cpp 复制代码
#include "echo.hpp"

int main()
{
    Echo_Server server(8080);
    server.start();
    return 0;
}

回显服务器 Echo_Server 简单性能测试

测试工具是 webbench

bash 复制代码
[hxh@VM-16-12-centos webbench-master]$ ./webbench -c 500 -t 60 http://127.0.0.1:8080/hello
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.

Benchmarking: GET http://127.0.0.1:8080/hello
500 clients, running 60 sec.

Speed=88003 pages/min, 96803 bytes/sec.
Requests: 88003 susceed, 0 failed.

SERVER 模块的编写完成,接下来就要编写另一个大的模块:HTTP 协议模块

HTTP 协议模块

Util 模块

这个模块是⼀个⼯具模块,主要提供 HTTP 协议模块所⽤到的⼀些比较零散的⼯具函数

要提供的工具:

cpp 复制代码
class Util
{
public:
    // 字符串分割函数 
    static size_t Split(const std::string& src,const std::string& sep,std::vector<std::string>* ret);

    // 读取文件的所有内容
    static bool Read_File(const std::string& filename,std::string* buf);
    
    // 向文件写入数据
    static bool Write_File(const std::string& filename,const std::string& content);
   
    // URL 编码
    static std::string URL_Encode(const std::string& URL, bool convert_space_to_plus);

    // 十六进制字符转十进制,URL 解码会用到
    static char Hex_To_Dec(const char c);

    // URL 解码
    static std::string URL_Decode(const std::string& URL, bool convert_plus_to_space);

    // 获取状态码的描述信息
    static std::string Statu_Desc(const int statu);

    // 根据文件后缀名获取文件 mime
    static std::string Ext_Mime(const std::string& filename);
  
    // 判断一个文件是否是目录
    static bool Is_Directory(const std::string& filename);

    // 判断一个文件是否是普通文件
    static bool Is_Reguler_File(const std::string& filename);

    // 判断 http 请求资源路径是否有效
    static bool Is_Valid_Path(const std::string& path);
};

具体实现:

cpp 复制代码
std::unordered_map<int,std::string> _statu_msg = 
{
    // 1xx:信息响应
    {100, "Continue"},
    {101, "Switching Protocols"},
    {102, "Processing"},
    {103, "Early Hints"},

    // 2xx:成功响应
    {200, "OK"},
    {201, "Created"},
    {202, "Accepted"},
    {203, "Non-Authoritative Information"},
    {204, "No Content"},
    {205, "Reset Content"},
    {206, "Partial Content"},
    {207, "Multi-Status"},
    {208, "Already Reported"},
    {226, "IM Used"},

    // 3xx:重定向
    {300, "Multiple Choices"},
    {301, "Moved Permanently"},
    {302, "Found"},
    {303, "See Other"},
    {304, "Not Modified"},
    {305, "Use Proxy"},
    {306, "Switch Proxy"},
    {307, "Temporary Redirect"},
    {308, "Permanent Redirect"},

    //4xx:客户端错误
    {400, "Bad Request"},
    {401, "Unauthorized"},
    {402, "Payment Required"},
    {403, "Forbidden"},
    {404, "Not Found"},
    {405, "Method Not Allowed"},
    {406, "Not Acceptable"},
    {407, "Proxy Authentication Required"},
    {408, "Request Timeout"},
    {409, "Conflict"},
    {410, "Gone"},
    {411, "Length Required"},
    {412, "Precondition Failed"},
    {413, "Payload Too Large"},
    {414, "URI Too Long"},
    {415, "Unsupported Media Type"},
    {416, "Range Not Satisfiable"},
    {417, "Expectation Failed"},
    {418, "I'm a teapot"},
    {421, "Misdirected Request"},
    {422, "Unprocessable Entity"},
    {423, "Locked"},
    {424, "Failed Dependency"},
    {426, "Upgrade Required"},
    {428, "Precondition Required"},
    {429, "Too Many Requests"},
    {431, "Request Header Fields Too Large"},
    {451, "Unavailable For Legal Reasons"},

    // 5xx:服务器错误
    {500, "Internal Server Error"},
    {501, "Not Implemented"},
    {502, "Bad Gateway"},
    {503, "Service Unavailable"},
    {504, "Gateway Timeout"},
    {505, "HTTP Version Not Supported"},
    {506, "Variant Also Negotiates"},
    {507, "Insufficient Storage"},
    {508, "Loop Detected"},
    {510, "Not Extended"},
    {511, "Network Authentication Required"}
};

std::unordered_map<std::string,std::string> _mime_msg = 
{
    // 文本类型
    {".txt", "text/plain"},
    {".html", "text/html"},
    {".htm", "text/html"},
    {".css", "text/css"},
    {".js", "application/javascript"},
    {".mjs", "application/javascript"},
    {".json", "application/json"},
    {".xml", "application/xml"},
    {".csv", "text/csv"},
    {".md", "text/markdown"},
    {".rtf", "application/rtf"},

    // 图片类型
    {".jpg", "image/jpeg"},
    {".jpeg", "image/jpeg"},
    {".png", "image/png"},
    {".gif", "image/gif"},
    {".bmp", "image/bmp"},
    {".ico", "image/x-icon"},
    {".svg", "image/svg+xml"},
    {".webp", "image/webp"},
    {".tiff", "image/tiff"},
    {".tif", "image/tiff"},

    // 音视频类型
    {".mp3", "audio/mpeg"},
    {".wav", "audio/wav"},
    {".ogg", "audio/ogg"},
    {".mp4", "video/mp4"},
    {".mpeg4", "video/mp4"},
    {".avi", "video/x-msvideo"},
    {".webm", "video/webm"},
    {".mov", "video/quicktime"},
    {".flv", "video/x-flv"},
    {".mkv", "video/x-matroska"},
    {".wmv", "video/x-ms-wmv"},
    {".m3u8", "application/vnd.apple.mpegurl"},

    // 文档类型
    {".pdf", "application/pdf"},
    {".doc", "application/msword"},
    {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
    {".xls", "application/vnd.ms-excel"},
    {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
    {".ppt", "application/vnd.ms-powerpoint"},
    {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
    {".odt", "application/vnd.oasis.opendocument.text"},
    {".ods", "application/vnd.oasis.opendocument.spreadsheet"},
    {".odp", "application/vnd.oasis.opendocument.presentation"},

    // 压缩包类型
    {".zip", "application/zip"},
    {".rar", "application/vnd.rar"},
    {".7z", "application/x-7z-compressed"},
    {".tar", "application/x-tar"},
    {".gz", "application/gzip"},
    {".tgz", "application/gzip"},

    // 应用程序/二进制
    {".exe", "application/vnd.microsoft.portable-executable"},
    {".bin", "application/octet-stream"},
    {".wasm", "application/wasm"},
    {".jar", "application/java-archive"},
    {".apk", "application/vnd.android.package-archive"},

    // 字体类型
    {".ttf", "font/ttf"},
    {".otf", "font/otf"},
    {".woff", "font/woff"},
    {".woff2", "font/woff2"},

    // 其他常见类型
    {".jsonld", "application/ld+json"},
    {".yaml", "text/yaml"},
    {".yml", "text/yaml"},
    {".c", "text/x-c"},
    {".cpp", "text/x-c++src"},
    {".h", "text/x-c"},
    {".hpp", "text/x-c++src"},
    {".py", "text/x-python"},
    {".java", "text/x-java"},
    {".sh", "application/x-sh"},
    {".bash", "application/x-sh"},
    {".rss", "application/rss+xml"},
    {".atom", "application/atom+xml"},
    {".eot", "application/vnd.ms-fontobject"}
};

class Util
{
public:
    // 字符串分割函数 
    static size_t Split(const std::string& src,const std::string& sep,std::vector<std::string>* ret)
    {
        size_t offset = 0;
        while(offset < src.size())
        {
            size_t pos = src.find(sep,offset);
            if(pos == std::string::npos)
            {
                ret->push_back(src.substr(offset,pos - offset));
                return ret->size();
            }
            if(pos != offset) ret->push_back(src.substr(offset,pos - offset));
            offset = pos + sep.size();
        }

        return ret->size();
    }

    // 读取文件的所有内容
    static bool Read_File(const std::string& filename,std::string* buf)
    {
        std::ifstream ifs(filename,std::ios::binary);
        if(ifs.is_open() == false)
        {
            ERR_LOG("open %s failed!!!",filename);
            return false;
        }

        // 跳转读取位置到末尾
        ifs.seekg(0,ifs.end);

        // 获取文件大小
        size_t fsize = ifs.tellg();

        // 跳转读取位置到开始
        ifs.seekg(0,ifs.beg);

        // 将文件内容读取到 buf
        buf->resize(fsize);
        ifs.read(&(*buf)[0],fsize);
        if(ifs.good() == false)
        {
            ERR_LOG("read %s failed!!!",filename);
            ifs.close();
            return false;
        }

        ifs.close();
        return true;
    }

    // 向文件写入数据
    static bool Write_File(const std::string& filename,const std::string& content)
    {
        // std::ios::trunc : 覆盖式写入
        std::ofstream ofs(filename,std::ios::binary | std::ios::trunc);
        if(ofs.is_open() == false)
        {
            ERR_LOG("open %s failed!!!",filename);
            return false;
        }

        ofs.write(content.c_str(),content.size());
        if(ofs.good() == false)
        {
            ERR_LOG("write %s failed!!!",filename);
            ofs.close();
            return false;
        }

        ofs.close();
        return true;
    }

    // URL 编码
    // URL 编码:避免 URL 中资源路径与查询字符串中的特殊字符与 HTTP 请求中特殊字符产生歧义
    // 编码格式:将特殊字符的 ascii 值,转换为两个 16 进制字符,前缀 % 
    // 不编码的特殊字符: RFC3986文档规定 .(点) - (横杠) _ (下划线) ~ (波浪号) 以及字母、数字属于绝对不编码字符
    // RFC3986 文档规定,编码格式 %HH,16 进制字符必须为两位,不满则补零
    // W3C 标准中规定,查询字符串中的空格,需要编码为 +, 解码则是 + 转空格
    static std::string URL_Encode(const std::string& URL, bool convert_space_to_plus)
    {
        std::string ret;
        for(auto& c : URL)
        {
            // .(点) - (横杠) _ (下划线) ~ (波浪号) 以及字母、数字属于绝对不编码字符
            if(c == '.' || c == '-' || c == '_' || c == '~' || isalnum(c))
            {
                ret += c;
                continue;
            }

            // W3C 标准中规定,查询字符串中的空格,需要编码为 +, 解码则是 + 转空格
            if(c == ' ' && convert_space_to_plus)
            {
                ret += '+';
                continue;
            }

            // 剩下的字符都要编码为 %HH 格式
            char tmp[4] = {0};
            snprintf(tmp,4,"%%%02X",c);
            ret += tmp;
        }

        return ret;
    }

    // 十六进制字符转十进制,URL 解码会用到
    static char Hex_To_Dec(const char c)
    {
        if(c >= '0' && c <= '9') return c - '0';
        else if(c >= 'a' && c <= 'z') return c - 'a' + 10;
        else if(c >= 'A' && c <= 'Z') return c - 'A' + 10;

        return -1;
    }

    // URL 解码
    static std::string URL_Decode(const std::string& URL, bool convert_plus_to_space)
    {
        std::string ret;
        int sz = URL.size();
        for(int i = 0; i < sz; i++)
        {
            if(URL[i] == '%' && (i + 2) < sz)
            {
                // 遇到 %,将其后的两个字符转换为数字
                // 转换规则:第一个数字左移 4 位,然后加上第二个数字
                char v1 = Hex_To_Dec(URL[i + 1]);
                char v2 = Hex_To_Dec(URL[i + 2]);
                char c  = (v1 >> 4) + v2;
                ret += c;
                i   += 2;
            }
            else if(URL[i] == '+' && convert_plus_to_space)
            {
                ret += ' ';
            }
            else ret += URL[i];
        }

        return ret;
    }

    // 获取状态码的描述信息
    static std::string Statu_Desc(const int statu)
    {
        auto it = _statu_msg.find(statu);
        if(it != _statu_msg.end()) return it->second;

        return "Unknown statu";
    }

    // 根据文件后缀名获取文件 mime
    static std::string Ext_Mime(const std::string& filename)
    {
        // 先获取文件后缀名
        size_t pos = filename.find_last_of('.');
        if(pos == std::string::npos) return "application/octet-stream";
        std::string ext = filename.substr(pos);

        // 根据后缀名,获取mime
        auto it = _mime_msg.find(ext);
        if(it == _mime_msg.end()) return "application/octet-stream";

        return it->second;
    }

    // 判断一个文件是否是目录
    static bool Is_Directory(const std::string& filename)
    {
        struct stat st;
        int ret = stat(filename.c_str(),&st);
        if(ret < 0) return false;

        return S_ISDIR(st.st_mode);
    }

    // 判断一个文件是否是普通文件
    static bool Is_Reguler_File(const std::string& filename)
    {
        struct stat st;
        int ret = stat(filename.c_str(),&st);
        if(ret < 0) return false;

        return S_ISREG(st.st_mode);
    }

    // 判断 http 请求资源路径是否有效
    static bool Is_Valid_Path(const std::string& path)
    {
        std::vector<std::string> subdir;
        Split(path,"/",&subdir);
        
        int level = 0;
        for(auto& dir : subdir)
        {
            if(dir == "..") 
            {
                level--;
                if(level < 0) return false;
            }
            else level++;
        }

        return true;
    }
};

HttpRequest 模块

这个模块是HTTP请求数据模块,⽤于保存HTTP请求数据被解析后的各项请求元素信息。要保存的信息有:请求方法、资源路径、协议版本、请求正文、资源路径的正则提取数据、头部字段以及查询字符串。

基本框架

cpp 复制代码
class HttpRequest
{
public:
    std::string _method;    // 请求方法
    std::string _path;      // 资源路径
    std::string _version;   // 协议版本
    std::string _body;      // 请求正文
    std::smatch _matches;   // 资源路径的正则提取数据
    std::unordered_map<std::string,std::string> _headers;   // 头部字段
    std::unordered_map<std::string,std::string> _params;    // 查询字符串
public:
    // 重置HttpRequest
    void Reset();

    // 添加头部字段
    void Add_Header(const std::string& key,const std::string& val);

    // 判断是否存在指定头部字段
    bool Has_Header(const std::string& key);

    // 获取指定头部字段
    std::string Get_Header(const std::string& key);

    // 添加查询字符串
    void Add_Param(const std::string& key,const std::string& val);

    // 判断是否存在指定查询字符串
    bool Has_Param(const std::string& key);

    // 获取指定查询字符串
    std::string Get_Param(const std::string& key);
  
    // 获取正文长度
    size_t Content_Length();

    // 判断是长连接还是短连接
    bool close();
};

具体实现

cpp 复制代码
class HttpRequest
{
public:
    std::string _method;    // 请求方法
    std::string _path;      // 资源路径
    std::string _version;   // 协议版本
    std::string _body;      // 请求正文
    std::smatch _matches;   // 资源路径的正则提取数据
    std::unordered_map<std::string,std::string> _headers;   // 头部字段
    std::unordered_map<std::string,std::string> _params;    // 查询字符串
public:
    // 重置HttpRequest
    void Reset()
    {
        _method.clear();
        _path.clear();
        _version.clear();
        _body.clear();
        std::smatch tmp;
        _matches.swap(tmp);
        _headers.clear();
        _params.clear();
    }

    // 添加头部字段
    void Add_Header(const std::string& key,const std::string& val)
    {
        _headers.insert(std::make_pair(key,val));
    }

    // 判断是否存在指定头部字段
    bool Has_Header(const std::string& key)
    {
        auto it = _headers.find(key);
        if(it == _headers.end()) return false;

        return true;
    }

    // 获取指定头部字段
    std::string Get_Header(const std::string& key)
    {
        if(Has_Header(key)) return _headers[key];
        return "";
    }

    // 添加查询字符串
    void Add_Param(const std::string& key,const std::string& val)
    {
        _params.insert(std::make_pair(key,val));
    }

    // 判断是否存在指定查询字符串
    bool Has_Param(const std::string& key)
    {
        auto it = _params.find(key);
        if(it == _params.end()) return false;

        return true;
    }

    // 获取指定查询字符串
    std::string Get_Param(const std::string& key)
    {
        if(Has_Param(key)) return _params[key];
        return "";
    }

    // 获取正文长度
    size_t Content_Length()
    {
        if(Has_Header("Content-Length")) return stol(_headers["Content-Length"]);
        return 0;
    }

    // 判断是长连接还是短连接
    bool Close()
    {
        // 没有Connection字段,或者有但值是close的都是短连接,否则是长连接
        if(Has_Header("Connection") && _headers["Connection"] == "keep-alive") return false;
        return true;
    }
};

HttpResponse 模块

这个模块是HTTP响应数据模块,⽤于业务处理后设置并保存HTTP响应数据的的各项元素信息,其中包括:状态码、是否设置了重定向、重定向URL、响应正文、头部字段。

基本框架

cpp 复制代码
class HttpResponse
{
public:
    int _statu;                     // 状态码
    bool _is_redirected;            // 是否设置了重定向
    std::string _body;              // 响应正文
    std::string _redirect_url;      // 重定向URL
    std::unordered_map<std::string,std::string> _headers; // 头部字段
public:
    HttpResponse():_statu(200),_is_redirected(false) {}
    HttpResponse(int statu):_statu(statu),_is_redirected(false) {}

    // 重置HttpResponse
    void Reset();
    
    // 添加头部字段
    void Add_Header(const std::string& key,const std::string& val);

    // 判断是否存在指定头部字段
    bool Has_Header(const std::string& key);

    // 获取指定头部字段
    std::string Get_Header(const std::string& key);
  
    // 设置正文
    void Set_Content(const std::string& body,const std::string& type = "text/html");

    // 设置重定向
    void Set_Redirect(const std::string& URL,int statu = 302);

    // 判断是长连接还是短连接
    bool close();
};

具体实现

cpp 复制代码
class HttpResponse
{
public:
    int _statu;                     // 状态码
    bool _is_redirected;            // 是否设置了重定向
    std::string _body;              // 响应正文
    std::string _redirect_url;      // 重定向URL
    std::unordered_map<std::string,std::string> _headers; // 头部字段
public:
    HttpResponse():_statu(200),_is_redirected(false) {}
    HttpResponse(int statu):_statu(statu),_is_redirected(false) {}

    // 重置HttpResponse
    void Reset()
    {
        _statu = 200;
        _is_redirected = false;
        _body.clear();
        _redirect_url.clear();
        _headers.clear();
    }
    
    // 添加头部字段
    void Add_Header(const std::string& key,const std::string& val)
    {
        _headers.insert(std::make_pair(key,val));
    }

    // 判断是否存在指定头部字段
    bool Has_Header(const std::string& key)
    {
        auto it = _headers.find(key);
        if(it == _headers.end()) return false;

        return true;
    }

    // 获取指定头部字段
    std::string Get_Header(const std::string& key)
    {
        if(Has_Header(key)) return _headers[key];
        return "";
    }

    // 设置正文
    void Set_Content(const std::string& body,const std::string& type = "text/html")
    {
        _body = body;
        Add_Header("Content-Type",type);
    }

    // 设置重定向
    void Set_Redirect(const std::string& URL,int statu = 302)
    {
        _statu = statu;
        _is_redirected = true;
        _redirect_url = URL;
    }

    // 判断是长连接还是短连接
    bool close()
    {
        // 没有Connection字段,或者有但值是close的都是短连接,否则是长连接
        if(Has_Header("Connection") && _headers["Connection"] == "keep-alive") return false;
        return true;
    }
};

HttpContext 模块

基本框架

cpp 复制代码
class HttpContext
{
private:
    int _resp_statu;            // 响应状态码
    HttpRecvStatu _recv_statu;  // 当前接收的阶段
    HttpRequest _request;       // 解析完成的请求
private:
    // 接收起始行
    bool Recv_Http_Line(Buffer* buf);
    
    // 解析起始行
    bool Parse_Http_Line(const std::string& line);

    // 接收头部字段
    bool Recv_Http_Head(Buffer* buf);

    // 解析头部字段
    bool Parse_Http_Head(const std::string& line);

    // 接收正文
    bool Recv_Http_Body(Buffer* buf);
public:
    HttpContext():_resp_statu(200),_recv_statu(RECV_HTTP_LINE) {}

    // 获取响应状态码
    int Get_Resp_Statu();

    // 获取接收阶段
    HttpRecvStatu Get_Recv_Statu();

    // 获取解析完成的请求
    HttpRequest& Get_Request();

    // 接收并解析HTTP请求
    void Recv_Http_Request(Buffer* buf);
};

具体实现

cpp 复制代码
class HttpContext
{
private:
    int _resp_statu;            // 响应状态码
    HttpRecvStatu _recv_statu;  // 当前接收的阶段
    HttpRequest _request;       // 解析完成的请求
private:
    // 接收起始行
    bool Recv_Http_Line(Buffer* buf)
    {
        // 阶段不对
        if(_recv_statu != RECV_HTTP_LINE) return false;

        // 获取起始行
        std::string line = buf->Get_Line();
        
        // 考虑buf中的起始行不完整,或者起始行太大超过4KB
        // 起始行不完整:
        if(line.size() == 0)
        {
            // 起始行大小超过了 4KB 都不完整
            if(buf->Readable_Size() > MAX_LINE) 
            {
                _recv_statu = RECV_HTTP_ERROR;
                _resp_statu = 414; // 414:URL TOO LONG
                return false;
            }

            // 起始行不完整,但又没有超过4KB,再等等
            return true;
        }
        // 起始行太大超过4KB:
        else if(line.size() > MAX_LINE)
        {
            _recv_statu = RECV_HTTP_ERROR;
            _resp_statu = 414; // 414:URL TOO LONG
            return false;
        }

        // 起始行读取完毕,开始解析起始行
        if(Parse_Http_Line(line) == false) return false;

        // 起始行解析完毕,进入读取头部字段阶段
        _recv_statu = RECV_HTTP_HEAD;

        return true;
    }

    // 解析起始行
    bool Parse_Http_Line(const std::string& line)
    {
        // 使用正则表达式解析起始行
        std::smatch matches;
        std::regex e("(GET|POST|HEAD|DELETE|PUT) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");
        if(std::regex_match(line,matches,e) == false) 
        {
            _recv_statu = RECV_HTTP_ERROR;
            _resp_statu = 400; // 400:bad request
            return false;
        }

        // 获取请求方法
        _request._method = matches[1];

        // 获取资源路径,需要进行URL解码,但是不需要+转空格
        _request._path = Util::URL_Decode(matches[2],false);

        // 获取协议版本
        _request._version = matches[4];
    
        // 查询字符串格式:key=value&key=value&key=value...
        // 解析查询字符串,先按照 & 将查询字符串分割
        std::vector<std::string> query_strings;
        Util::Split(matches[3],"&",&query_strings);
        // 再按照 = 将查询字符串分割
        for(const auto& query : query_strings)
        {
            size_t pos = query.find('=');
            if(pos == std::string::npos)
            {
                // 找不到 key=value的等号,出错
                _recv_statu = RECV_HTTP_ERROR;
                _resp_statu = 400; // 400:bad request
                return false;
            }

            // 获取 key 和 value,需要进行URL解码,并且需要+转空格
            std::string key = Util::URL_Decode(query.substr(0,pos),true);
            std::string value = Util::URL_Decode(query.substr(pos + 1),true);
            _request.Add_Param(key,value);
        }

        return true;
    }

    // 接收头部字段
    bool Recv_Http_Head(Buffer* buf)
    {
        // 阶段不对
        if(_recv_statu != RECV_HTTP_HEAD) return false;

        // 一行一行的取出数据,直到遇到空行为止
        while(true)
        {
            // 获取一行
            std::string line = buf->Get_Line();
            
            if(line.size() == 0)
            {
                if(buf->Readable_Size() > MAX_LINE) 
                {
                    _recv_statu = RECV_HTTP_ERROR;
                    _resp_statu = 414; // 414:URL TOO LONG
                    return false;
                }

                return true;
            }
            else if(line.size() > MAX_LINE)
            {
                _recv_statu = RECV_HTTP_ERROR;
                _resp_statu = 414; // 414:URL TOO LONG
                return false;
            }

            // 遇到空行,头部字段读取完毕
            if(line == "\n" || line == "\r\n") break;

            if(Parse_Http_Head(line) == false) return false;
        }

        // 头部字段读取完毕,接下来进入正文读取阶段
        _recv_statu = RECV_HTTP_BODY;

        return true;
    }

    // 解析头部字段
    bool Parse_Http_Head(const std::string& line)
    {
        size_t pos = line.find(": ");
        if(pos == std::string::npos) 
        {
            _recv_statu = RECV_HTTP_ERROR;
            _resp_statu = 400; // 400:bad request
            return false;
        }

        std::string key = line.substr(0,pos);
        std::string value = line.substr(pos + 2);
        _request.Add_Header(key,value);

        return true;
    }

    // 接收正文
    bool Recv_Http_Body(Buffer* buf)
    {
        // 阶段不对
        if(_recv_statu != RECV_HTTP_BODY) return false;

        // 获取正文长度
        size_t content_length = _request.Content_Length();
        if(content_length == 0)
        {
            // 没有正文,则接收完毕
            _recv_statu = RECV_HTTP_OVER;
            return true;
        }

        // 计算当前要读取的大小
        // 当前要读取的大小 = 正文长度 - 已经读取的长度
        size_t size_to_read = content_length - _request._body.size();

        // 再看看缓冲区还有多少数据
        if(buf->Readable_Size() >= size_to_read)
        {
            // 缓冲区包含所有剩余正文,读取 size_to_read 长度后读取完毕
            _request._body.append(buf->Read_Pos(),size_to_read);
            buf->Move_Read_Idx(size_to_read);
            _recv_statu = RECV_HTTP_OVER;
            
            return true;
        }

        // 缓冲区没有包含所有剩余正文,则把缓冲区的所有数据都读取到 _body,并等待下次读取
        _request._body.append(buf->Read_Pos(),buf->Readable_Size());
        buf->Move_Read_Idx(buf->Readable_Size());
        
        return true;
    }
public:
    HttpContext():_resp_statu(200),_recv_statu(RECV_HTTP_LINE) {}

    // 获取响应状态码
    int Get_Resp_Statu() { return _resp_statu; }

    // 获取接收阶段
    HttpRecvStatu Get_Recv_Statu() { return _recv_statu; }

    // 获取解析完成的请求
    HttpRequest& Get_Request() { return _request; }

    // 接收并解析HTTP请求
    void Recv_Http_Request(Buffer* buf)
    {
        switch(_recv_statu)
        {
            case RECV_HTTP_LINE: Recv_Http_Line(buf);
            case RECV_HTTP_HEAD: Recv_Http_Head(buf);
            case RECV_HTTP_BODY: Recv_Http_Body(buf);
        }
    }
};

HttpServer 模块

在这个模块中,我们设计了一张**请求路由表:**表中记录了针对哪种请求,应该使用哪个函数来进行业务处理的映射关系。当服务器收到了一个请求,就在请求路由表中查找有没有对应请求的处理函数,如果有,则执行对应的处理函数即可。这样做的好处:用户只需要实现业务处理函数,然后将请求与处理函数的映射关系添加到服务器中,而服务器只需要接收数据,解析数据,查找路由表映射关系,执行业务处理函数。我们给常见的请求方法(GET/POST/PUT/DELETE)都分别单独创建一张请求路由表。

注意:上面的请求路由表通常是处理功能性请求比如登陆请求。对于静态资源的请求,比如 html、image 文件的请求,则是在相对根目录中寻找是否存在对应资源。对于 GET 方法也创建了请求路由表,是因为有时候 GET 方法也可以是功能性请求。

基本框架

cpp 复制代码
class HttpServer
{
private:
    using Handler = std::function<void(const HttpRequest&,HttpResponse*)>; // 请求路由表的函数指针类型
    using Handlers = std::vector<std::pair<std::regex,Handler>>; // std::regex 是一个正则表达式编译后的结果
    Handlers _GET_Handlers;      // GET方法或者HEAD方法的请求路由表
    Handlers _POST_Handlers;     // POST方法的请求路由表
    Handlers _PUT_Handlers;      // PUT方法的请求路由表
    Handlers _DELETE_Handlers;   // DELETE方法的请求路由表
    std::string _base_dir; // 静态资源的根目录
    TcpServer _server;
private:
    void Error_Handler(const HttpRequest& req,HttpResponse* rsp);

    // 将HttpResponse中的要素按照http协议格式进行序列化然后发送
    void Write_Response(const ConnectionPtr& conn,HttpRequest& req,HttpResponse& rsp);

    // 判断是否是静态资源的请求
    bool Is_File_Request(HttpRequest& req);

    // 静态资源的请求处理
    void File_Handler(HttpRequest& req,HttpResponse* rsp);

    // 功能性请求的分类处理
    void Dispather(HttpRequest& req,HttpResponse* rsp,Handlers& handlers);
    
    // 请求路由 + 业务处理
    void Route(HttpRequest& req,HttpResponse* rsp);

    // 设置上下文
    void OnConnected(const ConnectionPtr& conn);

    // 缓冲区数据解析 + 处理
    void OnMessage(const ConnectionPtr& conn,Buffer* buf);
    
public:
    HttpServer(int port,int timeout = DEFAULT_TIMEOUT):_server(port)

    // 设置静态资源的根目录
    void Set_Base_Dir(const std::string& path); 

    // 添加Handler到GET方法的请求路由表
    void Add_GET_Handler(const std::string& pattern,Handler& handler);

    // 添加Handler到PUST方法的请求路由表
    void Add_POST_Handler(const std::string& pattern,Handler& handler);

    // 添加Handler到PUT方法的请求路由表
    void Add_PUT_Handler(const std::string& pattern,Handler& handler);

    // 添加Handler到DELETE方法的请求路由表
    void Add_DELETE_Handler(const std::string& pattern,Handler& handler);

    // 设置线程池的线程数量
    void Set_Thread_Count(const int count); 

    // 启动服务器
    void Start();
};

具体实现

cpp 复制代码
class HttpServer
{
private:
    using Handler = std::function<void(const HttpRequest&,HttpResponse*)>; // 请求路由表的函数指针类型
    using Handlers = std::vector<std::pair<std::regex,Handler>>; // std::regex 是一个正则表达式编译后的结果
    Handlers _GET_Handlers;      // GET方法或者HEAD方法的请求路由表
    Handlers _POST_Handlers;     // POST方法的请求路由表
    Handlers _PUT_Handlers;      // PUT方法的请求路由表
    Handlers _DELETE_Handlers;   // DELETE方法的请求路由表
    std::string _base_dir; // 静态资源的根目录
    TcpServer _server;
private:
    void Error_Handler(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::Statu_Desc(rsp->_statu);
        body += "</h1>";
        body += "</body>";
        body += "</html>";

        // 第二步:将页面数据,作为响应正文,放入rsp中
        rsp->Set_Content(body,"text/html");
    }

    // 将HttpResponse中的要素按照http协议格式进行序列化然后发送
    void Write_Response(const ConnectionPtr& conn,HttpRequest& req,HttpResponse& rsp)
    {
        // 第一步:完善头部字段
        if(req.Close()) rsp.Add_Header("Connection","close");
        else rsp.Add_Header("Connection","keep-alive");
        
        if(rsp._body.empty() == false && rsp.Has_Header("Content-Length") == false)
            rsp.Add_Header("Content-Length",std::to_string(rsp._body.size()));

        if(rsp._body.empty() == false && rsp.Has_Header("Content-Type") == false)
            rsp.Add_Header("Content-Type","application/octet-stream"); // 不知道正文是什么,默认认为是二进制流

        if(rsp._is_redirected) rsp.Add_Header("Location",rsp._redirect_url);

        // 第二步:将 rsp 的元素,按照 http 协议格式进行序列化
        std::stringstream rsp_str; // 序列化的结果
        // 起始行
        rsp_str << req._version << ' ' << std::to_string(rsp._statu) << ' ' << Util::Statu_Desc(rsp._statu) << "\r\n";
        // 头部字段
        for(auto& head : rsp._headers) rsp_str << head.first << ": " << head.second << "\r\n";
        // 空行
        rsp_str << "\r\n";
        // 正文
        rsp_str << rsp._body;

        // 第三步:通过 connection 发送数据
        conn->Send(rsp_str.str().c_str(),rsp_str.str().size());
    }

    // 判断是否是静态资源的请求
    bool Is_File_Request(HttpRequest& req)
    {
        // 1、必须设置了静态资源根目录
        if(_base_dir.empty()) return false;

        // 2、请求方法必须是 GET 方法或者是 HEAD 方法
        if(req._method != "GET" && req._method != "HEAD") return false;

        // 3、请求的资源路径必须是一个合法的路径
        if(Util::Is_Valid_Path(req._path) == false) return false;

        // 4、请求的资源必须存在且是一个普通文件
        // 定义一个临时对象req_path
        std::string req_path = _base_dir + req._path;
        if(req_path.back() == '/') 
        {
            // 客户请求的是目录,比如最简单的 '/' 或者 "/image/"
            // 就在路径后面添加 index.html 即该目录的首页
            req_path += "index.html";
        }
        // 文件不存在或者不是普通文件都会返回 false
        if(Util::Is_Reguler_File(req_path) == false) return false;

        // 确实是一个静态资源的请求,修改 req._path 为 req_path
        req._path = req_path;

        return true;
    }

    // 静态资源的请求处理
    void File_Handler(HttpRequest& req,HttpResponse* rsp)
    {
        // 定义一个临时对象req_path
        std::string req_path = _base_dir + req._path;
        if(req_path.back() == '/') 
        {
            // 客户请求的是目录,比如最简单的 '/' 或者 "/image/"
            // 就在路径后面添加 index.html 即该目录的首页
            req_path += "index.html";
        }

        // 读取文件到 rsp->_body
        if(Util::Read_File(req_path,&rsp->_body) == false) return;
        
        // 添加mime
        std::string mime = Util::Ext_Mime(req_path);
        rsp->Add_Header("Content-Type",mime);
    }

    // 功能性请求的分类处理
    void Dispather(HttpRequest& req,HttpResponse* rsp,Handlers& handlers)
    {
        // 在路由表中查找,找到了就执行,没有找到就返回 404
        for(auto& handler : handlers)
        {
            const std::regex& re = handler.first;
            const Handler& Functor = handler.second;
            if(std::regex_match(req._path,req._matches,re) == false) continue;
            return Functor(req,rsp); 
        }
        
        rsp->_statu = 404;
    }

    // 请求路由 + 业务处理
    void Route(HttpRequest& req,HttpResponse* rsp)
    {
        // 对请求方法进行分辨,是一个静态资源的请求,还是功能性请求
        // 先判断是不是静态资源的请求
        if(Is_File_Request(req)) return File_Handler(req,rsp);

        // 不是静态资源请求,可能是功能性请求
        if(req._method == "GET" || req._method == "HEAD") return Dispather(req,rsp,_GET_Handlers);
        else if(req._method == "POST")   return Dispather(req,rsp,_POST_Handlers);
        else if(req._method == "PUT")    return Dispather(req,rsp,_PUT_Handlers);
        else if(req._method == "DELETE") return Dispather(req,rsp,_DELETE_Handlers);

        // 不是上面的方法的任何一种
        rsp->_statu = 405; // 405:Method Not Allowd
    }

    // 设置上下文
    void OnConnected(const ConnectionPtr& conn)
    {
        conn->Set_Context(HttpContext());
        DBG_LOG("new connection %p",conn.get());
    }

    // 缓冲区数据解析 + 处理
    void OnMessage(const ConnectionPtr& conn,Buffer* buf)
    {
        while(buf->Readable_Size() > 0)
        {
            // 第一步:获取上下文
            HttpContext* context = conn->Get_Context()->get<HttpContext>();

            // 第二步:通过上下文对缓冲区的数据进行解析,得到HttpRequest对象
            context->Recv_Http_Request(buf);
            HttpResponse rsp(context->Get_Resp_Statu());
            // 解析可能会出错,如果解析出错,要向客户端返回出错页面,并且关闭连接
            iif(context->Get_Resp_Statu() >= 400) 
            {
                DBG_LOG("context->Get_Resp_Statu() >= 400");
                HttpRequest& req = context->Get_Request();
                // 填充一个错误显式页面到rsp中
                Error_Handler(req,&rsp);
                Write_Response(conn,req,rsp);
                context->Reset(); // ??? 
                buf->Move_Read_Idx(buf->Readable_Size()); // 清空缓冲区数据
                conn->Close();
                return;
            }
            // 解析也可能没有完成,数据不完整,退出循环等待下一次解析
            if(context->Get_Recv_Statu() != RECV_HTTP_OVER) return;
            HttpRequest& req = context->Get_Request();

            // 第三步:请求路由 + 业务处理
            Route(req,&rsp);

            // 第四步:将 rsp 组织成http协议格式进行发送
            Write_Response(conn,req,rsp);

            // 第五步:重置上下文信息,防止干扰下一次解析
            context->Reset();

            // 第六步:判断长短连接,短连接直接关闭,长连接继续循环
            if(rsp.close()) conn->Close(); 
        }
    }
public:
    HttpServer(int port,int timeout = DEFAULT_TIMEOUT):_server(port)
    {
        // 默认开启超时连接释放功能
        _server.Enable_Inactive_Release(timeout);

        // 设置回调函数
        _server.Set_Connected_Callback(std::bind(&HttpServer::OnConnected,this,std::placeholders::_1));
        _server.Set_Message_Callback(std::bind(&HttpServer::OnMessage,this,std::placeholders::_1,std::placeholders::_2));
    }

    // 设置静态资源的根目录
    void Set_Base_Dir(const std::string& path) 
    {
        // path 必须是一个目录
        assert(Util::Is_Directory(path) == true); 
        _base_dir = path; 
    }

    // 添加Handler到GET方法的请求路由表
    void Add_GET_Handler(const std::string& pattern,Handler& handler)
    {
        _GET_Handlers.push_back(std::make_pair(std::regex(pattern),handler));
    }

    // 添加Handler到PUST方法的请求路由表
    void Add_POST_Handler(const std::string& pattern,Handler& handler)
    {
        _POST_Handlers.push_back(std::make_pair(std::regex(pattern),handler));
    }

    // 添加Handler到PUT方法的请求路由表
    void Add_PUT_Handler(const std::string& pattern,Handler& handler)
    {
        _PUT_Handlers.push_back(std::make_pair(std::regex(pattern),handler));
    }

    // 添加Handler到DELETE方法的请求路由表
    void Add_DELETE_Handler(const std::string& pattern,Handler& handler)
    {
        _DELETE_Handlers.push_back(std::make_pair(std::regex(pattern),handler));
    }

    // 设置线程池的线程数量
    void Set_Thread_Count(const int count) { _server.Set_Sub_Thread_Count(count); }

    // 启动服务器
    void Start() { _server.Start(); }
};

整体测试

基于 HttpServer 模块搭建 http 回显服务器

cpp 复制代码
#include <string>
#include <iostream>
#include <vector>
#include <fstream>
#include "../server.hpp"
#include "http.hpp"

#define WWWROOT "./wwwroot/"

// 将 req 的元素组织成 http 协议的格式
std::string RequestStr(const HttpRequest& req)
{
    std::stringstream ss;
    // 起始行
    ss << req._method << ' ' << req._path << ' ' << req._version << "\r\n";
    // 头部字段
    for(auto& head : req._headers) ss << head.first << ": " << head.second << "\r\n";
    // 空行
    ss << "\r\n";
    // 正文
    ss << req._body;

    return ss.str();
}

void Hello(const HttpRequest& req,HttpResponse* rsp)
{
    rsp->Set_Content(RequestStr(req),"text/plain");
}

void Login(const HttpRequest& req,HttpResponse* rsp)
{
    rsp->Set_Content(RequestStr(req),"text/plain");
}

void Put_File(const HttpRequest& req,HttpResponse* rsp)
{
    rsp->Set_Content(RequestStr(req),"text/plain");
}

void Del_File(const HttpRequest& req,HttpResponse* rsp)
{
    rsp->Set_Content(RequestStr(req),"text/plain");
}



int main()
{

    HttpServer server(8080);
    server.Set_Thread_Count(3);
    server.Set_Base_Dir(WWWROOT);
    server.Add_GET_Handler("/hello",Hello);
    server.Add_POST_Handler("/login",Login);
    server.Add_PUT_Handler("/1234.txt",Put_File);
    server.Add_DELETE_Handler("/1234.txt",Del_File);
    server.Start();

    return 0; 
}

创建 wwwroot 目录,里面再创建一个 index.html 作为首页:

index.html:

html 复制代码
<html>
    <head>
        <meta charset="utf8">
    </head>
    <body>
        <form action="/login" method="post">
            <input type="text" name = "username"><br/> 
            <input type="password" name = "password"><br/> 
            <input type="submit" value="提交" name = "submit">
        </form>
    </body>
</html>

基础测试

使用浏览器访问静态资源根目录,经过服务器处理,访问 wwwroot/index.html。

任意输入,点击提交,服务器回显 /login 请求:

在正文部分可以看到刚才任意输入的字段:

复制代码
username=123456&password=123456&submit=%E6%8F%90%E4%BA%A4

向服务器发送 /hello 请求,服务器回显 http 请求:

在 URL 中附加一些查询字符串,回显的 http 请求也可以看见:

使用 postman向服务器发送 PUT 或 DELETE 请求:

长连接测试

cpp 复制代码
#include "../source/server.hpp"

int main()
{
    Socket client;
    client.Create_client(8080,"127.0.0.1");
    std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";

    while(true)
    {
        assert(client.Send(req.c_str(),req.size()) != -1);
        char buf[1024] = {0};
        assert(client.Recv(buf,1023));
        DBG_LOG("[%s]",buf);
        sleep(3);
    }

    client.Close();
    return 0;
}

向服务器发送请求,包含 Connection: keep-alive 头部字段,服务器在处理完请求后,会检查是否是长连接,如果是则继续监听该连接,如果不是则关闭。

连接超时测试

将服务器的超时时间设置为 10s,客户端代码与上面相同,只是客户端每 15s 向服务器发送一次消息,测试服务器是否会关闭该客户端。

错误请求测试

向服务器发送请求,请求包含 Content-Length=100 头部字段,即告诉服务器正文部分的长度有 100 字节,但实际发送给服务器的正文不足 100 字节,检查服务器的处理结果。

1、如果数据只发送一次,服务器得不到完整请求,就不会进行业务处理,最终超时关闭连接

cpp 复制代码
#include "../source/server.hpp"

int main()
{
    Socket client;
    client.Create_client(8080,"127.0.0.1");
    std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nHello Worled!!!";

    while(true)
    {
        assert(client.Send(req.c_str(),req.size()) != -1);
        char buf[1024] = {0};
        assert(client.Recv(buf,1023));
        DBG_LOG("[%s]",buf);
        sleep(15);
    }

    client.Close();
    return 0;
}

2、连续向服务器发送多次请求,服务器会将后边的请求当做前边的请求的正文处理,后处理就可能因为处理错误而关闭连接

业务处理超时测试

如果业务处理超时(不是非活跃超时),查看服务器的处理情况。当服务器达到了一个性能瓶颈,在一次业务处理中花费了太长的时间(超过了服务器设置的非活跃超时时间) 导致其他的连接也被连累超时

  • 假设现在 12345 描述符就绪了,在处理 1 的时候花费了 30s 处理完,超时了,导致 2345 描述符因为长时间没有刷新活跃度
  1. 如果接下来的 2345 描述符都是通信连接描述符,如果都就绪了,则并不影响,因为接下来就会进行处理并刷新活跃度
  2. 如果接下来的 2 号描述符是定时器事件描述符,定时器触发超时,执行定时任务,就会将 345 描述符给释放掉这时候一旦 345 描述符对应的连接被释放,接下来在处理 345 事件的时候就会导致程序崩溃(内存访问错误)

因此并不能直接对连接进行释放,而应该将释放操作压入到任务池中,等到事件处理完了执行任务池中的任务的时候,再去释放

回顾 connection 模块:

cpp 复制代码
// 实际的释放接口
void Disconnection_In_Loop()
{
    // 1、更新状态为 DISCONNECTED 已关闭状态
    _statu = DISCONNECTED;

    // 2、移除连接的事件监控
    _channel.Remove();

    // 3、关闭套接字
    _socket.Close();

    // 4、取消定时销毁任务(如果有)
    if(_loop->Is_Timer_Task_Exist(_conn_id)) 
    {
        Cancel_Inactive_Release_In_Loop();
    }

    // 5、调用关闭回调函数
    // 先调用组件使用者传递的关闭回调函数
    if(_closed_callback) _closed_callback(shared_from_this());
    // 再调用服务器连接关闭回调函数
    _server_closed_callback(shared_from_this());
}

void Disconnection()
{
    // 将连接的释放操作直接添加到任务队列中,而不是 Run_In_Loop,是为了防止
    // 处理其他连接太久而导致拖累其他链接,如果其他链接被拖累,也要先处理
    // 链接的请求再释放连接。
    _loop->Add_Task(std::bind(&Connection::Disconnection_In_Loop,this));
}

将服务器的超时时间设置为 10s ,对 /hello 的处理故意 sleep(15),观察服务器的处理结果

同时多条请求测试

cpp 复制代码
int main()
{
    Socket client;
    client.Create_client(8080,"127.0.0.1");
    std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
    req += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
    req += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";

    while(true)
    {
        assert(client.Send(req.c_str(),req.size()) != -1);
        char buf[1024] = {0};
        assert(client.Recv(buf,1023));
        DBG_LOG("[%s]",buf);
        sleep(3);
    }

    client.Close();

    return 0;
}

大文件传输测试

在客户端创建一个 300M 的大文件,文件名为 hello.txt

bash 复制代码
dd if=/dev/zero of=./hello.txt bs=100M count=3

使用 put 方法向服务器上传这个文件,服务器会保存文件

cpp 复制代码
int main()
{
    Socket client;
    client.Create_client(8080,"127.0.0.1");
    std::string req = "PUT /1234.txt HTTP/1.1\r\nConnection: keep-alive\r\n";
    std::string body;
    Util::Read_File("./hello.txt",&body);
    req += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n";

    assert(client.Send(req.c_str(),req.size()) != -1);
    assert(client.Send(body.c_str(),body.size()) != -1);
    char buf[1024] = {0};
    assert(client.Recv(buf,1023));
    DBG_LOG("[%s]",buf);
    sleep(3);

    client.Close();

    return 0;
}
cpp 复制代码
void Put_File(const HttpRequest& req,HttpResponse* rsp)
{
    std::string pathname = WWWROOT + req._path;
    Util::Write_File(pathname,req._body);
}

运行客户端之后,服务器 wwwroot 目录下会生成 1234.txt 文件,使用 md5sum 指令计算 hello.txt 和 1234.txt 的 md5 值,如果完全相同说明上传文件功能是没有问题的。

相关推荐
梦難20 分钟前
linux的系统命令
linux·服务器·网络
Java小白笔记22 分钟前
Java中PDF文件导出,生成链路与实现
服务器·网络·oracle
跨境技工小黎24 分钟前
YouTube联盟营销如何变现?如何利用IP代理提高流量变现效果?
服务器·网络·tcp/ip
大模型码小白40 分钟前
Spring AI 框架中集成 MCP 的完整指南:从服务端到客户端的全流程实践
大数据·运维·数据库·人工智能·python·sql·spring
Doraemomo40 分钟前
Linux编程-并发TCP服务器实现与IO多路复用
linux·服务器·tcp/ip
小小测试开发1 小时前
RAG应用评测:从指标体系到LLM-as-a-Judge的自动化落地
android·运维·人工智能·自动化
深念Y1 小时前
# CC-Switch + Claude/Codex 折腾教训记录
运维·服务器·网络·ai·agent·web·ccsiwtch
疯狂小猫咪1 小时前
教培 SaaS vs 定制开发:技术架构与总拥有成本对比
运维
黑泽明*1 小时前
云计算与服务器基础入门指南
服务器·云计算·perl