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

基于muduo库实现高并发服务器

文章目录

  • 一、项目介绍
  • 二、HTTP服务器
    • 1.概念
    • 2.Reactor模型
      • 2.1单Reactor单线程:单I/O多路复用+业务处理
      • 2.2单Reactor多线程:单I/O多路复用+线程池(业务处理)
      • 2.3多Reactor多线程:多I/O多路复用+线程池(业务处理)
  • 三、前置知识技术点功能用例
    • 1.C++11中的bind
    • 2.简单的秒级定时任务实现
    • 3.正则库的简单使用
    • 4.通用类型any类型的实现
  • 四、功能模块划分
    • 1.SERVER模块
      • 1.1Buffer模块
      • 1.2.Socket模块
      • 1.3Channel模块
      • 1.4Connection模块
      • 1.5Acceptor模块
      • 1.6TimerQueue模块
      • 1.7Poller模块
      • 1.8EventLoop模块
      • 1.9TcpServer模块
    • 2.HTTP协议模块
      • 2.1Util模块
      • 2.2HttpRequest模块
      • 2.3HttpResponse模块
      • 2.4HttpContext模块
      • 2.5HttpServer模块
  • 五、模块关系图
  • 六、SERVER服务器模块实现
    • 1.缓冲区Buffer类实现
    • 2.日志宏的实现
    • 3.Socket模块实现
    • 4.Channel模块实现
    • 3.描述符事件监控Poller类实现
    • 4.EventLoop类的实现
    • 4.通信连接管理Connection类实现
    • 5.监听描述符管理Acceptor类实现
    • 6.LoopThread类的实现
    • 8.LoopThreadPool类的实现
    • 9.服务器TcpServer类实现
    • 10.基于TcpServer实现回显服务器
  • 七、HTTP协议模块
    • 1.Util工具类实现
    • 2.HttpRequest请求类实现
    • 3.HttpResponse响应类实现
    • 4.HttpContext上下文类实现
    • 5.HttpServer类实现
    • 6.HttpServer 简单测试
  • 八、性能测试
    • 1. 服务器长连接测试
    • 2. 服务器超时连接测试
    • 3.服务器错误请求测试
    • 4.服务器业务处理超时测试
    • 5.服务器同时多条请求测试
    • 服务器大文件传输测试
    • 服务器性能压力测试
    • 4.服务器业务处理超时测试
    • 5.服务器同时多条请求测试
    • 服务器大文件传输测试
    • 服务器性能压力测试

一、项目介绍

本项目主要是模仿 muduo 库实现一个以主从 Reactor 为模型,以 OneThreadOneEventLoop 为事件驱动的高并发服务器组件。通过这个服务器组件,我们可以简洁快速的搭建出一个高性能的 TCP 服务器。并且组件内部会提供不同的应用层协议支持,组件使用者可以通过这些协议快速的完成一个应用服务器的搭建。

muduo 源码 – https://github.com/chenshuo/muduo/tree/master/muduo

muduo 介绍 – https://www.cyhone.com/articles/analysis-of-muduo/

二、HTTP服务器

1.概念

HTTP(Hyper Text Transfer Protocol),超文本传输协议是应用层协议,是一种简单的请求-响应协议(客户端根据自己的需要向服务器发送请求,服务器针对请求提供服务,完毕后通信结束)。但是需要注意的是HTTP协议是一个运行在TCP协议之上的应用层协议,这一点本质上是告诉我们,HTTP服务器其实就是个TCP服务器,只不过在应用层基于HTTP协议格式进行数据的组织和解析来明确客户端的请求并完成业务处理。

因此实现HTTP服务器简单理解,只需要以下几步即可

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

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

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

4.将服务结果⼀HTTP协议格式进行组织,发送给客户端

实现一个HTTP服务器很简单,但是实现一个高性能的服务器并不简单,这个单元中将讲解基于Reactor模式的高性能服务器实现。当然准确来说,因为我们要实现的服务器本身并不存在业务,咱们要实现的应该算是一个高性能服务器基础库,是一个基础组件。

2.Reactor模型

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

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

QQ截图20231222144306

单Reactor单线程:在单个线程中进行事件监控并处理

服务端处理思想:事件驱动触发模式

谁发送了数据,谁触发了事件就处理谁

技术支撑点:I/O多路转接技术

1.通过IO多路复用模型对所有的客户端进行IO事件监控

2.触发事件后,进行事件处理。哪个客户端触发了事件,就去处理谁:接收它的请求,进行业务处理,进行响应

​ a. 如果是新建连接请求,则获取新建连接,并添加至多路复用模型进行事件监控。

​ b. 如果是数据通信请求,则进行对应数据处理(接收数据,处理数据,发送响应)。

优点:所有操作均在同一线程中完成,思想流程较为简单,不涉及进程/线程间通信及资源争抢问题。因为是单线程操作,操作都是串行化的,思想较为简单,编码流程也较为简单〈不用考虑进程或者线程间的通信,以及安全问题)

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

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

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

QQ截图20231222144736

1.Reactor线程通过I/O多路复用模型进行客户端请求监控

2.触发事件后,进行事件处理

对所有的客户端进行IO事件监控,哪个客户端触发了事件,就去处理谁 处理:仅仅进行IO操作

​ a. 如果是新建连接请求,则获取新建连接,并添加至多路复用模型进行事件监控。

​ b. 如果是数据通信请求,则接收数据后分发给Worker线程池进行业务处理。

​ c. 工作线程处理完毕后,将响应交给Reactor线程进行数据响应

优点:充分利用了CPU多核资源,处理效率可以更高,降低了代码的耦合度

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

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

image-20250505202755041

image-20250505202821988

1.在主Reactor中处理新连接请求事件,有新连接到来则分发到子Reactor中监控

2.在子Reactor中进行客户端通信监控,有事件触发,则接收数据分发给Worker线程池

4.Worker线程池分配独立的线程进行具体的业务处理

​ a. 工作线程处理完毕后,将响应交给子Reactor线程进行数据响应

多Reactor多线程模式:基于单reactor多线程的缺点考虑,如果IO的时候,有连接到来无法处理,因此将连接处理单独拎出来。因此让一个Reactor线程仅仅进行新连接的处理,让其他的Reactor线程进行IO处理,IO Reactor线程拿到数据分发给业务线程池进行业务处理。因此多Reactor多线程模式,也叫做主从Reactor模型。

主Reactor线程:进行新连接事件监控。

**从属Reactor线程:进行lO事件监控 **

业务线程池:进行业务处理

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

但是大家也要理解:执行流并不是越多越好,因为执行流多了,反而会增加CPU切换调度的成本

目标定位:One Thread One Loop主从Reactor模型高并发服务器

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

三、前置知识技术点功能用例

1.C++11中的bind

bind (Fn&& fn, Args&&... args);

官方文档对于bind接口的概述解释:Bind function arguments

我们可以将bind接口看作是一个通用的函数适配器,它接受一个函数对象,以及函数的各项参数,然后返回一个新的函数对象,但是这个函数对象的参数已经被绑定为设置的参数。运行的时候相当于总是调用传入固定参数的原函数。

但是如果进行绑定的时候,给与的参数为 std::placeholders::_1, _2... 则相当于为新适配生成的函数对象的调用预留一个参数进行传递。

基于bind的作用,当我们在设计一些线程池,或者任务池的时候,就可以将将任务池中的任务设置为函数类型,函数的参数由添加任务者直接使用bind进行适配绑定设置,而任务池中的任务被处理,只需要取出一个个的函数进行执行即可。

这样做有个好处就是,这种任务池在设计的时候,不用考虑都有哪些任务处理方式了,处理函数该如何设计,有多少个什么样的参数,这些都不用考虑了,降低了代码之间的耦合度。

#include <iostream>
#include <string>
#include <vector>
#include <functional>void print(const std::string &str, const int num)
{std::cout << str << " " << num << std::endl;
}int main()
{print("hello world", 10);auto func1 = std::bind(print, "nihao shijie", 20);func1();auto func2 = std::bind(print, "你好,世界", std::placeholders::_1);func2(30);typedef std::function<void()> Task;std::vector<Task> array;array.push_back(std::bind(print, "hello world", 1));array.push_back(std::bind(print, "nihao shijie", 2));array.push_back(std::bind(print, "你好,世界", 3));for (auto &func : array){func();}return 0;
}
QQ截图20231225085605

2.简单的秒级定时任务实现

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

Linux提供给我们的定时器:

#include <sys/timerfd.h>int timerfd_create(int clockid, int flags);
clockid:
CLOCK_REALTIME--以系统时间作为计时基准值(如果系统时间发生了改变就会出问题)
CLOCK_MONOTONIC--以系统启动时间进行递增的一个基准值(定时器不会随着系统时间改变而改变)
flags:0-阻塞操作
返回值:文件描述符int timerfd_settime(int fd, int flags, struct itimerspec *new, struct itimerspec *old);
功能:启动定时器
fd: timerfd_create函数的返回值。文件描述符--创建的定时器的标识符
flags: 0-相对时间, 1-绝对时间;默认设置为0即可.
new: 用于设置定时器的新超时时间
old: 用于接收原来的超时时间struct timespec {time_t tv_sec; /* Seconds */long tv_nsec; /* Nanoseconds */
};
struct itimerspec {struct timespec it_interval; /* 第一次之后的超时间隔时间 */struct timespec it_value; /* 第一次超时时间 */
};
Linux下一切皆文件,定时器的操作也是跟文件操作并没有什么区别,而定时器定时的原理每隔一段时间(定时器的超时时间),定时器会在每次超时时,系统就会给这个描述符对应的定时器写入一个8字节数据,表示在上一次读取数据到当前读取数据期间超时了多少次。创建了一个定时器,定时器定设置的超时时间是3s,也就是说每3s计算一次超时
从启动开始,每隔3s中,系统都会给描述如写入一个1,表示从上一次读取数据到现在超时了1次
假设30s之后才读取数据,则这时候就会读取到一个10,表示上一次读取数据到限制超时了10

使用案例:

#include <iostream>
#include <string>
#include <sys/timerfd.h>
#include <unistd.h>
#include <cstdint>int main()
{int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);if (timerfd < 0){std::cerr << "timerfd create failed" << std::endl;exit(1);}struct itimerspec ims;// 第一次超时时间为1s后ims.it_value.tv_sec = 3;ims.it_value.tv_nsec = 0;// 第一次超时后,每次超时的间隔时ims.it_interval.tv_sec = 3;ims.it_interval.tv_nsec = 0;int n = timerfd_settime(timerfd, 0, &ims, nullptr);if (n < 0){std::cerr << "timefd settime failed" << std::endl;exit(2);}for (;;){uint64_t data;ssize_t n = read(timerfd, &data, 8);if (n > 0){std::cout << "超时了,距离上一次超时: " << data << std::endl;}}close(timerfd);return 0;
}

上边例子,是一个定时器的使用示例,是每隔3s钟触发一次定时器超时,否则就会阻塞在read读取数据这里。

基于这个例子,则我们可以实现每隔3s,检测一下哪些连接超时了,然后将超时的连接释放掉。

时间轮思想:

上述的例子,存在一个很大的问题,每次超时都要将所有的连接遍历一遍,如果有上万个连接,效率无疑是较为低下的。这时候大家就会想到,我们可以针对所有的连接,根据每个连接最近一次通信的系统时间建立一个小根堆,这样只需要每次针对堆顶部分的连接逐个释放,直到没有超时的连接为止,这样也可以大大提高处理的效率。

上述方法可以实现定时任务,但是这里给大家介绍另一种方案:时间轮

时间轮的思想来源于钟表,如果我们定了一个3点钟的闹铃,则当时针走到3的时候,就代表时间到了。

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

但是,同一时间可能会有大批量的定时任务,因此我们可以给数组对应位置下拉一个数组,这样就可以在同一个时刻上添加多个定时任务了。

当然,上述操作也有一些缺陷,比如我们如果要定义一个60s后的任务,则需要将数组的元素个数设置为60才可以,如果设置一小时后的定时任务,则需要定义3600个元素的数组,这样无疑是比较麻烦的。

因此,可以采用多层级的时间轮,有秒针轮,分针轮,时针轮, 60<time<3600则time/60就是分针轮对应存储的位置,当tick/3600等于对应位置的时候,将其位置的任务指向秒针,秒针轮进行移动。因为当前我们的应用中,倒是不用设计的这么麻烦,因为我们的定时任务通常设置的30s以内,所以简单的单层时间轮就够用了。

QQ截图20231225092948

但是,我们也得考虑一个问题,当前的设计是时间到了,则主动去执行定时任务,释放连接,那能不能在时间到了后,自动执行定时任务呢,这时候我们就想到一个操作–类的析构函数。

一个类的析构函数,在对象被释放时会自动被执行,那么我们如果将一个定时任务作为一个类的析构函数内的操作,则这个定时任务在对象被释放的时候就会执行。

但是仅仅为了这个目的,而设计一个额外的任务类,好像有些不划算,但是,这里我们又要考虑另一个问题,那就是假如有一个连接建立成功了,我们给这个连接设置了一个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的时候,这个任务才会被真正释放。

上述过程就是时间轮定时任务的思想了,当然这里为了更加简便的实现,进行了一些小小的调整实现。

总结:

1.同一时刻的定时任务只能添加一个,需要考虑如何在同一时刻支持添加多个定时任务

解决方案: 将时间轮的一维数组设计为二维数组(时间轮一位数组的每一个节点也是一个数组)

2.假设当前的定时任务是一个连接的非活跃销毁任务,这个任务什么时候添加到时间轮中比较合适

一个连接30s内都没有通信,则是一个非活跃连接,这时候就销毁。但是一个连接如果在建立的时候添加了一个30s后销毁的任务,但是这个连接30s内人家有数据通信,在第30s的时候不是一个非活跃连接。

思想:需要在一个连接有IO事件产生的时候,延迟定时任务的执行

作为一个时间轮定时器,本身并不关注任务类型,只要是时间到了就需要被执行

解决方案:类的析构函数+智能指针shared_ptr,通过这两个技术可以实现定时任务的延时

1.使用一个类,对定时任务进行封装,类实例化的每一个对象,就是一个定时任务对象,当对象被销毁的时候,再去执行定时任务(将定时任务的执行,放到析构函数中)

2.shared ptr用于对new的对象进行空间管理,当shared_ptr对一个对象进行管理的时候,内部有一个计数器,计数器为0的时候,则释放所管理的对象

int *a = new int;
std:shared ptr<int> pi(a); --- a对象只有在pi计数为0的时候,才会被释放
std:shared ptr<int> pi1(pi) --当针对pi又构建了一个shared_ptr对象,则pi和pi1计数器为2

当pi和pi1中任意一个被释放的时候,只是计数器-1,因此他们管理的a对象并没有被释放,只有当pi和pi1都被释放了,计数器为0了,这时候才会释放管理的a对象

基于这个思想,我们可以使用shared_ptr来管理定时器任务对象

但是std::shared_ptr pi2(a);但是如果pi2是针对的原始对象构造的。并不会跟pi和pi1,共享计数

使用案例:

