当前位置: 首页 > news >正文

仿muduo库的高并发服务器

文章目录

  • 前言
  • 项目介绍
  • 前置协议模式和基础功能支持
    • HTTP
    • Reactor
    • c++11 bind
    • 时间轮定时器
    • 正则表达式
    • c++17 通用类型any
    • 日志打印宏
  • 模块功能划分
    • SERVE模块
      • Buffer模块
      • Socket模块
      • Channel模块
      • Connection模块
      • Acceptor模块
      • TimerQueue模块
      • Poller模块
      • EventLoop模块
      • TcpServer模块
      • SERVE模块关系图
    • HTTP协议模块
      • HTTP模块关系图
  • serve
    • Buffer模块代码实现
    • socket模块代码实现
    • Channel模块代码实现
    • Poll模块代码实现
    • Eventloop模块代码实现
    • timerwheel和timefd整合模块代码实现
    • EventLoop模块关系图以及流程梳理
    • Connection模块代码实现
    • Acceptor模块代码实现
    • LoopTread及ThreadPool
    • TcpServe
    • serve总模块关系梳理
  • HTTP协议模块
  • 测试
    • WebBench
    • 长连接测试
    • 超时连接测试
    • 性能压力测试
  • END


前言

这篇博客将会记录并学习高并发服务器这个项目下的结构设计思维和代码细节实现,希望后来者对该项目感兴趣并愿意阅读这篇文章的朋友能有所收获,也希望能给出不足之处借以完善。


项目介绍

muduo 库作为一款基于 C++11 的高性能网络编程库,其功能围绕 “高效处理网络 IO 事件” 和 “简化多线程网络编程复杂度” 展开。由于适合处理大量网络连接和数据交互的场景中应用广泛,该库特别适合在 Linux 环境下开发高并发服务器(如分布式系统、中间件、游戏服务器等)。
而本篇博客便是仿照muduo库来实现高并发服务器组件,可以简洁快速的完成⼀个高性能的服务器搭建。
并且,通过组件内提供的不同的应用层协议支持,也可以快速完成一个高性能服务器的搭建(往下为了便于项目演示,将会在项目中提供HTTP协议的支持)。
由于实现的是⼀个高并发服务器组件,因此当前的项目中并不包含实际的业务内容。但是根据高内聚,低耦合的基本设计,这个组件可以轻而易举地通过后面设置回调函数来增加业务功能。

前置协议模式和基础功能支持

HTTP

htpp协议的具体细节不过多赘述,如想了解可以访问博主曾经对HTTP的总结博客:HTTP协议


需要注意的是HTTP协议是⼀个运行在TCP协议之上的应用层协议,这⼀点本质上是告诉我们,HTTP服务器其实就是个TCP服务器,只不过在应用层基于HTTP协议格式进行数据的组织和解析来明确客户端的请求并完成业务处理。
因此实现HTTP服务器简单理解,只需要以下几步即可

  1. 搭建⼀个TCP服务器,接收客户端请求。
  2. 以HTTP协议格式进行解析请求数据,明确客户端目的。
  3. 明确客户端请求目的后提供对应服务。
  4. 将服务结果⼀HTTP协议格式进行组织,发送给客户端

实现⼀个HTTP服务器很简单,但是实现⼀个高性能的服务器并不简单,这个单元中将基于Reactor模式的高性能服务器实现。

Reactor

Reactor模式,是指通过⼀个或多个输⼊同时传递给服务器进行请求处理时的事件驱动处理模式。
服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式。

简单理解就是使用I/O多路复用统⼀监听事件,收到事件后分发给处理进程或线程,是编写高性能网络服务器的必备技术之⼀。


Reactor模式分类

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

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

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

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

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

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

One Thread One Loop主从Reactor模型高并发服务器
主从Reactor模型服务器,也就是主Reactor线程仅仅监控监听描述符,获取新建连接,保证获取新连接的高效性,提高服务器的并发性能。
主Reactor获取到新连接后分发给⼦Reactor进行通信事件监控。而子Reactor线程监控各自的描述符的读写事件进行数据读写以及业务处理。One Thread One Loop的思想就是把所有的操作都放到⼀个线程中进行,⼀个线程对应⼀个事件处理的循环。
当前实现中,因为并不确定组件使用者的使用意向,因此并不提供业务层⼯作线程池的实现,只实现主从Reactor,而Worker⼯作线程池,可由组件库的使⽤者的需要自行决定是否使用和实现。
在这里插入图片描述


c++11 bind

std::bind 是一个强大的函数绑定工具,用于将函数(或函数对象、成员函数)与部分参数预先绑定,生成一个新的可调用对象(std::function)。它的核心作用是调整函数参数的数量和顺序,实现参数的 “占位” 和 “预绑定”,常用于回调函数、事件处理等场景。

std::bind 定义在< functional > 头文件中,语法格式如下:

  • 函数地址:可以是普通函数、静态成员函数、成员函数(需配合对象指针 / 引用)、lambda 表达式等。
  • 参数列表:可以是具体的值(预绑定),或用 std::placeholders::_1, _2, …(占位符)表示调用时传入的参数。

基本绑定普通函数 / 静态成员函数使用场景演示:

#include <iostream>
#include <functional>
using namespace std;int add(int a, int b) { return a + b; }int main() {// 绑定 add 的第一个参数为 10,第二个参数用 _1 占位(调用时传入)auto add10 = bind(add, 10, placeholders::_1);cout << add10(5) << endl;  // 等价于 add(10, 5),输出 15// 交换参数顺序(将 add(a,b) 转为 add(b,a))auto swap_add = bind(add, placeholders::_2, placeholders::_1);cout << swap_add(3, 4) << endl;  // 等价于 add(4, 3),输出 7return 0;
}

bind将会在后面回调函数的设置上大量使用,甚至三四层的回调bind,极度的降低了函数之间的调用关系程度,虽然还是很绕就是了


时间轮定时器

在当前的高并发服务中,对于一个长时间不通信,并且不关闭,空耗资源的连接,我们不得不去考虑连接的超时关闭问题。
这时就有了对于定时检查的需求,因此需要一个定时任务,定时的将超时过期的连接进行释放。


Linux提供了一个定时器

#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);int timerfd_settime(int fd, int flags, struct itimerspec *new, struct 
itimerspec *old);//函数执行成功返回0,失败返回-1,并设置errnostruct timespec {time_t tv_sec; /* Seconds */long tv_nsec; /* Nanoseconds */};struct itimerspec {struct timespec it_interval; /* 第⼀次之后的往后的每一次周期超时间隔时间 */ struct timespec it_value; /* 第⼀次超时时间 */ };
//定时器会在每次超时时,⾃动给fd中写⼊8字节的数据,表⽰在上⼀次读取数据到当前读取数据期间超时了多少次。

timerfd_create参数解释:

  • clockid: 1. CLOCK_REALTIME-系统实时绝对时间,如果修改了系统时间就会出问题;2. CLOCK_MONOTONIC-从开机到现在的时间是⼀种相对时间
  • flags: 0-默认阻塞属性

timer_settime参数解释:

  • fd: timerfd_create返回的文件描述符
  • flags: 0-相对时间, 1-绝对时间;默认设置为0即可.
  • new: 用于设置定时器的新超时时间
  • old: 若不为 NULL,则会存储定时器上一次的设置值(即调用 timer_settime 前的 itimerspec 配置)。

基础场景演示

#include <iostream>
#include <cstdio>
#include <string>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include <sys/timerfd.h>
#include <sys/select.h>
int main()
{/*创建⼀个定时器 */ int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);struct itimerspec itm;itm.it_value.tv_sec = 3;//设置第⼀次超时的时间 itm.it_value.tv_nsec = 0;itm.it_interval.tv_sec = 3;//第⼀次超时后,每隔多⻓时间超时 itm.it_interval.tv_nsec = 0;timerfd_settime(timerfd, 0, &itm, NULL);//启动定时器 /*这个定时器描述符将每隔三秒都会触发⼀次可读事件*/ time_t start = time(NULL);while(1) {uint64_t tmp;/*需要注意的是定时器超时后,则描述符触发可读事件,必须读取8字节的数据,保存的是⾃
上次启动定时器或read后的超时次数*/ int ret = read(timerfd, &tmp, sizeof(tmp));if (ret < 0) {return -1;}std::cout << tmp << " " << time(NULL) - start << std::endl;}close(timerfd);return 0;
}

时间轮思想

时间轮的思想来源于钟表,如果我们定了⼀个3点钟的闹铃,则当时针⾛到3的时候,就代表时间到了。
同样的道理,如果我们定义了⼀个数组,并且有⼀个指针,指向数组起始位置,这个指针每秒钟向后走动⼀步,走到哪里,则代表哪⾥的任务该被执行了,那么如果我们想要定⼀个3s后的任务,则只需要将任务添加到tick+3位置,则每秒中走⼀步,三秒钟后tick走到对应位置,这时候执行对应位置的任务即可。

  1. 缺陷一:在每一个数组位置代表1s情况下,如果定义一个60s后的任务,则需要将数组的元素个数设置为60才可以,如果设置⼀小时后的定时任务,则需要定义3600个元素的数组,这样无疑是比较麻烦的。

  2. 缺陷二:同⼀时间可能会有大批量的定时任务,但一个数组位置只能放置一个任务

关于上述问题我们可以采用多层时间轮来解决缺陷一,如设置秒级时间轮,分级时间轮,时级时间轮,60<time<3600则time/60就是分针轮对应存储的位置,当tick/3600等于对应位置的时候,将其位置的任务向分针,秒针轮进行移动。
当前应用则不需要那么麻烦,只需要关注缺陷二即可。

缺陷二只需要给数组对应位置下拉⼀个数组,即构成哈希桶结构,这样就可以在同⼀个时刻上添加多个定时任务了。
在这里插入图片描述

但是,我们也得考虑⼀个问题,当前的设计是时间到了,则主动去执⾏定时任务,释放连接,那能不能在时间到了后,自动执行定时任务呢,这时候我们就想到⼀个操作-类的析构函数。
⼀个类的析构函数,在对象被释放时会自动被执行(本质交给编译器来解决,非常典型的RAII思想),那么我们如果将⼀个定时任务作为⼀个类的析构函数内的操作,则这个定时任务在对象被释放的时候就会执行。
但是仅仅为了这个目的,而设计⼀个额外的任务类,好像有些不划算,但是,这⾥我们又要考虑另⼀个问题,那就是假如有⼀个连接建立成功了,我们给这个连接设置了⼀个30s后的定时销毁任务,但是在第10s的时候,这个连接进行了⼀次通信,那么我们应该时在第30s的时候关闭,还是第40s的时候关闭呢?无疑应该是第40s的时候。也就是说,这时候,我们需要让这个第30s的任务失效

该如何实现这个操作呢?
这里,我们就用到了智能指针shared_ptr,shared_ptr有个计数器,当计数为0的时候,才会真正释放⼀个对象,那么如果连接在第10s进行了⼀次通信,则我们继续向定时任务中,添加⼀个30s后(也就是第40s)的任务类对象的shared_ptr,则这时候两个任务shared_ptr计数为2,则第30s的定时任务被释放的时候,计数-1,变为1,并不为0,则并不会执行实际的析构函数,那么就相当于这个第30s的任务失效了,只有在第40s的时候,这个任务才会被真正释放。但是真正管理这个指针的是weak_ptr,主要原因便是两个shared_ptr对同一个原始对象构造的时候,两者并不会共享计数,这时候我们就需要一个结构在保存原始对象的同时不会对引用计数造成影响,同时还可以操作原始对象形成共享计数的智能指针。

//时间轮定时器
#include<iostream>
#include<functional>
#include<memory>
#include<vector>
#include<unordered_map>
#include<cstdint>
#include<unistd.h>
using FuncTask=std::function<void()>;
using ReleaseFunc=std::function<void()>;
class TimerTask
{
private:uint64_t _id;//定时器任务的对象编号uint32_t _timeout;//定时器超时时间bool _cancel;FuncTask _task_cb;//定时器要执行的任务ReleaseFunc _rf;//用来删除定时任务
public:TimerTask(uint64_t id,uint32_t timeset,FuncTask cb):_id(id), _timeout(timeset), _task_cb(cb),_cancel(false) {}~TimerTask(){if(_cancel==false)  _task_cb();  _rf();}void SetRealse(ReleaseFunc cb){_rf=cb;}void Cancel(){_cancel=true;}uint32_t DelayTime(){return _timeout;}
};class TimerWheel
{
private:using WeakTask=std::weak_ptr<TimerTask>;using PtrTask=std::shared_ptr<TimerTask>;int _tick;//指针,指到哪里哪里执行任务int _capacity;//表盘最大数量,最大延迟时间std::vector<std::vector<PtrTask>> _wheel;std::unordered_map<uint64_t,WeakTask> _timers;
private:void RemoveTimer(uint64_t id){auto it=_timers.find(id);if(it!=_timers.end()){_timers.erase(it);}}
public:TimerWheel():_capacity(60),_tick(0),_wheel(_capacity){}void TimerAdd(uint64_t id,uint32_t delay,FuncTask cb)//新增定时任务{PtrTask pt=std::make_shared<TimerTask>(id,delay,cb);pt->SetRealse(std::bind(&TimerWheel::RemoveTimer,this,id));_timers[id]=WeakTask(pt);int pos=(_tick+pt->DelayTime())%_capacity;_wheel[pos].push_back(pt);};void TimerRefresh(uint64_t id)//刷新定时任务,当有新连接到来,通过weakptr找到对应id生成一个shared令count++;{auto it=_timers.find(id);if(it==_timers.end()){return;}PtrTask pt=it->second.lock();//获取对应weak的shared,对应count++int pos=(_tick+pt->DelayTime())%_capacity;_wheel[pos].push_back(pt);};void RunTimerTask(){_tick=(_tick+1)%_capacity;_wheel[_tick].clear();//清空当前表盘刻度下的shared,自动执行析构函数}void TimerCancel(uint64_t id){auto it=_timers.find(id);if(it==_timers.end()){return;}PtrTask pt=it->second.lock();if(pt) pt->Cancel();}
};