#include <iostream>
#include <vector>
#include <memory>
#include <functional>
#include <unordered_map>
#include <unistd.h>// 定时器任务的回调函数,即超时时执行的任务
using TaskFunc = std::function<void()>;
// 定时器对象销毁的回调函数
using ReleaseFunc = std::function<void()>;// 定时器任务
class TimerTask
{
private:uint64_t _id;         // 定时器任务对象iduint32_t _timeout;    // 定时任务的超时时间bool _canceled;       // false表示没有被取消,true表示被取消了TaskFunc _task_cb;    // 定时器对象要执行的定时任务ReleaseFunc _release; // 用于删除TimerWheel中保存的定时器对象信息
public:TimerTask(const uint64_t &id, const uint32_t &delay, const TaskFunc &cb): _id(id), _timeout(delay), _task_cb(cb), _canceled(false) {}~TimerTask(){if (_canceled == false)_task_cb();_release();}void Canceled() { _canceled = true; }uint32_t DelayTime() { return _timeout; }void SetRelease(const ReleaseFunc &cb) { _release = cb; }
};// 管理定时器任务的shared_ptr
using PtrTask = std::shared_ptr<TimerTask>; 
// 管理定时器任务的weak_ptr
using WeakTask = std::weak_ptr<TimerTask>;// 时间轮
class TimerWheel
{
private:std::vector<std::vector<PtrTask>> _wheel;int _tick;     // 当前的秒针,走到哪里释放哪里,就相当于执行哪里的任务int _capacity; // 表盘的最大数量,即最大延迟时间std::unordered_map<uint64_t, WeakTask> _timers;private:void RemoveTimer(const uint64_t &id){auto it = _timers.find(id);if (it != _timers.end()){_timers.erase(it);}}public:TimerWheel() : _tick(0), _capacity(60), _wheel(_capacity) {}// 添加定时任务void TimerAdd(const uint64_t &id, const uint32_t &delay, const TaskFunc &cb){PtrTask ptr(new TimerTask(id, delay, cb));ptr->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));int pos = (_tick + delay) % _capacity;// 将shared_ptr管理的对象加入到时间轮中_wheel[pos].push_back(ptr);// 将shared_ptr管理的对象的weak_ptr加入到哈希表中_timers[id] = WeakTask(ptr);}// 刷新/延迟定时时间void TimerRefresh(const uint64_t &id){// 通过保存的定时器的weak_ptr构造一个shared_ptr出来,添加到轮子中auto it = _timers.find(id);if (it == _timers.end()){return;}// lock获取weak_ptr管理的对象对应的shared_ptrPtrTask ptr = it->second.lock();int delay = ptr->DelayTime();int pos = (_tick + delay) % _capacity;_wheel[pos].push_back(ptr);}// 取消定时任务void TimerCancel(const uint64_t &id){auto it = _timers.find(id);// 没有找到定时任务就没法刷新和延迟,直接退出if (it == _timers.end())return;PtrTask ptr = it->second.lock();if (ptr)ptr->Canceled();}// 这个函数应该每秒钟被执行一次,相当于秒针向后走了一步void RunTimerTask(){_tick = (_tick + 1) % _capacity;// 清空指定位置的数组,就会把数组中保存的所有管理定时器对象的shared_ptr释放掉_wheel[_tick].clear();}
};class Test
{
public:Test() { std::cout << "Test 构造" << std::endl; }~Test() { std::cout << "Test 析构" << std::endl; }
};void Release(const Test *t)
{delete t;
}int main()
{TimerWheel tw;Test *t = new Test();tw.TimerAdd(100, 5, std::bind(Release, t));for (int i = 0; i < 5; i++){sleep(1);// 刷新定时任务tw.TimerRefresh(100);// 向后移动指针tw.RunTimerTask();std::cout << "刷新了定时任务,需要在5秒钟之后进行销毁" << std::endl;}for (;;){sleep(1);std::cout << "------------------" << std::endl;tw.RunTimerTask(); // 向后移动秒针}return 0;
}

3.正则库的简单使用

正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。

正则表达式的使用,可以使得HTTP请求的解析更加简单(这里指的时程序员的工作变得的简单,这并不代表处理效率会变高,实际上效率上是低于直接的字符串处理的),使我们实现的HTTP组件库使用起来更加灵活。

我们可以在这里学习相关的语法:正则表达式教程–菜鸟教程

bool std:regex_match (const stdrstring &src, std:smatch &matches, std:regex &e)
src:原始字符串
matches: 正则表达式可以从原始字符串中匹配并提取符合某种规则的数据,提取的数据就放在matches中,是一个类似于数组的容器
e: 正则表达式的匹配规则
返回值:用于确定匹配是否成功

正则表达式的简单案例:

void regex_test()
{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){std::cout << "regex_match failed" << std::endl;}for (auto &str : matches){std::cout << str << std::endl;}
}// 输出结果
/numbers/1234
1234

HTTP请求行的匹配

int main()
{// HTTP请求行格式:  GET /www.baidu.com/login?user=xiaoming&pass=123123 HTTP/1.1\r\nstd::string request = "GET /www.baidu.com/login?user=xiaoming&pass=123123 HTTP/1.1\r\n";std::smatch matches;// 请求方法的匹配 GET HEAD POST PUT DELETE// GET|HEAD|POST|PUT|DELETE 表示匹配并提取其中的任意一个字符串// [^?*] 表示匹配非问号字符 后边的*表示0次或多次// \\?(.*) \\? 表示原始的 ? 到字符,(.*)表示提取问哈之后的任意字符0次或多次,直到遇空格// HTTP/1\\.[01] 表示匹配以 HTTP/1.开始,后边有个0或1的字符串std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");// (?:\n|\r\n)? (?:...)表示匹配某个格式的字符串,但是不提取// 最后的?表示的是匹配前面的表达式0次或者1次bool ret = std::regex_match(request, matches, e);if (ret == false){std::cout << "regex_match failed" << std::endl;}for (auto &str : matches){std::cout << str << std::endl;}return 0;
}

4.通用类型any类型的实现

在本项目中,我们要实现一个高并发的服务器组件,能够的接收并处理客户端发送过来的请求,就必然涉及到与客户端的通信,而通信就必然涉及到对套接字的操作;同时,由于 TCP 是面向字节流的,因此服务器在接收客户端数据的时候就可能出现 socket 中的数据不足一条完整请求的情况,此时我们请求处理到一半时就需要停下来等待 socket 中下一次的数据到来。

因此我们需要为客户端连接设置一个请求处理的上下文,用来保存请求接收、解析以及处理的状态,它决定着对于下一次从缓冲区中取出的数据如何进行处理、从哪里开始处理等。同时,对于一条完整的请求,我们还需要对其进行解析,得到各种关键的要素,比如 HTTP 请求中的请求方法、请求URL、HTTP版本等,这些信息都会被保存在请求处理上下文中。

那么我们应该如何保存请求接收、解析以及处理的各种状态信息呢,定义一个 HTTP 请求信息的结构用于填充吗?如果我们的服务器组件仅支持 HTTP 协议这样做是可以的,但我们设计的服务器的目标是要能够支持各种不同的应用层协议,便于我们组件的使用者能够根据自己不同的业务场景定制对应的应用层协议进行使用,因此我们就需要让这个结构能够保存不同类型的数据,此时就需要 any 出场了。

每一个Connection对连接进行管理,最终都不可避免需要涉及到应用层协议的处理,因此在Connection中需要设置协议处理的上下文来控制处理节奏。但是应用层协议千千万,为了降低耦合度,这个协议接收解析上下文就不能有明显的协议倾向,它可以是任意协议的上下文信息,因此就需要一个通用的类型来保存各种不同的数据结构。

在C语言中,通用类型可以使用void*来管理,但是在C++中,boost库和C++17给我们提供了一个通用类型any来灵活使用,如果考虑增加代码的移植性,尽量减少第三方库的依赖,则可以使用C++17特性中的any,或者自己来实现。而这个any通用类型类的实现其实并不复杂,以下是简单的部分实现。

#include <iostream>
#include <typeinfo>
#include <cassert>
#include <unistd.h>
#include <any>class holder
{
public:virtual ~holder() {}virtual const std::type_info &type() = 0;virtual holder *clone() = 0;
};template <class 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); }virtual ~placeholder() {}public:T _val;
};class Any
{
private:holder *_content;public:Any() : _content(nullptr) {}template <class 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 &swap(Any &other){std::swap(_content, other._content);return *this;}// 返回子类对象保存数据的指针template <class T>T *get(){// 想要获取数据的类型,必须和保存的数据,类型一致assert(typeid(T) == _content->type());if (_content == nullptr)return nullptr;return &((placeholder<T> *)_content)->_val;}// 赋值运算符的重载函数template <class T>Any &operator=(const T &val){// 为val构造一个临时的通用容器,然后与当前容器自身进行指针交换,临时对象释放的时候// 原先保存的数据也就释放Any(val).swap(*this);return *this;}Any &operator=(const Any &other){Any(other).swap(*this);return *this;}
};class Test
{
public:Test() { std::cout << "Test() 构造" << std::endl; }Test(const Test &t) { std::cout << "Test(const Test& t) 拷贝构造" << std::endl; }~Test() { std::cout << "~Test() 析构" << std::endl; }
};int main()
{std::any a;a = 10;int *pi = std::any_cast<int>(&a);std::cout << *pi << std::endl;a = std::string("hello");std::string *ps = std::any_cast<std::string>(&a);std::cout << *ps << std::endl;// Any a;// a = 10;// int *pi = a.get<int>();// std::cout << *pi << std::endl;// a = std::string("hello world");// std::string *ps = a.get<std::string>();// std::cout << *ps << std::endl;// {//     Test t;//     a = t;// }return 0;
}

下面是C++17中any的使用用例:

int main()
{std::any a;a = 10;int *pi = std::any_cast<int>(&a);std::cout << *pi << std::endl;a = std::string("hello");std::string *ps = std::any_cast<std::string>(&a);std::cout << *ps << std::endl;
}

需要注意的是,C++17的特性需要高版本的g++编译器支持,建议g++ 7.3及以上版本。

sudo yum install centos-release-scl-rh centos-release-scl
sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++
source /opt/rh/devtoolset-7/enable 
echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc
// 查看g++版本 
g++ -v

四、功能模块划分

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

SERVER模块:实现Reactor模型的TCP服务器

协议模块:对当前的Reactor模型服务器提供应用层协议支持

1.SERVER模块

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

监听连接管理:对监听连接进行管理。

通信连接管理:对通信连接进行管理。

超时连接管理:对超时连接进行管理。

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

Buffer 模块:实现通信套接字的用户态缓冲区,防止接收到的数据不是一条完整的数据,同时确保客户端响应的数据在套接字可写的情况下进行发送。

Socket 模块:对 socket 套接字的操作进行封装,使得程序中对于套接字的各项操作更加简便。

Channel 模块:对于一个描述符进行监控事件管理,便于在用户态对描述符的监控事件进行维护。

Connection 模块:对通信连接进行整体管理,一个连接的所有操作都通过此模块来完成,增加连接操作的灵活以及便捷性。

Acceptor 模块:对监听套接字进行管理,为客户端的新建连接创建 Connection 对象,并设置各种回调。

TimerQueue 模块:定时任务模块,让一个任务可以在指定的时间之后被执行。

Poller模块:对任意的描述符进行IO事件监控,本质上就是对 epoll 的各种操作进行封装,从而让对描述符进行事件监控的操作更加简单,此模块是 Channel 模块的一个子模块。

EventLoop 模块:对事件监控进行管理,为了确保线程安全,此模块一个模块对应一个线程,服务器中的所有的事件都是由此模块来完成。

LoopThread 模块:将 EventLoop 与 thread 整合到一起,向外部返回所实例化的 EventLoop 对象,即将 EventLoop 对象与线程一一绑定。

LoopThreadPool 模块:LoopThread 线程池,用于对所有的 LoopThread 进行管理及分配。

TcpServer 模块:对前边所有子模块进行整合,从而提供给组件使用者的可以便捷的完成一个高性能服务器搭建的模块。

1.1Buffer模块

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

功能:用于实现通信套接字的用户态缓冲区

意义:

​ 1.防止接收到的数据不是一条完整的数据,因此对接收的数据进行缓存

​ 2.对于客户端响应的数据,应该是在套接字可写的情况下进行发送

功能设计:

​ 1.向缓冲区添加数据

​ 2.从缓冲区中取出数据

1.2.Socket模块

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

功能:对socket套接字的操作进行封装

意义:程序中对于套接字的各项操作更加简便

功能设计:

​ 1.创建套接字

​ 2.绑定地址信息

​ 3.开始监听

​ 4.向服务器发起连接

​ 5.获取新连接

​ 6.接收数据

​ 7.发送数据

​ 8.创建一个监听连接

​ 9.创建一个客户端连接

​ 10.设置套接字选项–开启地址端口复用

​ 11.设置套接字阻塞属性–设置为非阻塞

1.3Channel模块

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

功能:对于一个描述符进行监控事件的管理

意义:对于描述符的监控事件在用户态更容易维护,以及触发事件后的操作流程更加的清晰

功能设计:

​ 1.对监控事件的管理:

​ 1.1描述符是否可读

​ 1.2描述符是否可写

​ 1.3对描述符监控可读

​ 1.4对描述符监控可写

​ 1.5解除对可读事件监控

​ 1.6解除对可写事件监控

​ 1.7解除所有事件监控

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

1.4Connection模块

功能:

​ 1.这是一个对于通信连接进行整体管理的一个模块。对一个连接的操作都是通过这个模块进行的

​ 2.Connection模块,一个连接有任何的事件该怎么处理都是由这个模块来进行处理的,因为组件的设计也不知道使用者要如何处理事件。因此只能是提供一些事件回调函数由使用者设置

意义:这个模块本身来说不是一个单独的功能模块,是一个对连接做管理的模块。增加连接操作的灵活以及使捷性

功能设计:

​ 1.关闭连接

​ 2.发送数据

​ 3.协议切换

​ 4.启动非活跃连接超时释放

​ 5.取消非活跃连接超时释放

​ 6.回调函数设置:

​ 1.连接建立完成的回调

​ 2.连接有新数据接收成功后的回调

​ 3.连接关闭时的回调

​ 4.产生任何事件进行的回调

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进行面向系统的实际数据发送。

1.5Acceptor模块

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

意义:

​ 1.当获取了一个新建连接的描述符之后,需要为这个通信连接,封装一个Connection对象,设置各种不同回调

​ 2.注意:因为Acceptor模块本身并不知道一个连接产生了某个事件该如何处理,因此获取一个通信连接后,Connection的封装,以及事件回调的设置都应该由服务器模块来进行

功能设计:回调函数设置,新建连接获取成功的回调设置,由服务器来指定

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

Acceptor模块内部包含有一个Socket对象:实现监听套接字的操作

Acceptor模块内部包含有一个Channel对象:实现监听套接字IO事件就绪的处理

具体处理流程如下:

1.实现向Channel提供可读事件的IO事件处理回调函数,函数的功能其实也就是获取新连接

2.为新连接构建一个Connection对象出来。

1.6TimerQueue模块

功能:定时任务模块,让一个任务可以在指定的时间之后被执行

意义:组件内部,对于非活跃连接希望在N秒之后被释放

功能设计:

​ 1.添加定时任务

​ 2.刷新定时任务:希望一个定时任务重新开始计时

​ 3.取消定时任务

TimerQueue模块是实现固定时间定时任务的模块,可以理解就是要给定时任务管理器,向定时任务管理器中添加一个任务,任务将在固定时间后被执行,同时也可以通过刷新定时任务来延迟任务的执行。这个模块主要是对Connection对象的生命周期管理,对非活跃连接进行超时后的释放功能。

TimerQueue模块内部包含有一个timerfd:Linux系统提供的定时器。

TimerQueue模块内部包含有一个Channel对象:实现对timerfd的IO时间就绪回调处理

1.7Poller模块

功能:对任意的描述符进行lO事件监控

意义:对epoll进行的封装,让对描述符进行事件监控的操作更加简单

功能接口:

​ 1.添加事件监控:Channel模块

​ 2.修改事件监控

​ 3.移除事件监控

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

1.8EventLoop模块

功能:

​ 1.进行事件监控管理的模块

​ 2.这个模块其实就是我们所说的one thread one loop中的loop,也是我们所说的reactor

​ 3.这个模块必然是一个模块对应一个线程

意义:

​ 1.对于服务器中的所有的事件都是由EventLoop模块来完成

​ 2.每一个Connection连接,都会绑定一个EventLoop模块和线程,因为外界对于连接的所有操作,都是要放到同一个线程中进行的

思想:

​ 1.对所有的连接进行事件监控,连接触发事件后调用回调进行处理

​ 2.对于连接的所有操作,都要放到EventLoop线程中执行

功能设计:

​ 1.将连接的操作任务添加到任务队列

​ 2.定时任务的添加

​ 3.定时任务的刷新

​ 4.定时任务的取消

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的阻塞。

1.9TcpServer模块

功能:对前边所有子模块的整合模块,是提供给用户用于搭建一个高性能服务器的模块

意义:让组件使用者可以更加轻使的完成—个服务器的搭建

功能设计:

​ 1.对于监听连接的管理

​ 2.对于通信连接的管理

​ 3.对于超时连接的管理

​ 4.对于事件监控的管理

​ 5.事件回调函数的设置:一个连接产生了一个事件,对于这个事件如何处理,只有组件使用者知道,因此一个事件的处理回调,一定是组件使用者,设置给TcpServer,TcpServer设置给各个Connection连接

这个模块是一个整体TCP服务器模块的封装,内部封装了Acceptor模块,EventLoop ThreadPool模块。

TcpServer中包含有一个EventLoop对象:以备在超轻量使用场景中不需要EventLoop线程池,只需要在主线程中完成所有操作的情况。

TcpServer模块内部包含有一个EventLoop ThreadPool对象:其实就是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。

2.HTTP协议模块

HTTP协议模块用于对高并发服务器模块进行协议支持,基于提供的协议支持能够更方便的完成指定协议服务器的搭建。而HTTP协议支持模块的实现,可以细分为以下几个模块。

2.1Util模块

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

2.2HttpRequest模块

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

2.3HttpResponse模块

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

2.4HttpContext模块

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

2.5HttpServer模块

这个模块是最终给组件使用者提供的HTTP服务器模块了,用于以简单的接口实现HTTP服务器的搭建。

HttpServer模块内部包含有一个TcpServer对象:TcpServer对象实现服务器的搭建

HttpServer模块内部包含有两个提供给TcpServer对象的接口:连接建立成功设置上下文接口,数据处理接口。

HttpServer模块内部包含有一个hash-map表存储请求与处理函数的映射表:组件使用者向HttpServer设置哪些请求应该使用哪些函数进行处理,等TcpServer收到对应的请求就会使用对应的函数进行处理。

整体的模块示意图如下:

QQ截图20240916091235

五、模块关系图

Connection 模块关系图,Acceptor 模块关系图,EventLoop 模块关系图

通信连接模块关系图.drawio

六、SERVER服务器模块实现

1.缓冲区Buffer类实现

Buffer模块:缓冲区模块

提供的功能:存储数据,取出数据

实现思想:

​ 1.实现缓冲区得有一块存储空间,采用vector vector底层其实使用的就是一块线性的空间

​ 2.要素:

​ 1.默认空间的大小

​ 2.当前的读取数据位置

​ 3.当前的写入数据位置

​ 3.操作:

​ 1.写入数据:当前写入位置指向哪里,就从哪里开始写入,如果后续剩余的空间不够了,考虑整体缓冲区空间是否足够(因为读位置也会向后偏移,前边有可能会有空闲空间)如果空间足够,将数据移动到起始位置即可,如果空间不够,就进行扩容,从当前写位置开始扩容足够空间大小,数据一旦写入成功,当前写位置就要向后偏移

​ 2.读取数据:当前的读取位置指向哪里,就从哪里开始读取,前提是有数据可读,可读数据大小:当前写入位置减去当前读取位置

Buffer 模块的设计思想如下:

QQ截图20240916091718

框架设计:

class Buffer
{
private:std::vector<char> _buffer;/*位置是一个相对偏移量,而不是绝对地址*/uint64_t _read_index;// 读位置uint64_t _write_index;// 写位置
public:// 1.获取当前写位置地址// 2.确保可写空间足够(移动+扩容)// 3.获取前沿空闲空间大小// 4.获取后沿空闲空间大小// 5.将写位置向后移动指定长度// 6.获取当前读位置地址// 7.获取可读数据大小// 8.将读位置向后移动指定长度// 9.清理功能
};

具体实现:

#define BUFFER_DEFAULT_SIZE 1024
class Buffer
{
private:std::vector<char> _buffer; // 使用vector进行内存空间管理uint64_t _reader_idx;      // 读偏移uint64_t _writer_idx;      // 写偏移public:Buffer() : _reader_idx(0), _writer_idx(0), _buffer(BUFFER_DEFAULT_SIZE) {}~Buffer() {}public:// 获取起始地址char *Begin() { return &*_buffer.begin(); }// 获取当前写入起始位置 _buffer的空间起始地址加上写偏移量char *WritePosition() { return Begin() + _writer_idx; }// 获取当前读物起始位置char *ReadPosition() { return Begin() + _reader_idx; }// 获取前沿空闲空间大小(缓冲区末尾)--写偏移之后的空闲空间,总体空间大小减去写偏移uint64_t TailIdleSize() { return _buffer.size() - _writer_idx; }// 获取后沿空闲空间大小(缓冲区起始)--读偏移之前的空闲空间--读偏移之前的空闲空间uint64_t HeadIdleSize() { return _reader_idx; }// 获取可读数据大小 写偏移-读偏移uint64_t ReadAbleSize() { return _writer_idx - _reader_idx; }// 将读偏移向后移动void MoveReadOffset(const uint64_t &len){if (len == 0)return;// 读偏移向后移动的大小,必须小于可读数据大小assert(len <= ReadAbleSize());_reader_idx += len;}// 将写偏移向后移动void MoveWriteOffset(const uint64_t &len){if (len == 0)return;// 写偏移向后移动的大小,必须小于后边的空闲空间大小assert(len <= TailIdleSize());_writer_idx += len;}// 确保可写空间足够(整体空间足够了就一定数据,否则就扩容)void EnsureWriteSpace(const uint64_t &len){// 1.如果末尾空闲空间大小足够,直接返回if (len <= TailIdleSize()){return;}// 2.末尾空闲空间不够,则判断加上起始空闲空间大小是否足够,足够就将数据移动到起始位置else if (len <= TailIdleSize() + HeadIdleSize()){// 将数据移动到起始位置uint64_t readablesize = ReadAbleSize(); // 把当前数据大小先保存起来// 将数据拷贝到起始位置// std::copy(ReadPosition(),WritePosition(),Begin());std::copy(ReadPosition(), ReadPosition() + readablesize, Begin());_reader_idx = 0;            // 将读偏移归0_writer_idx = readablesize; // 将写位置置为可读数据大小}// 3.总体空间不够,则需要扩容,不移动数据,直接给写偏移之后扩容足够空间即可else{_buffer.resize(_writer_idx + len);}}// 写入数据void Write(const void *buffer, const uint64_t &len){// 1.保证有足够的空间 2.将数据拷贝进去if (len == 0)return;EnsureWriteSpace(len);const char *d = (const char *)buffer;std::copy(d, d + len, WritePosition());}// 写入数据并且写偏移向后移动void WriteAndPush(const void *buffer, const uint64_t &len){Write(buffer, len);MoveWriteOffset(len);}// 写入字符串void WriteString(std::string &data){// return Write(&data[0], data.size());return Write(data.c_str(), data.size());}// 写入字符串并且写偏移向后移动void WriteStringAndPush(std::string &data){WriteString(data);MoveWriteOffset(data.size());}// 写入一个Buffer对象void WriteBuffer(Buffer &data){return Write(data.ReadPosition(), data.ReadAbleSize());}void WriteBufferAndPush(Buffer &data){WriteBuffer(data);MoveWriteOffset(data.ReadAbleSize());}// 读取数据void Read(void *buffer, const uint64_t &len){// 要求要获取的数据大小必须小于可读数据大小assert(len <= ReadAbleSize());std::copy(ReadPosition(), ReadPosition() + len, (char *)buffer);}// 读取数据并读偏移向后移动void ReadAndPop(void *buffer, const uint64_t &len){Read(buffer, len);MoveReadOffset(len);}// 读取数据放入一个字符串中std::string ReadAsString(const uint64_t &len){// 要求要获取的数据大小必须小于可读数据大小assert(len <= ReadAbleSize());std::string str;str.resize(len);Read(&str[0], len);return str;}std::string ReadAsStringAndPop(const uint64_t &len){assert(len <= ReadAbleSize());std::string str = ReadAsString(len);MoveReadOffset(len);return str;}// 找到换行字符char *FindCRLF(){char *res = (char *)memchr(ReadPosition(), '\n', ReadAbleSize());return res;}// 获取一行数据std::string GetOneLine(){char *pos = FindCRLF();if (pos == nullptr)return "";// +1是为了将换行字符也取出来return ReadAsString(pos - ReadPosition() + 1);}std::string GetOneLineAndPop(){std::string str = GetOneLine();MoveReadOffset(str.size());return str;}// 清空缓冲区void Clear(){_reader_idx = 0;_writer_idx = 0;}
};

2.日志宏的实现

#include <iostream>
#include <ctime>
#include <thread>// 日志宏实现一
#define NORMAL 0
#define DEBUG 1
#define ERROR 2
#define LOG_LEVEL DEBUG#define LOG(level,format,...) do{\if(level < NORMAL) break;\time_t t = time(nullptr);/*获取时间戳*/\struct tm* ltm = localtime(&t);/*将时间戳转换为格式化的时间*/ \char buffer[32] = {0};\strftime(buffer,31,"%H:%M:%S",ltm);/*获取时分秒*/\/*##解除必须传递可变参数的限制*/\fprintf(stdout,"[%p %s %s:%d]" format "\n",(void*)pthread_self(),buffer,__FILE__,__LINE__,##__VA_ARGS__);\
}while(0)#define NOR_LOG(format,...) LOG(NORMAL,format,##__VA_ARGS__)
#define DBG_LOG(format,...) LOG(DEBUG,format,##__VA_ARGS__)
#define ERR_LOG(fromat,...) LOG(ERROR,format,##__VA_ARGS__)// 日志宏实现二    
enum
{NORMAL,DEBUG,WARNING,ERROR,FATAL
};// 将日志等级转换为字符串
const char *level_to_string(int level)
{switch (level){case NORMAL:return "NORMAL";case DEBUG:return "DEBUG";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "";}
}#define LogMessage(level, format, ...)                                                                                                                       \do                                                                                                                                                       \{                                                                                                                                                        \const char *level_str = level_to_string(level); /*日志等级*/                                                                                     \time_t ts = time(nullptr);                      /*时间戳*/                                                                                        \struct tm *lt = localtime(&ts);                 /*格式化时间*/                                                                                  \char buffer[32] = {0};                                                                                                                               \strftime(buffer, sizeof(buffer) - 1, "%y-%m-%d %H:%M:%S", lt);                                         /*格式化时间到字符串*/               \fprintf(stdout, "[%s][%s][%s:%d] " format "\n", level_str, buffer, __FILE__, __LINE__, ##__VA_ARGS__); /*##解除必须传递可变参数的限制*/ \} while (0)

3.Socket模块实现

功能设计:

​ 1.创建套接字

​ 2.绑定地址信息

​ 3.开始监听

​ 4.向服务器发起连接

​ 5.获取新连接

​ 6.接收数据

​ 7.发送数据

​ 8.创建一个监听连接

​ 9.创建一个客户端连接

​ 10.设置套接字选项–开启地址端口复用

​ 11.设置套接字阻塞属性–设置为非阻塞

QQ截图20240916091929

具体实现时的一些细节如下:

  • Socket 类的目的是对 socket 原生的各种操作进行封装,便于我们后面使用,但即使是这样,创建一个服务端/客户端连接的步骤也显得较为繁琐,所以我们在 Socket 类中提供了直接创建一个服务端连接以及直接创建一个客户端连接的接口。
  • 在 TCP 中,一个连接 bind 了一个地址与端口后,一旦连接断开则会进入 time_wait 状态,此时连接不会立即释放,会继续占用地址和端口,这种策略是用来保护客户端的,但它也会造成我们服务器崩溃后不能立即重新启动,因此我们需要对服务端连接设置套接字选项,开启地址与端口复用。
  • 我们通过 recv/send 系统调用来读取与发送 socket 中的数据时,一般会直接将 socket 缓冲区读空或者写满,而由于套接字默认是阻塞的,因此这会导致我们的程序阻塞在 recv/send 函数这里,因此我们还需要为套接字设置非阻塞属性。

具体实现:

#define MAX_LISTEN 1024
class Socket
{
private:int _sockfd;public:Socket() : _sockfd(-1) {}Socket(int fd) : _sockfd(fd) {}~Socket() { Close(); }int Fd() { return _sockfd; }public:// 创建套接字bool Create(){// int socket(int domain,int type,int protocol)_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (_sockfd < 0){LogMessage(FATAL, "socket create failed");return false;}return true;}// 绑定地址信息bool Bind(const std::string &ip, const uint16_t &port){struct sockaddr_in local;memset(&local, 0, 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(local);// int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);int ret = bind(_sockfd, (struct sockaddr *)&local, len);if (ret < 0){LogMessage(FATAL, "socket bind failed");return false;}return true;}// 开始监听bool Listen(int backlog = MAX_LISTEN){// int listen(int sockfd, int backlog);int ret = listen(_sockfd, backlog);if (ret < 0){LogMessage(FATAL, "socket listen failed");return false;}return true;}// 向服务器发起连接bool Connect(const std::string &ip, const uint16_t &port){struct sockaddr_in server;memset(&server, 0, sizeof server);server.sin_family = AF_INET;server.sin_port = htons(port);server.sin_addr.s_addr = inet_addr(ip.c_str());socklen_t len = sizeof(server);// int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);int ret = connect(_sockfd, (struct sockaddr *)&server, len);if (ret < 0){LogMessage(FATAL, "socket connect failed");return false;}return true;}// 获取新连接int Accept(){// int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);int newfd = accept(_sockfd, nullptr, nullptr);if (newfd < 0){LogMessage(FATAL, "connect newfd failed");return -1;}return newfd;}// 接收数据ssize_t Recv(void *buf, size_t len, int flags = 0){// ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t ret = recv(_sockfd, buf, len, flags);if (ret <= 0){// EAGAIN表示当前socket的接收缓冲区中没有数据了,在非阻塞的情况下才会有这个错误// EINTR 表示当前socket的阻塞等待,被信号打断了if (errno == EAGAIN || errno == EINTR){return 0;}LogMessage(FATAL, "recv message failed");return -1;}// 返回实际接收到的长度return ret;}// 非阻塞接收数据ssize_t NonBlockRecv(void *buf, size_t len){// MSG_DONTWAIT表示当接收为非阻塞return Recv(buf, len, MSG_DONTWAIT);}// 发送数据ssize_t Send(const void *buf, size_t len, int flags = 0){// ssize_t send(int sockfd, const void *buf, size_t len, int flags);ssize_t ret = send(_sockfd, buf, len, flags);if (ret < 0){if (errno == EINTR || errno == EAGAIN){return 0;}LogMessage(FATAL, "send message failed");return -1;}// 返回实际发送的长度return ret;}// 非阻塞发送数据ssize_t NonBlockSend(const void *buf, size_t len){if (len == 0)return 0;// MSG_DONTWAIT表示当前发送为非阻塞return Send(buf, len, MSG_DONTWAIT);}// 关闭套接字void Close(){if (_sockfd != -1){close(_sockfd);_sockfd = -1;}}// 创建一个服务端连接bool CreateServer(const uint16_t &port, const std::string &ip = "0.0.0.0", bool block_flag = false){// 1.创建套接字,2.设置非阻塞3.绑定地址,4.开始监听,5.启动地址重用if (Create() == false)return false;if (block_flag)NonBlock();if (Bind(ip, port) == false)return false;if (Listen() == false)return false;ReuseAddress();return true;}// 创建一个客户端连接bool CreateClient(const uint16_t &port, const std::string &ip){// 1.创建套接字,2,连接服务器if (Create() == false)return false;if (Connect(ip, port) == false)return false;return true;}// 设置套接字选项 --开启地址端口重用void ReuseAddress(){// int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);int opt = 1;setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof opt);opt = 1;setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void *)&opt, sizeof opt);// int val = 1;// setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, (void*)&val, sizeof(val));}// 设置套接字属性--设置为非阻塞void NonBlock(){// int fcntl(int fd, int cmd, ... /* arg */ );int flag = fcntl(_sockfd, F_GETFL, 0);fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);}
};

4.Channel模块实现

Channel类设计

目的:对描述符的监控事件管理

1.事件管理:

​ 描述符是否可读

​ 描述符是否可写

​ 对描述符监控可写

​ 对描述符监控可读

​ 解除可写事件监控

​ 解除可读事件监控

2.事件触发后的处理的管理

​ 1.需要处理的事件:可读,可写,挂断,错误,任意

​ 2.事件处理的回调函数

QQ截图20240916092257

具体实现时的一些细节如下:

  • 通信描述符事件触发后会调用回调函数进行处理,而这个回调函数是由 Connection 模块设置给 Channel 模块的,因为 Connection 是对通信连接进行整体管理的一个模块,Channel 模块只是 Connection 模块的一个子模块。
  • 为了保证线程安全,添加/修改/移除事件监控的操作需要放到 Connection 对象关联的 EventLoop 对应的线程中去执行,同时,对描述监控事件的修改最后也必须通过 Poller 模块中的 epoll 相关函数来完成,而 Poller 模块也是 EventLoop 的一个子模块。
成员:因为后边使用epoll进行事件监控
EPOLLIN  可读
EPOLLOUT  可写
EPOLLRDHUP 连接断开
EPOLLPRI   优先数据
EPOLLERR 出错了
EPOLLHUP  挂断

以上的事件都是使用一个数值uint32_t 进行保存

要进行事件管理,就需要有一个uint32_t 类型的成员保存当前需要监控的事件

事件处理这里,因为有五种事件需要处理,就需要五个回调函数

主要框架:

class Channel
{
private: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();void SetReadCallback(const EventCallback& cb);void SetWriteCallback(const EventCallback& cb);void SetErrorCallback(const EventCallback& cb);void SetCloseCallback(const EventCallback& cb);void SetEventCallback(const EventCallback& cb);bool ReadAble(); // 当前是否监控了可读bool WriteAble(); // 当前是否监控了可写void EnableRead(); // 启动可读事件void EnableWrite(); // 启动可写事件void DisableRead(); // 关闭读事件监控void DisableWrite();//关闭写事件监控void DisableAll();//关闭所有事件监控void Remove(); // 移除监控void Update();void HandleEvent();//事件处理,一旦发生了事件,就调用这个函数,自己触发 什么事件如何处理自己决定
};

具体实现:

class Poller;
class EventLoop;
class Channel
{
private:int _fd; // 文件描述符EventLoop *_loop;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(const uint32_t &revents) { _revents = revents; }// 设置回调函数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 (_events & EPOLLIN); }// 当前是否监控了可写bool WriteAble() { return (_events & EPOLLOUT); }// 启动可读事件void EnableRead() { _events |= EPOLLIN; Update(); }// 启动可写事件void EnableWrite() { _events |= EPOLLOUT; Update(); }// 关闭读事件监控void DisableRead() { _events &= (~EPOLLIN); Update(); }// 关闭写事件监控void DisableWrite() { _events &= (~EPOLLOUT); Update(); }// 关闭所有事件监控void DisableAll() { _events = 0; Update(); }// 移除监控void Remove();// 添加监控void Update();// 事件处理,一旦发生了事件,就调用这个函数,自己触发 什么事件如何处理自己决定void HandleEvent(){// 可读事件就绪if ((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI)){if (_read_callback){_read_callback();}if (_event_callback){_event_callback();}}// 可写事件就绪if (_revents & EPOLLOUT){if (_write_callback)_write_callback();if (_event_callback)_event_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();}}
};
// 移除监控
void Channel::Remove() { return _loop->RemoveEvent(this); }
// 添加监控
void Channel::Update() { return _loop->UpdateEvent(this); }