正则表达式

正则表达式(Regular Expression,简称 regex 或 regexp)是一种用于匹配、查找、替换字符串的强大工具,通过特定的模式(Pattern)来描述字符序列的规则。
正则表达式的使用,可以使得HTTP请求的解析更加简单(这⾥指的时程序员的工作变得的简单,这并不代表处理效率会变高,实际上效率上是低于直接的字符串处理的),使我们实现的HTTP组件库使用起来更加灵活。

在 C++ 中,正则表达式的支持主要通过 C++11 引入的 < regex > 头文件实现,提供了模式匹配、搜索、替换等功能。

  1. 头文件核心组件
  • std::regex 存储正则表达式模式的对象,构造时需传入模式字符串(支持原始字符串 R"(…)" 避免转义符冲突)。
  • std::smatch 用于存储匹配结果的容器(针对字符串 std::string),类似的还有 std::cmatch(针对 C 风格字符串 const char*)。
  1. 核心函数参数以及解释
// 针对 std::string 类型
bool regex_match(const std::string& s, std::smatch& m, const std::regex& e, //std::regex_constants::match_flag_type flags = std::regex_constants::match_default);
  1. 第一个参数:待匹配的字符串

    • 类型:const std::string&(或 C 风格字符串 const char*,对应 cmatch 版本)。
    • 含义:需要被检查的目标字符串。
  2. 第二个参数(可选):匹配结果容器

    • 类型:std::smatch&(针对 std::string)或 std::cmatch&(针对 C 字符串)。
    • 含义:如果提供该参数,匹配成功后会将结果(包括整个匹配串、分组内容等)存储到容器中。
  3. 第三个参数:正则表达式模式

    • 类型:const std::regex&。
    • 含义:预定义的正则表达式对象(需包含匹配规则)。

具体细节不多介绍,具体规则在使用中体现并解释,以下是简单示例

#include <iostream>
#include <string>
#include <regex>int main()
{std::string str = "/numbers/1234";//匹配以 /numbers/起始,后边跟了一个或多个数字字符的字符串,并且在匹配的过程中提取这个匹配到的数字字符串std::regex e("/numbers/(\\d+)");std::smatch matches;bool ret = std::regex_match(str, matches, e);if (ret == false) {return -1;}for (auto &s : matches) {std::cout << s << std::endl;}return 0;
}

运行结果
在这里插入图片描述
对于存储匹配到的字符串的matches,有以下几点需要注意:

  • m[0] 始终表示整个匹配的字符串。
  • 若正则表达式中有分组(()),m[1]、m[2]… 依次表示第 1、2… 个分组的内容。
  • 若匹配失败,容器内容未定义。
  • 在正则表达式中,分组之间的匹配是顺序执行的,即前一个分组匹配完成后,下一个分组会从前一个分组匹配结束的位置继续向后匹配

正则表达式提取HTTP,其中困难点主要在于匹配规则的理解:

#include <iostream>
#include <string>
#include <regex>int main()
{//HTTP请求行格式:  GET /xusen/login?user=xiaoming&pass=123123 HTTP/1.1\r\nstd::string str = "get /xusen/login?user=xiaoming&pass=123123 HTTP/1.1\r\n";std::smatch matches;//请求方法的匹配  GET HEAD POST PUT DELETE ....std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);//GET|HEAD|POST|PUT|DELETE   表示匹配并提取其中任意一个字符串//[^?]*     [^?]匹配非问号字符, 后边的*表示0次或多次,空格表示结束当前字符串的匹配//\\?(.*)   \\?  表示原始的?字符 (.*)表示提取?之后的任意字符0次或多次,直到遇到空格//HTTP/1\\.[01]  表示匹配以HTTP/1.开始,后边有个0或1的字符串//(?:\n|\r\n)?   (?: ...) 表示匹配某个格式字符串,但是不提取, 最后的?表示的是匹配前边的表达式0次或1次bool ret = std::regex_match(str, matches, e);if (ret == false) {return -1;}std::string method = matches[1];std::transform(method.begin(), method.end(), method.begin(), ::toupper);std::cout << method << std::endl;for (int i = 0; i < matches.size(); i++) {std::cout << i << " : ";std::cout << matches[i] << std::endl;}return 0;
}

运行结果
在这里插入图片描述

c++17 通用类型any

每⼀个Connection对连接进行管理,最终都不可避免需要涉及到应用层协议的处理,因此在Connection中需要设置协议处理的上下文来控制处理节奏。但是应用层协议数不胜数,为了降低耦合度,这个协议接收解析上下文就不能有明显的协议倾向,它可以是任意协议的上下文信息,因此就需要⼀个灵活可变通用的类型来保存各种不同的数据结构。
C语言中,通⽤类型可以使用void*来管理,但是在C++中,boost库和C++17给我们提供了⼀个通用类型any来灵活使用,如果考虑增加代码的移植性,尽量减少第三方库的赖,则可以使用C++17特性中的any,或者自己来实现。

具体可总结为以下两点:

  1. 每个连接必须有一个请求接收与解析的上下文
  2. 上下文的类型或者说结构格式不能固定,因为服务器可能会一直增多支持的协议类型。不同协议可能有不同的上下文

因此:需要一个容器,来接纳不同的类型数据结构

ANY类总体设计思想:定义一any类,在里面嵌套子类和父类,子类存储具体的数据,而父类则通过指针指向子类,以此便可以指向不同子类,实现子类数据结构的操作和方法。
需要注意的点:any类的构造实现,以及赋值运算符的两种重载都基于对成员变量的交换和返回自身对象引用以便于连续赋值运算

#include <iostream>
#include <typeinfo>
#include <assert.h>
class any
{
private:class holder{public:virtual const std::type_info &type() = 0; // 获取子类保存的数据类型virtual holder *clone() = 0;              // 针对当前对象自身,clone出新的子类对象virtual ~holder() {};};template <typename T>class placeholder : public holder{public:placeholder(const T &val) : _val(val) {}virtual const std::type_info &type() { return typeid(T); }    // 获取子类保存的数据类型virtual holder *clone() { return new placeholder<T>(_val); }; // 针对当前对象自身,clone出新的子类对象;public:T _val;};holder *_content;public:any() : _content(NULL) {}template <class T>any(const T &val) : _content(new placeholder<T>(val)) {}any(const any &other) : _content(other._content ? other._content->clone() : NULL) {}~any() { delete _content; }template <typename T>T *get(){assert(typeid(T) == _content->type());return &((placeholder<T> *)_content)->_val;};any &swap(any &other){std::swap(_content, other._content);return *this;}// 赋值重载template <typename T>any &operator=(const T &val){// 为当前val构造any临时对象并做交换,此时原先指针被交换到临时变量里面,当对象释放的时候指针也被释放any(val).swap(*this);return *this;};any &operator=(const any &other){any(other).swap(*this);return *this;};
};

日志打印宏

由于对代码错误或者关键信息的排查,日志是不可避免的,引入日志模式也不需要写个特别复杂的模块出来,只需要定义简单宏变量即可

#define INFO 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
#define LEVEL_LOG(format,...) LOG(INFO,format,...)

主要陌生点在于时间的获取,并且需要了解##VA_ARGS 是 C/C++ 中可变参数宏(Variadic Macros) 的语法,用于在宏定义中处理数量不定参数,关键点在于日志等级的定义以及是否打印的控制

模块功能划分

在对功能模块的学习之前,还请抱着了解的心思来看,如果对各个模块的功能和思想感到模糊,请别烦恼,实际该结合代码才能深刻了解每个模块的意义,因此功能模块的划分仅仅是将大体框架展现,真正的实现细节在后面代码中才能体现

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

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

SERVE模块

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

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

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

Buffer模块

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

Socket模块

Socket模块是对套接字操作封装的⼀个模块,主要实现的socket的各项操作。

Channel模块

Channel模块是对⼀个描述符需要进行的IO事件管理的模块,实现对描述符可读,可写,错误…事件的管理操作,以及Poller模块对描述符进行IO事件监控就绪后,根据不同的事件,回调不同的处理函数功能。

Connection模块

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

  • Connection模块内部包含有三个由组件使用者传入的回调函数:连接建立完成回调,事件回调,新数据回调,关闭回调。
  • Connection模块内部包含有两个组件使⽤者提供的接口:数据发送接口,连接关闭接口
  • Connection模块内部包含有两个用户态缓冲区:用户态接收缓冲区,用户态发送缓冲区
  • Connection模块内部包含有⼀个Socket对象:完成描述符⾯向系统的IO操作
  • Connection模块内部包含有⼀个Channel对象:完成描述符IO事件就绪的处理

具体处理流程如下:

  1. 实现向Channel提供可读,可写,错误等不同事件的IO事件回调函数,然后将Channel和对应的描述符添加到Poller事件监控中。
  2. 当描述符在Poller模块中就绪了IO可读事件,则调用描述符对应Channel中保存的读事件处理函数,进行数据读取,将socket接收缓冲区全部读取到Connection管理的用户态接收缓冲区中。然后调用由组件使用者传入的新数据到来回调函数进行处理。
  3. 组件使⽤者进行数据的业务处理完毕后,通过Connection向使⽤者提供的数据发送接口,将数据写⼊Connection的发送缓冲区中。
  4. 启动描述符在Poll模块中的IO写事件监控,就绪后,调用Channel中保存的写事件处理函数,将发送缓冲区中的数据通过Socket进行面向系统的实际数据发送。

Acceptor模块

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

  • Acceptor模块内部包含有⼀个Socket对象:实现监听套接字的操作
  • Acceptor模块内部包含有⼀个Channel对象:实现监听套接字IO事件就绪的处理
    具体处理流程如下:
  1. 实现向Channel提供可读事件的IO事件处理回调函数,函数的功能其实也就是获取新连接
  2. 为新连接构建⼀个Connection对象出来。

TimerQueue模块

TimerQueue模块是实现固定时间定时任务的模块,可以理解就是要给定时任务管理器,向定时任务管理器中添加⼀个任务,任务将在固定时间后被执行,同时也可以通过刷新定时任务来延迟任务的执行。
这个模块主要是对Connection对象的⽣命周期管理,对⾮活跃连接进行超时后的释放功能。
TimerQueue模块内部包含有⼀个timerfd:linux系统提供的定时器。
TimerQueue模块内部包含有⼀个Channel对象:实现对timerfd的IO时间就绪回调处理

Poller模块

Poller模块是对epoll进行封装的⼀个模块,主要实现epoll的IO事件添加,修改,移除,获取活跃连接功能。

EventLoop模块

EventLoop模块可以理解就是我们上边所说的Reactor模块,它是对Poller模块,TimerQueue模块,Socket模块的⼀个整体封装,进行所有描述符的事件监控。
EventLoop模块必然是⼀个对象对应⼀个线程的模块,线程内部的目的就是运行EventLoop的启动函数。
EventLoop模块为了保证整个服务器的线程安全问题,因此要求使⽤者对于Connection的所有操作⼀定要在其对应的EventLoop线程内完成,不能在其他线程中进行(比如组件使用者使用Connection发送数据,以及关闭连接这种操作)。
EventLoop模块保证⾃⼰内部所监控的所有描述符,都要是活跃连接,⾮活跃连接就要及时释放避免资源浪费。

  • EventLoop模块内部包含有⼀个eventfd:eventfd其实就是linux内核提供的⼀个事件fd,专门用于事件通知。
    • EventLoop模块内部包含有⼀个Poller对象:用于进行描述符的IO事件监控。
    • EventLoop模块内部包含有⼀个TimerQueue对象:用于进行定时任务的管理。
    • EventLoop模块内部包含有⼀个PendingTask队列:组件使用者将对Connection进行的所有操作,
    都加⼊到任务队列中,由EventLoop模块进⾏管理,并在EventLoop对应的线程中进行执行。
    • 每⼀个Connection对象都会绑定到⼀个EventLoop上,这样能保证对这个连接的所有操作都是在⼀个线程中完成的。

具体操作流程:

  1. 通过Poller模块对当前模块管理内的所有描述符进行IO事件监控,有描述符事件就绪后,通过描述符对应的Channel进行事件处理。
  2. 所有就绪的描述符IO事件处理完毕后,对任务队列中的所有操作顺序进行执行。
  3. 由于epoll的事件监控,有可能会因为没有事件到来⽽持续阻塞,导致任务队列中的任务不能及时得到执行,因此创建了eventfd,添加到Poller的事件监控中,用于实现每次向任务队列添加任务的时候,通过向eventfd写入数据来唤醒epoll的阻塞。

TcpServer模块

这个模块是⼀个整体Tcp服务器模块的封装,内部封装了Acceptor模块,EventLoopThreadPool模块。

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

具体操作流程如下:

  1. 在实例化TcpServer对象过程中,完成BaseLoop的设置,Acceptor对象的实例化,以及EventLoop线程池的实例化,以及std::shared_ptr的hash表的实例化。
  2. 为Acceptor对象设置回调函数:获取到新连接后,为新连接构建Connection对象,设置Connection的各项回调,并使⽤shared_ptr进⾏管理,并添加到hash表中进⾏管理,并为Connection选择⼀个EventLoop线程,为Connection添加⼀个定时销毁任务,为Connection添加事件监控,
  3. 启动BaseLoop

SERVE模块关系图

在这里插入图片描述

在这里插入图片描述

HTTP协议模块

HTTP协议模块用于对高并发服务器模块进行协议支持,基于提供的协议支持能够更方便的完成指定协议服务器的搭建。


  • Util模块
    这个模块是一个工具模块,主要提供HTTP协议模块所用到的一些工具函数,比如url编解码,文件读写…等等。

  • HttpRequest模块
    HTTP请求数据模块,用于保存HTTP请求数据被解析后的各项请求元素信息。

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

  • HttpContext
    HTTP请求接收的上下文模块,主要为了防止在一次接收的数据中,不是一个完整的HTTP请求,则解析过程并未完成,无法进行完整的请求处理,需要在下次接收到新数据后继续根据上下文进行解析,最终会得到一个HttpRequest请求信息对象,因此需要在请求数据的接收以及解剖部分需要一个上下文来进行接收和处理节奏。

  • HttpServe模块
    这个模块就类似于Tcpserve了,是集合上面几个模块最终给组件使用者提供的HTTP服务器模块了,用于简单的接口实现HTTP服务器的搭建。

    1. HttpServe模块内部包含有一个 Tcpserve对象:Tcpserve对象实现服务器的搭建
    2. HttpServe模块内部包含有两个提供给TcpServe对象的接口:连接建立成功设置上下文接口,数据处理接口
    3. HttpServe模块内部包含有一个hashmap表存储请求和处理函数的映射表;组件使用者向HttpServe设置那些请求应该使用那些函数进行处理,等TcpServe收到对应的请求就会使用对应的函数进行处理。

    HTTP模块关系图

    在这里插入图片描述

serve

以下每个模块代码编译和测试均以通过不再展示,仅展示代码和编译过程中遇到的问题,如感兴趣可自行仿写或拷贝代码进行编译测试。


Buffer模块代码实现

buffer模块就是一个缓冲区模块,所提供的功能:存储数据,取出数据

  • 实现思想:缓冲区采用vector< char >,提供线性内存空间
  • 要素
    1.初始默认的空间大小
    2.当前读取数据位置
    3.当前写入数据位置
    4.通过对写偏移和读偏移指针来控制数据的读出和写入
    5.偏移为相对偏移,均存在于初始空间的首地址上

操作:

  1. 写入数据:当前写入位置指向哪里,就从哪里开始写入 。
    需考虑后续空间剩余,足够:将数据移动到起始位置。不够:扩容到合适空间大小。
    数据写入成功,写位置偏移。
  2. 读取数据:读位置指向哪里从哪里开始读取,可读数据大小为写位置减去读位置。

代码实现:

#include <iostream>
#include <vector>
#include<string.h>
#include <assert.h>
#define BUFFER_DEFAULT_SIZE 1024
class Buffer
{
private:std::vector<char> _buffer; // 缓冲区uint64_t _reader_idx;      // 读偏移,偏移为相对缓冲区偏移uint64_t _writer_idx;      // 写偏移
public:Buffer() : _buffer(BUFFER_DEFAULT_SIZE), _reader_idx(0), _writer_idx(0) {}char *Begin() { return &*_buffer.begin(); }// 获取当前写入位置char *WriterPosition() { return Begin() + _writer_idx; }// 获取当前读取位置char *ReaderPosition() { return Begin() + _reader_idx; }// 获取前沿空间大小uint64_t HeadIdelSize() { return _reader_idx; }// 获取后沿空间大小uint64_t TailIdelSize() { return _buffer.size() - _writer_idx; }// 获取可读数据大小uint64_t ReadAbleSize() { return _writer_idx - _reader_idx; }// 将读偏移向后移动void MoveReadOffset(uint64_t len){if (len == 0)return;// 向后移动的大小必须小于可读数据大小,这样一来rpos一定一直小于wposassert( len <= ReadAbleSize());_reader_idx += len;}// 将写偏移向后移动void MoveWriteOffset(uint64_t len){assert(len <= TailIdelSize());_writer_idx += len;}// 确保空间足够void EnsureWriteSpace(uint64_t len){if (len <= TailIdelSize())return;// 末尾空间不够移动到前面,判断前后空间之和是否足够if (len <= TailIdelSize() + HeadIdelSize()){uint64_t ras = ReadAbleSize();std::copy(ReaderPosition(), ReaderPosition() + ras, Begin());_reader_idx = 0; // 读位置归零,写位置归数据大小_writer_idx = ras;}else{// 总体空间不够,扩容在原写偏移上_buffer.resize(_writer_idx + len);}}// 读数据void Read(void *buf, uint64_t len){assert(len <= ReadAbleSize());std::copy(ReaderPosition(), ReaderPosition() + len, (char*)buf);}void ReadAndPop(void *buf, uint64_t len){Read(buf, len);MoveReadOffset(len);}std::string ReadAsString(uint64_t len){assert(len <= ReadAbleSize());std::string str;str.resize(len);Read(&str[0], len); // 直接用str.c_Str的类型为const char*,这里用重载获得首元素地址return str;}std::string ReadAsStringAndPop(uint64_t len){std::string str=ReadAsString(len);MoveReadOffset(len);return str;}// 写数据void Write(const void *data, uint64_t len){// 保证有足够的空间写入进去EnsureWriteSpace(len);const char *d = (const char *)data;std::copy(d, d + len, WriterPosition());}void WriteAndPush(const void *data, uint64_t len){Write(data, len);MoveWriteOffset(len);}void WriteString(const std::string &data){Write(data.c_str(), data.size());}void WriteStringAndPush(const std::string &data){WriteString(data);MoveWriteOffset(data.size());}void WriteBuffer(Buffer &data){return Write(data.ReaderPosition(), data.ReadAbleSize());}void WriteBufferAndPush(Buffer &data){WriteBuffer(data);MoveWriteOffset(data.ReadAbleSize());}char* FindCRLF(){void* res=memchr(ReaderPosition(),'\n',ReadAbleSize());return (char*)res;}std::string GetLine(){char* pos=FindCRLF();if(pos==NULL){return "";}return ReadAsString(pos-ReaderPosition()+1);//将换行符也提取出来}std::string GetLineAndPop(){std::string str=GetLine();MoveReadOffset(str.size());return str;}// 清空缓冲区void Clear(){// 偏移量归零,覆盖写入即可_reader_idx = 0;_writer_idx = 0;}
};

在写buffer代码时遇到了四个问题

  1. 在read函数里面,对copy的使用最后一个输出型参数buf类型的是void*,由于对copy函数的不熟悉,编译器报错一点也看不懂,把报错喂给了豆包,显示copy函数使用了void * 最后对类型强转(char*)即可
  2. \写成了/报waring很容易找到了问题。
  3. assert断言对区间的判定应该是开区间
  4. 对于前后沿空间的逻辑判断出错

socket模块代码实现

socket模块的功能是搭建操作系统和网络之间的桥梁

实现思想:依赖于套接字描述符操作
关键点:

  1. 创建套接字的时候需要设置套接字选项,开启地址端口重用
  2. 为了配合后面的epoll模型的使用,套接字需要设置为非阻塞
#define MAX_LINEN 1024
class Socket
{
private:int _sockfd;
public:Socket():_sockfd(-1){}Socket(int fd):_sockfd(fd){}~Socket(){}int Fd(){return _sockfd;}//创建套接字bool Create(){//int socket(int domain,int type,int protocol)_sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);if(_sockfd<0){ERR_LOG("CREATE SOKCET ERR");return false;}return true;};//绑定地址bool Bind(uint16_t port,const std::string &ip){struct sockaddr_in addr;addr.sin_family=AF_INET;addr.sin_port=htons(port);addr.sin_addr.s_addr=inet_addr(ip.c_str());socklen_t len=sizeof(struct sockaddr_in);int ret=bind(_sockfd,(struct sockaddr*)&addr,len);if(ret<0){ERR_LOG("BIND SOKCET ERR");return false;}return true;};//监听bool Listen(int backlog=MAX_LINEN){int ret=listen(_sockfd,backlog);if(ret<0){ERR_LOG("LISTEN SOKCET ERR");return false;}return true;}//连接bool Connect(const std::string& ip,uint16_t port){struct sockaddr_in addr;addr.sin_family=AF_INET;addr.sin_port=htons(port);addr.sin_addr.s_addr=inet_addr(ip.c_str());socklen_t len=sizeof(struct sockaddr_in);int ret=connect(_sockfd,(struct sockaddr*)&addr,len);if(ret<0){ERR_LOG("CONNECT SOKCET ERR");return false;}return true;};//获取新连接int Accept(){//int accept(int sockfd,struct sockaddr* addr,socklen_t* len)int newfd=accept(_sockfd,NULL,NULL);if(newfd<0){ERR_LOG("SOCKET ACCEPT ERR");return 1;}return newfd;};//接收数据ssize_t Recv(void* buf,size_t len,int flag=0){//ssize_t recv(int sockfd,void* buf,size_t len,int flag)ssize_t ret=recv(_sockfd,buf,len,flag);//EAGAIN 当前socket的接收缓冲区没有数据了,非阻塞情况下的错误//EINTR 当前socket阻塞等待,被信号打断if(ret<=0){if(errno==EAGAIN||errno==EINTR){return 0;//表示这次没有接收到数据}ERR_LOG("SOCKET RECV FAILED!");return -1;}return ret;};ssize_t NonBlockRecv(void* buf,size_t len){return Recv(buf,len,MSG_DONTWAIT);//MSG_DONTWAIT表示非阻塞}//发送数据ssize_t Send(const void* buf,size_t len,int flag=0){ssize_t ret=send(_sockfd,buf,len,flag);if(ret<0){ERR_LOG("SOCKET SEND FAILED!");return -1;}return ret;};ssize_t NonBlockSend(void* buf,size_t len){return Send(buf,len,MSG_DONTWAIT);}//创建一个服务器端连接bool CreateServer(uint16_t port,const std::string& ip="0.0.0.0",bool block_flag=false){//1.创建套接字设置为非阻塞 2.绑定 3.监听 4.地址复用 if(Create()==false) return false;if(block_flag==true) NonBlock();if(Bind(port,ip)==false) return false;if(Listen()==false) return false;ReuseAddress();return true;};//创建一个客户端连接bool CreateClient(uint16_t port,const std::string&ip){//创建套接字 连接if(Create()==false) return false;if(Connect(ip,port)==false) return false;return true;};//开启地址端重用void ReuseAddress(){// int setsockopt(int fd, int leve, int optname, void *val, int vallen)int val = 1;setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void*)&val, sizeof(int));val = 1;setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void*)&val, sizeof(int));};//设置为非阻塞void NonBlock(){//int fcntl(int fd, int cmd, ... /* arg */ );int flag = fcntl(_sockfd, F_GETFL, 0);fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);};//关闭套接字void Close(){close(_sockfd);};
};

socket模块的实现属于是老生常谈了,虽然博主还是有很多模糊的地方,不过在编译过程中没有遇到错误,除了测试的时候buffer字符串没有初始化置零导致send和recv的字符串有乱码。

Channel模块代码实现

对于channel类,其关键点在于epoll模型对事件的监控,因此这个模块是epoll的上层,主要用于管理epoll下的函数,因此后面联调才会对功能进行测试,当前只做编译通过测试

实现思想:对事件描述符进行管理,需要监控以及需要触发事件描述符管理,设置回调函数对触发事件的文件描述符进行回调执行相关任务
关键点:

  1. 了解都有那些事件
  2. 触发的事件由hander处理
  3. 回调函数_event_callback 作为 “无论什么事件都调用的回调函数”,其设计目的是提供一个通用的事件处理入口,用于处理所有事件共有的逻辑,或者作为事件触发后的 “兜底” 操作。
class Poller;
class EventLoop;
class Channel
{
private:EventLoop *_loop;int _fd;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(EventLoop *loop, int fd) : _fd(fd), _events(0), _revents(0), _loop(loop) {};int Fd() { return _fd; }uint32_t Events() { return _events; } // 获取想要监控的事件void SetREvents(uint32_t events) { _revents = events; }void SetReadCallback(const EventCallback &cb) { _read_callback = cb; };void SetWriteCallback(const EventCallback &cb) { _write_callback = cb; };void SetErrorCallback(const EventCallback &cb) { _error_callback = cb; };void SetCloseCallback(const EventCallback &cb) { _close_callback = cb; };void SetEventCallback(const EventCallback &cb) { _event_callback = cb; };bool ReadAble() { return _revents & EPOLLIN; };   // 当前是否可读bool WriteAble() { return _revents & EPOLLOUT; }; // 当前是否可写void EableRead(){_events |= EPOLLIN;Updata();}; // 启动读事件监控void EableWrite(){_events |= EPOLLOUT;Updata();}; // 启动写事件监控void DisableRead(){_events &= ~EPOLLIN;Updata();}; // 关闭读事件监控void DisableWrite(){_events &= ~EPOLLOUT;Updata();}; // 关闭写事件监控void DisableAll(){_events = 0;Updata();}; // 关闭所有事件监控void Remove(); // 移除监控void Updata();void HandleEvent(){if ((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI)){if (_read_callback){_read_callback();}}// 又可能释放连接的操作一次只触发一个if (_revents & EPOLLOUT){if (_event_callback)_event_callback();if (_write_callback)_write_callback();}else if (_revents & EPOLLERR){if (_event_callback)_event_callback();if (_error_callback)_error_callback();}else if (_revents & EPOLLHUP){if (_event_callback)_event_callback();if (_close_callback)_close_callback();}}; // 事件处理,一旦连续触发事件,调用这个函数,自己决定事件如何处理
};

channel相当于一个管理函数,因此除去代码中注释的点,好像也没有特别需要注意的点了

Poll模块代码实现

描述符IO事件监控模块,通过对epoll的使用使文件描述符达到多路复用

功能:添加/修改文件描述符的监控(不存在添加,存在则修改),移除文件描述符的监控

实现思想:

  1. 拥有epoll权柄
  2. 拥有struct epoll_event结构数组监控时保存所有的活跃数组
  3. 使用hash表管理描述符和描述符对应的事件管理Channel对象

逻辑流程:

  1. 监控文件描述符
  2. 通过文件描述符找到channel并返回就绪的文件描述符对应channel
#define MAX_EPOLLEVENTS 1024
class Poller
{
private:int _epfd;struct epoll_event _evs[MAX_EPOLLEVENTS];std::unordered_map<int, Channel *> _channels;private:// 对epoll直接操作void Updata(Channel *channel, int opt){int fd = channel->Fd();struct epoll_event ev;ev.data.fd = fd;ev.events = channel->Events();int ret = epoll_ctl(_epfd, opt, fd, &ev);if (ret < 0){ERR_LOG("EPOLL CTL ERROR");abort(); // 退出程序}return;};bool HashChannel(Channel *channels){auto it = _channels.find(channels->Fd());if (it == _channels.end()){return false;}return true;}public:Poller(){_epfd=epoll_create(MAX_EPOLLEVENTS);if(_epfd<0){ERR_LOG("EPOLL CREATE ERR");abort();}};// 添加或者修改监控文件void UpdtaEvent(Channel *channels){bool ret = HashChannel(channels);if (ret == false)//添加{_channels[channels->Fd()]=channels;Updata(channels, EPOLL_CTL_ADD);}Updata(channels, EPOLL_CTL_MOD);}// 移除监控void RemoveEvent(Channel *channels){auto it = _channels.find(channels->Fd());if (it != _channels.end()){_channels.erase(it);}Updata(channels, EPOLL_CTL_DEL);}// 开始监控返回活跃连接void Poll(std::vector<Channel*>* active){int nfs=epoll_wait(_epfd,_evs,MAX_EPOLLEVENTS,-1);if(nfs<0){if(errno==EINTR){return;}ERR_LOG("EPOLL WAIT ERR,%s",strerror(errno));abort();}for(int i=0;i<nfs;i++){auto it =_channels.find(_evs[i].data.fd);assert(it!=_channels.end());it->second->SetREvents(_evs[i].events);//设置实际就绪的事件active->push_back(it->second);}}
};
void Channel::Remove(){ _poller->RemoveEvent(this);};
void Channel::Updata(){_poller->UpdtaEvent(this);}    

这个模块的实现并不是一帆风顺的,编译上并没有出很大的错误,但是有一处逻辑判断出错导致找了很久,在remove里面对成员函数_channels的迭代器获取之后,判断是这个迭代器是否存在,!=写成了==,然后直接跳过了if语句,导致这个成员变量里面的成员一直存在。其他 的好像也没啥了,就是测试的时候变量定义不规范,注册回调注册错了,导致找了一下午的bug。


Eventloop模块代码实现

eventloop是和线程一一关联的,主要任务便是事件的监控,并且对事件进行处理

监控一个连接,当这个连接有事件就绪,就进行处理,但是如果描述符在多个事件中触发事件处理,就会有线程安全问题。
因此这里需要将一个连接的事件监控,以及事件处理,还有其他操作都放到同一个线程中进行。


eventfd
eventfd 是 Linux 系统提供的一种用于进程间或线程间事件通知的机制,它通过一个文件描述符(fd)传递事件。

这里的用处便是在EventLoop模块中实现线程间的事件通知功能。

信号机制不也可以传递事件通知吗?以下是两者的对比

维度eventfd信号(Signal)
本质基于文件描述符(fd)的同步事件通知机制,依赖 I/O 多路复用。内核向进程发送的异步中断信号,用于处理异常或异步事件。
触发方式由用户态主动写入数据(write)触发,是 “主动通知”。由内核或其他进程通过 kill/sigqueue 发送,是 “被动接收”。
通知内容传递一个 64 位无符号整数(计数器),可携带简单数值信息。仅传递信号编号(如 SIGINT、SIGUSR1),无额外数据(除实时信号外)。
适用场景线程间 / 进程间的同步通知(如线程池任务完成、异步操作回调)。处理异常(如 SIGSEGV)、外部中断(如 Ctrl+C)、定时事件(SIGALRM)等。

函数原型:

int eventfd(unsigned int initval,int flags);

功能:创建一个eventfd对象,实现事件通知
参数:
initval:计数初值
flags:EFD_CLOEXEC --禁止进程复制 EFD_NONBLOCK --启动非阻塞属性
返回值:返回一个文件描述符用于操作

eventfd 可将非 I/O 事件(如线程间同步、任务完成通知)转换为 “文件描述符事件”,即定义eventfd并交给channel对象管理。这样一来所有事件(I/O 事件 + 用户态事件)都能通过同一个 epoll_wait 等待和处理,无需单独维护多个事件循环(如一个循环处理 I/O,另一个循环处理线程通知)。在后面的实现中epoll 可同时监控 “客户端套接字可读”(I/O 事件)和 “线程池任务完成”(eventfd 事件),通过一个事件循环统一分发处理。

eventfd也是通过文件操作完成的
注意:read/writeIO时的数据只能是一个8字节数据

  • 子线程完成任务后,通过 write 向 eventfd 写入一个值(触发事件)。
  • 主线程通过 epoll 监控 eventfd,一旦触发事件就执行后续处理(如处理任务结果)。

eventloop处理流程:

  1. 在线程中对描述符进行事件监控
  2. 描述符就绪则处理事件 (为保证处理回调函数的操作都在自身线程中,这里引入一个加锁的任务队列,这样如果要执行的任务不是处于当前线程,则压入队列,直到当前线程的到来)
  3. 就绪事件处理完后,再去任务队列中将所有任务一一执行。

压入任务池之后的任务可能很长时间都得不到执行,因为可能会阻塞在epoll_wait上,因此在压入任务池的同时需要唤醒eventfd,也就是往eventfd里面写入数据借以触发


timerwheel和timefd整合模块代码实现

这个模块将会和channel,poller同时作为eventloop的子模块也就是内置对象来使用

单独的时间轮是没有时间的概念的,因此需要在timewheel结构上引入timefd来实现时间上的概念。
首先我们要明白timefd可以视为一个计数器,当我们设置的时间到了之后,那么内核就会为这个计数器加一,这就意味着这个文件描述符可读了
同时在timewheel中也得存在一个封装的channel对象用来管理timefd,当触发可读事件的时候eventloop能让timefd执行read操作,否则会持续触发EPOLLIN 事件(因为内核缓冲区非空)。导致后面的时间轮的滴答指针不停歇的走。

定时器模块的整合:

  • timefd:实现内核每隔一段时间,给进程一次超时时间(timefd可读)

  • timewheel:实现每次执行Runtimetask,都可以执行一波到期的定时任务

    • 对timerfd设置为一秒钟,则当channel传入timerfd并设置回调函数的时候,那么这个回调函数便会一秒钟执行一次,也就是每秒钟timewheel向后走一次,此外由于对timerfd的操作可能存在多线程中,为保证线程的安全,需要把内部的函数引入到eventloop一个线程中去
using FuncTask = std::function<void()>;
using ReleaseFunc = std::function<void()>;
class TimerTask
{
private:uint64_t _id;      // 定时器任务的对象编号uint32_t _timeout; // 定时器超时时间bool _cancel;FuncTask _task_cb; // 定时器要执行的任务ReleaseFunc _rf;   // 用来删除定时任务
public:TimerTask(uint64_t id, uint32_t timeset, FuncTask cb) : _id(id), _timeout(timeset), _task_cb(cb), _cancel(false) {}~TimerTask(){if (_cancel == false)_task_cb();_rf();}void SetRealse(ReleaseFunc cb) { _rf = cb; }void Cancel() { _cancel = true; }uint32_t DelayTime() { return _timeout; }
};class TimerWheel
{
private:using WeakTask = std::weak_ptr<TimerTask>;using PtrTask = std::shared_ptr<TimerTask>;int _tick;     // 指针,指到哪里哪里执行任务int _capacity; // 表盘最大数量,最大延迟时间std::vector<std::vector<PtrTask>> _wheel;std::unordered_map<uint64_t, WeakTask> _timers;EventLoop *_loop;int _timefd;//定时器描述符std::unique_ptr<Channel> _timer_channel;private://将_timers中对应timeid的weak_ptr删除void RemoveTimer(uint64_t id){auto it = _timers.find(id);if (it != _timers.end()){_timers.erase(it);}}static int CreateTimeFd(){int timefd = timerfd_create(CLOCK_MONOTONIC, 0);if (timefd < 0){ERR_LOG("CREATE TIMER ERROR");abort();}struct itimerspec itime;itime.it_value.tv_sec = 1;itime.it_value.tv_nsec = 0;itime.it_interval.tv_sec = 1;itime.it_interval.tv_nsec = 0;timerfd_settime(timefd, 0, &itime, NULL);return timefd;}void ReadTimer(){uint64_t timers;int ret = read(_timefd, &timers, 8);if (ret < 0){ERR_LOG("READ TIMERFD FAILED");abort();}return;}void RunTimerTask(){_tick = (_tick + 1) % _capacity;_wheel[_tick].clear(); // 清空当前表盘刻度下的shared,自动执行析构函数}void Ontime(){ReadTimer();RunTimerTask();}void TimerAddInLoop(uint64_t id, uint32_t delay, FuncTask cb) // 新增定时任务{PtrTask pt = std::make_shared<TimerTask>(id, delay, cb);pt->SetRealse(std::bind(&TimerWheel::RemoveTimer, this, id));_timers[id] = WeakTask(pt);int pos = (_tick + pt->DelayTime()) % _capacity;_wheel[pos].push_back(pt);};void TimerRefreshInLoop(uint64_t id) // 刷新定时任务,当有新连接到来,通过weakptr找到对应id生成一个shared令count++;{auto it = _timers.find(id);if (it == _timers.end()){return;}PtrTask pt = it->second.lock(); // 获取对应weak的sharedint pos = (_tick + pt->DelayTime()) % _capacity;_wheel[pos].push_back(pt);};void TimerCancelInLoop(uint64_t id){auto it = _timers.find(id);if (it == _timers.end()){return;}PtrTask pt = it->second.lock();if (pt)pt->Cancel();}
public:TimerWheel(EventLoop *loop) : _capacity(60), _tick(0), _wheel(_capacity), _loop(loop), _timefd(CreateTimeFd()), _timer_channel(new Channel(_loop, _timefd)){_timer_channel->SetReadCallback(std::bind(&TimerWheel::Ontime, this));_timer_channel->EableRead();}// times有可能在多线程中进行操作,需要考虑线程安全问题,不加锁,把定时器的所有操作放到一个线程当中void TimerAdd(uint64_t id, uint32_t delay, FuncTask cb){_loop->RunInLoop(std::bind(&TimerWheel::TimerAddInLoop, this, id, delay, cb));} // 新增定时任务void TimerRefresh(uint64_t id){_loop->RunInLoop(std::bind(&TimerWheel::TimerRefreshInLoop, this, id));}void TimerCancel(uint64_t id){_loop->RunInLoop(std::bind(&TimerWheel::TimerCancelInLoop, this, id));}};

EventLoop模块关系图以及流程梳理

在这里插入图片描述

模块流程:基于EventLoop不断执行任务或者通信,首先创建EventLoop这个模块中有三个模块。

  1. 时间管理模块timerwheel
  2. 描述符管理模块Channel
  3. 监听套接字epoll模块

当我们创建eventloop对象的那一刻,timewheel便开始执行,其中时间轮上的指针每隔一秒往后移动一位,它每一秒便被回调执行读出readfd的数据并清空当前时间下的事件(这个事件会是清空超时任务的回调函数),如此循环。

通过Socket模块创建一个监听套接字,传入Channel模块做管理,这个channel模块会同时传入EventLooop对象的地址,悬挂在EventLoop对象下面,为这个管理监听套接字的channel对象设置回调acceptor,并设置可读事件监控,也就是EnableRead,此时Channel模块会监视监听套接字的可读事件,也就是有客户端来通信的时候,会自动回调acceptor!

新的通信套接字来临的时候,acceptor中会创建套接字并关联channel,同样悬挂在EventLoop对象下面,并设置读,写,关闭,事件来临回调,并且会为这个channel创建一个timeid用于时间管理,这个timeid会传入上面eventloop的时间管理,这个时间管理便是检测当前连接的活跃性。channel的事件来临函数回调,channel的事件来临(SetEventCallBack)回调会执行 Eventloop来timerefresh这个timeid。

Connection模块代码实现

这个模块会对连接进行全访问的管理,对通信连接的所有操作都是通过这个模块提供的功能完成。这个模块功能的实现是最复杂最多的一个。成员变量中要把前面的所有模块的对象都使用进来,而成员函数不仅要包含四个和业务层接触的回调函数,还有和通信channel对象接触的需要考虑缓冲区的回调函数,更有上下文的考量以及数据取消和设置的关心

管理:

  1. 套接字的管理,能够进行套接字的操作
  2. 连接事件的管理,可读,可写,错误,挂断,任意
  3. 缓冲区的管理,便于socket数据的接收和发送
  4. 协议的上下文管理,记录请求数据的处理过程
  • 因为连接接收到数据之后该如何处理,由用户的需求决定,因此必须有业务处理回调函数
  • 一个连接建立成功后,该如何处理,要有连接建立成功的回调函数
  • 一个连接关闭前,该如何处理,要有关闭连接回调函数
  • 任意事件的处理,需要有任意事件回调函数

功能:

  1. 发送数据—把数据放到发送缓冲区
  2. 关闭连接–在释放连接之前,需处理输入输出缓冲区是否有数据待处理
  3. 启动非活跃连接的超时销毁功能
  4. 取消非活跃连接的超时销毁功能
  5. 协议切换–一个连接接收数据后如何进行业务处理,取决于上下文,以及业务处理回调函数

注意:Connection对所有连接操作,如果连接已经被释放,会导致越界访问,程序崩溃。所以得用个智能指针对connection对象管理,这样就能保证任意一个地方对connection对象进行操作的时候,保存了一份shared_ptr。
同时活跃事件的刷新要放在handleread之后,因为事件业务层处理可能会很长时间。
还有得知道每个回调函数中还会设置回调函数其实就是面对应用层的,同时还有基础功能会面对套接字。
还有一点,就是我们要深刻理解缓冲区的概念,它是套接字表示层和应用层的一个桥梁。

class Connection;
// DISCONECTED -- 连接关闭状态;   CONNECTING -- 连接建立成功-待处理状态
// CONNECTED -- 连接建立完成,各种设置已完成,可以通信的状态;  DISCONNECTING -- 待关闭状态
typedef enum
{DISCONECTED,CONNECTING,CONNECTED,DISCONNECTING
} ConnStatu;
using PtrConnection = std::shared_ptr<Connection>;
class Connection : public std::enable_shared_from_this<Connection>
{
private:uint64_t _conn_id;// uint64_t _timerid定时器ID简化用_coon_id作为timer_idint _sockfd;                   // 连接关联的文件描述符bool _enable_inactive_release; // 连接是否启动非活跃销毁的判断标志,默认为falseEventLoop *_loop;ConnStatu _statu;Socket _socket;     // 套接字操作管理Channel _channel;   // 连接的事件管理Buffer _in_buffer;  // 输入缓冲区-----存放从socket中读取到的数据,配合message处理Buffer _out_buffer; // 输出缓冲区Any _context;       // 请求的接收处理上下文private:// 这四个回调函数,是让服务器模块来设置的(其实服务器模块的处理回调也是组件使用者设置的 )using MessageCallback = std::function<void(const PtrConnection &, Buffer *)>;using ConnectedCallback = std::function<void(const PtrConnection &)>;using ClosedCallback = std::function<void(const PtrConnection &)>;using AnyEventCallback = std::function<void(const PtrConnection &)>;ConnectedCallback _connected_callback;MessageCallback _message_callback;ClosedCallback _close_callback;AnyEventCallback _event_callback;ClosedCallback _server_closed_callback;private:// 五个channel事件的回调,socket回调后,主要负责上层业务的处理//这样数据的接收和发送等就不用再做考虑了,只需要考虑业务层即可// 描述符可读事件触发后调用的事件,接收socket数据放到接收缓冲区中,然后调用_message_callback,也就是数据业务上的处理需求void HandleRead(){char buf[65536];int ret = _socket.NonBlockRecv(buf, 65536);DBG_LOG("接收到数据%s,大小为%d",buf,ret);if (ret < 0){return ShutDownInLoop();}// 将数据放入缓 冲区_in_buffer.WriteAndPush(buf, ret);if (_in_buffer.ReadAbleSize() > 0){return _message_callback(shared_from_this(), &_in_buffer);}};// 描述符可写事件触发后调用的函数,将发送缓冲区中的数据进行发送void HandleWrite(){//_out_buffer中保存的就是要发送的数据ssize_t ret = _socket.NonBlockSend(_out_buffer.ReaderPosition(), _out_buffer.ReadAbleSize());if (ret < 0){// 发送错误就该关闭文件了if (_in_buffer.ReadAbleSize() > 0){_message_callback(shared_from_this(), &_in_buffer);}return Release();}_out_buffer.MoveReadOffset(ret); // 一定发送完把读偏移向后偏移// //如果当前是连接关闭状态,则有数据,发送完数据释放连接,没有数据直接释放if (_out_buffer.ReadAbleSize() == 0){_channel.DisableWrite();if (_statu == DISCONNECTING){return Release();}}return;};// 描述符触发挂断事件void HandleClose(){// 一旦连接挂断,套接字就什么都干不了了,因此有数据待处理就处理一下if (_in_buffer.ReadAbleSize() > 0){_message_callback(shared_from_this(), &_in_buffer);}return Release();};// 描述符触发出错事件void HandleError(){// 一旦连接挂断,套接字就什么都干不了了,因此有数据待处理就处理一下if (_in_buffer.ReadAbleSize() > 0){_message_callback(shared_from_this(), &_in_buffer);}return ReleaseInLoop();};// 描述符触发任意事件void HandleEvent(){// 刷新任务活跃度,调用组件使用者的任意回调if (_enable_inactive_release == true){_loop->TimerRefresh(_conn_id);}if (_event_callback)_event_callback(shared_from_this());};// 连接获取之后,所处的状态下要进行各种设置(给channel设置事件回调,启动读监控)void EstablishedInLoop(){// 1.修改连接状态 2.启动读事件监控 3.事件回调调用assert(_statu == CONNECTING); // 当前事件必须为上层半连接状态_statu = CONNECTED;_channel.EableRead();if (_connected_callback)_connected_callback(shared_from_this());};// 实际释放接口void ReleaseInLoop(){DBG_LOG("RELEASE 被调用");// 1.修改连接状态_statu = DISCONNECTING;// 2.移除连接的事件监控_channel.Remove();// 3.关闭连接符_socket.Close();// 4.当前队列还有定时器队列中还有定时销毁任务,则取消任务if (_loop->HasTimer(_conn_id)){CancelInactiveReleaseInLoop();}// 5.调用关闭回调函数if (_close_callback)_close_callback(shared_from_this());// 5.移除服务器内部的连接信息if (_server_closed_callback)_server_closed_callback(shared_from_this());};// 这个接口并不是实际发送接口,而是把数据放到了发送缓冲区,启动可读事件监控void SendInLoop(Buffer buf){if (_statu == DISCONECTED)return;_out_buffer.WriteBufferAndPush(buf);if (_channel.WriteAble() == false){_channel.EableWrite();}};void ShutDownInLoop(){_statu = DISCONNECTING; // 设置连接为半g关闭连接状态if (_in_buffer.ReadAbleSize() > 0){if (_message_callback)_message_callback(shared_from_this(), &_in_buffer);}// 要么就是写入数据的时候出错关闭,要么就是没有待发送数据,直接关闭if (_out_buffer.ReadAbleSize() > 0){if (_channel.WriteAble() == false){_channel.EableWrite();}}if (_out_buffer.ReadAbleSize() == 0){ReleaseInLoop();}};// 启动非活跃连接超时释放规则void EnableInactiveReleaseInLoop(int sec){_enable_inactive_release = true;if (_loop->HasTimer(_conn_id)){return _loop->TimerRefresh(_conn_id);}return _loop->TimerAdd(_conn_id, sec, std::bind(&Connection::Release, this));};// 取消非活跃销毁任务void CancelInactiveReleaseInLoop(){_enable_inactive_release = false;if (_loop->HasTimer(_conn_id))_loop->TimerCancel(_conn_id);};void UpgradeInLoop(const Any &context,const ConnectedCallback &conn,const MessageCallback &msg,const ClosedCallback &closed,const AnyEventCallback &event){_context = context;_connected_callback = conn;_message_callback = msg;_close_callback = closed;_event_callback = event;};public:Connection(EventLoop *loop, uint64_t conn_id, int sockfd): _conn_id(conn_id), _sockfd(sockfd), _loop(loop),_enable_inactive_release(false), _statu(CONNECTING),_socket(sockfd), _channel(loop, sockfd){_channel.SetCloseCallback(std::bind(&Connection::HandleClose, this));_channel.SetEventCallback(std::bind(&Connection::HandleEvent, this));_channel.SetReadCallback(std::bind(&Connection::HandleRead, this));_channel.SetWriteCallback(std::bind(&Connection::HandleWrite, this));_channel.SetEventCallback(std::bind(&Connection::HandleEvent, this));_channel.SetErrorCallback(std::bind(&Connection::HandleError, this));};~Connection(){DBG_LOG("RELEASE CONNECTION:%p", this); //我看到了就是这里};int Fd() { return _sockfd; }int Id() { return _conn_id; }bool Connected() { return _statu == CONNECTED; }void SetContext(const Any &context) { _context = context; }Any *GetContext() { return &_context; }void SetConnectedCallback(const ConnectedCallback &cb) { _connected_callback = cb; }void SetMessageCallback(const MessageCallback &cb) { _message_callback = cb; }void SetClosedCallback(const ClosedCallback &cb) { _close_callback = cb; }void SetAnyEventCallback(const AnyEventCallback &cb) { _event_callback = cb; }void SetSrvClosedCallback(const ClosedCallback &cb) { _server_closed_callback = cb; }void Establish(){_loop->RunInLoop(std::bind(&Connection::EstablishedInLoop, this));}// 发送数据,将数据放到发送缓冲区,启动写事件监控void Send(const char *data, size_t len){Buffer buf;buf.WriteAndPush(data,len);_loop->RunInLoop(std::bind(&Connection::SendInLoop, this, buf));};// 提供给组件使用者的关闭接口---需要判断缓冲区是否还有数据void ShutDown(){_loop->RunInLoop(std::bind(&Connection::ShutDownInLoop, this));};void Release(){_loop->RunInLoop(std::bind(&Connection::ReleaseInLoop, this));}// 启动非活跃销毁,并定义多长时间无通信就是非活跃,添加定时任务void EnableInactiveRelease(int sec){_loop->RunInLoop(std::bind(&Connection::EnableInactiveReleaseInLoop, this, sec));};// 取消非活跃销毁void CancelInactiveRelease(){_loop->RunInLoop(std::bind(&Connection::CancelInactiveReleaseInLoop, this));}// 切换协议---重置上下文即阶段性处理函数void Upgrade(const Any &context, const ConnectedCallback &conn, const MessageCallback &msg,const ClosedCallback &closed, const AnyEventCallback &event){_loop->AssertInLoop();_loop->RunInLoop(std::bind(&Connection::UpgradeInLoop, this, context, conn, msg, closed, event));}
};

这个模块测试挺难测试的,出现了段错误是由于buffer模块的读数据后往后移动写成了写向后移动。
然后还有一个就是测试的时候直接把new出来的conn对象传到bind函数里面作为参数。std::bind(connectionDestory, conn);
std::bind是函数适配器,会构造一个仿函数对象,仿函数对象内部会保存起来绑定的参数,也就是把这个conn传进去,实际上就给构造的仿函数对象中直接保存了一份新的conn,以便于调用的时候把这个保存的成员当作参数来进行传递调用,这样析构函数就没法正常运行了

这个模块好难,好多成员变量,好多函数,好多细节。。。。。。。

Acceptor模块代码实现

这个模块主要是对监听套接字的封装,相比于通信连接套接字简单了很多,只需要考虑监听套接字的创建,和将其挂到eventloop模块下面,以及监听套接字的channel管理即可

class Acceptor
{
private:Socket _socket;Channel _channel;EventLoop *_loop;using AcceptCallback = std::function<void(int)>;AcceptCallback _accept_callback;private:void HandleRead(){int newsocketfd = _socket.Accept();if (newsocketfd < 0){return;}if (_accept_callback)_accept_callback(newsocketfd);}int CreateServe(uint16_t port){bool ret = _socket.CreateServer(port);assert(ret == true);return _socket.Fd();}public:Acceptor(EventLoop *loop, uint16_t port) : _socket(CreateServe(port)), _loop(loop), _channel(loop, _socket.Fd()){_channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));}void SetAcceptorCallback(const AcceptCallback &cb) { _accept_callback = cb; }void Listen() { _channel.EableRead(); }~Acceptor() {}
};

这个测试直接通过了,最顺利的一集,hah。

LoopTread及ThreadPool

LoopThread模块主要是将EventLoop和线程整合起来,两者是一一对应的。EventLoop模块实例化的对象,在构造的时候会初始化成员变量_thread_id,而后运行一个操作的时候判断当前是否运行在EventLoop模块对应的线程中,就是将线程ID和EventLoop模块中的_thread_id进行一个比较,相同就表示在同一个线程,不同就表示当前运行线程并不是EventLoop线程

注意:

  • EventLoop模块在实例化对象的时候,必须处于线程内部,EventLoop实例化对象时会设置自己的_thread_id,如果先创建多个EventLopp对象,然后创建多个线程,将各个线程的ID重新给EventLoop的_thread_id设置的话,从对象构造到设置_thread_id之间是不可控的,有可能切换线程运行等等。。。因此需要先创建线程,再在线程的入口函数中实例化EventLoop对象。

思想:

  1. 创建线程
  2. 在线程中实例化EventLoop对象
    向外部返回实例化的EventLoop

创建完线程和eventloop的关联之后,我们需要对线程做一个管理和分配
这就是LoopThreadPool模块的功能

功能:

  1. 线程数量可配置
  2. 对所有线程管理,其实就是管理0个或多个LoopThread对象
  3. 提供线程分配功能,如果是零个线程,直接分配给主线程EventLoop处理,多个线程则采用轮转(其实可以权重分配)思想,进行线程分配
class LoopThread
{
private://用于实现_loop获取的同步关系,避免创建线程,但_loop还灭有实例化之前去获取_loopstd::mutex _mutex;         //互斥锁std::condition_variable _cond;  //条件变量EventLoop* _loop;std::thread _thread;
private://实例化EventLoop对象,唤醒cond上阻塞的线程,并且开始运行EventLoop模块的功能void ThreadEntry(){EventLoop loop;{std::unique_lock<std::mutex> lock(_mutex);_loop=&loop;_cond.notify_all();}loop.Start();};
public://创建线程设定线程入口函数LoopThread():_loop(NULL),_thread(std::thread(&LoopThread::ThreadEntry,this)){}EventLoop* GetLoop(){EventLoop* loop;{std::unique_lock<std::mutex> lock(_mutex);_cond.wait(lock,[&](){return _loop!=NULL;});loop=_loop;}return loop;}
};class LoopThreadPool
{
private:int _thread_cout;int _next_idx;EventLoop* _baseloop;std::vector<LoopThread*> _threads;std::vector<EventLoop*>  _loops; 
public:LoopThreadPool(EventLoop* loop):_baseloop(loop),_thread_cout(0),_next_idx(0){}void Create(){if(_thread_cout>0){_threads.resize(_thread_cout);_loops.resize(_thread_cout);for(int i=0;i<_thread_cout;i++){_threads[i]=new LoopThread();_loops[i]=_threads[i]->GetLoop();}}return;}void SetThreadCount(int count){_thread_cout=count;}EventLoop* NextLoop(){if(_thread_cout==0){return _baseloop;}_next_idx=(_next_idx+1)%_thread_cout;return _loops[_next_idx];}
};

TcpServe

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

管理:

  1. Acceptor对象,创建一个监听套接字
  2. EventLoop对象,baseloop对象,实现对建通套接字的事件监控
  3. std::unordered_map<uint64_t,PtrConnection> _conns,实现对所有新建连接的管理
  4. LoopThreadPool对象,创建loop线程池,对新建连接进行事件监控和管理

功能:

  1. 设置从属线程池数量
  2. 启动服务器
  3. 设置各种回调函数,用户设置给TcpServe,TcpServe设置给获取的新连接
  4. 是否启动非活跃连接超时的销毁功能
  5. 添加定时任务

流程:

  1. 在TcpServe中实例化Acceptor对象,以及一个EventLoop对象(baseloop);
  2. 将Acceptor挂到baseloop上进行事件监控
  3. 一旦Acceptor对象就绪事件,会触发读监控,则执行读事件回调函数获取新连接
  4. 对新连接,创建一个Connection进行管理
  5. 对新连接对应的Connection设置功能回调
class TcpServe
{
private:int _port;uint64_t _next_id;                                  // 自动增长的连接IDint _timeout;                                       // 非活跃销毁时间bool _enable_inactive_release;                      // 是否启动非活跃标志位EventLoop _baseloop;                                // 主线程的EventLoop对象,负责监听事件的处理Acceptor _acceptor;                                 // 监听套接字管理对象LoopThreadPool _pool;                               // 从属EventLoop线程池std::unordered_map<uint64_t, PtrConnection> _conns; // 保存管理所有连接对应的shared——ptr对象private:using MessageCallback = std::function<void(const PtrConnection &, Buffer *)>;using ConnectedCallback = std::function<void(const PtrConnection &)>;using ClosedCallback = std::function<void(const PtrConnection &)>;using AnyEventCallback = std::function<void(const PtrConnection &)>;using Functor = std::function<void()>;ConnectedCallback _connected_callback;MessageCallback _message_callback;ClosedCallback _close_callback;AnyEventCallback _event_callback;ClosedCallback _server_closed_callback;private:void RunAfterInLoop(const Functor &task, int delay){_next_id++;_baseloop.TimerAdd(_next_id, delay, task);}void NewConnection(int fd){_next_id++;PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd));conn->SetMessageCallback(_message_callback);conn->SetConnectedCallback(_connected_callback);conn->SetClosedCallback(_close_callback);conn->SetAnyEventCallback(_event_callback);conn->SetSrvClosedCallback(std::bind(&TcpServe::RemoveConnection, this, std::placeholders::_1));if (_enable_inactive_release)conn->EnableInactiveRelease(_timeout); // 启动非活跃销毁conn->Establish();                         //_conns.insert(std::make_pair(_next_id, conn));};void RemoveConnectionInLoop(const PtrConnection &conn){int id = conn->Id();auto it = _conns.find(id);if (it != _conns.end()){_conns.erase(it);}}// 从管理Connection的_conns中移除连接信息void RemoveConnection(const PtrConnection &conn){_baseloop.RunInLoop(std::bind(&TcpServe::RemoveConnectionInLoop, this, conn));}public:TcpServe(int port) : _port(port), _next_id(0), _enable_inactive_release(false), _acceptor(&_baseloop, port), _pool(&_baseloop) {_acceptor.SetAcceptorCallback(std::bind(&TcpServe::NewConnection, this, std::placeholders::_1));_acceptor.Listen();//将监听套接字挂到baseloop上};void SetThreadCount(int cout) {return _pool.SetThreadCount(cout);};void SetConnectedCallback(const ConnectedCallback &cb) { _connected_callback = cb; }void SetMessageCallback(const MessageCallback &cb) { _message_callback = cb; }void SetClosedCallback(const ClosedCallback &cb) { _close_callback = cb; }void SetAnyEventCallback(const AnyEventCallback &cb) { _event_callback = cb; }void SetSrvClosedCallback(const ClosedCallback &cb) { _server_closed_callback = cb; }void EnableInactiveRelease(int timeout){_timeout = timeout;_enable_inactive_release = true;}void RunAfter(const Functor &task, int delay) {}; // 定时任务void Start() { _pool.Create();  _baseloop.Start();  };
};

最后还得考虑一个问题,就是连接如果断开异常的处理,当连接断开的时候如果服务器还继续send那么就会触发异常,信号为SIGPIPE,操作系统会把服务器挂掉,所以得忽略这个异常

struct NetWork
{NetWork(){signal(SIGPIPE,SIG_IGN);}
};static NetWork nw;

serve总模块关系梳理

这里我将自上而下的梳理一下这个模块的调用关系

首先echoserve是对tcpserve的一个封装,echoserve中会初始化线程池的数量并且在启动的时候会调用线程池类创建线程对象,而每个线程对象的入口函数会创建一个loop对象,每个loop在后面创建connection的时候会轮次分配,由于connection的成员函数都传入了runinloop中,这样下来就可以保证后面的connection可以在线程中运行,并且也能使用定时器等功能喽。

并且里面有三个成员函数将会作为通信到来的connection的回调函数
而tcpserve中有个很重要的函数NewConnection,这个函数将会被设置为Acceptor的成员函数中的一个回调,而这个回调函数指针又会在一个handleread中被调用对应函数,handleread又被设置为channel的读事件回调,有新连接到来时,对应文件描述符会触发可读事件,于是执行handeread,handleread会直接通过封装的socket中的成员函数accept,创建套接字,并把对应文件描述符传入了对NewConnection的回调函数中,创建一个connection对象
同时定时器也会被设置,流程便是NewConnection中new的对象中会传入TcpServe定义的成员变量_next_id这个变量将会作为事件器中的id并且connection中会自带销毁函数,也就是timewheel中走到对应设置的时间会回调的函数。

在这里插入图片描述

主波梳理完流程成燃尽成舍利子了

HTTP协议模块

其实这个模块主要还是基于HTTP协议的理解做字符串的处理,因此在写代码的时候应该不会有太大的逻辑阻力,还是没有遇到很大的困难就写完这个模块了。
至于注意点的话好像也没有什么需要注意的,硬要说的话注意基础打牢才能轻松写完这个模块,博主基础就不是很牢固其实。


httpserve需要特别说明:
这个模块是用于实现HTTP服务器的搭建的

需要在内部设计一张请求路由表:
表中记录了针对那个请求,应该使用那个函数来进行约为处理的映射关系
服务器收到请求,在请求路由表中查找对应请求的处理函数,如果有,则执行对应的处理函数即可,什么请求,怎么处理,由用户来决定,服务器收到请求只需要执行函数即可

这样做的目的:用户只需要实现业务处理函数,然后将请求和处理函数的映射关系添加到服务器中,而服务器只需要接收数据,解析数据,查找路由表映射关系,执行业务处理函数

需要的要素和功能:

  1. GET请求的路由映射表
  2. POST请求的路由映射表
  3. PUT请求的路由映射表
  4. DELETE请求的路由映射表 ---- 路由映射表记录对应请求方法的请求的处理函数映射关系 —更多的是功能性请求的处理
  5. 静态资源的相对根目录 --实现静态资源请求的处理
  6. 高性能TCP服务器 —进行连接的IO操作

#include "../serve.hpp"
#include <sys/stat.h>
#include <regex>
#define DEFALT_TIMEOUT 10std::unordered_map<int, std::string> _statu_msg = {{100, "Continue"},{101, "Switching Protocol"},{102, "Processing"},{103, "Early Hints"},{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"},{300, "Multiple Choice"},{301, "Moved Permanently"},{302, "Found"},{303, "See Other"},{304, "Not Modified"},{305, "Use Proxy"},{306, "unused"},{307, "Temporary Redirect"},{308, "Permanent Redirect"},{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"},{425, "Too Early"},{426, "Upgrade Required"},{428, "Precondition Required"},{429, "Too Many Requests"},{431, "Request Header Fields Too Large"},{451, "Unavailable For Legal Reasons"},{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 = {{".aac", "audio/aac"},{".abw", "application/x-abiword"},{".arc", "application/x-freearc"},{".avi", "video/x-msvideo"},{".azw", "application/vnd.amazon.ebook"},{".bin", "application/octet-stream"},{".bmp", "image/bmp"},{".bz", "application/x-bzip"},{".bz2", "application/x-bzip2"},{".csh", "application/x-csh"},{".css", "text/css"},{".csv", "text/csv"},{".doc", "application/msword"},{".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},{".eot", "application/vnd.ms-fontobject"},{".epub", "application/epub+zip"},{".gif", "image/gif"},{".htm", "text/html"},{".html", "text/html"},{".ico", "image/vnd.microsoft.icon"},{".ics", "text/calendar"},{".jar", "application/java-archive"},{".jpeg", "image/jpeg"},{".jpg", "image/jpeg"},{".js", "text/javascript"},{".json", "application/json"},{".jsonld", "application/ld+json"},{".mid", "audio/midi"},{".midi", "audio/x-midi"},{".mjs", "text/javascript"},{".mp3", "audio/mpeg"},{".mpeg", "video/mpeg"},{".mpkg", "application/vnd.apple.installer+xml"},{".odp", "application/vnd.oasis.opendocument.presentation"},{".ods", "application/vnd.oasis.opendocument.spreadsheet"},{".odt", "application/vnd.oasis.opendocument.text"},{".oga", "audio/ogg"},{".ogv", "video/ogg"},{".ogx", "application/ogg"},{".otf", "font/otf"},{".png", "image/png"},{".pdf", "application/pdf"},{".ppt", "application/vnd.ms-powerpoint"},{".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},{".rar", "application/x-rar-compressed"},{".rtf", "application/rtf"},{".sh", "application/x-sh"},{".svg", "image/svg+xml"},{".swf", "application/x-shockwave-flash"},{".tar", "application/x-tar"},{".tif", "image/tiff"},{".tiff", "image/tiff"},{".ttf", "font/ttf"},{".txt", "text/plain"},{".vsd", "application/vnd.visio"},{".wav", "audio/wav"},{".weba", "audio/webm"},{".webm", "video/webm"},{".webp", "image/webp"},{".woff", "font/woff"},{".woff2", "font/woff2"},{".xhtml", "application/xhtml+xml"},{".xls", "application/vnd.ms-excel"},{".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},{".xml", "application/xml"},{".xul", "application/vnd.mozilla.xul+xml"},{".zip", "application/zip"},{".3gp", "video/3gpp"},{".3g2", "video/3gpp2"},{".7z", "application/x-7z-compressed"}};class Util
{
public:// 字符串分割static size_t Split(const std::string &src, const std::string &sep, std::vector<std::string> *arry){size_t offset = 0;while (offset < src.size()){auto pos = src.find(sep, offset); // 在src字符串偏移量offset处,开始向后查找sep字符/字串,返回查找的位置if (pos == std::string::npos)     // 没找到对应字符{// 剩余部分放入arry中arry->push_back(src.substr(offset));return arry->size();}if (pos == offset){offset = pos + sep.size();continue;}arry->push_back(src.substr(offset, pos - offset));offset = pos + sep.size();}return arry->size();};// 读取文件内容static bool ReadFile(const std::string &filename, std::string *buf){std::ifstream ifs(filename, std::ios::binary);if (ifs.is_open() == false){DBG_LOG("OPEN%s FILE FAILED!", filename.c_str());return false;}size_t fsize = 0;ifs.seekg(0, ifs.end); // 跳转读写位置到末尾fsize = ifs.tellg();   // 获取当前读写位置相对于起始位置的偏移量,从末尾偏移刚好就是文件大小buf->resize(fsize);ifs.read(&(*buf)[0], fsize);if (ifs.good() == false){DBG_LOG("READ %s FILE FAILED!", filename.c_str());ifs.close();return false;}ifs.close();return true;};// 向文件写入数据static bool WriteFile(const std::string &filename, const std::string &buf){std::ofstream ofs(filename, std::ios::binary | std::ios::trunc);if (ofs.is_open() == false){printf("OPEN %s FILE FAILED!!", filename.c_str());return false;}ofs.write(buf.c_str(), buf.size());if (ofs.good() == false){ERR_LOG("WRITE %s FILE FAILED!", filename.c_str());ofs.close();return false;}ofs.close();return true;};// URL编码// URL编码,避免URL中资源路径与查询字符串中的特殊字符与HTTP请求中特殊字符产生歧义// 编码格式:将特殊字符的ascii值,转换为两个16进制字符,前缀%   C++ -> C%2B%2B//  不编码的特殊字符: RFC3986文档规定 . - _ ~ ,字母,数字属于绝对不编码字符// RFC3986文档规定,编码格式 %HH// W3C标准中规定,查询字符串中的空格,需要编码为+, 解码则是+转空格static std::string UrlEncode(const std::string url, bool is_space_to_plus){std::string res;for (auto &c : url){if (c == '.' || c == '-' || c == '_' || c == '~' || isalnum(c)){res += c;continue;}if (c == ' ' && is_space_to_plus){res += '+';continue;}char tmp[4] = {0};snprintf(tmp, 4, "%%%02X", c);res += tmp;}return res;};// 十六进制转十进制static char HEXTOI(char c){if (c >= '0' && c <= '9'){return c - '0';}else if (c >= 'a' && c <= 'z'){return c - 'a' + 10;}else if (c >= 'A' && c <= 'Z'){return c - 'A' + 10;}return -1;}static std::string UrlDecode(const std::string url, bool convert_plus_to_space){// 遇到了%,则将紧随其后的2个字符,转换为数字,第一个数字左移4位,然后加上第二个数字  + -> 2b  %2b->2 << 4 + 11std::string res;for (int i = 0; i < url.size(); i++){if (url[i] == '+' && convert_plus_to_space == true){res += ' ';continue;}if (url[i] == '%' && (i + 2) < url.size()){char v1 = HEXTOI(url[i + 1]);char v2 = HEXTOI(url[i + 2]);char v = v1 * 16 + v2;res += v;i += 2;continue;}res += url[i];}return res;}// 响应状态码的描述信息获取static std::string StatuDesc(int statu){auto it = _statu_msg.find(statu);if (it != _statu_msg.end()){return it->second;}return "Unknow";}// 根据文件后缀名获取文件mimestatic std::string ExtMime(const std::string &filename){// a.b.txt  先获取文件扩展名size_t pos = filename.find_last_of('.');if (pos == std::string::npos){return "application/octet-stream";}// 根据扩展名,获取mimestd::string ext = filename.substr(pos);auto it = _mime_msg.find(ext);if (it == _mime_msg.end()){return "application/octet-stream";}return it->second;}// 判断一个文件是否是一个目录static bool IsDirectory(const std::string &filename){struct stat st;int ret = stat(filename.c_str(), &st);if (ret < 0){return false;}return S_ISDIR(st.st_mode);}// 判断一个文件是否是一个普通文件static bool IsRegular(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请求的资源路径有效性判断//  /index.html  --- 前边的/叫做相对根目录  映射的是某个服务器上的子目录//  想表达的意思就是,客户端只能请求相对根目录中的资源,其他地方的资源都不予理会//  /../login, 这个路径中的..会让路径的查找跑到相对根目录之外,这是不合理的,不安全的static bool ValidPath(const std::string &path){// 思想:按照/进行路径分割,根据有多少子目录,计算目录深度,有多少层,深度不能小于0std::vector<std::string> subdir;Split(path, "/", &subdir);int level = 0;for (auto &dir : subdir){if (dir == ".."){level--; // 任意一层走出相对根目录,就认为有问题if (level < 0)return false;continue;}level++;}return true;}
};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() : _version("HTTP/1.1") {}void ReSet(){_method.clear();_path.clear();_version = "HTTP/1.1";_body.clear();std::smatch match;_matches.swap(match);_headers.clear();_params.clear();}// 插入头部字段void SetHeader(const std::string &key, const std::string &val){_headers.insert(std::make_pair(key, val));}// 判断是否存在指定头部字段bool HasHeader(const std::string &key) const{auto it = _headers.find(key);if (it == _headers.end()){return false;}return true;}// 获取指定头部字段的值std::string GetHeader(const std::string &key) const{auto it = _headers.find(key);if (it == _headers.end()){return "";}return it->second;}// 插入查询字符串void SetParam(const std::string &key, const std::string &val){_params.insert(std::make_pair(key, val));}// 判断是否有某个指定的查询字符串bool HasParam(const std::string &key) const{auto it = _params.find(key);if (it == _params.end()){return false;}return true;}// 获取指定的查询字符串std::string GetParam(const std::string &key) const{auto it = _params.find(key);if (it == _params.end()){return "";}return it->second;}// 获取正文长度size_t ContentLength() const{// Content-Length: 1234\r\nbool ret = HasHeader("Content-Length");if (ret == false){return 0;}std::string clen = GetHeader("Content-Length");return std::stol(clen);}// 判断是否是短链接bool Close() const{// 没有Connection字段,或者有Connection但是值是close,则都是短链接,否则就是长连接if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive"){return false;}return true;}
};class HttpResponse
{
public:int _statu;bool _redirect_flag;std::string _body;std::string _redirect_url;std::unordered_map<std::string, std::string> _headers;public:HttpResponse() : _redirect_flag(false), _statu(200) {}HttpResponse(int statu) : _redirect_flag(false), _statu(statu) {}void ReSet(){_statu = 200;_redirect_flag = false;_body.clear();_redirect_url.clear();_headers.clear();}// 插入头部字段void SetHeader(const std::string &key, const std::string &val){_headers.insert(std::make_pair(key, val));}// 判断是否存在指定头部字段bool HasHeader(const std::string &key){auto it = _headers.find(key);if (it == _headers.end()){return false;}return true;}// 获取指定头部字段的值std::string GetHeader(const std::string &key){auto it = _headers.find(key);if (it == _headers.end()){return "";}return it->second;}void SetContent(const std::string &body, const std::string &type = "text/html"){_body = body;SetHeader("Content-Type", type);}void SetRedirect(const std::string &url, int statu = 302){_statu = statu;_redirect_flag = true;_redirect_url = url;}// 判断是否是短链接bool Close(){// 没有Connection字段,或者有Connection但是值是close,则都是短链接,否则就是长连接if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive"){return false;}return true;}
};typedef enum
{RECV_HTTP_ERROR,RECV_HTTP_LINE,RECV_HTTP_HEADE,RECV_HTTP_BODY,RECV_HTTP_OVER
} HttpRecvStatu;
#define MAX_LINE 8196
class HttpContext
{
private:int _resp_statu;           // 响应状态码HttpRecvStatu _recv_statu; // 当前接收和解析的阶段状态HttpRequest _request;      // 以及解析完成得到的请求信息
private:// 这个函数主要是为了把获取到的请求行的信息分配填充到request的变量中去bool ParseHttpLine(std::string &line){std::smatch matches;// 请求方法的匹配  GET HEAD POST PUT DELETE ....std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);// GET|HEAD|POST|PUT|DELETE   表示匹配并提取其中任意一个字符串//[^?]*     [^?]匹配非问号字符, 后边的*表示0次或多次,空格表示结束当前字符串的匹配//\\?(.*)   \\?  表示原始的?字符 (.*)表示提取?之后的任意字符0次或多次,直到遇到空格// HTTP/1\\.[01]  表示匹配以HTTP/1.开始,后边有个0或1的字符串//(?:\n|\r\n)?   (?: ...) 表示匹配某个格式字符串,但是不提取, 最后的?表示的是匹配前边的表达式0次或1次bool ret = std::regex_match(line, matches, e);if (ret == false){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUESTreturn -1;}_request._method = matches[1];_request._path = matches[2];_request._version = matches[4];// 获取查询字符串,以&分割每个键值对,然后分开每个键值对并插入request中std::vector<std::string> query_string_arry;Util::Split(matches[3], "&", &query_string_arry);for (auto &str : query_string_arry){size_t pos = str.find("=");if (pos == std::string::npos){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400;return false;}std::string key = Util::UrlDecode(str.substr(0, pos), true);std::string val = Util::UrlDecode(str.substr(pos + 1), true);_request.SetParam(key, val);}return true;};bool RecvHttpLine(Buffer *buf){std::string line = buf->GetLineAndPop();if (line.size() == 0){if (buf->ReadAbleSize() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URI TO LONGreturn false;}return true;}if (line.size() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URI TO LONGreturn false;}// 首行处理完毕,进入头部获取阶段bool ret = ParseHttpLine(line);if (ret == false){return false;}// 首行处理完毕,进入头部获取阶段_recv_statu = RECV_HTTP_HEADE;return true;};bool ParseHttpHead(std::string line){// key: val\r\nkey: val\r\n....if (line.back() == '\n')line.pop_back(); // 末尾是换行则去掉换行字符if (line.back() == '\r')line.pop_back(); // 末尾是回车则去掉回车字符size_t pos = line.find(": ");if (pos == std::string::npos){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; //return false;}std::string key = line.substr(0, pos);std::string val = line.substr(pos + 2);_request.SetHeader(key, val);return true;};bool RecvHttpHead(Buffer *buf){if (_recv_statu != RECV_HTTP_HEADE)return false;// 一行一行取出数据,直到遇到空行为止, 头部的格式 key: val\r\nkey: val\r\n....while (1){std::string line = buf->GetLineAndPop();// 2. 需要考虑的一些要素:缓冲区中的数据不足一行, 获取的一行数据超大if (line.size() == 0){// 缓冲区中的数据不足一行,则需要判断缓冲区的可读数据长度,如果很长了都不足一行,这是有问题的if (buf->ReadAbleSize() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URI TOO LONGreturn false;}// 缓冲区中数据不足一行,但是也不多,就等等新数据的到来return true;}if (line.size() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URI TOO LONGreturn false;}if (line == "\n" || line == "\r\n"){break;}bool ret = ParseHttpHead(line);if (ret == false){return false;}}// 头部处理完毕,进入正文获取阶段_recv_statu = RECV_HTTP_BODY;return true;};bool RecvHttpBody(Buffer *buf){if (_recv_statu != RECV_HTTP_BODY)return false;// 1. 获取正文长度size_t content_length = _request.ContentLength();if (content_length == 0){// 没有正文,则请求接收解析完毕_recv_statu = RECV_HTTP_OVER;return true;}// 2. 当前已经接收了多少正文,其实就是往  _request._body 中放了多少数据了size_t real_len = content_length - _request._body.size(); // 实际还需要接收的正文长度// 3. 接收正文放到body中,但是也要考虑当前缓冲区中的数据,是否是全部的正文//   3.1 缓冲区中数据,包含了当前请求的所有正文,则取出所需的数据if (buf->ReadAbleSize() >= real_len){_request._body.append(buf->ReaderPosition(), real_len);buf->MoveReadOffset(real_len);_recv_statu = RECV_HTTP_OVER;return true;}//  3.2 缓冲区中数据,无法满足当前正文的需要,数据不足,取出数据,然后等待新数据到来_request._body.append(buf->ReaderPosition(), buf->ReadAbleSize());buf->MoveReadOffset(buf->ReadAbleSize());return true;};public:HttpContext() : _resp_statu(200), _recv_statu(RECV_HTTP_LINE) {}void ReSet(){_resp_statu = 200;_recv_statu = RECV_HTTP_LINE;_request.ReSet();}int RespStatu() { return _resp_statu; }HttpRecvStatu RecvStatu() { return _recv_statu; }HttpRequest &Request() { return _request; }// 接收并解析HTTP请求void RecvHttpRequest(Buffer *buf){// 不同的状态,做不同的事情,但是这里不要break, 因为处理完请求行后,应该立即处理头部,而不是退出等新数据switch (_recv_statu){case RECV_HTTP_LINE:RecvHttpLine(buf);case RECV_HTTP_HEADE:RecvHttpHead(buf);case RECV_HTTP_BODY:RecvHttpBody(buf);}return;}
};class HttpServer {private:using Handler = std::function<void(const HttpRequest &, HttpResponse *)>;using Handlers = std::vector<std::pair<std::regex, Handler>>;Handlers _get_route;Handlers _post_route;Handlers _put_route;Handlers _delete_route;std::string _basedir; //静态资源根目录TcpServe _server;private:void ErrorHandler(const HttpRequest &req, HttpResponse *rsp) {//1. 组织一个错误展示页面std::string body;body += "<html>";body += "<head>";body += "<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>";body += "</head>";body += "<body>";body += "<h1>";body += std::to_string(rsp->_statu);body += " ";body += Util::StatuDesc(rsp->_statu);body += "</h1>";body += "</body>";body += "</html>";//2. 将页面数据,当作响应正文,放入rsp中rsp->SetContent(body, "text/html");}//将HttpResponse中的要素按照http协议格式进行组织,发送void WriteReponse(const PtrConnection &conn, const HttpRequest &req, HttpResponse &rsp) {//1. 先完善头部字段if (req.Close() == true) {rsp.SetHeader("Connection", "close");}else {rsp.SetHeader("Connection", "keep-alive");}if (rsp._body.empty() == false && rsp.HasHeader("Content-Length") == false) {rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));}if (rsp._body.empty() == false && rsp.HasHeader("Content-Type") == false) {rsp.SetHeader("Content-Type", "application/octet-stream");}if (rsp._redirect_flag == true) {rsp.SetHeader("Location", rsp._redirect_url);}//2. 将rsp中的要素,按照http协议格式进行组织std::stringstream rsp_str;rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(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;//3. 发送数据conn->Send(rsp_str.str().c_str(), rsp_str.str().size());}bool IsFileHandler(const HttpRequest &req) {// 1. 必须设置了静态资源根目录if (_basedir.empty()) {return false;}// 2. 请求方法,必须是GET / HEAD请求方法if (req._method != "GET" && req._method != "HEAD") {return false;}// 3. 请求的资源路径必须是一个合法路径if (Util::ValidPath(req._path) == false) {return false;}// 4. 请求的资源必须存在,且是一个普通文件//    有一种请求比较特殊 -- 目录:/, /image/, 这种情况给后边默认追加一个 index.html// index.html    /image/a.png// 不要忘了前缀的相对根目录,也就是将请求路径转换为实际存在的路径  /image/a.png  ->   ./wwwroot/image/a.pngstd::string req_path = _basedir + req._path;//为了避免直接修改请求的资源路径,因此定义一个临时对象if (req._path.back() == '/')  {req_path += "index.html";}if (Util::IsRegular(req_path) == false) {return false;}return true;}//静态资源的请求处理 --- 将静态资源文件的数据读取出来,放到rsp的_body中, 并设置mimevoid FileHandler(const HttpRequest &req, HttpResponse *rsp) {std::string req_path = _basedir + req._path;if (req._path.back() == '/')  {req_path += "index.html";}bool ret = Util::ReadFile(req_path, &rsp->_body);if (ret == false) {return;}std::string mime = Util::ExtMime(req_path);rsp->SetHeader("Content-Type", mime);return;}//功能性请求的分类处理void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers) {//在对应请求方法的路由表中,查找是否含有对应资源请求的处理函数,有则调用,没有则发挥404//思想:路由表存储的时键值对 -- 正则表达式 & 处理函数//使用正则表达式,对请求的资源路径进行正则匹配,匹配成功就使用对应函数进行处理//  /numbers/(\d+)       /numbers/12345for (auto &handler : handlers) {const std::regex &re = handler.first;const Handler &functor = handler.second;bool ret = std::regex_match(req._path, req._matches, re);if (ret == false) {continue;}return functor(req, rsp);//传入请求信息,和空的rsp,执行处理函数}rsp->_statu = 404;}void Route(HttpRequest &req, HttpResponse *rsp) {//1. 对请求进行分辨,是一个静态资源请求,还是一个功能性请求//   静态资源请求,则进行静态资源的处理//   功能性请求,则需要通过几个请求路由表来确定是否有处理函数//   既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405if (IsFileHandler(req) == true) {//是一个静态资源请求, 则进行静态资源请求的处理return FileHandler(req, rsp);}if (req._method == "GET" || req._method == "HEAD") {return Dispatcher(req, rsp, _get_route);}else if (req._method == "POST") {return Dispatcher(req, rsp, _post_route);}else if (req._method == "PUT") {return Dispatcher(req, rsp, _put_route);}else if (req._method == "DELETE") {return Dispatcher(req, rsp, _delete_route);}rsp->_statu = 405;// Method Not Allowedreturn ;}//设置上下文void OnConnected(const PtrConnection &conn) {conn->SetContext(HttpContext());DBG_LOG("NEW CONNECTION %p", conn.get());}//缓冲区数据解析+处理void OnMessage(const PtrConnection &conn, Buffer *buffer) {while(buffer->ReadAbleSize() > 0){//1. 获取上下文HttpContext *context = conn->GetContext()->get<HttpContext>();//2. 通过上下文对缓冲区数据进行解析,得到HttpRequest对象//  1. 如果缓冲区的数据解析出错,就直接回复出错响应//  2. 如果解析正常,且请求已经获取完毕,才开始去进行处理context->RecvHttpRequest(buffer);HttpRequest &req = context->Request();HttpResponse rsp(context->RespStatu());if (context->RespStatu() >= 400) {//进行错误响应,关闭连接ErrorHandler(req, &rsp);//填充一个错误显示页面数据到rsp中WriteReponse(conn, req, rsp);//组织响应发送给客户端context->ReSet();buffer->MoveReadOffset(buffer->ReadAbleSize());//出错了就把缓冲区数据清空conn->ShutDown();//关闭连接return;}if (context->RecvStatu() != RECV_HTTP_OVER) {//当前请求还没有接收完整,则退出,等新数据到来再重新继续处理return;}//3. 请求路由 + 业务处理Route(req, &rsp);//4. 对HttpResponse进行组织发送WriteReponse(conn, req, rsp);//5. 重置上下文context->ReSet();//6. 根据长短连接判断是否关闭连接或者继续处理if (rsp.Close() == true) conn->ShutDown();//短链接则直接关闭}return;}public:HttpServer(int port, int timeout = DEFALT_TIMEOUT):_server(port) {_server.EnableInactiveRelease(timeout);_server.SetConnectedCallback(std::bind(&HttpServer::OnConnected, this, std::placeholders::_1));_server.SetMessageCallback(std::bind(&HttpServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));}void SetBaseDir(const std::string &path) {assert(Util::IsDirectory(path) == true);_basedir = path;}/*设置/添加,请求(请求的正则表达)与处理函数的映射关系*/void Get(const std::string &pattern, const Handler &handler) {_get_route.push_back(std::make_pair(std::regex(pattern), handler));}void Post(const std::string &pattern, const Handler &handler) {_post_route.push_back(std::make_pair(std::regex(pattern), handler));}void Put(const std::string &pattern, const Handler &handler) {_put_route.push_back(std::make_pair(std::regex(pattern), handler));}void Delete(const std::string &pattern, const Handler &handler) {_delete_route.push_back(std::make_pair(std::regex(pattern), handler));}void SetThreadCount(int count) {_server.SetThreadCount(count);}void Listen() {_server.Start();}
};