3.描述符事件监控Poller类实现

Poller模块:描述符IO事件监控模块

意义:通过epoll实现对描述符的 IO 事件监控

功能:

​ 1.添加/修改描述符的事件监控(不存在则添加,存在则修改)

​ 2.移除描述符的事件监控

封装思想:

​ 1.必须拥有一个epoll的操作句柄

​ 2.拥有一个struct epoll_event 结构数组,监控时保存所有的活跃事件

​ 3.使用hash表管理描述符与描述符对应的事件管理Channel事件

逻辑流程:

​ 1.对描述符进行监控,通过Channel才知道描述符需要监控什么事件

​ 2.当描述符就绪了,通过描述符在hash表中找到对应的Channel(得到了Channel才能知道什么事件如何处理)当描述符就绪了,返回描述符对应的Channel

QQ截图20240916092538

具体实现时的一些细节如下:

  • 由于描述符需要被监控的事件 _events 以及事件触发后的各种回调函数都保存在 Channel 中,并且就绪事件也需要保存到 Channel 的 _revents 中,因此在 Poller 中我们需要保存描述符与 Channel 的关联关系,这样才能知道要添加哪些事件监控,以及事件就绪后应该如何处理。
#define MAX_EPOLLEVENTS 1024
class Poller
{
private:int _epfd;struct epoll_event _evs[MAX_EPOLLEVENTS];std::unordered_map<int,Channel*>;
private://1.判断要更新事件的描述符是否存在//2.针对epoll直接操作(添加,修改,移除)
public://1.添加或更新描述符所监控的事件//2.移除描述符的监控//3.开始监控,获取就绪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 Update(Channel* channel,int op);// 判断一个channel是否已经添加了事件监控bool HashChannel(Channel* channel);public:Poller();// 添加或修改监控事件void UpdateEvent(Channel* channel);// 移除监控void RemoveEvent(Channel* channel);// 开始监控,返回活跃连接void Poll(std::vector<Channel*> active);
};

具体实现:

#define MAX_EPOLLEVENT 1024
class Poller
{
private:int _epfd;struct epoll_event _evs[MAX_EPOLLEVENT];std::unordered_map<int, Channel *> _channels;private:// 对epoll的直接操作void Update(Channel *channel, int op){// int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);struct epoll_event ev;ev.events = channel->Events();ev.data.fd = channel->Fd();int ret = epoll_ctl(_epfd, op, channel->Fd(), &ev);if (ret < 0){LogMessage(FATAL, "epollctl failed");}return;}// 判断一个channel是否已经添加了事件监控bool HasChannel(Channel *Channel){auto it = _channels.find(Channel->Fd());return it != _channels.end();// if(it == _channels.end()) return false;// return true;}public:Poller(){// int epoll_create(int size);_epfd = epoll_create(MAX_EPOLLEVENT);if (_epfd < 0){LogMessage(FATAL, "epoll create failed");abort();}}// 添加或修改监控事件void UpdateEvent(Channel *channel){bool ret = HasChannel(channel);// 不存在则添加if (ret == false){_channels.insert(std::make_pair(channel->Fd(), channel));return Update(channel, EPOLL_CTL_ADD);}// 存在则修改return Update(channel, EPOLL_CTL_MOD);}// 移除监控void RemoveEvent(Channel *channel){// bool ret = HasChannel(channel);// if(ret != false)// {//     _channels.erase(channel->Fd());//     return Update(channel,EPOLL_CTL_DEL);// }auto it = _channels.find(channel->Fd());if (it != _channels.end()){_channels.erase(it);return Update(channel, EPOLL_CTL_DEL);}}// 开始监控,返回活跃连接void Poll(std::vector<Channel *> *actives){// int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);int nfds = epoll_wait(_epfd, _evs, MAX_EPOLLEVENT, -1);if (nfds < 0){if (errno == EINTR){return;}LogMessage(FATAL, "epoll wait failed");abort();}for (int i = 0; i < nfds; i++){auto it = _channels.find(_evs[i].data.fd);assert(it != _channels.end());// 设置实际的就绪事件it->second->SetRevents(_evs[i].events);actives->push_back(it->second);}return;}
};

4.EventLoop类的实现

eventfd:一种事件通知机制

创建一个描述符用于实现事件通知,eventfd本质在内核中管理的就是一个计数器,创建eventfd就会在内核中创建一个计数器(结构),每当eventfd中写入一个数值–用于表示事件通知次数,可以使用read进行数据的读取,读取到的数据就是通知的次数,假设每次给eventfd中写入一个1,就表示通知了一次,连续写了三次之后,再去read读取出来的数字就是3,读取之后计数清0。

用处:在EventLoop模块中实现线程间事件通知功能

#include <sys/eventfd.h>
int eventfd(unsigned int initval,int flags);
功能:创建一个eventfd对象,实现事件通知
参数:
initval:计数初值
flags:
EFD_CLOEXEC--禁止进程复制
EFD_NONBLCK--启动非阻塞属性
返回值:返回一个文件描述符用于操作
eventdfd也是通过read/write/close进行操作
注意:read/write进行IO的时候数据只能是一个8字节数据

使用案例:

#include <iostream>
#include <sys/eventfd.h>
#include <unistd.h>
#include <cstdint>int main()
{int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);if (efd < 0){std::cerr << "eventfd create error" << std::endl;}uint64_t val = 1;write(efd, &val, sizeof(val));write(efd, &val, sizeof(val));write(efd, &val, sizeof(val));uint64_t res;int ret = read(efd, &res, sizeof(res));if (ret < 0){std::cerr << "read failed" << std::endl;}std::cout << res << std::endl;return 0;
}

EventLoop:进行事件监控以及事件处理的模块

关键点:这个模块与线程是一一对应关联的

监控了一个连接,而这个连接一旦就绪,就要进行事件处理,但是如果这个描述符,在多个线程中都触发了事件,进行处理,就会存在线程安全问题,因此我们需要将一个连接的事件监控,以及连接事件处理,以及其他操作都放在一个线程中进行

如何保证一个连接的所有操作都在EventLoop对应的线程中,解决方案:给EventLoop模块中,添加一个任务队列,对连接的所有操作,都进行一次封装,将对连接的操作并不直接执行,而是当做任务添加到任务队列中

EventLoop处理流程:

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

2.有描述符就绪则对描述符进行事件处理(如何保证处理回调函数中的操作都在线程中)

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

EventLoop分为epoll和task任务队列。epoll中文件描述符就绪了,进行事件处理–调用回调函数,处理过程中调用了send,而这个send是封装后的send,实际上内部是将数据的发送操作,压入队列,等到所有的就绪事件都处理完了,然后从task任务队列中一 一取出实际要进行操作执行。这样能够保证对于连接的所有操作,都是在一个线程中进行的,不涉及线程安全问题,但是对于任务队列的操作有线程安全问题,只需要给task的操作加上一把锁即可。

数据:

1.事件监控:即Poller模块,有事件就绪则进行事件处理

2.执行任务队列中的任务:一个线程安全的任务队列

注意点:因为有可能因为等待描述符IO事件就绪,导致执行流阻塞,这时候任务队列中的任务将得不到执行,因此得有一个事件通知的东西,能够唤醒事件监控的阻塞

当事件就绪,需要处理的时候,处理过程中,如果对连接要进行某些操作:这些操作必须在EventLoop对应的线程中执行,保证对连接的各项操作都是线程安全的。

1.如果执行的操作本就在线程中,不需要将操作压入队列了,可以直接执行

2.如果执行的操作不在线程中,才需要加入任务池,等待事件处理完了然后执行任务

QQ截图20240916092755

具体实现时的一些细节如下:

  • 当我们监控了一个客户端连接后,一旦这个连接触发了事件,就需要调用对应的回调函数进行事件处理,而在我们处理事件的过程中如果此连接触发了新的事件,那么新事件的处理就有可能被分配到其他线程中去执行,这样就有可能会导致线程安全问题。
  • 那么我们需要为每一个连接的操作都加一把锁来保证线程安全吗?这样做当然是可以的,但是没必要,因为当我们的连接很多时就需要创建很多的锁,这会造成不必要的资源开销;我们仅需将一个连接的事件监控,连接的事件处理以及连接的所有其他操作都放在同一个线程中去完成即可,即让连接与线程一一对应。
  • 虽然连接无法直接与线程一一对应,但是 EventLoop 模块是与线程是一一对应的,因此我们只需将一个连接与一个 EventLoop 模块相绑定,从而间接完成连接与线程的一一绑定。
  • 但是这样仍不保险,因为组件使用者可能自己设计了任务线程池,再一次对任务进行了分摊,在这种情况下我们并不能保证连接的所有操作都在同一个线程中完成,那么如何保证一个连接的所有操作都必定在 EventLoop 对应的线程中呢?
  • 我们的解决方案是给 EventLoop 模块中添加一个任务队列,对连接的所有操作并不直接执行,而是将其进行一次封装,然后当作任务添加到任务队列中,最后等到连接所有的就绪事件处理完了 (都添加都任务队列中了),再去将任务队列中的所有任务一一执行;此时我们仅需要对这个任务队列加一把锁保证其线程安全即可。
  • 我们举个例子,在一号线程中我们对连接1进行了事件监控,此时连接触发了事件A,事件A在一号线程中被执行,执行过程中触发了事件B,由于一号线程忙碌,因此事件B被分配到二号线程中执行 (假设外部设置了任务线程池),但事件A和事件B其实并没有被真正执行,而是仅仅压入任务队列后就返回了,最后得到所有就绪事件都被压入任务队列后,我们再在一号线程中逐个取出任务队列中的任务执行,从而保证线程安全。
  • 最后,因为有可能因为等待描述符IO事件就绪,导致执行流流程阻塞,这时候任务队列中的任务将得不到执行,因此需要使用 eventfd 来进行事件通知,唤醒事件监控的阻塞。

定时器模块的整合:

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

timewheel:实现每次执行Runtimetask,都可以执行一波到期的定时任务,要实现一个完整的秒级定时器,就需要将这两个功能整合到一起。

timerfd设置每秒钟触发一次定时事件,当事件被触发,则运行一次timerwheel的runtimertask,执行一下所有的过期定时任务

QQ截图20240916100421

具体实现时的一些细节如下:

  • 在前面我们学习了 timerfd 的使用以及 timerwheel 的设计思想,而要实现一个完整的秒级定时器,就需要将这两个功能整合到一起:
  • 一方面,我们将 timerfd 的超时时间设置为 1s,这样 timerfd 每秒钟就会触发一次可读事件 (timerfd 可读事件监控可以通过 EventLoop 来实现);另一方面,每当 timerfd 触发可读事件,我们就执行一次 TimerWheel 中的 RunTimerTask 函数,即执行秒针所在位置的所有超时事件。
  • 这样,我们在 TimerWheel 定时器中记录所有的超时事件,然后使用 timerfd 模拟来模拟定时器秒针的移动,从而实现了非活跃连接在 N 秒后释放的功能。

timewheel模块代码

// 定时器任务的回调函数,即超时时执行的任务
using TaskFunc = std::function<void()>;
// 定时器对象销毁的回调函数
using ReleaseFunc = std::function<void()>;
// 定时器任务
class TimerTask
{
private:uint64_t _id;         // 定时器任务对象iduint32_t _timeout;    // 超时定时任务的超时时间bool _canceled;       // 超时任务:false表示没有被取消,true表示被取消了TaskFunc _task_cb;    // 定时器对象要执行的定时任务ReleaseFunc _release; // 用于删除TimerWheel中保存的定时器对象信息
public:TimerTask(const uint64_t &id, const uint32_t &delay, const TaskFunc &cb): _id(id), _timeout(delay), _task_cb(cb), _canceled(false) {}~TimerTask(){if (_canceled == false)_task_cb();_release();}void Canceled() { _canceled = true; }uint32_t DelayTime() { return _timeout; }void SetRelease(const ReleaseFunc &cb) { _release = cb; }
};// 管理定时器任务的shared_ptr
using PtrTask = std::shared_ptr<TimerTask>;
// 管理定时器任务的weak_ptr
using WeakTask = std::weak_ptr<TimerTask>;
class EventLoop;
// 时间轮
class TimerWheel
{
private:int _tick;     // 当前的秒针,走到哪里释放哪里,就相当于执行哪里的任务int _capacity; // 表盘的最大数量,即最大延迟时间std::vector<std::vector<PtrTask>> _wheel;std::unordered_map<uint64_t, WeakTask> _timers;EventLoop *_loop;int _timerfd; // 定时器描述符--可读事件回调就是读取定时器,并执行定时任务std::unique_ptr<Channel> _timer_channel;private:void RemoveTimer(const uint64_t &id){auto it = _timers.find(id);if (it != _timers.end()){_timers.erase(it);}}// 创建定时器描述符static int CreateTimerfd(){// 创建一个定时器int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);if (timerfd < 0){LogMessage(FATAL, "create timerfd failed");abort();}// 设置超时时间struct itimerspec ims;// 第一次超时时间为1s后ims.it_value.tv_sec = 1;ims.it_value.tv_nsec = 0;// 第一次超时后,每次超时的间隔时ims.it_interval.tv_sec = 1;ims.it_interval.tv_nsec = 0;// 启动定时器int n = timerfd_settime(timerfd, 0, &ims, nullptr);if (n < 0){LogMessage(FATAL, "timerfd settime failed");abort();}return timerfd;}// 读取定时器描述符的数据int ReadTimerfd(){uint64_t times;ssize_t ret = read(_timerfd, &times, 8);if (ret < 0){LogMessage(FATAL, "read timerfd failed");abort();}return times;}// 这个函数应该每秒钟被执行一次,相当于秒针向后走了一步void RunTimerTask(){_tick = (_tick + 1) % _capacity;// 清空指定位置的数组,就会把数组中保存的所有管理定时器对象的shared_ptr释放掉_wheel[_tick].clear();}// 定时器读事件的回调函数void OnTime(){// 根据实际超时的次数,执行对应的超时任务int times = ReadTimerfd();for (int i = 0; i < times; i++){RunTimerTask();}}// 添加定时任务void TimerAddInLoop(const uint64_t &id, const uint32_t &delay, const TaskFunc &cb){PtrTask ptr(new TimerTask(id, delay, cb));ptr->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));int pos = (_tick + delay) % _capacity;// 将shared_ptr管理的对象加入到时间轮中_wheel[pos].push_back(ptr);// 将shared_ptr管理的对象的weak_ptr加入到哈希表中_timers[id] = WeakTask(ptr);}// 刷新/延迟定时时间void TimerRefreshInLoop(const uint64_t &id){// 通过保存的定时器的weak_ptr构造一个shared_ptr出来,添加到轮子中auto it = _timers.find(id);// 没找着定时任务,没法刷新,没法延迟if (it == _timers.end()){return;}// lock获取weak_ptr管理的对象对应的shared_ptrPtrTask ptr = it->second.lock();int delay = ptr->DelayTime();int pos = (_tick + delay) % _capacity;_wheel[pos].push_back(ptr);}// 取消定时任务void TimerCancelInLoop(const uint64_t &id){auto it = _timers.find(id);// 没有找到定时任务就没法刷新和延迟,直接退出if (it == _timers.end())return;PtrTask ptr = it->second.lock();if (ptr)ptr->Canceled();}public:TimerWheel(EventLoop *loop): _tick(0), _capacity(60), _wheel(_capacity),_loop(loop), _timerfd(CreateTimerfd()),_timer_channel(new Channel(_loop, _timerfd)){_timer_channel->SetReadCallback(std::bind(&TimerWheel::OnTime, this));_timer_channel->EnableRead(); // 启动读事件}// 定时器有个成员_timers成员,定时器信息的操作有可能在多个线程中进行,因此要考虑线程安全// 如果不想加锁,那就把对定时器的所有操作,都放到一个线程中执行// 添加定时任务void TimerAdd(const uint64_t &id, const uint32_t &delay, const TaskFunc &cb);// 刷新/延迟定时时间void TimerRefresh(const uint64_t &id);// 取消定时任务void TimerCancel(const uint64_t &id);// 存在线程安全问题,不能被外界使用者调用,只能在模块内,在对应的EventLoop线程内调用bool HasTimer(const uint64_t &id){auto it = _timers.find(id);// 没有找到定时任务就没法刷新和延迟,直接退出if (it == _timers.end())return false;return true;}
};
// 添加定时任务
void TimerWheel::TimerAdd(const uint64_t &id, const uint32_t &delay, const TaskFunc &cb)
{return _loop->RunInLoop(std::bind(&TimerWheel::TimerAddInLoop, this, id, delay, cb));
}
// 刷新/延迟定时时间
void TimerWheel::TimerRefresh(const uint64_t &id)
{return _loop->RunInLoop(std::bind(&TimerWheel::TimerRefreshInLoop, this, id));
}
// 取消定时任务
void TimerWheel::TimerCancel(const uint64_t &id)
{return _loop->RunInLoop(std::bind(&TimerWheel::TimerCancelInLoop, this, id));
}

EventLoop类的实现:

using Functor = std::function<void()>;
class EventLoop
{
private:std::thread::id _thread_id; // 线程IDint _event_fd;              // eventfd唤醒IO事件监控有可能导致的阻塞std::unique_ptr<Channel> _event_channel;Poller _poller;              // 进行描述符的事件监控std::vector<Functor> _tasks; // 任务池std::mutex _mutex;           // 实现任务池操作的线程安全TimerWheel _timer_wheel;     // 定时器模块
private:// 执行任务池中的所有任务void RunAllTask(){std::vector<Functor> functor;{std::unique_lock<std::mutex> lock(_mutex);_tasks.swap(functor);}for (auto &func : functor){func();}return;}// 创建一个eventfd对象,实现事件通知static int CreateEventFd(){// int eventfd(unsigned int initval, int flags);// EFD_CLOEXEC--禁止进程复制// EFD_NONBLCK--启动非阻塞属性int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);if (efd < 0){LogMessage(FATAL, "eventfd create failed");abort();}return efd;}// 读取eventfd中的数据,来完成清0-线程接收通知void ReadEventFd(){uint64_t res = 0;int ret = read(_event_fd, &res, sizeof(res));if (ret <= 0){// EINTR----被信号打断  EAGAIN -- 表示无数据可读if (errno == EINTR || errno == EAGAIN){return;}LogMessage(FATAL, "read eventfd failed");abort();}return;}// 向eventfd写入数据,实现线程的通知void WeakUpEventFd(){uint64_t val = 1;int ret = write(_event_fd, &val, sizeof(val));if (ret <= 0){if (errno == EINTR){return;}LogMessage(FATAL, "write eventfd failed");abort();}return;}public:EventLoop() : _thread_id(std::this_thread::get_id()),_event_fd(CreateEventFd()),_event_channel(new Channel(this, _event_fd)),_timer_wheel(this){// 给eventfd 添加可读事件回调函数,读取eventfd事件通知次数_event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventFd, this));// 启动eventfd的读事件监控_event_channel->EnableRead();}// 三步走---事件监控--》就绪事件处理--》执行任务void Start(){for (;;){// 1.事件监控std::vector<Channel *> actives;_poller.Poll(&actives);// 2.就绪事件处理for (auto &channel : actives){channel->HandleEvent();}// 3.执行任务RunAllTask();}}// 用于判断当前线程是否是EventLoop对应的线程bool IsInLoop(){return _thread_id == std::this_thread::get_id();}void AssertInLoop(){assert(_thread_id == std::this_thread::get_id());}// 判断将要执行的任务是否处于当前线程中void RunInLoop(const Functor &cb){if (IsInLoop()){return cb();}return QueueInLoop(cb);}// 将操作压入任务池void QueueInLoop(const Functor &cb){{std::unique_lock<std::mutex> lock(_mutex);_tasks.push_back(cb);}// 唤醒有可能因为没有事件就绪,而导致的epoll阻塞// 其实就是给eventfd写入一个数据,eventfd就会触发可读事件WeakUpEventFd();}// 添加/修改描述符的事件监控void UpdateEvent(Channel *channel){return _poller.UpdateEvent(channel);}// 移除描述符的事件监控void RemoveEvent(Channel *channel){return _poller.RemoveEvent(channel);}// 添加定时任务void TimerAdd(const uint64_t &id, const uint32_t &delay, const TaskFunc &cb){return _timer_wheel.TimerAdd(id, delay, cb);}// 刷新定时任务void TimerRefresh(const uint64_t &id){return _timer_wheel.TimerRefresh(id);}// 取消定时任务void TimerCancel(const uint64_t &id){return _timer_wheel.TimerCancel(id);}bool HasTimer(const uint64_t &id){return _timer_wheel.HasTimer(id);}
};

4.通信连接管理Connection类实现

Connection:

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

管理:
1.套接字的管理,能够进行套接字的操作

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

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

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

5.回调函数的管理

因为连接接收到数据之后该如何处理,需要由用户决定,因此必须有业务处理回调函数

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

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

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

功能:
1.发送数据—给用户提供的发送数据接口,并不是真正的发送接口,而只是把数据放到发送缓冲区,然后启动写事件监控

2关闭连接—给用户提供的关闭连接接口,应该在实际释放连接之前,看看输入输出缓冲区是否有数据待处理

3.启动非活跃连接的超时销毁功能

4.取消非活跃连接的超时销毁功能

5.协议切换-一个连接接收数据后如何进行业务处理,取决于上下文,以及数据的业务处理回调函数

Connection模块是对连接的管理模块,对于连接的所有操作都是通过这个模块完成的

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

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

QQ截图20240916100647

具体实现时的一些细节如下:

  • Connection 模块是对连接进行全方位管理的一个模块,而管理具体包括套接字的管理 – 使连接能够进行套接字的操作,连接事件的管理 – 包括可读,可写,错误,挂断以及任意事件,缓冲区的管理 – 便于 socket 数据的接收和发送,协议上下文的管理 – 用于记录请求数据的处理过程,以及回调函数的管理 – 提供连接建立完成、接收新数据、连接关闭、任意事件的回调函数设置接口,让组件使用者能够根据需要进行设置。
  • Connection 模块需要提供数据发送接口,但这并不是真正的发送接口,而只是把数据放到用户态发送缓冲区,然后描述符启动写事件监控,待到 socket 缓冲区可写后再真正发送数据;同样,关闭连接接口也并不是直接关闭连接,而应该在实际释放连接之前,看看输入输出缓冲区中是否有数据待处理,有则处理后再真正关闭连接;最后,一个连接接收到数据后应该如何进行业务处理,取决于上下文以及数据的业务处理回调函数,即上层协议,而切换协议接口的作用就是更改协议对应的上下文以及各种回调函数 (通用容器 Any)。
  • 由于对连接的所有操作都是通过 Connection 模块来完成的,因此可能出现对连接进行某种操作的时候,Connection 对象已经被释放的场景,从而造成内存访问错误,导致程序崩溃 (虽然其他线程中对连接的所有操作都会被放入任务队列中,最后在连接对应的 EventLoop 关联的线程中去执行,但是任务队列中任务的执行也存在先后顺序);因此我们使用 shared_ptr 对 Connection 对象进行管理,然后在任意一个地方对 Connection 对象进行操作的时候都保存一份 shared_ptr,这样就算其他地方进行了释放操作,也只是将 shared_ptr 的计数器 -1,而不会导致 Connection 的实际释放。
class Connection;
// DISCONNECTED -- 连接关闭状态   CONNECTING -- 连接建立成功
// CONNECTED -- 连接建立完成,各种设置已完成,可以通信的状态
// DISCONNECTING -- 待关闭的状态
typedef enum
{DISCONNECTED,CONNECTING,CONNECTED,DISCONNECTING
} ConnStatu;using PtrConnection = std::shared_ptr<Connection>;
class Connection
{
private:uint64_t _conn_id; // 连接的唯一ID// uint64_t _timer_id;            // 定时器ID,必须是唯一的,这块为了简化操作使用conn_id作为定时器IDint _sockfd;                   // 连接关联的文件描述符bool _enable_inactive_release; // 连接是否启动非活跃销毁的判断标志,默认为falseConnStatu _statu;              // 连接的状态Socket _socket;                // 套接字操作管理Channel _channel;              // 连接的事件管理Buffer _in_buffer;             // 输入缓冲区--存放从socket中读到的数据Buffer _out_buffer;            // 输出缓冲区--存放要发送给对端的数据Any _context;                  // 请求的接收处理的上下文/*这四个回调函数,是让服务器模块来设置的(其实服务器模块的处理回调也是组件使用者设置的)*//*换句话说,这几个回调都是组件使用者使用的*/using ConnectedCallback = std::function<void(const PtrConnection &)>;using MessageCallback = std::function<void(const PtrConnection &, Buffer *)>;using ClosedCallback = std::function<void(const PtrConnection &)>;using AnyEventCallback = std::function<void(const PtrConnection &)>;ConnectedCallback _connected_callback;MessageCallback _message_callback;ClosedCallback _closed_callback;AnyEventCallback _event_callback;ClosedCallback _server_closed_callback;private:// 五个Channel的事件回调函数void HandleRead();//描述符可读事件触发后的回调函数void HandleWrite();//描述符可写事件触发后的回调函数void HandleClose();//描述符挂断事件触发后的回调函数void HandleError();//描述符错误事件触发后的回调函数void HandleEvent();//描述符任意事件触发后的回调函数void EstableishdInLoop();//连接获取之后,所处的状态下要进行的各种设置(给Channel设置事件回调,启动读监控)void ReleaseInLoop();void SendInLoop(const char *data, int len);void ShutDownInLoop();void EnableInactiveReleaseInLoop(int sec);void CancelInactiveReleaseInLoop();void UpgrateInLoop(const Any &context,const ConnectedCallback &conn,const MessageCallback &msg,const ClosedCallback &closed,const AnyEventCallback &event);public:Connection(EventLoop *loop, const uint64_t &conn_id, int sockfd);~Connection();int Fd();                            // 获取管理的文件描述符int Id();                            // 获取连接IDbool Connected();                    // 是否处于CONNECTED状态void SetContext(const Any &context); // 设置上下文Any *GetContext();                   // 获取上下文,返回的是指针void SetConnextedCallback(const ConnectedCallback &cb);void SetMessageCallback(const MessageCallback &cb);void SetClosedCallback(const ClosedCallback &cb);void SetAnyEventedCallback(const AnyEventCallback &cb);// 连接建立就绪后,进行channel回调设置,启动读监控,调用_connected_callbackvoid Established();// 发送数据,将数据放到发送缓冲区,启动写事件监控void Send(const char *data, size_t len);// 提供给组件使用者的关闭接口 --并不实际关闭,需要判断有没有数据待处理void Shutdown();// 启动非活跃销毁,并定义多长时间无通信就是非活跃,添加定时任务void EnableInactiveRelease(int sec);// 取消非活跃销毁void CancleInactiveRelease(int sec);// 切换协议--重置上下文以及阶段性处理函数void Update(const Any &context, const ConnectedCallback &conn, const MessageCallback &msg,const ClosedCallback &closed, const AnyEventCallback &event);
};

完整代码:

class Connection;
// DISCONNECTED -- 连接关闭状态   CONNECTING -- 连接建立成功
// CONNECTED -- 连接建立完成,各种设置已完成,可以通信的状态
// DISCONNECTING -- 待关闭的状态
typedef enum
{DISCONNECTED,CONNECTING,CONNECTED,DISCONNECTING
} ConnStatu;using PtrConnection = std::shared_ptr<Connection>;
class Connection : public std::enable_shared_from_this<Connection>
{
private:uint64_t _conn_id; // 连接的唯一ID// uint64_t _timer_id;            // 定时器ID,必须是唯一的,这块为了简化操作使用conn_id作为定时器IDint _sockfd;                   // 连接关联的文件描述符bool _enable_inactive_release; // 连接是否启动非活跃销毁的判断标志,默认为falseEventLoop *_loop;              // 连接所关联的一个EventLoopConnStatu _statu;              // 连接的状态Socket _socket;                // 套接字操作管理Channel _channel;              // 连接的事件管理Buffer _in_buffer;             // 输入缓冲区--存放从socket中读到的数据Buffer _out_buffer;            // 输出缓冲区--存放要发送给对端的数据Any _context;                  // 请求的接收处理的上下文/*这四个回调函数,是让服务器模块来设置的(其实服务器模块的处理回调也是组件使用者设置的)*//*换句话说,这几个回调都是组件使用者使用的*/using ConnectedCallback = std::function<void(const PtrConnection &)>;using MessageCallback = std::function<void(const PtrConnection &, Buffer *)>;using ClosedCallback = std::function<void(const PtrConnection &)>;using AnyEventCallback = std::function<void(const PtrConnection &)>;ConnectedCallback _connected_callback;MessageCallback _message_callback;ClosedCallback _closed_callback;AnyEventCallback _event_callback;/*组件内的连接关闭回调--组件内设置的,因为服务器组件内会把所有的连接管理起来,一旦某个连接要关闭*//*就应该从管理的地方移除掉自己的信息*/ClosedCallback _server_closed_callback;private:// 五个Channel的事件回调函数// 描述符可读事件触发后的回调函数void HandleRead(){// 1.接收socket的数据,放到缓冲区char buffer[65536];ssize_t ret = _socket.NonBlockRecv(buffer, 65535);if (ret < 0){// 出错了,不能直接关闭连接return ShutDownInLoop();}else if (ret == 0){// 这里等于0表示的是没有读到数据,而并不是连接断开了,连接断开返回的是-1return;}// 将数据放入输入缓冲区_in_buffer.WriteAndPush(buffer, ret);// 2.调用message_callback进行业务处理if (_in_buffer.ReadAbleSize() > 0){// shared_from_this从当前对象自身获取自身的shared_ptr管理对象// 需要当前类继承enable_shared_from_this<T>return _message_callback(shared_from_this(), &_in_buffer);}}// 描述符可写事件触发后的回调函数,将发送缓冲区中的数据进行发送void HandleWrite(){// _out_buffer中保存的数据就是要发送的数据ssize_t ret = _socket.NonBlockSend(_out_buffer.ReadPosition(), _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(){return HandleClose();}// 描述符任意事件触发后的回调函数void HandleEvent(){if (_enable_inactive_release == true){_loop->TimerRefresh(_conn_id);}if (_event_callback){_event_callback(shared_from_this());}}// 连接获取之后,所处的状态下要进行的各种设置(给Channel设置事件回调,启动读监控)void EstableishdInLoop(){// 1.修改连接状态 2.启动读事件监控  3.调用回调函数// 当前状态必须一定是上层的半连接状态assert(_statu == CONNECTING);// 当前函数执行完毕则连接进入已完成连接状态_statu = CONNECTED;// 一旦启动读事件监控就可能会立即触发读事件,如果这时候启动了非活跃连接销毁_channel.EnableRead();if (_connected_callback)_connected_callback(shared_from_this());}// 这个接口是实际的释放接口void ReleaseInLoop(){// 1.修改连接状态,将其设置为DISCONNECTED_statu = DISCONNECTED;// 2.移除连接的事件监控_channel.Remove();// 3.关闭描述符_socket.Close();// 4.如果当前定时器队列中还有定时销毁任务,则取消任务if (_loop->HasTimer(_conn_id))CancelInactiveReleaseInLoop();// 5.调用关闭回调函数,避免先移除服务器管理的连接信息导致Connection被释放,再去处理会出错,因此先调用用户的回调函数if (_closed_callback)_closed_callback(shared_from_this());// 6.移除服务器内部管理的连接信息if (_server_closed_callback)_server_closed_callback(shared_from_this());}// 这个接口不是真正的发送接口,而只是把数据放到了发送缓冲区,启动了可写事件监控void SendInLoop(Buffer &buffer){if (_statu == DISCONNECTED)return;_out_buffer.WriteBufferAndPush(buffer);if (_channel.WriteAble() == false){_channel.EnableWrite();}}// 这个关闭操作并不是实际的连接关闭释放操作,需要判断还有没有数据待处理,待发送void ShutDownInLoop(){// 将连接设置为半连接状态_statu == DISCONNECTING;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.EnableWrite();}}if (_out_buffer.ReadAbleSize() == 0){Release();}}// 启动非活跃连接超时的释放规则void EnableInactiveReleaseInLoop(int sec){// 1.将判断标志 _enable_inactive_release 置为true_enable_inactive_release = true;// 2.如果当前定时器销毁任务已经存在,那就刷新延迟一下时间if (_loop->HasTimer(_conn_id)){return _loop->TimerRefresh(sec);}// 3.如果不存在定时销毁任务,则新增_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 UpgrateInLoop(const Any &context,const ConnectedCallback &conn,const MessageCallback &msg,const ClosedCallback &closed,const AnyEventCallback &event){_context = context;_connected_callback = conn;_message_callback = msg;_closed_callback = closed;_event_callback = event;}public:Connection(EventLoop *loop, const uint64_t &conn_id, int sockfd): _conn_id(conn_id), _sockfd(sockfd), _enable_inactive_release(false), _loop(loop),_statu(CONNECTING), _socket(sockfd), _channel(loop, _sockfd){_channel.SetCloseCallback(std::bind(&Connection::HandleClose, this));_channel.SetReadCallback(std::bind(&Connection::HandleRead, this));_channel.SetWriteCallback(std::bind(&Connection::HandleWrite, this));_channel.SetErrorCallback(std::bind(&Connection::HandleError, this));_channel.SetEventCallback(std::bind(&Connection::HandleEvent, this));}~Connection(){LogMessage(NORMAL, "release connection: %p", this);}// 获取管理的文件描述符int Fd() { return _sockfd; }// 获取连接IDint Id() { return _conn_id; }// 是否处于CONNECTED状态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) { _closed_callback = cb; }void SetAnyEventedCallback(const AnyEventCallback &cb) { _event_callback = cb; }void SetSrvClosedCallback(const ClosedCallback &cb) { _server_closed_callback = cb; }// 连接建立就绪后,进行channel回调设置,启动读监控,调用_connected_callbackvoid Established(){return _loop->RunInLoop(std::bind(&Connection::EstableishdInLoop, this));}// 发送数据,将数据放到发送缓冲区,启动写事件监控void Send(const char *data, size_t len){// 外界传入的data,可能是个临时的空间,我们现在只是把发送操作压入了任务池// 有可能并没有被立即执行// 因此有可能执行的时候,data指向的空间有可能已经被释放了。Buffer buffer;buffer.WriteAndPush(data, len);return _loop->RunInLoop(std::bind(&Connection::SendInLoop, this, std::move(buffer)));}// 提供给组件使用者的关闭接口 --并不实际关闭,需要判断有没有数据待处理void Shutdown(){return _loop->RunInLoop(std::bind(&Connection::ShutDownInLoop, this));}void Release(){_loop->QueueInLoop(std::bind(&Connection::ReleaseInLoop, this));}// 启动非活跃销毁,并定义多长时间无通信就是非活跃,添加定时任务void EnableInactiveRelease(int sec){return _loop->RunInLoop(std::bind(&Connection::EnableInactiveReleaseInLoop, this, sec));}// 取消非活跃销毁void CancleInactiveRelease(){return _loop->RunInLoop(std::bind(&Connection::CancelInactiveReleaseInLoop, this));}// 切换协议--重置上下文以及阶段性处理函数void Update(const Any &context, const ConnectedCallback &conn, const MessageCallback &msg,const ClosedCallback &closed, const AnyEventCallback &event){_loop->AssertInLoop();_loop->RunInLoop(std::bind(&Connection::UpgrateInLoop, this, context, conn, msg, closed, event));}
};