测试

WebBench

Webbench 是一款用于测试 Web 服务器性能的开源压力测试工具,主要用于评估服务器在高并发场景下的处理能力(如 QPS、响应时间、吞吐量等)。它轻量级、易使用,适合快速测试 HTTP 服务器的性能表现。
接下来的测试会有一部分依赖于这个软件。

懒得安装的可以直接从博主gitte上获取webbench

长连接测试

在这里插入图片描述

超时连接测试

在这里插入图片描述

性能压力测试

我的云服务器是两核,2GB,带宽3Mbps
受限于云服务器,只向服务器创建了一千并发量,测试了60秒

在这里插入图片描述

END

写项目的过程并不是一番风顺的,特别是在测试的时候出现的各种问题或者vscode崩掉,有很多错误,但好在能花时间去改正,并且在这些时间中更加了解每行代码的作用以及为什么要这么去编码。
边犯错边弥补,以及后面的HTTP模块和边缘测试还是有点潦草的。后面有时间还会对其进行复盘和修正。

http://www.dtcms.com/a/482167.html

相关文章:

  • DNS优选 2.8.2 | 优选最快DNS,访问受限网站,去网站广告
  • 网络编程就是做网站么枣庄网页制作公司
  • 【目标跟踪n雷达二维EKF】雷达对单目标跟踪,滤波(使用扩展卡尔曼)增强定位能力,二维,目标状态未知,雷达数量可调。给出MATLAB代码
  • 从鉴酱酒:传承文化,品味佳酿
  • 响应式网站开发图标wordpress 注册 登陆不了
  • 如何在 MySQL 中实现慢查询监控
  • Python 切片的核心概念
  • Linux用户空间/内核空间获取用户空间地址的页表
  • AB Download Manager(下载管理工具) 中文绿色版
  • 深圳建设网站公司排名网页制作作业网站
  • Python3 AI 编程助手
  • C# WPS操作PPT,全屏,缩率图,备注,跳转播放
  • 医药公司网站建设备案网站做戒酒通知书
  • 高效存储大List对象到Redis的解决方案,使用分片存储和压缩技术
  • 阿德莱德学习推理与导航!PEAP-LLM:基于大语言模型的参数高效动作规划
  • 科技赋能畜牧业|小吉快检 BL-08plus 推动行业数字化转型
  • Qt多线程渲染架构设计与实现思考
  • 亚马逊云科技 WAF 指南(十)用 Amazon Q Developer CLI 解决 DDoS 防护与 SEO 冲突问题
  • 网络营销是什么 能做什么seo项目经理
  • 咨询行业网站建设公司太仓市建设局网站
  • 自己开外销网站怎么做手机分销网站
  • 那个网站可以做ppt赚钱建设银行网站查询密码怎么开通
  • EI输入整形振动抑制方法介绍
  • Python爬虫实战手册
  • 教程: 在网页中利用原生CSS实现3D旋转动画
  • 机器学习从零到精通:理论、实践与工业级应用完整指南
  • 泰州模板建站源码移动端网页
  • 机器学习中的灰色预测算法:原理、实现与实战应用完整教程
  • 教育培训网站开发企业软件管理系统排名
  • jvm中的栈