5.监听描述符管理Acceptor类实现

Acceptor模块:对监听套接字进行管理

1.创建一个监听套接字

2.启动读事件监控

3.事件触发后,获取新连接

4.调用新连接获取成功之后的回调函数

为新连接创建Connection进行管理(这一步不是Acceptor模块的操作,应该是服务器模块)

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

对于新连接如何处理,应该服务器模块来管理的

服务器模块,实现了一个对于新连接描述符处理的函数,将这个函数设置给Acceptor模块中的回调函数

QQ截图20240916101529

实现时的一些细节如下:

  • 由于 Acceptor 仅对监听套接字进行管理,所以它的设计流程很简单:
    1. 创建一个监听套接字用于监听客户端连接。
    2. 启动监听套接字的可读事件监控。
    3. 当可读事件触发后获取客户端新连接。
    4. 调用新连接获取成功后的回调函数,为新连接创建 Connection 对象进行管理。
  • 需要注意的是,服务器监听到一个新的客户端连接后,应该为新连接创建 Connection 对象,但由于 Acceptor 模块只对监听套接字进行管理,所以获取到新的客户端连接后需要由服务器模块对其进行处理,比如为其创建 Connection 对象,设置各种回调函数,因此 Acceptor 模块中仅有一个服务器模块设置的获取到新连接后的回调函数。

完整代码:

class Acceptor
{
private:Socket _socket;   // 同于创建监听套接字EventLoop *_loop; // 对于监听套接字进行事件监控Channel _channel; // 对于监控套接字进行事件管理using AcceptCallback = std::function<void(int)>;AcceptCallback _accept_callback;private:int CreateServer(const uint16_t &port){bool ret = _socket.CreateServer(port);assert(ret == true);return _socket.Fd();}// 监听套接字的读事件回调处理函数,获取新连接,调用_accept_callback函数进行新连接处理void HandleRead(){int newfd = _socket.Accept();if (newfd < 0){LogMessage(FATAL, "accept new fd failed");return;}if (_accept_callback)_accept_callback(newfd);}public:/*不能将启动读事件监控,放到构造函数中,必须在设置回调函数后,再去启动*//*否则有可能造成启动监控后,立即有事件,处理的时候,回调函数还没设置:新连接得不到处理,且资源泄漏*/Acceptor(EventLoop *loop, const uint16_t &port): _socket(CreateServer(port)), _loop(loop),_channel(_loop, _socket.Fd()){_channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));}void SetAcceptCallback(const AcceptCallback &cb){_accept_callback = cb;}void Listen(){_channel.EnableRead();}
};

6.LoopThread类的实现

目标:将EventLoop模块与线程整合起来

EventLoop模块与线程是一一对应的

EventLoop模块实例化的对象,在构造的时候就会初始化_thread_id

而后边当运行一个操作的时候判断当前是否运行EventLoop模块对应的线程中,就是将线程ID与EventLoop中的_thread_id进行一个比较,相同就表示同一个线程,不同就表示当前运行的并不是EventLoop线程

含义:EventLoop模块在实例化对象的时候,必须在线程的内部

EventLoop实例化对象时会设置自己的_thread_id

如果我们先创建了多个EventLoop对象,再设置新的_thread_id期间是不可控的

因此我们必须先创建线程,然后在线程的入口函数中,去实例化EventLoop对象

构造一个新的模块:LoopThread

这个模块的功能:将EventLoop与Thread整合到一起

思想:

1.创建线程

2.在线程中实例化EventLoop对象

功能:可以向外部返回实例化的EventLoop

我们上面在设计 EventLoop 模块时提到 EventLoop 模块与线程是一一对应的,并且由于 EventLoop 模块在构造时就会使用当前线程 id 来作为 EventLoop 对象所关联的线程的 id – _thread_id(std::this_thread::get_id());同时,我们后面在运行一个操作的时候判断当前是否运行在 EventLoop 模块对应的线程中,就是将线程 ID 与 EventLoop 模块中的 _thread id 进行比较,相同表示在同一个线程,不同则表示当前运行线程并不是 EventLoop 线程。

因此,EventLoop 模块必须在线程内部实例化,即先为 EventLoop 对象创建一个线程,然后在该线程的入口函数中去实例化 EventLoop 对象,这样该线程就会与 EventLoop 对象相关联 (实例化时该线程 id 被用于初始化 EventLoop 对象的 _thread_id)。

需要注意的是,我们不能事先创建多个 EventLoop 对象,然后创建多个线程,最后将各个线程的 id 重新赋值给 EventLoop 进行关联,因为这样在构造 EventLoop 对象到设置新的 _thread_id 期间,EventLoop 产生的操作将是不可控的。

基于以上思想,我们需要构建一个 LoopThread 模块,这个模块的功能是将 EventLoop 与 thread 整合到一起,向外部返回所实例化的 EventLoop 对象。

代码实现:

class LoopThread
{
private:/*用于实现_loop获取的同步关系,避免线程创建了,但是_loop还没有实例化之前去获取_loop*/std::mutex _mutex;             // 互斥锁std::condition_variable _cond; // 条件变量EventLoop *_loop;              // EventLoop指针变量,这个对象需要在线程内实例化std::thread _thread;           // EventLoop对应的线程private:/*实例化 EventLoop 对象,唤醒_cond上有可能阻塞的线程,并且开始运行EventLoop模块的功能*/void ThreadEntry(){EventLoop loop;{std::unique_lock<std::mutex> lock(_mutex);_loop = &loop;_cond.notify_all();}loop.Start();}public:/*创建线程,设定线程入口函数*/LoopThread() : _thread(std::thread(&LoopThread::ThreadEntry, this)), _loop(nullptr) {}/*返回当前线程关联的EventLoop对象指针*/EventLoop *GetEventLoop(){EventLoop *loop = nullptr;{std::unique_lock<std::mutex> lock(_mutex); // 加锁_cond.wait(lock, [&](){ return _loop != nullptr; }); //_loop为nullptr就一直阻塞loop = _loop;}return loop;}
};

8.LoopThreadPool类的实现

LoopThreadPool针对LoopThread设计一个线程池

LoopThreadPool模块:对所有的LoopThread进行管理和分配

功能:

1.线程数量可配置(0个或多个)

注意事项:在服务器中,主从Reactor模型是主线程只负责新连接获取,从属Reactor负责线连接的事件监控及业务处理,因此当前的线程池,有可能从属线程数量为0,也就是实现单Reactor服务器,一个线程既负责获取新连接,也负责连接到处理

2.对所有的线程进行管理,其实就是管理0个或多个LoopThread对象

3.提供线程分配的功能:

当主线程获取了一个新连接,需要将新连接挂到从属线程进行事件监控及处理

假设有0个从属线程,则直接分配给主线程的EventLoop,进行处理。

假设有多个从属线程,则采用RR轮转思想,进行线程的分配(将对应的EventLoop获取到,设置对应的Connection)

我们上面针对 EventLoop 设计 LoopThread 模块,由于客户端连接有多个,而每一个客户端连接都对应一个 Connection 模块、EventLoop 模块以及 LoopThread 模块,因此我们需要针对 LoopThread 设计一个线程池 – LoopThreadPool,用于对所有的 LoopThread 进行管理及分配。 LoopThreadPool 模块所要完成的功能如下:

  • 线程数量可配置 (0个或多个)。

    需要注意的是,在服务器中,由于主从 Reactor 模型是主线程只负责新连接获取,从属线程负责新连接的事件监控及处理,因此当前的线程池中从属线程的数量有可能会为0,也就是实现单 Reactor 服务器,仅有一个线程,其即负责获取连接,也负责连接的处理。

  • 对所有的线程进行管理 – 管理0个或多个 LoopThread 对象.

  • 提供线程分配的功能 – 当主线程获取了一个新连接时,将新连接挂到从属线程上进行事件监控及处理。

    假设有0个从属线程,则直接分配给主线程的 EventLoop 进行处理;假设有多个从属线程,则采用 RR 轮转思想,进行线程的分配 (将被选择线程的 EventLoop 对象获取到,然后设置给对应的 Connection 对象)

class LoopThreadPool
{
private:int _thread_count;//从属线程的数量int _next_loop_id;EventLoop* _baseloop;//主EventLoop,运行在主线程,从属线程数量为0,则所有的操作都在baseloop中运行std::vector<LoopThread*> _threads;//保存所有的LoopThread对象std::vector<EventLoop*> _loops;//从属线程数量大于0则从_loops中进行线程EventLoop分配
public:LoopThreadPool();void SetThreadCount(int count);//设置线程数量void Create();//创建所有的从属线程EventLoop* GetNextLoop();
};

完整代码:

class LoopThreadPool
{
private:int _thread_count;                  // 从属线程的数量int _next_idx;                      // RR轮转的下标EventLoop *_base_loop;              // 主EventLoop,运行在主线程,从属线程数量为0,则所有的操作都在baseloop中运行std::vector<LoopThread *> _threads; // 保存所有的LoopThread对象std::vector<EventLoop *> _loops;    // 从属线程数量大于0则从_loops中进行线程EventLoop分配public:LoopThreadPool(EventLoop *baesloop): _thread_count(0), _next_idx(0), _base_loop(baesloop) {}void SetThreadCount(const int &count) { _thread_count = count; }void Create(){if (_thread_count > 0){_threads.resize(_thread_count);_loops.resize(_thread_count);for (int i = 0; i < _thread_count; i++){_threads[i] = new LoopThread();_loops[i] = _threads[i]->GetEventLoop();}}}EventLoop *GetNextLoop(){if (_thread_count == 0){return _base_loop;}_next_idx = (_next_idx + 1) % _thread_count;return _loops[_next_idx];}
};

9.服务器TcpServer类实现

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

管理:

1.Acceptor对象,创建一个监听套接字

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

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

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

功能:

1.设置从属线程池数量

2.启动服务器

3.设置各种回调函数(连接建立完成,消息,关闭,任意),用户设置给TcpServer,TcpServer设置给获取的新连接

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

5.添加定时任务功能

流程:

1.在TcpServer中实例化一个Acceptot对象,以及一个EventLoop对象(baseloop)

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

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

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

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

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

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

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

QQ截图20240916102214

完整代码:

class TcpServer
{
private:uint64_t _next_id; // 这是一个自动增长的连接IDuint16_t _port;int _timeout;                                       // 这是非活跃连接的统计时间---多长时间无通信就是非活跃连接bool _enable_inactive_release;                      // 是否启动了非活跃连接超时销毁的判断标志EventLoop _baseloop;                                // 这是主线程的EventLoop对象,负责监听事件的处理Acceptor _acceptor;                                 // 这是监听套接字的管理对象LoopThreadPool _pool;                               // 这是从属EventLoop线程池std::unordered_map<uint64_t, PtrConnection> _conns; // 保存管理所有连接对应的shared_ptr对象using ConnectedCallback = std::function<void(const PtrConnection &)>;using MessageCallback = std::function<void(const PtrConnection &, Buffer *)>;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 _closed_callback;AnyEventCallback _event_callback;private:// 添加定时任务void RunAfterInLoop(const Functor &task, int delay){_next_id++;_baseloop.TimerAdd(_next_id, delay, task);}// 为新连接构造一个Connection进行管理void NewConnection(int fd){_next_id++;PtrConnection conn(new Connection(_pool.GetNextLoop(), _next_id, fd));conn->SetConnectedCallback(_connected_callback);conn->SetClosedCallback(_closed_callback);conn->SetMessageCallback(_message_callback);conn->SetAnyEventedCallback(_event_callback);conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, std::placeholders::_1));// 启动非活跃连接释放if (_enable_inactive_release)conn->EnableInactiveRelease(_timeout);conn->Established();_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(id);}}// 从管理Connection的_conns中移除连接信息void RemoveConnection(const PtrConnection &conn){_baseloop.RunInLoop(std::bind(&TcpServer::RemoveConnectionInLoop, this, conn));}public:TcpServer(const uint16_t &port): _next_id(0),_port(port),_enable_inactive_release(false),_acceptor(&_baseloop, port),_pool(&_baseloop){// 设置监听套接字读事件的回调函数_acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));// 将监听套接字挂到baseloop上_acceptor.Listen();}// 设置线程数量void SetThreadCount(int count) { _pool.SetThreadCount(count); }void SetConnectedCallback(const ConnectedCallback &cb) { _connected_callback = cb; }void SetMessageCallback(const MessageCallback &cb) { _message_callback = cb; }void SetClosedCallback(const ClosedCallback &cb) { _closed_callback = cb; }void SetAnyEventCallback(const AnyEventCallback &cb) { _event_callback = cb; }// 启动非活跃连接销毁void EnableInactiveRelease(int timeout){_enable_inactive_release = true;_timeout = timeout;}// 用于添加一个定时任务void RunAfter(const Functor &task, int delay){_baseloop.RunInLoop(std::bind(&TcpServer::RunAfterInLoop, this, task, delay));}// 启动服务器void Start(){// 线程池的创建_pool.Create();// 事件监控--》就绪事件处理--》执行任务_baseloop.Start();}
};

10.基于TcpServer实现回显服务器

EchoServer.hpp

#include "../server.hpp"class EchoServer
{
private:TcpServer _server;private:void OnConnected(const PtrConnection &conn){DLOG("NEW CONNECTION: %p", conn.get());}void OnClosed(const PtrConnection &conn){DLOG("CLOSE CONNECTION: %p", conn.get());}void OnMessage(const PtrConnection &conn, Buffer *buf){conn->Send(buf->ReadPosition(), buf->ReadAbleSize());buf->MoveReadOffset(buf->ReadAbleSize());conn->Shutdown();}public:EchoServer(const uint16_t &port) : _server(port){_server.SetThreadCount(2);_server.EnableInactiveRelease(10);_server.SetConnectedCallback(std::bind(&EchoServer::OnConnected, this, std::placeholders::_1));_server.SetClosedCallback(std::bind(&EchoServer::OnClosed, this, std::placeholders::_1));_server.SetMessageCallback(std::bind(&EchoServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));}void Start(){_server.Start();}
};

tcpClient.cc

#include "../source/server.hpp"int main()
{// 创建客户端连接Socket clientsock;clientsock.CreateClient(8080, "127.0.0.1");for (int i = 0; i < 5; i++){// 向服务器发送数据std::string str = "hello server";clientsock.Send(str.c_str(), str.size());// 接收服务器响应char buffer[1024];ssize_t n = clientsock.Recv(buffer, sizeof(buffer) - 1);buffer[n] = 0;LogMessage(NORMAL,"[echo]# %s", buffer);sleep(1);}while (true)sleep(1);clientsock.Close();return 0;
}

main.cc

#include "echo.hpp"int main()
{EchoServer server(8080);server.Start();return 0;
}

七、HTTP协议模块

1.Util工具类实现

QQ截图20240916102501

在Util工具类中主要实现一下的功能:

1.读取文件内容

2.向文件写入数据

3.URL编码

在RFC3986文档中规定的URL绝对不编码字符:. - _ ~以及数字和字母

还有一个就是在不同的一些标准中的特殊处理

W3C标准中规定param中的空格必须被编码为+

REF 2396中规定URL中保留字符需要转换为%HH格式

4.URL解码

5.响应状态码描述的获取

6.根据文件后缀名获取文件mime

7.判断一个文件是否是目录

8.判断一个文件是否是一个普通的文件

9.http请求的资源路径有效性判断

代码实现:

std::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:// 字符串分割函数,将src字符串按照sep字符进行分割,得到的各个字串放到arry中,最终返回字串的数量static size_t Split(const std::string &src, const std::string &sep, std::vector<std::string> *array){// abc,,,de,f,size_t offset = 0;// 有10个字符,offset是查找的起始位置,范围应该是0~9,offset==10就代表已经越界了while (offset < src.size()){// 在src字符串偏移量offset处,开始向后查找sep字符/字串,返回查找到的位置size_t pos = src.find(sep, offset);// 没有找到特定的字符将剩余的部分当作一个字串,放入arry中if (pos == std::string::npos){// if (pos == src.size())//     break;array->push_back(src.substr(offset));return array->size();}// 连续分割字符则跳过字符if (offset == pos){offset = pos + sep.size();continue;}array->push_back(src.substr(offset, pos - offset));offset = pos + sep.size();}return array->size();}// 读取文件的所有内容,将读取的内容放到一个Buffer中static bool ReadFile(const std::string &filename, std::string *buf){std::ifstream ifs(filename, std::ios::binary);if (ifs.is_open() == false){LogMessage(FATAL, "%s open failed", filename.c_str());return false;}size_t fsize = 0;// 跳转读写位置到末尾ifs.seekg(0, ifs.end);// 获取当前读写位置相对于起始位置的偏移量,从末尾偏移刚好就是文件大小fsize = ifs.tellg();// 跳转到起始位置ifs.seekg(0, ifs.beg);// 开辟文件大小的空间buf->resize(fsize);ifs.read(&(*buf)[0], fsize);if (ifs.good() == false){LogMessage(FATAL, "%s read 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){LogMessage(FATAL, "%s open failed", filename.c_str());return false;}ofs.write(buf.c_str(), buf.size());if (ofs.good() == false){LogMessage(FATAL, "%s write failed", filename.c_str());ofs.close();return false;}ofs.close();return true;}// URL编码,避免URL中资源路径与查询字符串中的特殊字符与HTTP请求中特殊字符产生歧义// 编码格式:将特殊字符的ascii值,转换为两个16进制字符,前缀%   C++ -> C%2B%2B// 不编码的特殊字符: RFC3986文档规定 . - _ ~ 字母,数字属于绝对不编码字符// RFC3986文档规定,编码格式 %HH// W3C标准中规定,查询字符串中的空格,需要编码为+, 解码则是+转空格static std::string UrlEncode(const std::string &url, bool convert_space_to_plus = false){std::string res;for (auto ch : url){// isalnum 是 C++ 中用于判断字符是否为字母或数字的函数if (ch == '.' || ch == '-' || ch == '_' || ch == '~' || isalnum(ch)){res += ch;continue;}if (ch == ' ' && convert_space_to_plus){res += '+';continue;}// 剩下的字符都是需要编码成为 %HH 格式char tmp[4] = {0};snprintf(tmp, 4, "%%%02X", ch);res += tmp;}return res;}static char HEXTOI(const char ch){if (ch >= '0' && ch <= '9'){return ch - '0';}else if (ch >= 'a' && ch <= 'z'){return ch - 'a' + 10;}else if (ch >= 'A' && ch <= 'Z'){return ch - 'A' + 10;}elsereturn -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] == '%' && (i + 2) < url.size()){char val1 = HEXTOI(url[i + 1]);char val2 = HEXTOI(url[i + 2]);char val = val1 * 16 + val2;i += 2;res += val;continue;}if (url[i] == '+' && convert_plus_to_space){res += ' ';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 "UnKnown";}// 根据文件后缀名获取文件mimestatic std::string ExtMime(const std::string &filename){// a.b.txt  先获取文件扩展名size_t pos = filename.rfind(".");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;// stat 函数用于获取文件的状态信息,包括文件大小、权限、最后访问时间等// int stat(const char *path, struct stat *buf);int ret = stat(filename.c_str(), &st);if (ret < 0){return false;}// S_ISDIR 是一个用于检查文件类型的宏,通常在 <sys/stat.h> 头文件中定义。// 这个宏用于确定传递给 stat 函数的文件是否为目录// #define S_ISDIR(mode)  (((mode) & S_IFMT) == S_IFDIR)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;}// #define S_ISREG(mode)  (((mode) & S_IFMT) == S_IFREG)// 这个宏用于确定传递给 stat 函数的文件是否为常规文件(regular file)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;}elselevel++;}return true;}
};

2.HttpRequest请求类实现

QQ截图20240916102544

HttpRequest模块:

Http请求信息模块:存储http请求信息要素,提供简单的功能性接口

请求信息要素:

请求行:请求方法 URL 协议版本

URL:资源路径 查询字符串

GET /search/1234?word=C++&en=utf8 HTTP/1.1

请求头部:key:value\r\n key:value\r\n…

Content-Length:0\r\n

正文

要素:请求方法,资源路径,查询字符串,头部字段,正文,协议版本

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

代码实现:

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 &value){_headers.insert(std::make_pair(key, value));}// 判断是否存在指定头部字段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") && GetHeader("Connection") == "keep-alive"){return false;}return true;}
};

3.HttpResponse响应类实现

QQ截图20240916102606

HttpResponse模块:

功能:存储http响应信息要素,提供简单的功能性接口

响应信息要素:

1.响应状态码

2.头部字段

3.响应正文

4.重定向信息(是否进行了重定向的标志,重定向的路径)

功能性接口:

0.为了便于成员的访问,因此将成员设置为共有成员

1.头部字段的新增,查询,获取

2.正文的设置

3.重定向的设置

4.长短连接的判断

代码实现:

class HttpResponse
{
public:int _statu;                                            // 响应状态码bool _redirect_flag;                                   // 是否重定向标志std::string _body;                                     // 正文std::string _redirect_url;                             // 重定向的urlstd::unordered_map<std::string, std::string> _headers; // 头部字段public:HttpResponse() : _statu(200), _redirect_flag(false) {}HttpResponse(int statu) : _statu(statu), _redirect_flag(false) {}void ReSet(){_statu = 200;_redirect_flag = false;_body.clear();_redirect_url.clear();_headers.clear();}// 插入头部字段void SetHeader(const std::string &key, const std::string &value){_headers.insert(std::make_pair(key, value));}// 判断是否存在指定头部字段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 SetContent(const std::string &body, const std::string &type = "text/html"){_body = body;SetHeader("Content-Length", type);}// 设置重定向void SetRedirect(const std::string &url, const 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;}
};

4.HttpContext上下文类实现

QQ截图20240916102636

代码实现:

#define MAX_LINE 8192
typedef enum
{RECV_HTTP_ERROR,RECV_HTTP_LINE,RECV_HTTP_HEAD,RECV_HTTP_BODY,RECV_HTTP_OVER
} HttpRecvStatu;class HttpContext
{
private:int _resp_statu;           // 响应状态码HttpRecvStatu _recv_statu; // 当前接收及解析的阶段状态HttpRequest _request;      // 已经解析得到的请求信息private:bool PraseHttpLine(std::string &line){std::smatch matches;std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);bool ret = std::regex_match(line, matches, e);if (ret == false){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUESTreturn false;}// 0 : GET /www.baidu/login?user=xiaoming&pass=123123 HTTP/1.1// 1 : GET// 2 : /bitejiuyeke/login// 3 : user=xiaoming&pass=123123// 4 : HTTP/1.1// 请求方法的获取_request._method = matches[1];std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);// 资源路径的获取,需要进行URL解码操作,但是不需要+转空格_request._path = Util::UrlDecode(matches[2], false);// 协议版本的获取_request._version = matches[4];// 查询字符串的获取与处理std::vector<std::string> query_string_array;std::string query_string = matches[3];// 查询字符串的格式 key=val&key=val....., 先以 & 符号进行分割,得到各个字串Util::Split(query_string, "&", &query_string_array);// 针对各个字串,以 = 符号进行分割,得到key 和val, 得到之后也需要进行URL解码for (auto &str : query_string_array){size_t pos = str.find("=");if (pos == std::string::npos){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUESTreturn false;}std::string key = Util::UrlDecode(str.substr(0, pos), true);std::string value = Util::UrlDecode(str.substr(pos + 1), true);_request.SetParam(key, value);}return true;}bool RecvHttpLine(Buffer *buf){if (_recv_statu != RECV_HTTP_LINE)return false;// 1. 获取一行数据,带有末尾的换行std::string line = buf->GetOneLineAndPop();// 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;}bool ret = PraseHttpLine(line);if (ret == false){return false;}// 首行处理完毕,进入头部获取阶段_recv_statu = RECV_HTTP_HEAD;return true;}bool RecvHttpHead(Buffer *buf){if (_recv_statu != RECV_HTTP_HEAD)return false;// 一行一行取出数据,直到遇到空行为止, 头部的格式 key: val\r\nkey: val\r\n....while (true){std::string line = buf->GetOneLineAndPop();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 = PraseHttpHead(line);if (ret == false)return false;}// 头部处理完毕,进入正文获取阶段_recv_statu = RECV_HTTP_BODY;return true;}bool PraseHttpHead(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; // BAD REQUESTreturn false;}std::string key = line.substr(0, pos);std::string value = line.substr(pos + 2);_request.SetHeader(key, value);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_length = content_length - _request._body.size();// 3. 接收正文放到body中,但是也要考虑当前缓冲区中的数据,是否是全部的正文//   3.1 缓冲区中数据,包含了当前请求的所有正文,则取出所需的数据if (real_length <= buf->ReadAbleSize()){_request._body.append(buf->ReadPosition(), real_length);buf->MoveReadOffset(real_length);_recv_statu = RECV_HTTP_OVER;return true;}//  3.2 缓冲区中数据,无法满足当前正文的需要,数据不足,取出数据,然后等待新数据到来_request._body.append(buf->ReadPosition(), 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_HEAD:RecvHttpHead(buf);case RECV_HTTP_BODY:RecvHttpBody(buf);}}
};

5.HttpServer类实现

HttpServer模块:用于实现HTTP服务器的搭建

设计一张请求路由表:

表中记录了针对哪个请求,应该使用哪个函数来进行业务处理的映射关系

当服务器收到了一个请求,就在请求路由表中,查找有没有对应请求的处理函数,如果有,则执行对应的处理函数即可说白了,什么请求,怎么处理,由用户来设定,服务器收到了请求只需要执行函数即可

这样做的好处:用户只需要实现业务处理函数,然后将请求与处理函数的映射关系,添加到服务器中

而服务器只需要接收数据,解析数据,查找路由表映射关系,执行业务处理函数。

要实现简便的搭建HTTP服务器,所需要的要素和提供的功能要素:

1.GET请求的路由映射表

2.POST请求的路由映射表

3.PUT请求的路由映射表

4.DELETE请求的路由映射表﹐—路由映射表记录对应请求方法的请求的处理函数映射关系—更多是功能性请求的处理

5.静态资源相对根目录—实现静态资源请求的处理

6.高性能TCP服务器—进行连接的IO操作

接口:

服务器处理流程:

1.从socket接收数据,放到接收缓冲区

2.调用OnMessage回调函数进行业务处理

3.对请求进行解析,得到了一个HttpRequest结构,包含了所有的请求要素

4.进行请求的路由查找–找到对应请求的处理方法

​ 1.静态资源请求—一些实体文件资源的请求,html,image…

将静态资源文件的数据读取出来,填充到HttpResponse结构中

​ 2.功能性请求—在请求路由映射表中查找处理函数,找到了则执行函数

具体的业务处理,并进行HttpResponse结构的数据填充

5.对静态资源请求/功能性请求进行处理完毕后,得到了一个填充了响应信息的HttpResponse对象,组织http格式响应,进行发送

接口:

添加请求-处理函数映射信息(GET/POST/PUT/DELETE)设置静态资源根目录

设置是否启动超时连接关闭

设置线程池中线程数量启动服务器

OnConnected —用于给TcpServer设置协议上下文

OnMessage -----用于进行缓冲区数据解析处理

获取上下文,进行缓冲区数据解析

请求的路由查找

静态资源请求查找和处理功能性请求的查找和处理组织响应进行回复

HTTP 服务器的运行流程如下:

  1. 从 socket 中接收数据,放到接收缓冲区。
  2. 调用 OnMessage 回调函数进行业务处理。
  3. 对请求进行解析,得到了一个 HttpRequest 结构对象,其中包含了所有的请求要素信息。
  4. 进行请求的路由查找 – 找到请求对应的处理方法。
    • 如果是静态资源请求,比如 html 页面,image 文件等,则将静态资源文件的数据读取出来,填充到 HttpResponse 结构中。
    • 如果是功能性请求,则在请求路由映射表中查找处理函数,找到了则执行函数进行具体的业务处理,并进行 HttpResponse 结构的数据填充。
  5. 对静态资源请求/功能性请求进行处理完毕后,已经得到了一个填充了响应信息的 HttpResponse 对象,将其组织成为 HTTP 格式响应,发送给客户端即可。

代码实现:

#define DEFAULT_TIME_OUT 30
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; // 静态资源根目录TcpServer _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 WriteResponse(const PtrConnection &conn, const HttpRequest &req, HttpResponse &rsp){// 1. 先完善头部字段if (req.Close() == true)rsp.SetHeader("Connection", "close");elsersp.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";// }for (auto &[key, val] : rsp._headers){rsp_str << key << ": " << val << "\r\n";}rsp_str << "\r\n";rsp_str << rsp._body;// 3. 发送数据conn->Send(rsp_str.str().c_str(), rsp_str.str().size());}// 功能性请求的分类处理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;// 传入请求信息,和空的rsp,执行处理函数return functor(req, rsp);}rsp->_statu = 404;}// 判断是否为静态资源请求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;}// 静态资源请求的处理void 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 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());LogMessage(NORMAL, "NEW CONNECTION %p", conn.get());}// 缓冲区数据解析+处理void OnMessage(const PtrConnection &conn, Buffer *buf){while (buf->ReadAbleSize() > 0){// 1. 获取上下文HttpContext *context = conn->GetContext()->get<HttpContext>();// 2. 通过上下文对缓冲区数据进行解析,得到HttpRequest对象//   1. 如果缓冲区的数据解析出错,就直接回复出错响应//   2. 如果解析正常,且请求已经获取完毕,才开始去进行处理context->RecvHttpRequest(buf);HttpRequest &req = context->Request();HttpResponse rsp(context->RespStatu());if (context->RespStatu() >= 400){// 进行错误响应,关闭连接// 填充一个错误显示页面数据到rsp中ErrorHandler(req, &rsp);// 组织响应发送给客户端WriteResponse(conn, req, rsp);context->ReSet();// 出错了就把缓冲区数据清空buf->MoveReadOffset(buf->ReadAbleSize());// 关闭连接conn->Shutdown();return;}// 当前请求还没有接收完整,则退出,等新数据到来再重新继续处理if (context->RecvStatu() != RECV_HTTP_OVER)return;// 3. 请求路由 + 业务处理Route(req, &rsp);// 4. 对HttpResponse进行组织发送WriteResponse(conn, req, rsp);// 5. 重置上下文context->ReSet();// 6. 根据长短连接判断是否关闭连接或者继续处理--//短链接则直接关闭if (rsp.Close() == true){conn->Shutdown();}}}public:HttpServer(int port, int timeout = DEFAULT_TIME_OUT) : _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) { _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(); }
};

6.HttpServer 简单测试

下面我们分别对 HttpServer 的 GPT、POST、PUT 以及 DELETE 请求进行简单测试。

测试代码如下:

#include "./http.hpp"#define WWWROOT "./wwwroot/"std::string RequestStr(const HttpRequest &req)
{std::stringstream ss;ss << req._method << " " << req._path << " " << req._version << "\r\n";for (auto &[key, val] : req._params){ss << key << ": " << val << "\r\n";}for (auto &[key, val] : req._headers){ss << key << ": " << val << "\r\n";}ss << "\r\n";ss << req._body;return ss.str();
}void Hello(const HttpRequest &req, HttpResponse *rsp)
{rsp->SetContent(RequestStr(req), "text/plain");
}
void Login(const HttpRequest &req, HttpResponse *rsp)
{rsp->SetContent(RequestStr(req), "text/plain");
}
void PutFile(const HttpRequest &req, HttpResponse *rsp)
{std::string pathname = WWWROOT + req._path;Util::WriteFile(pathname, req._body);
}
void DelFile(const HttpRequest &req, HttpResponse *rsp)
{rsp->SetContent(RequestStr(req), "text/plain");
}int main()
{HttpServer server(8080);server.SetThreadCount(3);server.SetBaseDir(WWWROOT);server.Get("/hello", Hello);server.Post("/login", Login);server.Put("/1234.txt", PutFile);server.Delete("/1234.txt", DelFile);server.Listen();return 0;
}

项目目录结构如下:

|-- echo
|   |-- echo.hpp
|   |-- main.cc
|   `-- makefile
|-- http
|   |-- http.hpp     // HttpServer
|   |-- main
|   |-- main.cc     // 测试程序
|   |-- makefile
|   `-- wwwroot     //静态资源根目录
|       |-- 1234.txt
|       `-- index.html
|-- log.hpp
|-- main.cc
|-- makefile
`-- server.hpp    // TcpServer

静态资源根目录中的文件信息如下:

<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>

回显服务器测试结果如下:

QQ截图20240914101830

登录请求测试结果如下:

QQ截图20240914101948 QQ截图20240914102014

八、性能测试

面我们已经完成了 SERVER 模块和协议模块的开发,并进行了简单的功能测试,下面我们来进行一些边界性的功能测试,观察服务器在边界情况下能够正常运行。

1. 服务器长连接测试

创建一个客户端,设置 Connection 头部字段为 keep-alive,观察客户端是否能够持续与服务器进行通信。

/*** @details 长连接测试1:创建一个客户端持续给服务器发送数据,直到超过超时时间看看是否正常*/#include "../source/server.hpp"int main()
{// 创建客户端连接Socket clientsock;clientsock.CreateClient(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";for (;;){int ret = clientsock.Send(req.c_str(), req.size());assert(ret > 0);char buffer[1024] = {0};ret = clientsock.Recv(buffer, 1023);LogMessage(DEBUG, "[%s]", buffer);sleep(3);}clientsock.Close();return 0;
}

从测试结果可以看到,客户端能够持续与服务器进行通信,并且服务器也不会在 10s 后将客户端连接释放,而是等待客户端主动退出后才会释放:

QQ截图20240914102629

过几分钟之后再关闭客户端

QQ截图20240914103227

客户端收到的响应

QQ截图20240914102711

2. 服务器超时连接测试

客户端连接上服务器后,长时间不给服务器发送数据,观察超时时间 (10s) 后服务器是否会将客户端连接进行释放。

/*** @brief 超时连接测试1:创建一个客户端,给服务器发送一次数据之后,不动了,查看服务器是否会正常的超时关闭连接
*/#include "../source/server.hpp"int main()
{// 创建客户端连接Socket clientsock;clientsock.CreateClient(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";for (;;){int ret = clientsock.Send(req.c_str(), req.size());assert(ret > 0);char buffer[1024] = {0};ret = clientsock.Recv(buffer, 1023);LogMessage(DEBUG, "[%s]", buffer);sleep(15);}clientsock.Close();return 0;
}

从测试结果可以看到,服务器经过超时时间后自动将客户端连接释放:

QQ截图20240914103421

3.服务器错误请求测试

给服务器发送一个请求,添加头部字段 Content-Length 为100,但实际发送的正文长度不足100,观察服务器的处理结果;我们的预期结果有两种:

  1. 如果客户端只发生一次请求,由于服务器未接收到完整请求(正文数据不足),所以会等待新数据到来,不会给与客户端响应,直到连接超时释放。
  2. 如果客户端发送多次请求,那么服务器会将后面的请求字段作为第一次请求的正文,完成业务处理后发送一次响应,但这样很有可能会导致后面的请求解析错误。

测试代码

/*** @brief 给服务器发送一个数据,告诉服务器要发送1024字节的数据,但是实际发送的数据不足1024,查看服务器处理结果* @details 1.如果服务器只发送一次,服务器将得不到完整的请求,就不会处理业务,客户端也就得不到响应,最终超时关闭连接* 2.连着给服务器发送了多次 小的请求,服务器会将后边的请求当做前边请求的正文来进行处理,* 而后便处理的时候有可能就因为处理错误而关闭连接*//*** @details 连着给服务器发送了多次 小的请求,服务器解析出错,返回错误,关闭连接,* 但是关闭连接的时候如果缓冲区里面有数据,由不足一行数据,取不出来,而关闭连接的时候有数据又会启动* 有消息的事件,就会再次触发OnMassage回调函数,一直循环,解决办法,解析出错,就情况缓冲区的数据* 第一次解析错误的时候,状态码为RECV_HTTP_ERROR,没有更新状态,那么下次再进去的时候,状态还是RECV_HTTP_ERROR* 此时解析http的函数并不会读取数据,所以缓冲区中的数据一直处理不完,解决办法重置状态*/#include "../source/server.hpp"int main()
{// 创建客户端连接Socket clientsock;clientsock.CreateClient(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 world";for (;;){assert(clientsock.Send(req.c_str(), req.size()) != -1);assert(clientsock.Send(req.c_str(), req.size()) != -1);assert(clientsock.Send(req.c_str(), req.size()) != -1);assert(clientsock.Send(req.c_str(), req.size()) != -1);char buffer[1024] = {0};assert(clientsock.Recv(buffer, 1023));LogMessage(DEBUG, "[%s]", buffer);sleep(3);}clientsock.Close();return 0;
}

测试结果如下:

QQ截图20240914104127

4.服务器业务处理超时测试

当服务器达到性能瓶颈,即处理一次业务花费的时间超过了服务器设置的非活跃连接超时时间时,查看服务器的处理情况。我们的预期结果如下:

  • 由于服务器进行单次业务处理的时间超过了连接的超时时间,所以可能导致其他连接被拖累从而超时释放,具体来说,假设现在4 5 6 7描述符就绪,并且在处理4号描述符就绪事件时超时,那么会出现以下两种情况:
    1. 如果4后面的5 6 7号都是通信连接描述符,则并不影响,因为4号描述符就绪事件处理完毕后就会处理它们的就绪事件并刷新其活跃度。
    2. 如果5号描述符是定时器描述符,此时定时器触发超时,就会执行定时任务,由于6、7号描述符被4号描述符拖累,达到了超时时间,因此会被释放,从而导致在进行6 7业务处理时发生内存访问错误 (6号同理)。
  • 因此,在本次事件处理过程中,并不能直接释放通信连接,而应该将释放操作压入任务队列中,待就绪事件全部处理完毕后再真正释放连接。
/* 业务处理超时,查看服务器的处理情况当服务器达到了一个性能瓶颈,在一次业务处理中花费了太长的时间(超过了服务器设置的非活跃超时时间)1. 在一次业务处理中耗费太长时间,导致其他的连接也被连累超时,其他的连接有可能会被拖累超时释放假设现在  12345描述符就绪了, 在处理1的时候花费了30s处理完,超时了,导致2345描述符因为长时间没有刷新活跃度1. 如果接下来的2345描述符都是通信连接描述符,如果都就绪了,则并不影响,因为接下来就会进行处理并刷新活跃度2. 如果接下来的2号描述符是定时器事件描述符,定时器触发超时,执行定时任务,就会将345描述符给释放掉这时候一旦345描述符对应的连接被释放,接下来在处理345事件的时候就会导致程序崩溃(内存访问错误)因此这时候,在本次事件处理中,并不能直接对连接进行释放,而应该将释放操作压入到任务池中,等到事件处理完了执行任务池中的任务的时候,再去释放
*//*** 对于释放连接,ShutDownInLoop 不应该立即进行释放ReleaseInLoop,而是应该压入任务队列中* 这样就不需要在只要有读写事件之前就刷新一次活跃度,只需要在业务处理完毕之后刷新活跃度即可* OnTime() 定时器需要返回当前超时的次数,来进行执行超时任务*  // 根据实际超时的次数,执行对应的超时任务*/#include "../source/server.hpp"int main()
{for (int i = 0; i < 10; i++){pid_t pid = fork();if (pid < 0){LogMessage(ERROR, "fork error");return -1;}else if (pid == 0){// 创建客户端连接Socket clientsock;clientsock.CreateClient(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";for (;;){int ret = clientsock.Send(req.c_str(), req.size());assert(ret > 0);char buffer[1024] = {0};ret = clientsock.Recv(buffer, 1023);LogMessage(DEBUG, "[%s]", buffer);}clientsock.Close();exit(0);}}while(1) sleep(1);return 0;
}

测试结果如下:

QQ截图20240914104704

5.服务器同时多条请求测试

客户端一次性给服务器发送多条请求,观察服务器处理结果。

/*一次性给服务器发送多条数据,然后查看服务器的处理结果*/
/*每一条请求都应该得到正常处理*/#include "../source/server.hpp"int main()
{// 创建客户端连接Socket clientsock;clientsock.CreateClient(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";for (;;){int ret = clientsock.Send(req.c_str(), req.size());assert(ret > 0);char buffer[1024] = {0};ret = clientsock.Recv(buffer, 1023);LogMessage(DEBUG, "[%s]", buffer);sleep(3);}clientsock.Close();return 0;
}

测试结果如下,服务器能够正常处理并响应:

QQ截图20240914105030

服务器大文件传输测试

使用PUT方法向服务器传输大文件,观察服务器处理结果。

/*** 大文件传输测试,给服务器上传一个大文件,服务器将文件保存下来,观察处理结果* 上次的文件和服务器保存的文件一致*/// 创建一个大文件
// dd if=/dev/zero of=./hello.txt bs=1G count=1#include "../source/server.hpp"
#include "../source/http/http.hpp"int main()
{// 创建客户端连接Socket clientsock;clientsock.CreateClient(8080, "127.0.0.1");std::string req = "PUT /1234.txt HTTP/1.1\r\nConnection: keep-alive\r\n";std::string body;Util::ReadFile("./hello.txt", &body);req += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n";assert(clientsock.Send(req.c_str(), req.size()) != -1);assert(clientsock.Send(body.c_str(), req.size()) != -1);char buffer[1024] = {0};assert(clientsock.Recv(buffer, 1023) != -1);LogMessage(DEBUG, "[%s]", buffer);sleep(15);clientsock.Close();return 0;
}

服务器内存情况以及 test.txt 文件情况如下:

QQ截图20240914105515

测试结果如下,服务器能够正常处理并响应:

QQ截图20240914112535

由于服务器资源有限,只能上传100M数据

QQ截图20240914112644

服务器性能压力测试

使用服务器压力测试工具 WebBench 模拟多个客户端同时访问服务器,测试服务器的并发量 (可以同时处理多少个客户端的请求而不会出现连接失败) 以及 QPS (每秒钟处理的包的数量)。

测试环境如下:

  • 服务器为2核2G带宽4M的云服务器。
  • 在服务器上运行 WebBench 程序。
  • 使用 WebBench 程序以 1000 的并发量,进行 1h 的测试。
./webbench -c 1000 -t 3600 http://127.0.0.1:8080/hello

测试结果如下 ( 2000 QPS ):

QQ截图20240914104127

4.服务器业务处理超时测试

当服务器达到性能瓶颈,即处理一次业务花费的时间超过了服务器设置的非活跃连接超时时间时,查看服务器的处理情况。我们的预期结果如下:

  • 由于服务器进行单次业务处理的时间超过了连接的超时时间,所以可能导致其他连接被拖累从而超时释放,具体来说,假设现在4 5 6 7描述符就绪,并且在处理4号描述符就绪事件时超时,那么会出现以下两种情况:
    1. 如果4后面的5 6 7号都是通信连接描述符,则并不影响,因为4号描述符就绪事件处理完毕后就会处理它们的就绪事件并刷新其活跃度。
    2. 如果5号描述符是定时器描述符,此时定时器触发超时,就会执行定时任务,由于6、7号描述符被4号描述符拖累,达到了超时时间,因此会被释放,从而导致在进行6 7业务处理时发生内存访问错误 (6号同理)。
  • 因此,在本次事件处理过程中,并不能直接释放通信连接,而应该将释放操作压入任务队列中,待就绪事件全部处理完毕后再真正释放连接。
/* 业务处理超时,查看服务器的处理情况当服务器达到了一个性能瓶颈,在一次业务处理中花费了太长的时间(超过了服务器设置的非活跃超时时间)1. 在一次业务处理中耗费太长时间,导致其他的连接也被连累超时,其他的连接有可能会被拖累超时释放假设现在  12345描述符就绪了, 在处理1的时候花费了30s处理完,超时了,导致2345描述符因为长时间没有刷新活跃度1. 如果接下来的2345描述符都是通信连接描述符,如果都就绪了,则并不影响,因为接下来就会进行处理并刷新活跃度2. 如果接下来的2号描述符是定时器事件描述符,定时器触发超时,执行定时任务,就会将345描述符给释放掉这时候一旦345描述符对应的连接被释放,接下来在处理345事件的时候就会导致程序崩溃(内存访问错误)因此这时候,在本次事件处理中,并不能直接对连接进行释放,而应该将释放操作压入到任务池中,等到事件处理完了执行任务池中的任务的时候,再去释放
*//*** 对于释放连接,ShutDownInLoop 不应该立即进行释放ReleaseInLoop,而是应该压入任务队列中* 这样就不需要在只要有读写事件之前就刷新一次活跃度,只需要在业务处理完毕之后刷新活跃度即可* OnTime() 定时器需要返回当前超时的次数,来进行执行超时任务*  // 根据实际超时的次数,执行对应的超时任务*/#include "../source/server.hpp"int main()
{for (int i = 0; i < 10; i++){pid_t pid = fork();if (pid < 0){LogMessage(ERROR, "fork error");return -1;}else if (pid == 0){// 创建客户端连接Socket clientsock;clientsock.CreateClient(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";for (;;){int ret = clientsock.Send(req.c_str(), req.size());assert(ret > 0);char buffer[1024] = {0};ret = clientsock.Recv(buffer, 1023);LogMessage(DEBUG, "[%s]", buffer);}clientsock.Close();exit(0);}}while(1) sleep(1);return 0;
}

测试结果如下:

QQ截图20240914104704

5.服务器同时多条请求测试

客户端一次性给服务器发送多条请求,观察服务器处理结果。

/*一次性给服务器发送多条数据,然后查看服务器的处理结果*/
/*每一条请求都应该得到正常处理*/#include "../source/server.hpp"int main()
{// 创建客户端连接Socket clientsock;clientsock.CreateClient(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";for (;;){int ret = clientsock.Send(req.c_str(), req.size());assert(ret > 0);char buffer[1024] = {0};ret = clientsock.Recv(buffer, 1023);LogMessage(DEBUG, "[%s]", buffer);sleep(3);}clientsock.Close();return 0;
}

测试结果如下,服务器能够正常处理并响应:

QQ截图20240914105030

服务器大文件传输测试

使用PUT方法向服务器传输大文件,观察服务器处理结果。

/*** 大文件传输测试,给服务器上传一个大文件,服务器将文件保存下来,观察处理结果* 上次的文件和服务器保存的文件一致*/// 创建一个大文件
// dd if=/dev/zero of=./hello.txt bs=1G count=1#include "../source/server.hpp"
#include "../source/http/http.hpp"int main()
{// 创建客户端连接Socket clientsock;clientsock.CreateClient(8080, "127.0.0.1");std::string req = "PUT /1234.txt HTTP/1.1\r\nConnection: keep-alive\r\n";std::string body;Util::ReadFile("./hello.txt", &body);req += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n";assert(clientsock.Send(req.c_str(), req.size()) != -1);assert(clientsock.Send(body.c_str(), req.size()) != -1);char buffer[1024] = {0};assert(clientsock.Recv(buffer, 1023) != -1);LogMessage(DEBUG, "[%s]", buffer);sleep(15);clientsock.Close();return 0;
}

服务器内存情况以及 test.txt 文件情况如下:

QQ截图20240914105515

测试结果如下,服务器能够正常处理并响应:

QQ截图20240914112535

由于服务器资源有限,只能上传100M数据

QQ截图20240914112644

服务器性能压力测试

使用服务器压力测试工具 WebBench 模拟多个客户端同时访问服务器,测试服务器的并发量 (可以同时处理多少个客户端的请求而不会出现连接失败) 以及 QPS (每秒钟处理的包的数量)。

测试环境如下:

  • 服务器为2核2G带宽4M的云服务器。
  • 在服务器上运行 WebBench 程序。
  • 使用 WebBench 程序以 1000 的并发量,进行 1h 的测试。
./webbench -c 1000 -t 3600 http://127.0.0.1:8080/hello

测试结果如下 ( 2000 QPS ):

QQ截图20240916131352
http://www.xdnf.cn/news/286075.html

相关文章:

  • Nginx核心功能2
  • Linux:权限的理解
  • 健康养生:从生活点滴启航
  • 解决pycharm检测不到已经装好的conda的pytorch环境
  • 项目成本管理_挣得进度ES
  • 网络:cookie和session
  • 【硬核攻坚】告别CUDA OOM!DeepSeek部署显存瓶颈终极解决方案:三大策略高效落地
  • [特殊字符]Git 操作实战:如何将本地项目提交到远程 Gitee 仓库
  • RocketMQ与Kafka的区别
  • Nuxt3还能用吗?
  • 直方图反向投影
  • Three.js + React 实战系列 - 项目展示区开发详解 Projects 组件(3D 模型 + 动效 + 状态切换)✨
  • android-ndk开发(2): macOS 安装 ndk
  • PyTorch_自动微分模块
  • 时间同步服务核心知识笔记:原理、配置与故障排除
  • 因为gromacs必须安装cuda(系统自带的NVIDIA驱动不行),这里介绍下如何安装cuda
  • 学习路线(机器人软件架构)
  • Java常用注解大全(基于JDK17+SpringBoot3)
  • 对ubuntu的简单介绍
  • Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
  • 题目 3321: 蓝桥杯2025年第十六届省赛真题-画展布置
  • SpringMVC 框架核心知识点详解与实战
  • 精益数据分析(41/126):深入解读移动应用商业模式的关键指标与策略
  • linux 高并发 文件句柄数 fs 及 tcp端口数调优
  • 泉州2025年首次网签备案登记的商品住宅并在本年度进行装修、改造及家装物品和材料购置的,在上述补贴额度的基础上上浮2万元,单个产权人补贴最高不超过5万元。
  • VScode中关于Copilot的骚操作
  • ByteArrayOutputStream 类详解
  • 基于yolov11的打电话玩手机检测系统python源码+pytorch模型+评估指标曲线+精美GUI界面
  • 一文说清-什么是强化学习
  • zst-2001 历年真题 程序设计语言