主从Reactor模型实现并发服务器
1. 实现目标
实现One Thread One Loop式主从Reactor模型实现高并发服务器。
通过这里实现的高并发服务器组件,可以简介快速的完成一个高性能服务器的搭建。
并且,通过组件内提供不同应用层协议的支持,也可以快速完成一个高性能应用服务器的搭建(当前项目中提高HTTP协议组建的支持)。
这里我们实现的是一个高并发服务器组建,不包含实际业务。
2. HTTP服务器
2.1 概念
HTTP(Hyper Text Transfer Protocol),超文本传输协议是应用层协议,是一种简单的请求-响应协议(客户端根据自己的需要向服务器发送请求,服务器针对请求提供服务,完毕后通信结束)。
协议细节在之前的文章中已经讲过,这里不再赘述。但是需要注意的是HTTP协议是一个运行在TCP协议之上的应用层协议,这一点本质上是告诉我们,HTTP服务器其实就是个TCP服务器,只不过在应用层基于HTTP协议格式进行数据的组织和解析来明确客户端的请求并完成业务处理。
2.2 实现步骤
因此实现HTTP服务器简单理解,只需要以下几步即可:
- 搭建一个TCP服务器,接收客户端请求。
- 以HTTP协议格式进行解析请求数据,明确客户端目的。
- 明确客户端请求目的后提高对应服务。
- 将服务器结果以HTTP协议格式进行组织,发送给客户端。
实现⼀个HTTP服务器很简单,但是实现⼀个高性能的服务器并不简单,这篇文章中将讲解基于Reactor模式的高性能服务器实现。
当然准确来说,因为我们要实现的服务器本身并不存在业务,咱们要实现的应该算是⼀个高性能服务器基础库,是一个基础组件。
3. Reactor模型
3.1 概念
Reactor 模式,是指通过一个或多个输入同时传递给服务器进行请求处理时的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式。 简单理解就是使用 I/O 多路复用统一监听事件,收到事件后分发给处理进程或线程,是编写高性能网络服务器的必备技术之一。
3.2 分类
单 Reactor 单线程:单 I/O 多路复用+业务处理
- 通过 I/O 多路复用模型进行客户端请求监控。
- 出发时间后,进行事件处理。
- 如果是新建连接请求,则获取新建连接,并添加至多路复用模型进行事件监控。
- 如果是数据通信请求,则进行对应数据处理(接收数据,处理数据,发送响应)。
优点:所有操作均在同一线程中完成,思想流程较为简单,不涉及进程/线程间通信及资源争抢问题。
缺点:无法有效利用CPU多核资源,很容易达到性能瓶颈。
使用场景:适用于客户端数量较少,且处理速度较为快速的场景。(处理较慢或活跃连接较多,会导致串行处理的情况下,后处理的连接长时间无法得到响应)。
单 Reactor 多线程:单 I/O 多路复用+线程池(业务处理)
- Reactor 线程通过 I/O 多路复用模型进行客户端请求监控。
- 触发事件后,进行事件处理。
- 如果是新建连接请求,则获取新建连接,并添加至多路复用模型进行事件监控。
- 如果是数据通信请求,则接收数据后分发给 Worker 线程池进行业务处理。
- 工作线程处理完毕后,将 Reactor 线程进行数据响应。
优点:充分利用CPU多核资源。
缺点:多线程间的数据共享访问控制较为复杂,单个Reactor 承担所有事件的监听和响应,在单线程中运行,高并发场景下容易成为性能瓶颈。
多 Reactor 多线程:多 I/O 多路复用+线程池(业务处理)
- 在主 Reactor 中处理新连接请求事件,有新连接到来则分发到子 Reactor 中监控。
- 在子 Reactor 中进行客户端通信监控,有事件触发,则接收数据分发给 Worker 线程池。
- Worker 线程池分配独立的线程进行具体的业务处理。
- 工作线程处理完毕后,将响应交给子 Reactor 线程进行数据响应。
优点:充分利用 CPU 多核资源,主从Reactor各司其职。
4. 目标定位:One Thread One Loop主从Reactor模型高并发服务器
这里要实现的是主从 Reactor 模型服务器,也就是主 Reactor 线程仅仅监控监听描述符,获取新建连接,保证获取新连接的高效性,提高服务器的并发性能。
主 Reactor 获取到新连接后分发给子 Reactor 进行通信事件监控。而子 Reactor 线程监控各自的描述符的读写事件进行数据读写以及业务处理。
One Thread One Loop 的思想就是把所有的操作都放到一个线程中进行,一个线程对应一个事件处理的循环。
当前实现中,因为并不确定组件使用者的使用意向,因此并不提供业务层工作线程池的实现,只实现主从 Reactor,而 Worker 工作线程池,可由组件库的使用者的需要自行决定是否使用和实现。
5. 功能模块划分
基于以上的理解,我们要实现的是一个带有协议支持的 Reactor 模型高性能服务器,因此将整个项目的实现划分为两个大的模块:
- SERVER模块:实现 Reactor 模型的TCP服务器。
- 协议模块:对当前 Reactor 模型服务器提供应用层协议支持。
5.1 SERVER模块
SERVER模块就是对所有的连接以及线程进行管理,让它们各司其职,在合适的时候做合适的事,最终完成高性能服务器组件的实现。
而具体的管理也分为三个方面:
- 监听连接管理:对监听连接进行管理。
- 通信连接管理:对通信连接进行管理。
- 超时连接管理:对超时连接进行管理。
基于上述思想,server模块又可以分为下面的几个子模块。
5.1.1 Buffer 模块
缓冲区模块,实现通信中用户态接收缓冲区和发送缓冲区的功能。
5.1.2 Socket 模块
对套接字进行封装的模块,实现socket的各项操作。
5.1.3 Channel 模块
对描述符需要进行的 IO 事件管理的模块,实现对描述符可读、可写、错误等事件的管理操作,以及后面的Poller模块对描述符进行 IO 事件的监控就绪后,根据不同的事件,回调不同的处理函数功能。
5.1.4 Connection 模块
对Buffer、Socket、Channel进行整体封装,实现对一个通信套接字的整体管理,每一个进行数据通信的套接字(也就是accept获取到的新连接)都使用 Connection 进行管理。
具体内容如下:
- 包含三个有组件使用者传入的回调函数:连接建立完成回调、事件回调、新数据回调、关闭回调。
- 包含两个组件使用者提供的接口:数据发送接口、连接关闭接口。
- 包含两个用户态缓冲区(Buffer):接收缓冲区、发送缓冲区。
- 包含一个 Socket 对象:完成描述符向系统的 IO 操作。
- 包含一个 Channel 对象:完成描述符 IO 事件就绪的处理。
具体处理流程如下:
- 实现向 Channel 提供可读,可写,错误等不同事件的 IO 件回调函数,然后将 Channel 和对应的描述符添加到 Poller 事件监控中。
- 当描述符在 Poller 模块中就绪了 IO 可读事件,则调用描述符对应 Channel 中保存的读事件处理函数,进行数据读取,将 socket 接收缓冲区全部读取到 Connection 管理的用户态接收缓冲区中。然后调用由组件使用者传如的新数据到来回调函数进行处理。
- 组件使用者进行数据的业务处理完毕后,通过 Connection 向使用者提供的数据发送接口,将数据写入 Connection 的发送缓冲区中。
- 启动描述符在 Poller 模块中的 IO 写事件监控,就绪后,调用 Channel 中保存的写事件处理函数,将发送缓冲区中的数据通过Socket进行面向系统的实际数据发送。
5.1.5 Acceptor 模块
对 Socket 和 Channel 模块进行整体封装,实现对一个监听套接字的整体管理。
具体内容如下:
- 包含一个 Socket 对象:实现监听套接字的操作。
- 包含一个 Channel 对象:实现监听套接字 IO 事件就绪的处理。
具体处理流程如下:
- 实现向 Channel 提供可读事件的 IO 件处理回调函数,函数的功能其实也就是获取新连接。
- 为新连接构建一个 Connection 对象出来。
5.1.6 TimerQueue 模块
实现固定事件定时任务的模块,给定定时任务管理器,向定时任务管理器中添加任务,任务将在固定时间后被执行,同时也可以通过刷新定时任务的执行。
这个模块主要是对 Connection 对象的生命周期管理,对非活跃连接进行超时后的释放功能。
TimerQueue 模块内部包含有一个 timerfd:linux系统提供的定时器。
TimerQueue 模块内部包含有一个 Channel 对象:实现对timerfd的IO时间就绪回调处理。
5.1.7 Poller 模块
对 epoll 进行封装,主要实现 epoll 的 IO 事件添加、修改、移除、获取活跃连接功能。
5.1.8 EventLoop 模块
实现上述 Reactor 模块的功能,它是对 Poller、TimerQueue、Socket 模块的整体封装,进行所有描述符的事件监控。
该模块一个对象对应一个线程,线程内部运行的就是 EventLoop 的启动函数。
为了保证线程安全问题,要求对于 Connection 的所有操作必须在对应的 EventLoop 线程内完成,不允许在其他线程中进行(如组件使用者直接使用 Connection 发送数据、关闭连接等)。
EventLoop 模块保证自己内部所监控的所有描述符,都要是活跃连接,非活跃连接就要及时释放避免资源浪费。
具体内容如下:
- 包含一个 eventfd:用于事件通知
- 包含一个 Poller 对象:用于进行描述符的 IO 事件监控。
- 包含一个 TimerQueue 对象:用于进行定时任务的管理。
- 包含一个 PendingTask 队列:组件使用者将对 Connection 进行的所有操作,都加入到任务队列中,由 EventLoop 模块进行管理,并在 EventLoop 对应线程中执行。
- 每一个 Connection 对象都会绑定到一个EventLoop上,这样能保证对这个连接的所有操作都是在一个线程中完成的。
具体操作如下:
- 通过 Poller 模块对当前模块管理内的所有描述符进行 IO 事件监控,有描述符事件就绪后,通过描述符对应的 Channel 进行事件处理。
- 所有就绪的描述符 IO 事件处理完毕后,对任务队列中的所有操作顺序进行执行。
- 由于 epoll 的事件监控,有可能会因为没有事件到来而持续阻塞,导致任务队列中的任务不能及时得到执行,因此创建了 eventfd,添加到 Poller 的事件监控中,用于实现每次向任务队列添加任务的时候,通过向eventfd写入数据来唤醒 epoll 阻塞。
5.1.9 TcpServer 模块
对整体 TCP 服务器模块的封装,内部封装了 Acceptor、EventLoopThreadPool模块。
具体内容如下:
-
包含一个 EventLoop 对象:以备在超清凉使用场景中不需要 EventLoop 线程池,只需要在主线程中完成所有操作的情况。
-
包含一个 EventLoopThreadPool 对象:其实就是 EventLoop 线程池,也就是子Reactor线程池。
-
包含一个 Acceptor 对象:即监听套接字,完成获取客户端的新连接,并进行处理。
-
包含一个 std::shared_ptr<Connection> 的哈希表:保存所有新建连接对应的 Connection,注意,所有的 Connection 都是用 shared_ptr 进行管理,以便自动实现对资源的释放。
具体操作流程如下:
- 在实例化 TcpServer 对象过程中,完成 BaseLoop 的设置,Acceptor 对象的实例化,以及 EventLoop 线程池的实例化,以及 std::shared_ptr<Connection> 的 hash 表的实例化。
- 为 Acceptor 对象设置回调函数:获取到新连接后,为新连接构建 Connection 对象,设置 Connection 的各项回调,并使用shared_ptr进行管理,并添加到 hash 表中进行管理,并为 Connection 选择⼀个 EventLoop 线程,为 Connection 添加一个定时销毁任务,为 Connection 添加事件监控。
- 启动 BaseLoop。
5.1.10 模块关系图

5.2 HTTP 协议模块
HTTP 协议模块用于对高并发服务器模块进行协议支持,基于提供的协议支持能够更方便的完成指定协议服务器的搭建。
而 HTTP 协议支持模块的实现,可以细分为以下几个模块。
5.2.1 Util 模块
工具模块,提供 HTTP 协议模块所用到的一些工具函数,如 url 编解码、文件读写等。
5.2.2 HttpRequest 模块
HTTP 请求数据模块,用于保存 HTTP 请求数据被解析后的各项请求元素信息。
5.2.3 HttpResponse 模块
HTTP 数据请求模块,用于业务处理后设置并保存 HTTP 响应数据的各项元素信息,最终会被按照 HTTP 协议响应格式组织称为响应信息发送给客户端。
5.2.4 HttpContext 模块
HTTP 请求接收的上下文模块,主要是为了防止在一次接收的数据中,不是一个完整的 HTTP 请求,则解析过程并未完成,无法进行完整的请求处理,需要在下次接收到新数据后继续根据上下文进行解析,最终得到一个 HttpRequest 请求信息对象,因此在请求数据的接收以及解析部分需要一个上下文来进行控制接收和处理节奏。
5.2.5 HttpServer 模块
最终给组件使用者提供的 HTTP 服务器模块,用于以简单的接口实现 HTTP 服务器的搭建。
具体内容如下:
- 包含有一个 TcpServer 对象:TcpServer 对象实现服务器的搭建。
- 包含两个提供给 TcpServer 的接口:连接建立成功设置上下文接口、数据处理接口。
- 包含一个 hashmap 表存储请求与处理函数的映射表:组件使用者向HttpServer 设置哪些请求应该使用哪些函数进行处理,等 TcpSerever 收到对应的请求就会使用对应的函数进行处理。
6. 前置知识
6.1 C++11中的bind
c++
std::bind(Fn&& fn, Args&&... args);
官方文档对于bind接口的概述解释:Bind function arguments。
我们可以将bind接口看作是一个通用的函数适配器,它接受一个函数对象,以及函数的各项参数,然后返回一个新的函数对象,但是这个函数对象的参数已经被绑定为设置的参数。运行的时候相当于总是调用传⼊固定参数的原函数。
但是如果进行绑定的时候,给与的参数为 std::placeholders::_1, _2... 则相当于为新适配生成的函数对象的调用预留一个参数进行传递。
cpp
#include <iostream>
#include <functional>
#include <unistd.h>
class Test {
public:
Test() { std::cout << "构造" << std::endl; }
~Test() { std::cout << "析构" << std::endl; }
};
void del(const Test *t, int num) {
std::cout << num << std::endl;
delete t;
}
int main()
{
Test *t = new Test;
/*bind作用也可以简单理解为给一个函数绑定好参数,然后返回一个参数已经设定好或者预留好的函数,可以在合适的时候进⾏调用*/
/*⽐如,del函数,要求有两个参数,一个Test*, 一个int,而这⾥,想要基于del函数,适配⽣成一个新的函数,这个函数固定第1个参数传递t变量,第二个参数预留出来,在调用的时候进行设置*/
std::function<void(int)> cb = std::bind(del, t, std::placeholders::_1);
cb(10);
while(1) sleep(1);
return 0;
}
基于bind的作用,当我们在设计以些线程池,或者任务池的时候,就可以将将任务池中的任务设置为函数类型,函数的参数由添加任务者直接使用bind进行适配绑定设置,而任务池中的任务被处理,只需要取出一个个的函数进行执行即可。
这样做有个好处就是,这种任务池在设计的时候,不用考虑都有哪些任务处理方式了,处理函数该如何设计,有多少个什么样的参数,这些都不用考虑了,降低了代码之间的耦合度。
cpp
#include <iostream>
#include <string>
#include <vector>
#include <functional>
void print(const std::string &str) {
std::cout << str << std::endl;
}
int main()
{
using Functor = std::function<void()>;
std::vector<Functor> task_pool;
task_pool.push_back(std::bind(print, "世界"));
task_pool.push_back(std::bind(print, "你好"));
task_pool.push_back(std::bind(print, "我是"));
task_pool.push_back(std::bind(print, "好学生"));
for (auto &functor : task_pool) {
functor();
}
return 0;
}
6.2 简单的秒级定时任务的实现
在当前的高并发服务器中,我们不得不考虑一个问题,那就是连接的超时关闭问题。我们需要避免一个连接长时间不通信,但是也不关闭,空耗资源的情况。
这时候我们就需要一定时任务,定时的将超时过期的连接进行释放。
6.2.1 Linux 提供的定时器
c
#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; /* 第一次超时时间 */
};
// 定时器会在每次超时时,自动给fd中写⼊8字节的数据,表示在上一次读取数据到当前读取数据期间超时了多少次。
示例:
c
#include <iostream>
#include <cstdio>
#include <string>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include <sys/timerfd.h>
#include <sys/select.h>
int main()
{
/*创建一个定时器 */
int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
struct itimerspec itm;
itm.it_value.tv_sec = 3;//设置第一次超时的时间
itm.it_value.tv_nsec = 0;
itm.it_interval.tv_sec = 3;//第一次超时后,每隔多长时间超时
itm.it_interval.tv_nsec = 0;
timerfd_settime(timerfd, 0, &itm, NULL);//启动定时器
/*这个定时器描述符将每隔三秒都会触发一次可读事件*/
time_t start = time(NULL);
while(1) {
uint64_t tmp;
/*需要注意的是定时器超时后,则描述符触发可读事件,必须读取8字节的数据,保存的是自上次启动定时器或read后的超时次数*/
int ret = read(timerfd, &tmp, sizeof(tmp));
if (ret < 0) {
return -1;
}
std::cout << tmp << " " << time(NULL) - start << std::endl;
}
close(timerfd);
return 0;
}
上边例子,是一个定时器的使用示例,是每隔 3s 钟触发一次定时器超时,否则就会阻塞在read读取数据这里。
基于这个例子,则我们可以实现每隔3s,检测一下哪些连接超时了,然后将超时的连接释放掉。
6.2.2 时间轮思想
上述的例子,存在一个很大的问题,每次超时都要将所有的连接遍历一遍,如果有上万个连接,效率无疑是较为低下的。
这时候大家就会想到,我们可以针对所有的连接,根据每个连接最近一次通信的系统时间建立一个小根堆,这样只需要每次针对堆顶部分的连接逐个释放,直到没有超时的连接为止,这样也可以大大提高处理的效率。
上述方法可以实现定时任务,但是这里介绍另一种方法:时间轮。
时间轮的思想来源于钟表,如果我们定了一个3点钟的闹铃,则当时针走到3的时候,就代表时间到了。
同样的道理,如果我们定义了一个数组,并且有一个指针,指向数组起始位置,这个指针每秒钟向后走动一步,走到哪里,则代表哪里的任务该被执行了,那么如果我们想要定一个3s后的任务,则只需要将任务添加到 tick+3 位置,则每秒中走一步,三秒钟后tick走到对应位置,这时候执行对应位置的任务即可。
但是,同一时间可能会有大批量的定时任务,因此我们可以给数组对应位置下拉一个数组,这样就可以在同一个时刻上添加多个定时任务了。

当然,上述操作也有一些缺陷,比如我们如果要定义一个60s后的任务,则需要将数组的元素个数设置为60才可以,如果设置一小时后的定时任务,则需要定义3600个元素的数组,这样无疑是比较麻烦的。
因此,可以采用多层级的时间轮,有秒针轮,分针轮,时针轮,60<time<3600则time/60就是分针轮对应存储的位置,当tick/3600等于对应位置的时候,将其位置的任务向分针,秒针轮进行移动。
但是我们当前的应用中,无需这么麻烦的设计,因为我们的定时任务通常在 30s 内,使用简单的单层齿轮即可。
但是,我们也得考虑一个问题,当前的设计是时间到了,则主动去执行定时任务,释放连接,那能不能在时间到了后,自动执行定时任务呢,这时候我们就想到一个操作:类的析构函数。
一个类的析构函数,在对象被释放时会自动被执行,那么我们如果将一个定时任务作为一个类的析构函数内的操作,则这个定时任务在对象被释放的时候就会执行。
但是仅仅为了这个目的,而设计一个额外的任务类,好像有些不划算,但是,这里我们又要考虑另一个问题,那就是假如有一个连接建立成功了,我们给这个连接设置了一个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的时候,这个任务才会被真正释放。
上述过程就是时间轮定时任务的思想。
示例:
cpp
#include <iostream>
#include <functional>
#include <memory>
#include <vector>
#include <unordered_map>
#include <unistd.h>
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;
// 定时器任务
class TimerTask {
private:
uint64_t _id; // 定时器任务对象ID
uint32_t _timeout; // 定时任务的超时时间
bool _canceled; // 该定时任务是否被取消
TaskFunc _task_cb; // 定时器要执行的任务
ReleaseFunc _release; // 用于删除TimerWheel中保存的定时器对象信息
public:
TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb): _id(id), _timeout(delay), _task_cb(cb), _canceled(false) {}
~TimerTask() { if (!_canceled) _task_cb(); _release(); }
void Cancel() { _canceled = true; }
void SetRelease(const ReleaseFunc &cb) { _release = cb; }
uint32_t DelayTime() { return _timeout; }
};
class TimerWheel {
private:
using WaekTask = std::weak_ptr<TimerTask>;
using PtrTask = std::shared_ptr<TimerTask>;
int _tick; // 秒针,走到哪里执行哪里的任务
int _capacity; // 定时器任务的超时时间
std::vector<std::vector<PtrTask>> _wheel;
std::unordered_map<uint64_t, WaekTask> _timers;
private:
void RemoveTimer(uint64_t id) {
auto it = _timers.find(id);
if (it != _timers.end()) _timers.erase(it);
}
public:
TimerWheel(): _capacity(60), _tick(0), _wheel(60) {}
// 添加定时任务
void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb) {
PtrTask pt(new TimerTask(id, delay, cb));
pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
_wheel[(_tick + delay) % _capacity].push_back(pt);
_timers[id] = WaekTask(pt);
}
// 刷新(重新计时)指定定时任务
void TimerRefresh(uint64_t id) {
// 通过weak_ptr构造一个shared_ptr,添加到轮子中
auto it = _timers.find(id);
// 没找到,无法刷新,直接返回
if (it == _timers.end()) return;
PtrTask pt = it->second.lock();
_wheel[(_tick + pt->DelayTime()) % _capacity].push_back(pt);
}
// 取消定时任务
void TimerCancel(uint64_t id) {
auto it = _timers.find(id);
// 没找到,无法取消,直接返回
if (it == _timers.end()) return;
PtrTask pt = it->second.lock();
if (pt) pt->Cancel();
}
// 每秒钟走一次
void RunTimerTask() {
_tick = (_tick + 1) % _capacity;
_wheel[_tick].clear(); // 清空指定位置数组
}
};
class Test {
public:
Test() { std::cout << "构造" << std::endl; }
~Test() { std::cout << "析构" << std::endl; }
};
void DelTest(Test *t) {
delete t;
}
int main() {
TimerWheel tw;
Test *t = new Test();
tw.TimerAdd(888, 5, std::bind(DelTest, t));
for (int i = 0; i < 5; i++) {
tw.TimerRefresh(888); // 刷新定时任务
tw.RunTimerTask(); // 向后移动秒针
std::cout << "刷新定时任务,需要5s后才会释放" << std::endl;
sleep(1);
}
// tw.TimerCancel(888);
while (true) {
std::cout << "---------------------------" << std::endl;
tw.RunTimerTask();
sleep(1);
}
return 0;
}
6.3 正则库的简单使用
正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种字串、将匹配的子串替换或者从某个串中取出符合条件的字串等。
正则表达式的使用,可以使得 HTTP 请求的解析更加简单(这里指的时程序员的工作变得的简单,这并不代表处理效率会变高,实际上效率上是低于直接的字符串处理的),使我们实现的 HTTP 组件库使用起来更加灵活。
正则库文档:<regex> - C++ Reference
正则表达式语法参考:修改后的 ECMAScript 正则表达式语法 - cppreference.cn - C++参考手册
这里主要介绍regex_match。
bool std::regex_match(const std::string &src, std::smatch &matches, std::regex &e);
// src:原始字符串
// matches:正则表达式从原始字符串中匹配并提取符合某种规则的数据,提取的数据就放在mathches中,是一个类似于数组的容器(类似于vector<string>)
// e:正则表达式的匹配规则
// 返回值:返回匹配是否成功
使用示例:
cpp
#include <iostream>
#include <string>
#include <regex>
int main() {
// HTTP请求行格式 GET /http/login?user=zhangsan&passwd=123456 HTTP/1.1\r\n
std::string str = "GET /http/login?user=zhangsan&passwd=123456 HTTP/1.1\r\n";
std::smatch matches;
// 请求方法 GET/HEAD/POST/PUT/DELETE...
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");
// GET|HEAD|POST|PUT|DELETE:匹配指定字符串
// [^?]*:匹配0个或多个除?字符外的任意字符
// \\?(.*):匹配?字符开头的0个或多个字符,直到遇到空格,由于cpp中'\'有转义字符的含义所以需要使用'\\'代表一个反斜杠
// HTTP/1\\.[01]:匹配HTTP/1.开头的,后面跟着0或1的字符串
// (?:\n|\r\n)?:匹配\n或者\r\n,?:开头的代表该项的内容只进行匹配,不提取添加到matches,最后的?代表匹配前面的表达式0次或1次
// (?:\\?(.*))?:?开头的字符串可以有也可以没有,并且这个?丢弃不要,只要后面的内容
bool ret = std::regex_match(str, matches, e);
if (ret == false) return -1;
for (auto &s : matches) std::cout << s << std::endl;
return 0;
}
6.4 通用类型 any
每一个 Connection 对连接进行管理,最终都不可避免需要涉及到应用层协议的处理,因此在 Connection 中需要设置协议处理的上下文来控制处理节奏。但是应用层协议千千万,为了降低耦合度,这个协议接收解析上下文就不能有明显的协议倾向,它可以是任意协议的上下文信息,因此就需要⼀个通用的类型来保存各种不同的数据结构。
在C语言中,通用类型可以使用 void* 来管理,但是在 C++ 中,boost 库和 C++17 给我们提供了一个通用类型 any 来灵活使用,如果考虑增加代码的移植性,尽量减少第三方库的依赖,则可以使用 C++17 特性中的 any,或者自己来实现。而这个 any 通用类型类的实现其实并不复杂。
C++17 any 的参考文档:标准库头文件 (C++17) - cppreference.cn - C++参考手册
下面我们自己简单设计实现一个any类。
cpp
#include <iostream>
#include <typeinfo>
#include <string>
#include <unistd.h>
class Any {
private:
class holder {
public:
virtual ~holder() {}
virtual const std::type_info &type() = 0;
virtual holder *clone() = 0;
};
template<typename T>
class placeholder : public holder {
public:
placeholder(const T &val): _val(val) {}
virtual const std::type_info &type() { return typeid(_val); }
virtual holder *clone() { return new placeholder(_val); } ;
public:
T _val;
};
holder *_content;
public:
Any(): _content(nullptr) {}
template<typename T>
Any(const T &val): _content(new placeholder<T>(val)) {}
Any(const Any &other): _content(other._content ? other._content->clone() : nullptr) {}
~Any() { delete _content; }
Any &swap(Any &other) {
std::swap(_content, other._content);
return *this;
}
template<typename T>
T *get() {
if (!_content) return nullptr;
if (typeid(T) != _content->type()) exit(1); // 不允许不同类型之间进行获取
return &((placeholder<T>*)_content)->_val;
}
template<typename T>
Any &operator=(const T &val) {
Any(val).swap(*this);
return *this;
}
Any &operator=(const Any &other) {
Any(other).swap(*this);
return *this;
}
};
class Test {
public:
Test() { std::cout << "构造" << std::endl; }
Test(const Test &t) { std::cout << "拷贝" << std::endl; }
~Test() { std::cout << "析构" << std::endl; }
};
int main() {
Any a;
a = Test();
a = Test();
// Any a;
// a = 10;
// int *pa = a.get<int>();
// std::cout << *pa << std::endl;
// a = std::string("nihao");
// std::string *ps = a.get<std::string>();
// std::cout << *ps << std::endl;
return 0;
}
下面是 C++17库中的 any 的使用示例。
cpp
#include <iostream>
#include <any>
#include <string>
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;
return 0;
}
7. SERVER 服务器模块实现
7.1 日志宏(Comm.hpp)
为了方便后续代码调试信息,这里设计一个简单的日志宏。
cpp
#pragma once
#include <iostream>
#include <thread>
#include <ctime>
// 简单的日志宏
#define INF 0
#define DBG 1
#define ERR 2
#define DEFAULT_LOG_LEVEL DBG
#define LOG(level, format, ...) do {\
if (level < DEFAULT_LOG_LEVEL) break;\
time_t t = time(nullptr);\
struct tm *m = localtime(&t);\
char ts[32] = {0};\
strftime(ts, 31, "%H:%M:%S", m);\
fprintf(stdout, "[%p][%s][%s:%d]" format "\n", (void*)pthread_self(), ts, __FILE__, __LINE__, ##__VA_ARGS__);\
} while(0)
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)
7.2 缓冲区 Buffer 模块实现(Buffer.hpp)
Buffer类用于实现用户态缓冲区,提供数据缓冲,取出等功能。
这里使用vector<char>作为缓冲区的底层,不使用string的原因在于,如果传输的是二进制数据,可能会存在一些问题。
7.2.1 代码实现
cpp
#pragma once
#include <string>
#include <vector>
#include <cassert>
#include <cstring>
#include <stdint.h>
#include "Comm.hpp"
// 缓冲区初始大小
#define BUFFER_DEFAULT_SIZE 1024
class Buffer {
private:
std::vector<char> _buffer; // 使用vector作为缓冲区
uint64_t _reader_idx; // 读偏移
uint64_t _writer_idx; // 写偏移
public:
Buffer(): _buffer(BUFFER_DEFAULT_SIZE), _reader_idx(0), _writer_idx(0) {}
// 获取当前缓冲区起始地址
char *Begin() { return &_buffer[0]; }
// 获取当前写入起始地址
char *WritePosition() { return &_buffer[_writer_idx]; }
// 获取当前读取起始地址
char *ReadPosition() { return &_buffer[_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(uint64_t len) {
if (len == 0) return;
assert(len <= ReadAbleSize());
_reader_idx += len;
}
// 将写指针向后偏移
void MoveWriteOffset(uint64_t len) {
if (len == 0) return;
assert(len <= TailIdleSize());
_writer_idx += len;
}
// 确保可写空间足够(不够就扩容)
void EnsureWriteSpace(uint64_t len) {
// 后沿空间足够,直接返回
if (len <= TailIdleSize()) return;
// 再加上前沿空间,如果足够就把数据往前移
if (len <= HeadIdleSize() + TailIdleSize()) {
uint64_t rsz = ReadAbleSize();
std::copy(ReadPosition(), WritePosition(), Begin());
_reader_idx = 0;
_writer_idx = rsz;
return;
}
// 如果还不够,直接扩容
DBG_LOG("resize %ld", _writer_idx + len);
_buffer.resize(_writer_idx + len);
}
// 写入数据,flag代表读取后是否进行写指针偏移(是否将缓冲区中已读数据删除)
void Write(const void *data, uint64_t len, bool flag) {
// 如果没有数据,就不必继续写入了
if (len == 0) return;
EnsureWriteSpace(len);
std::copy((const char*)data, ((const char*)data + len), WritePosition());
if (flag) MoveWriteOffset(len);
}
/*重载各种写入方式*/
void Write(char *data, uint64_t len, bool flag) { Write((void*)data, len, flag); }
void Write(const std::string &data, bool flag) { Write((void*)data.c_str(), data.size(), flag); }
void Write(Buffer &data, bool flag) { return Write(data.ReadPosition(), data.ReadAbleSize()); }
// 读取数据,flag代表读取后是否进行读指针偏移(是否将缓冲区中已读数据删除)
void Read(void *buf, uint64_t len, bool flag) {
// 读取的长度必须小于可读的大小
assert(len <= ReadAbleSize());
std::copy(ReadPosition(), ReadPosition() + len, (char*)buf);
if (flag) MoveReadOffset(len);
}
/*重载各种读取方式*/
std::string Read(uint64_t len, bool flag) {
std::string str;
str.resize(len);
Read((void*)&str[0], len, flag);
return str;
}
// 找到并返回行尾的指针(\n)
char *FindCRLF() {
return (char*)memchr(ReadPosition(), '\n', ReadAbleSize());
}
// 获取一行的内容,flag代表获取后是否删除对应内容
std::string GetLine(bool flag) {
char *pos = FindCRLF();
if (!pos) return "";
return Read((uint64_t)(pos - ReadPosition() + 1), flag);
}
// 清空缓冲区
void Clear() {
_reader_idx = 0;
_writer_idx = 0;
}
};
#undef BUFFER_DEFAULT_SIZE
7.2.2 关系图

7.3 套接字 Socket 模块实现(Socket.hpp)
将原生套接字进行封装,为方便使用者进行使用,提供了CreateServer和CreateClient接口,直接一步创建服务端和客户端的套接字,同时提供了读/写/监听等接口。
7.3.1 代码实现
cpp
#pragma once
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
// 监听队列的最大长度限制
#define MAX_LISTEN 1024
class Socket {
private:
int _sockfd;
public:
Socket(): _sockfd(-1) {}
Socket(int fd): _sockfd(fd) {}
~Socket() { Close(); }
// 获取当前套接字描述符
int Fd() { return _sockfd; }
// 创建套接字
bool Create() {
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0) {
ERR_LOG("create socket error...");
return false;
}
return true;
}
// 绑定地址信息
bool Bind(const std::string &ip, uint16_t port) {
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret = bind(_sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in));
if (ret < 0) {
ERR_LOG("bind address error");
return false;
}
return true;
}
// 服务端-开始监听
bool Listen(int backlog = MAX_LISTEN) {
int ret = listen(_sockfd, backlog);
if (ret < 0) {
ERR_LOG("listen error");
return false;
}
return true;
}
// 服务端-获取新连接
int Accept() {
int newfd = accept(_sockfd, nullptr, nullptr);
if (newfd < 0) {
ERR_LOG("connect server error");
return -1;
}
return newfd;
}
// 客户端-向指定服务器发起连接
bool Connect(const std::string &ip, uint16_t port) {
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret = connect(_sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in));
if (ret < 0) {
ERR_LOG("connect server error");
return false;
}
return true;
}
// 接收数据,flag的参数即传入系统调用recv的参数,默认为0
ssize_t Recv(void *buf, size_t len, int flag = 0) {
int ret = recv(_sockfd, buf, len, flag);
if (ret <= 0) {
// 当前非阻塞不可读或系统调用被信号中断时,直接返回。
if (errno == EAGAIN || errno == EINTR) return 0;
ERR_LOG("socket recv error");
return -1;
}
return ret;
}
// 非阻塞接收数据
ssize_t NonBlockRecv(void *buf, size_t len) {
return Recv(buf, len, MSG_DONTWAIT);
}
// 发送数据,flag的参数即传入系统调用send的参数,默认为0
ssize_t Send(void *buf, size_t len, int flag = 0) {
int ret = send(_sockfd, buf, len, flag);
if (ret < 0) {
// 当前非阻塞不可写或系统调用被信号中断时,直接返回。
if (errno == EAGAIN || errno == EINTR) return 0;
ERR_LOG("socket send error");
return -1;
}
return ret;
}
// 非阻塞发送数据
ssize_t NonBlockSend(void *buf, size_t len) {
if (len == 0) return 0;
return Send(buf, len, MSG_DONTWAIT);
}
// 关闭套接字
void Close() {
if (_sockfd != 0) {
close(_sockfd);
_sockfd = -1;
}
}
// 设置套接字选项 - 设置为非阻塞
void NonBlock() {
int flag = fcntl(_sockfd, F_GETFL, 0);
fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
}
// 设置套接字选项 - 开启地址端口重用
void ReuseAddress() {
int val = 1;
// 启用 SO_REUSEADDR(允许复用地址)
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void*)&val, sizeof(int));
val = 1;
// 启用 SO_REUSEPORT(允许多绑定)
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void*)&val, sizeof(int));
}
// 创建一个服务端连接
bool CreateServer(uint64_t port, const std::string &ip = "0.0.0.0", int flag = 0) {
// 1.创建套接字
if (!Create()) return false;
// 2.绑定地址
if (!Bind(ip, port)) return false;
// 3.开始监听
if (!Listen()) return false;
// 4.设置非阻塞
if (flag) NonBlock();
// 5.启动地址端口重用
ReuseAddress();
return true;
}
// 创建一个客户端连接
bool CreateClient(uint64_t port, const std::string &ip) {
// 1.创建套接字
if (!Create()) return false;
// 2.连接服务器
if (!Connect(ip, port)) return false;
return true;
}
};
#undef MAX_LISTEN
7.3.2 关系图

7.4 监控事件管理 Channel 模块实现(Channel.hpp)
这个模块用来管理每个连接触发事件时执行的回调。
7.4.1 代码实现
cpp
#pragma once
#include <functional>
#include <stdint.h>
#include <sys/epoll.h>
class EventLoop;
class Channel {
private:
int _fd; // 当前管理的事件的描述符
EventLoop *_loop; // 上层的EventLoop对象指针
uint32_t _events; // 当前需要监控的事件
uint32_t _revents; // 当前连接触发的事件
using EventCallback = std::function<void()>;
EventCallback _read_callback; // 可读事件被触发的回调函数
EventCallback _write_callback; // 可写事件被触发的回调函数
EventCallback _error_callback; // 错误事件被触发的回调函数
EventCallback _close_callback; // 连接断开事件被触发的回调函数
EventCallback _event_callback; // 任意事件被触发的回调函数
public:
Channel(EventLoop *loop, int fd): _fd(fd), _events(0), _revents(0), _loop(loop) {}
// 获取当前管理事件的描述符
int Fd() { return _fd; }
// 获取想要监控的事件
uint32_t Events() { return _events; }
// 设置实际就绪的事件
void SetREvents(uint32_t events) { _revents = events; }
// 设置各种事件触发的回调函数
void SetReadCallback(const EventCallback &cb) { _read_callback = cb; }
void SetWriteCallback(const EventCallback &cb) { _write_callback = cb; }
void SetErrorCallback(const EventCallback &cb) { _error_callback = cb; }
void SetCloseCallback(const EventCallback &cb) { _close_callback = cb; }
void SetEventCallback(const EventCallback &cb) { _event_callback = cb; }
// 当前是否监控了可读
bool ReadAble() { return (_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(); }
// 从epoll移除监控
void Remove(); // 这里需要从上层的EventLoop对象中操作,所以需要放到EventLoop类后实现
// 将监控信息同步到epoll中
void Update(); // 这里需要从上层的EventLoop对象中操作,所以需要放到EventLoop类后实现
//事件处理,一旦连接触发了事件,就调用这个函数,自己触发了什么事件,如何处理自己决定
void HandleEvent() {
// _event_callback在每次触发时都必须执行,但是在触发错误事件和关闭事件时需要在它们之前执行
// 不论下面三个事件中触发了哪个,都执行可读事件回调
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();
}
}
};
7.4.2 关系图

7.5 描述符事件监控 Poller 模块实现(Poller.hpp)
这个模块封装了原生的epoll,可以直接添加Channel对象开启对应事件的监控。
7.5.1 代码实现
cpp
#pragma once
#include <unordered_map>
#include <cstring>
#include <cassert>
#include "Comm.hpp"
#include "Channel.hpp"
// 最大监控事件
#define MAX_EPOLLEVENTS 1024
class Poller {
private:
int _epfd; // epoll监控描述符
struct epoll_event _evs[MAX_EPOLLEVENTS]; // 触发的事件集合
std::unordered_map<int, Channel*> _channels; // Channel集合,用于快速找到对应的Channel
private:
// 对epoll的直接操作(epoll_ctl)
void Update(Channel *channel, int op) {
int fd = channel->Fd();
struct epoll_event ev;
ev.data.fd = fd;
ev.events = channel->Events();
int ret = epoll_ctl(_epfd, op, fd, &ev);
if (ret < 0) ERR_LOG("epollctl error");
}
// 判断一个Channel是否已经添加了事件监控
bool HasChannel(Channel *channel) { return _channels.count(channel->Fd()); }
public:
Poller() {
_epfd = epoll_create(MAX_EPOLLEVENTS);
if (_epfd < 0) {
ERR_LOG("EPOLL CREATE FAILED!!");
abort();//退出程序
}
}
// 添加/修改监控事件
void UpdateEvent(Channel *channel) {
if (HasChannel(channel) == false) {
// 不存在则添加
_channels[channel->Fd()] = channel;
Update(channel, EPOLL_CTL_ADD);
}
else {
// 存在则更新
Update(channel, EPOLL_CTL_MOD);
}
}
//移除监控
void RemoveEvent(Channel *channel) {
auto it = _channels.find(channel->Fd());
// 不存在则直接返回
if (it == _channels.end()) return;
// 移除channels中对应的数据
_channels.erase(it);
// 移除epoll监控对应的内容
Update(channel, EPOLL_CTL_DEL);
}
//开始监控,返回活跃连接
void Poll(std::vector<Channel*> *active) {
int nfds = epoll_wait(_epfd, _evs, MAX_EPOLLEVENTS, -1);
if (nfds < 0) {
if (errno == EINTR) return;
ERR_LOG("eoollwait error:%s\n", strerror(errno));
abort(); //退出程序
}
for (int i = 0; i < nfds; i++) {
auto it = _channels.find(_evs[i].data.fd);
// 如果就绪的事件并不存在于_channels,程序出错
assert(it != _channels.end());
it->second->SetREvents(_evs[i].events);//设置实际就绪的事件
active->push_back(it->second);
}
return;
}
};
7.5.2 关系图

7.6 定时任务管理器 TimerWheel 模块实现(TimerWheel.hpp)
这里实现了定时任务管理器,主要用于超时连接释放的问题。
7.6.1 代码实现
cpp
#pragma once
#include <memory>
#include <functional>
#include <stdint.h>
#include <unistd.h>
#include <sys/timerfd.h>
#include "Comm.hpp"
#include "Channel.hpp"
class EventLoop;
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;
// 定时任务类,每个定时任务创建一个对象
class TimerTask{
private:
uint64_t _id; // 定时器任务对象ID
uint32_t _timeout; // 定时任务的超时时间
bool _canceled; // false-表示没有被取消, true-表示被取消
TaskFunc _task_cb; // 定时器对象要执行的定时任务
ReleaseFunc _release; // 用于删除TimerWheel中保存的定时器对象信息
public:
TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb): _id(id), _timeout(delay), _task_cb(cb), _canceled(false) {}
// 在析构时执行指定的定时任务
~TimerTask() {
if (_canceled == false) {
DBG_LOG("timeout, execute the task, taskid: %ld", _id);
_task_cb();
}
; _release();
}
// 将对象设置被取消
void Cancel() { _canceled = true; }
// 设置删除对象时执行的回调
void SetRelease(const ReleaseFunc &cb) { _release = cb; }
// 获取对象超时时间
uint32_t DelayTime() { return _timeout; }
};
class TimerWheel {
private:
using WeakTask = std::weak_ptr<TimerTask>;
using PtrTask = std::shared_ptr<TimerTask>;
int _tick; // 当前的秒针,走到哪里释放哪里,释放哪里,就相当于执行哪里的任务
int _capacity; // 表盘最大数量 - 即能接受的最大延迟时间
std::vector<std::vector<PtrTask>> _wheel; // 时间轮
std::unordered_map<uint64_t, WeakTask> _timers; // 通过id快速找到任务的weakptr指针,方便获取sharedptr对象
EventLoop *_loop; // 上层的EventLoop指针
int _timerfd; // 定时器描述符 - 可读事件回调就是读取计数器,执行定时任务
std::unique_ptr<Channel> _timer_channel; // 定时器的事件管理
private:
// 移除指定ID的任务
void RemoveTimer(uint64_t id) {
auto it = _timers.find(id);
if (it != _timers.end()) _timers.erase(it);
}
// 创建定时器fd
static int CreateTimerfd() {
int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
if (timerfd < 0) {
ERR_LOG("TIMERFD CREATE FAILED!");
abort();
}
struct itimerspec itime;
itime.it_value.tv_sec = 1;
itime.it_value.tv_nsec = 0; //第一次超时时间为1s后
itime.it_interval.tv_sec = 1;
itime.it_interval.tv_nsec = 0; //第一次超时后,每次超时的间隔为1s
timerfd_settime(timerfd, 0, &itime, nullptr);
return timerfd;
}
// 读取计时器fd,获取超时的次数
int ReadTimefd() {
uint64_t times;
//有可能因为其他描述符的事件处理花费事件比较长,然后在处理定时器描述符事件的时候,有可能就已经超时了很多次
//read读取到的数据times就是从上⼀次read之后超时的次数
int ret = read(_timerfd, ×, 8);
if (ret < 0) {
ERR_LOG("read timefd error");
abort();
}
return times;
}
// 将秒针向后走一步
void RunTimerTask() {
_tick = (_tick + 1) % _capacity;
_wheel[_tick].clear(); // 清空指定位置的数组,就会把数组中保存的所有管理定时器对象的shared_ptr释放掉
}
// 执行超时任务
void OnTime() {
//根据实际超时的次数,执行对应的超时任务
int times = ReadTimefd();
for (int i = 0; i < times; i++) RunTimerTask();
}
// 设置新的超时任务
void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc &cb) {
PtrTask pt(new TimerTask(id, delay, cb));
pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
int pos = (_tick + delay) % _capacity;
_wheel[pos].push_back(pt);
_timers[id] = WeakTask(pt);
}
// 刷新超时任务
void TimerRefreshInLoop(uint64_t id) {
//通过保存的定时器对象的weak_ptr构造一个shared_ptr出来,添加到轮子中
auto it = _timers.find(id);
// 如果不存在,无法刷新,直接返回
if (it == _timers.end()) return;
//lock获取weak_ptr管理的对象对应的shared_ptr,并根据超时时间重新添加到时间轮中
PtrTask pt = it->second.lock();
int delay = pt->DelayTime();
int pos = (_tick + delay) % _capacity;
_wheel[pos].push_back(pt);
}
// 取消指定的定时任务
void TimerCancelInLoop(uint64_t id) {
auto it = _timers.find(id);
if (it == _timers.end()) return;
PtrTask pt = it->second.lock();
if (pt) pt->Cancel();
}
public:
TimerWheel(EventLoop *loop): _capacity(60), _tick(0), _wheel(_capacity), _loop(loop), _timerfd(CreateTimerfd()), _timer_channel(new Channel(_loop, _timerfd)) {
// 设置读事件回调
_timer_channel->SetReadCallback(std::bind(&TimerWheel::OnTime, this));
//启动读事件监控
_timer_channel->EnableRead();
}
/*下面三个接口需要调用需要调用上层的EventLoop对象的接口,所以此处只定义,需要在EventLoop后实现*/
// 添加定时任务
void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb);
// 刷新定时任务
void TimerRefresh(uint64_t id);
// 取消定时任务
void TimerCancel(uint64_t id);
/*这个接口存在线程安全问题--这个接口实际上不能被外界使用者调用,只能在模块内,在对应的EventLoop线程内执行*/
// 判断定时任务是否存在
bool HasTimer(uint64_t id) { return _timers.count(id); }
};
7.6.2 关系图

7.7 Reactor 线程池 EventLoop 模块实现
进行事件监控以及事件处理的模块。
注意:为了防止一个描述符在多个线程中出发事件(线程安全问题),这个模块与线程需要一一对应,需要将一个连接的监控事件以及连接事件处理,放到同一个线程中去运行。
为了解决这个问题,我们需要保证一个连接的所有操作都在 EventLoop 对应的线程中,所以需要给 EventLoop 模块中添加一个任务队列,对连接的所有操作都进行一次封装,把不是当前线程的任务放到任务队列中,等到所有就绪事件处理完了,再到任务队列中把这些任务一一执行,此时只需要在执行任务队列中的任务的时候加上一把锁就可以保证线程安全。
7.7.1 代码实现
cpp
#pragma once
#include <functional>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <sys/eventfd.h>
#include "Channel.hpp"
#include "Poller.hpp"
#include "TimerWheel.hpp"
class EventLoop {
private:
using Functor = std::function<void()>;
std::thread::id _thread_id; // 线程ID
int _event_fd; // eventfd 事件通知描述符
std::unique_ptr<Channel> _event_channel; // eventfd 事件管理
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 &f : functor) f();
}
// 创建eventfd
static int CreateEventFd() {
// 设置创建子进程时关闭文件描述符,设置为非阻塞
int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (efd < 0) {
ERR_LOG("create eventfd error");
abort();
}
return efd;
}
// 读取eventfd
void ReadEventfd() {
uint64_t res = 0;
int ret = read(_event_fd, &res, sizeof(res));
if (ret < 0) {
if (errno == EINTR || errno == EAGAIN) return;
ERR_LOG("read eventfd error");
abort();
}
}
// 向eventfd中写入数据
void WeakUpEventFd() {
uint64_t val = 1;
int ret = write(_event_fd, &val, sizeof(val));
if (ret < 0) {
if (errno == EINTR) return;
ERR_LOG("write eventfd error");
abort();
}
}
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() {
while(true) {
//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(uint64_t id, uint32_t delay, const TaskFunc &cb) { return _timer_wheel.TimerAdd(id, delay, cb); }
// 刷新定时任务
void TimerRefresh(uint64_t id) { return _timer_wheel.TimerRefresh(id); }
// 取消定时任务
void TimerCancel(uint64_t id) { return _timer_wheel.TimerCancel(id); }
// 判断该定时任务是否存在
bool HasTimer(uint64_t id) { return _timer_wheel.HasTimer(id); }
};
// 实现Channel模块中的Remove和Update
void Channel::Remove() { return _loop->RemoveEvent(this); }
void Channel::Update() { return _loop->UpdateEvent(this); }
// 实现TimerWheel模块中的TimerAdd、TimerRefresh和TimerCancel
void TimerWheel::TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb) { _loop->RunInLoop(std::bind(&TimerWheel::TimerAddInLoop, this, id, delay, cb)); }
void TimerWheel::TimerRefresh(uint64_t id) { _loop->RunInLoop(std::bind(&TimerWheel::TimerRefreshInLoop, this, id)); }
void TimerWheel::TimerCancel(uint64_t id) { _loop->RunInLoop(std::bind(&TimerWheel::TimerCancelInLoop, this, id)); }
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():_loop(nullptr), _thread(std::thread(&LoopThread::ThreadEntry, this)) {}
// 返回当前线程关联的EventLoop对象指针
EventLoop *GetLoop() {
EventLoop *loop = nullptr;
{
std::unique_lock<std::mutex> lock(_mutex);//加锁
//loop为nullptr就一直阻塞
_cond.wait(lock, [&](){ return _loop != nullptr; });
loop = _loop;
}
return loop;
}
};
class LoopThreadPool {
private:
int _thread_count; // 线程数量
int _next_idx; // 下一个EventLoop的下标
EventLoop *_baseloop; // 主Reactor模块的指针
std::vector<LoopThread*> _threads; // 从Reactor模块线程池
std::vector<EventLoop*> _loops; // 从Reactor模块对象指针
public:
LoopThreadPool(EventLoop *baseloop):_thread_count(0), _next_idx(0), _baseloop(baseloop) {}
// 设置线程数量
void SetThreadCount(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]->GetLoop();
}
}
}
// 返回下一个 EventLoop 对象指针
EventLoop *NextLoop() {
// 如果线程池里没有子线程,直接返回基础对象
if (_thread_count == 0) return _baseloop;
_next_idx = (_next_idx + 1) % _thread_count;
return _loops[_next_idx];
}
};
7.7.2 关系图

7.8 通信连接管理 Connection 模块实现(Connection.hpp)
对于通信连接进行整体管理。
为了保证连接在所有的工作完成之后才可以进行关闭,所以这里使用shared_ptr对其进行管理,但是当需要调用上层的回调函数时,需要将自身的shared_ptr对象传递回去,我们可以让这个类继承自std::enable_shared_from_this<Connection>,这样就可以使用shared_from_this()函数获取到自身的shared_ptr对象。
7.8.1 代码实现
cpp
#pragma once
#include <memory>
#include <any>
#include "Buffer.hpp"
#include "Socket.hpp"
#include "EventLoop.hpp"
// DISCONECTED -- 连接关闭状态
// CONNECTING -- 连接建立成功 - 待处理状态
// CONNECTED -- 连接建立完成,各种设置已完成,可以通信的状态
// DISCONNECTING -- 待关闭状态
typedef enum { DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING } ConnStatu;
class Connection;
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作为定时器ID
int _sockfd; // 连接关联的文件描述符
bool _enable_inactive_release; // 连接是否启动非活跃销毁的判断标志,默认为false
EventLoop *_loop; // 连接所关联的一个EventLoop
ConnStatu _statu; // 连接状态
Socket _socket; // 套接字操作管理
Channel _channel; // 连接的事件管理
Buffer _in_buffer; // 输入缓冲区---存放从socket中读取到的数据
Buffer _out_buffer; // 输出缓冲区---存放要发送给对端的数据
std::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的事件回调函数*/
// 描述符可读事件触发后调用的函数,接收socket数据放到接收缓冲区中,然后调用_message_callback
void HandleRead() {
// 1. 接收socket的数据,放到缓冲区
char buf[65536];
ssize_t ret = _socket.NonBlockRecv(buf, 65535);
// 如果出错了,先完成未完成的工作,而不是直接关闭连接
if (ret < 0) return ShutdownInLoop();
// 将数据放入输入缓冲区,写入之后顺便将写偏移向后移动
_in_buffer.Write(buf, ret, true);
// 2. 调⽤message_callback进行业务处理
// shared_from_this--从当前对象自身获取自身的shared_ptr管理对象
if (_in_buffer.ReadAbleSize() > 0) _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) Release();
}
}
// 描述符触发挂断事件
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());
}
// 连接获取之后,所处的状态下要进行各种设置(启动读监控,调用回调函数)
void EstablishedInLoop() {
// 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 &buf) {
if (_statu == DISCONNECTED) return;
_out_buffer.Write(buf, true);
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(_conn_id);
// 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 UpgradeInLoop(const std::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, 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.SetEventCallback(std::bind(&Connection::HandleEvent, this));
_channel.SetReadCallback(std::bind(&Connection::HandleRead, this));
_channel.SetWriteCallback(std::bind(&Connection::HandleWrite, this));
_channel.SetErrorCallback(std::bind(&Connection::HandleError, this));
}
~Connection() { DBG_LOG("release connection:%p", this); }
// 获取连接对应的文件描述符
int Fd() { return _sockfd; }
// 获取连接ID
int Id() { return _conn_id; }
// 是否处于CONNECTED(连接建立完成)状态
bool Connected() { return (_statu == CONNECTED); }
// 设置上下文--连接建立完成时进行调用
void SetContext(const std::any &context) { _context = context; }
//获取上下文,返回的是指针
std::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 SetAnyEventCallback(const AnyEventCallback&cb) { _event_callback = cb; }
void SetSrvClosedCallback(const ClosedCallback&cb) { _server_closed_callback = cb; }
//连接建立就绪后进行的设置
void Established() { _loop->RunInLoop(std::bind(&Connection::EstablishedInLoop, this)); }
//发送数据,将数据放到发送缓冲区,启动写事件监控
void Send(const char *data, size_t len) {
//外界传入的data,可能是个临时的空间,我们现在只是把发送操作压入了任务池,有可能并没有被立即执行
//因此有可能执行的时候,data指向的空间有可能已经被释放了。
Buffer buf;
buf.Write(data, len, true);
_loop->RunInLoop(std::bind(&Connection::SendInLoop, this, std::move(buf)));
}
// 先处理未处理事件,再释放连接
void Shutdown() { _loop->RunInLoop(std::bind(&Connection::ShutdownInLoop, this)); }
// 释放连接
void Release() { _loop->QueueInLoop(std::bind(&Connection::ReleaseInLoop, this)); }
//启动非活跃销毁,并定义非活跃时长,添加定时任务
void EnableInactiveRelease(int sec) { _loop->RunInLoop(std::bind(&Connection::EnableInactiveReleaseInLoop, this, sec)); }
//取消非活跃销毁
void CancelInactiveRelease() { _loop->RunInLoop(std::bind(&Connection::CancelInactiveReleaseInLoop, this)); }
// 切换协议---重置上下文以及阶段性回调处理函数 -- 而且这个接口必须在EventLoop线程中立即执行
// 防止新的事件触发后,处理的时候,切换任务还没有被执行--会导致数据使用原协议处理了。
void Upgrade(const std::any &context, const ConnectedCallback &conn, const MessageCallback &msg,
const ClosedCallback &closed, const AnyEventCallback &event) {
_loop->AssertInLoop();
_loop->RunInLoop(std::bind(&Connection::UpgradeInLoop, this, context, conn, msg, closed, event));
}
};
7.8.2 关系图

7.9 监听描述符管理 Acceptor 模块实现(Acceptor.hpp)
本模块封装了监听套接字的逻辑,支持直接创建监听套接字,直接开启监听。
7.9.1 代码实现
cpp
#pragma once
#include "Socket.hpp"
#include "EventLoop.hpp"
class Acceptor {
private:
Socket _socket; // 监听套接字
EventLoop *_loop; // 上层EventLoop指针
Channel _channel; // 监听套接字事件管理
using AcceptCallback = std::function<void(int)>;
AcceptCallback _accept_callback; // 监听套接字回调
private:
// 监听套接字的读事件回调处理函数---获取新连接,调用_accept_callback函数进行新连接处理
void HandleRead() {
int newfd = _socket.Accept();
if (newfd < 0) return ;
if (_accept_callback) _accept_callback(newfd);
}
// 创建监听套接字
int CreateServer(int port) {
bool ret = _socket.CreateServer(port);
assert(ret == true);
return _socket.Fd();
}
public:
/*不能将启动读事件监控,放到构造函数中,必须在设置回调函数后,再去启动*/
/*否则有可能造成启动监控后,立即有事件,处理的时候,回调函数还没设置:新连接得不到处理,且资源泄漏*/
Acceptor(EventLoop *loop, int 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(); }
};
7.9.2 关系图

7.10 服务器TcpServer 模块实现(TcpServer.hpp)
将上述各模块进行协同,搭建出TcpServer服务器的框架,使用者可通过此框架快速搭建TCP服务器。
7.10.1 代码实现
cpp
#pragma once
#include "Connection.hpp"
#include "Acceptor.hpp"
class TcpServer {
private:
uint64_t _next_id; // 一个自动增长的连接ID
int _port; // 服务器绑定的端口
int _timeout; // 非活跃连接的统计时间---多长时间无通信就是非活跃连接
bool _enable_inactive_release; // 是否启动了非活跃连接超时销毁的判断标志
EventLoop _baseloop; // 主线程的EventLoop对象,负责监听事件的处理
Acceptor _acceptor; // 监听套接字的管理对象
LoopThreadPool _pool; // 从EventLoop线程池
//保存管理所有连接对应的shared_ptr对象
std::unordered_map<uint64_t, PtrConnection> _conns;
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.NextLoop(), _next_id, fd));
conn->SetMessageCallback(_message_callback);
conn->SetClosedCallback(_closed_callback);
conn->SetConnectedCallback(_connected_callback);
conn->SetAnyEventCallback(_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(it);
}
//从管理Connection的_conns中移除连接信息
void RemoveConnection(const PtrConnection &conn) { _baseloop.RunInLoop(std::bind(&TcpServer::RemoveConnectionInLoop, this, conn)); }
public:
TcpServer(int port): _port(port), _next_id(0), _enable_inactive_release(false), _acceptor(&_baseloop, port), _pool(&_baseloop) {
_acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));
_acceptor.Listen(); //将监听套接字挂到baseloop上
}
// 设置线程数量
void SetThreadCount(int count) { return _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) { _timeout = timeout; _enable_inactive_release = true; }
// 添加一个定时任务
void RunAfter(const Functor &task, int delay) { _baseloop.RunInLoop(std::bind(&TcpServer::RunAfterInLoop, this, task, delay)); }
// 启动服务器
void Start() { _pool.Create(); _baseloop.Start(); }
};
7.10.2 关系图

8. 基于 TcpServer 实现回显服务器
8.1 EchoServer 模块实现(EchoServer.hpp)
cpp
#include "../../source/TcpServer/TcpServer.hpp"
class EchoServer
{
private:
TcpServer _server;
private:
// 创建连接后打印一条提示信息
void OnConnected(const PtrConnection &conn) { DBG_LOG("new connection:%p", conn.get()); }
// 关闭连接后打印一条提示信息
void OnClosed(const PtrConnection &conn) { DBG_LOG("close connection:%p", conn.get()); }
// 业务处理:将收到的信息进行打印,然后发回
void OnMessage(const PtrConnection &conn, Buffer *buf) {
std::string str(buf->ReadPosition(), buf->ReadPosition() + buf->ReadAbleSize());
buf->MoveReadOffset(buf->ReadAbleSize());
DBG_LOG("client %d say: %s", conn->Id(), str.c_str());
conn->Send(str.c_str(), str.size());
}
public:
EchoServer(int port) : _server(port) {
// 设置从属线程数量
_server.SetThreadCount(2);
// 开启非活跃连接销毁
_server.EnableInactiveRelease(10);
// 绑定关闭连接回调
_server.SetClosedCallback(std::bind(&EchoServer::OnClosed, this, std::placeholders::_1));
// 绑定创建连接后的回调
_server.SetConnectedCallback(std::bind(&EchoServer::OnConnected, this, std::placeholders::_1));
// 绑定业务处理的回调
_server.SetMessageCallback(std::bind(&EchoServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));
}
void Start() { _server.Start(); }
};
8.2 主程序
cpp
#include "echo_server.hpp"
int main()
{
EchoServer server(8888);
server.Start();
return 0;
}
8.3 EchoServer 模块关系图

9. HTTP 协议支持模块实现
9.1 实用工具 Util 模块实现(Util.hpp)
这个模块提供一些常用的工具:
- 根据提供的分隔符分割指定字符串
- 读取指定文件的所有内容,并放到buffer中
- 向指定文件中写入指定内容
- 16进制字符串十进制数字
- url编码,将传入字符串进行url编码,返回编码后的字符串
- url解码,将传入字符串进行url解码,返回解码后的字符串
- 获取响应状态码的描述信息
- 根据文件后缀名获取指定位置文件
- 判断一个文件是否是一个目录
- 判断一个文件是否是普通文件
- 判断http请求的资源路径是否合法
cpp
#pragma once
#include <fstream>
#include <string>
#include <vector>
#include <unordered_map>
#include <unistd.h>
#include <sys/stat.h>
#include "../../TcpServer/Comm.hpp"
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> *arry) {
size_t offset = 0;
// 如果有10个字符,offset是查找的起始位置,范围应该是0~9,offset==10就代表已经越界了
while(offset < src.size()) {
size_t pos = src.find(sep, offset);//在src字符串偏移量offset处,开始向后查找sep字符/字串,返回查找到的位置
if (pos == std::string::npos) {//没有找到特定的字符
//将剩余的部分当作一个字串,放入arry中
if(pos == src.size()) break;
arry->push_back(src.substr(offset));
return arry->size();
}
if (pos == offset) {
offset = pos + sep.size();
continue;//当前字串是一个空的,没有内容
}
arry->push_back(src.substr(offset, pos - offset));
offset = pos + sep.size();
}
return arry->size();
}
// 读取文件的所有内容,将读取的内容放到指定缓冲区中
static bool ReadFile(const std::string &filename, std::string *buf) {
std::ifstream ifs(filename, std::ios::binary);
if (ifs.is_open() == false) {
printf("open %s file error", filename.c_str());
return false;
}
size_t fsize = 0;
ifs.seekg(0, ifs.end);//跳转读写位置到末尾
fsize = ifs.tellg(); //获取当前读写位置相对于起始位置的偏移量,从末尾偏移刚好就是文件大小
ifs.seekg(0, ifs.beg);//跳转到起始位置
buf->resize(fsize); //开辟文件大小的空间
ifs.read(&(*buf)[0], fsize);
if (ifs.good() == false) {
printf("read %s file error", filename.c_str());
ifs.close();
return false;
}
ifs.close();
return true;
}
// 向文件写入数据
static bool WriteFile(const std::string &filename, const std::string &buf) {
std::ofstream ofs(filename, std::ios::binary | std::ios::trunc);
if (ofs.is_open() == false) {
printf("open %s file error", filename.c_str());
return false;
}
ofs.write(buf.c_str(), buf.size());
if (ofs.good() == false) {
ERR_LOG("write %s file error", 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) {
std::string res;
for (auto &c : url) {
if (c == '.' || c == '-' || c == '_' || c == '~' || isalnum(c)) {
res += c;
continue;
}
// 如果需要将空格转+,则进行转换
if (c == ' ' && convert_space_to_plus == true) {
res += '+';
continue;
}
//剩下的字符都是需要编码成为 %HH 格式
char tmp[4] = {0};
//snprintf 与 printf比较类似,都是格式化字符串,只不过一个是打印,一个是放到一块空间中
snprintf(tmp, 4, "%%%02X", c); // 转换成%HH格式
res += tmp;
}
return res;
}
// 16进制字符转十进制数字
static char HEXTOI(char c) {
if (c >= '0' && c <= '9') {
return c - '0';
}else if (c >= 'a' && c <= 'z') {
return c - 'a' + 10;
}else if (c >= 'A' && c <= 'Z') {
return c - 'A' + 10;
}
return -1;
}
// URL解码
static std::string UrlDecode(const std::string url, bool convert_plus_to_space) {
//遇到了%,则将紧随其后的2个字符,转换为数字,第一个数字左移4位,然后加上第二个数字 + -> 2b %2b->2 << 4 + 11
std::string res;
for (int i = 0; i < url.size(); i++) {
if (url[i] == '+' && convert_plus_to_space == true) {
res += ' ';
continue;
}
if (url[i] == '%' && (i + 2) < url.size()) {
char v1 = HEXTOI(url[i + 1]);
char v2 = HEXTOI(url[i + 2]);
char v = v1 * 16 + v2;
res += v;
i += 2;
continue;
}
res += url[i];
}
return res;
}
// 获取响应状态码的描述信息
static std::string StatuDesc(int statu) {
auto it = _statu_msg.find(statu);
if (it != _statu_msg.end()) {
return it->second;
}
return "Unknow";
}
//根据文件后缀名获取文件mime
static std::string ExtMime(const std::string &filename) {
// a.b.txt 先获取文件扩展名
size_t pos = filename.find_last_of('.');
if (pos == std::string::npos) {
return "application/octet-stream";
}
//根据扩展名,获取mime
std::string ext = filename.substr(pos);
auto it = _mime_msg.find(ext);
if (it == _mime_msg.end()) {
return "application/octet-stream";
}
return it->second;
}
//判断一个文件是否是一个目录
static bool IsDirectory(const std::string &filename) {
struct stat st;
int ret = stat(filename.c_str(), &st);
// 不存在直接返回
if (ret < 0) return false;
// 存在则判断是否是目录
return S_ISDIR(st.st_mode);
}
//判断一个文件是否是一个普通文件
static bool IsRegular(const std::string &filename) {
struct stat st;
int ret = stat(filename.c_str(), &st);
// 不存在则直接返回
if (ret < 0) return false;
// 存在则判断是否是普通文件
return S_ISREG(st.st_mode);
}
// http请求的资源路径有效性判断
// /index.html --- 前边的/叫做相对根目录 映射的是某个服务器上的子目录
// 想表达的意思就是,客户端只能请求相对根目录中的资源,其他地方的资源都不予理会
// /../login, 这个路径中的..会让路径的查找跑到相对根目录之外,这是不合理的,不安全的
static bool ValidPath(const std::string &path) {
//思想:按照/进行路径分割,根据有多少子目录,计算目录深度,有多少层,深度不能小于0
std::vector<std::string> subdir;
Split(path, "/", &subdir);
int level = 0;
for (auto &dir : subdir) {
if (dir == "..") {
level--; //任意一层走出相对根目录,就认为有问题
if (level < 0) return false;
continue;
}
level++;
}
return true;
}
};
9.2 HTTP请求 HttpRequest 模块实现(HttpRequest.hpp)
用于保存 HTTP 请求数据被解析后的各项请求元素信息。
其中包含的信息:
- 请求方法
- 资源路径
- 协议版本
- 请求正文
- 路径资源的正则提取数据
- 头部字段
- 查询字符串
cpp
#pragma once
#include <regex>
#include "Util.hpp"
class HttpRequest {
public:
std::string _method; //请求方法
std::string _path; //资源路径
std::string _version; //协议版本
std::string _body; //请求正文
std::smatch _matches; //资源路径的正则提取数据
std::unordered_map<std::string, std::string> _headers; //头部字段
std::unordered_map<std::string, std::string> _params; //查询字符串
public:
HttpRequest():_version("HTTP/1.1") {}
// 清空数据
void ReSet() {
_method.clear();
_path.clear();
_version = "HTTP/1.1";
_body.clear();
std::smatch match;
_matches.swap(match);
_headers.clear();
_params.clear();
}
// 添加头部字段
void SetHeader(const std::string &key, const std::string &val) {
_headers[key] = val;
}
// 判断是否存在指定头部字段
bool HasHeader(const std::string &key) const {
return _headers.count(key) > 0;
}
// 获取指定头部字段的值,不存在返回空串
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[key] = val;
}
// 判断是否有某个指定的查询字符串
bool HasParam(const std::string &key) const {
return _params.count(key) > 0;
}
// 获取指定的查询字符串,不存在返回空串
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\n
bool ret = HasHeader("Content-Length");
if (ret == false) {
return 0;
}
std::string clen = GetHeader("Content-Length");
return std::stol(clen);
}
// 判断是否是短链接
bool Close() const {
// 没有Connection字段,或者有Connection但是值是close,则都是短链接,否则就是长连接
if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive") {
return false;
}
return true;
}
};
9.3 HttpResponse 响应模块实现(HttpResponse.hpp)
用于业务处理后设置并保存 HTTP 响应数据的各项元素信息,最终会被按照 HTTP 协议响应格式组织称为响应信息发送给客户端。
其中包含的信息:
- http状态码
- 是否为重定向响应
- 响应消息体
- 重定向url
- http响应头
cpp
#pragma once
#include <Util.hpp>
class HttpResponse {
public:
int _statu; // http状态码
bool _redirect_flag; // 是否是重定向响应
std::string _body; // 响应消息体
std::string _redirect_url; // 重定向的url
std::unordered_map<std::string, std::string> _headers; // 存储http响应头部分
public:
HttpResponse():_redirect_flag(false), _statu(200) {}
HttpResponse(int statu):_redirect_flag(false), _statu(statu) {}
// 重置对象状态
void ReSet() {
_statu = 200;
_redirect_flag = false;
_body.clear();
_redirect_url.clear();
_headers.clear();
}
// 添加头部字段
void SetHeader(const std::string &key, const std::string &val) {
_headers[key] = val;
}
// 判断是否存在指定头部字段
bool HasHeader(const std::string &key) {
return _headers.count(key) > 0;
}
// 取指定头部字段的值,不存在返回空
std::string GetHeader(const std::string &key) {
auto it = _headers.find(key);
if (it == _headers.end()) return "";
return it->second;
}
// 设置响应体内容,并自动添加头部
void SetContent(const std::string &body, const std::string &type = "text/html") {
_body = body;
SetHeader("Content-Type", type);
}
// 设置为重定向
void SetRedirect(const std::string &url, int statu = 302) {
_statu = statu;
_redirect_flag = true;
_redirect_url = url;
}
//判断是否是短链接
bool Close() {
// 没有Connection字段,或者有Connection但是值是close,则都是短链接,否则就是长连接
if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive") {
return false;
}
return true;
}
};
9.4 HTTP上下文 HttpContext 模块实现(HttpContext.hpp)
HTTP 请求接收的上下文模块,主要为了防止一次接收到的信息不完整,每次接收到信息后都将放到这里进行解析,如果是完整的请求,就将其取出,组成收到的HttpRequest,如果不是完整的请求,就等待下一次收到信息再进行组织。
cpp
#pragma once
#include "HttpRequest.hpp"
#include "../../TcpServer/Buffer.hpp"
// 设置五种状态表示解析进行的情况
typedef enum {
RECV_HTTP_ERROR,// 解析错误
RECV_HTTP_LINE, // 解析请求行
RECV_HTTP_HEAD, // 解析请求头部
RECV_HTTP_BODY, // 解析请求体
RECV_HTTP_OVER // 解析完成
} HttpRecvStatu;
// 一行的最大长度,超过这个长度视为出错
#define MAX_LINE 8192
class HttpContext {
private:
int _resp_statu; // 响应状态码
HttpRecvStatu _recv_statu; // 当前接收及解析的阶段状态
HttpRequest _request; // 已经解析得到的请求信息
private:
// 解析http请求行
bool ParseHttpLine(const std::string &line) {
// 正则表达式解析http报文
std::smatch matches;
// 匹配http请求行
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 Request
return false;
}
//0 : GET /hello/login?user=xiaoming&pass=123123 HTTP/1.1
//1 : GET
//2 : /hello/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_arry;
std::string query_string = matches[3];
//查询字符串的格式 key=val&key=val....., 先以 & 符号进行分割,得到各个字串
Util::Split(query_string, "&", &query_string_arry);
//针对各个字串,以 = 符号进行分割,得到key 和val, 得到之后也需要进行URL解码
for (auto &str : query_string_arry) {
size_t pos = str.find("=");
if (pos == std::string::npos) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 400;//BAD REQUEST
return false;
}
std::string key = Util::UrlDecode(str.substr(0, pos), true);
std::string val = Util::UrlDecode(str.substr(pos + 1), true);
_request.SetParam(key, val);
}
return true;
}
// 获取http请求行
bool RecvHttpLine(Buffer *buf) {
if (_recv_statu != RECV_HTTP_LINE) return false;
//1. 获取一行数据,带有末尾的换行
std::string line = buf->GetLine(true);
//2. 需要考虑的一些要素:缓冲区中的数据不足一行/获取的一行数据超大
if (line.size() == 0) {
//缓冲区中的数据不足一行,则需要判断缓冲区的可读数据长度,如果很长了都不足一行,这是有问题的
if (buf->ReadAbleSize() > MAX_LINE) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414;//URI Too Long
return false;
}
//缓冲区中数据不足一行,但是也不多,就等等新数据的到来
return true;
}
if (line.size() > MAX_LINE) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414;//URI TOO LONG
return false;
}
// 如果当前有完成的请求行,进行解析
bool ret = ParseHttpLine(line);
if (ret == false) return false;
//首行处理完毕,进入头部获取阶段
_recv_statu = RECV_HTTP_HEAD;
return true;
}
// 解析http头部信息
bool ParseHttpHead(std::string &line) {
//key: val\r\nkey: val\r\n....
if (line.back() == '\n') line.pop_back();//末尾是换行则去掉换行字符
if (line.back() == '\r') line.pop_back();//末尾是回车则去掉回车字符
size_t pos = line.find(": ");
if (pos == std::string::npos) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 400;// Bad Request
return false;
}
std::string key = line.substr(0, pos);
std::string val = line.substr(pos + 2);
_request.SetHeader(key, val);
return true;
}
// 获取http请求头部
bool RecvHttpHead(Buffer *buf) {
if (_recv_statu != RECV_HTTP_HEAD) return false;
//一行一行取出数据,直到遇到空行为止, 头部的格式 key: val\r\nkey: val\r\n....
while(1){
std::string line = buf->GetLine(true);
//2. 需要考虑的一些要素:缓冲区中的数据不足一行/获取的一行数据超大
if (line.size() == 0) {
//缓冲区中的数据不足一行,则需要判断缓冲区的可读数据长度,如果很长了都不足一行,这是有问题的
if (buf->ReadAbleSize() > MAX_LINE) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414;//URI Too Long
return false;
}
//缓冲区中数据不足一行,但是也不多,就等等新数据的到来
return true;
}
if (line.size() > MAX_LINE) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414;//URI Too Long
return false;
}
// 读到空行说明请求头部获取完毕,跳出循环
if (line == "\n" || line == "\r\n") break;
bool ret = ParseHttpHead(line);
if (ret == false) {
return false;
}
}
//头部处理完毕,进入正文获取阶段
_recv_statu = RECV_HTTP_BODY;
return true;
}
// 获取http正文
bool RecvHttpBody(Buffer *buf) {
if (_recv_statu != RECV_HTTP_BODY) return false;
// 1. 获取正文长度
size_t content_length = _request.ContentLength();
if (content_length == 0) {
//没有正文,则请求接收解析完毕
_recv_statu = RECV_HTTP_OVER;
return true;
}
// 2. 当前已经接收了多少正文,其实就是往 _request._body 中放了多少数据了
size_t real_len = content_length - _request._body.size();//实际还需要接收的正文长度
// 3. 接收正文放到body中,但是也要考虑当前缓冲区中的数据,是否是全部的正文
// 3.1 缓冲区中数据,包含了当前请求的所有正文,则取出所需的数据
if (buf->ReadAbleSize() >= real_len) {
_request._body.append(buf->ReadPosition(), real_len);
buf->MoveReadOffset(real_len);
_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);
}
return;
}
};
9.5 HTTP服务器 HttpServer 模块实现(HttpServer.hpp)
最终给组件使用者提供的 HTTP 服务器模块,用于以简单的接口实现 HTTP 服务器的搭建。
cpp
#pragma once
#include "../../TcpServer/TcpServer.hpp"
#include "Util.hpp"
#include "HttpRequest.hpp"
#include "HttpResponse.hpp"
#include "HttpContext.hpp"
// 默认超时时间
#define DEFALT_TIMEOUT 10
class HttpServer {
private:
using Handler = std::function<void(const HttpRequest &, HttpResponse *)>;
using Handlers = std::vector<std::pair<std::regex, Handler>>;
Handlers _get_route; // GET 请求路由表
Handlers _post_route; // POST 请求路由表
Handlers _put_route; // PUT 请求路由表
Handlers _delete_route; // DELETE 请求路由表
std::string _basedir; // 静态资源根目录
TcpServer _server;
private:
// 组织一个错误展示页面
void ErrorHandler(const HttpRequest &req, HttpResponse *rsp) {
std::string body;
body += "<html>";
body += "<head>";
body += "<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>";
body += "</head>";
body += "<body>";
body += "<h1>";
body += std::to_string(rsp->_statu);
body += " ";
body += Util::StatuDesc(rsp->_statu);
body += "</h1>";
body += "</body>";
body += "</html>";
//2. 将页面数据,当作响应正文,放入rsp中
rsp->SetContent(body, "text/html");
}
// 将HttpResponse中的要素按照http协议格式进行组织,发送
void WriteReponse(const PtrConnection &conn, const HttpRequest &req, HttpResponse &rsp) {
// 1. 先完善头部字段
// 设置长连接/短连接
if (req.Close() == true) rsp.SetHeader("Connection", "close");
else rsp.SetHeader("Connection", "keep-alive");
// 设置正文长度
if (rsp._body.empty() == false && rsp.HasHeader("Content-Length") == false)
rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));
// 设置类型
if (rsp._body.empty() == false && rsp.HasHeader("Content-Type") == false)
rsp.SetHeader("Content-Type", "application/octet-stream"); // 如果没有对应的文件类型,使用通用二进制类型
// 设置重定向
if (rsp._redirect_flag == true)
rsp.SetHeader("Location", rsp._redirect_url);
//2. 将rsp中的要素,按照http协议格式进行组织
std::stringstream rsp_str;
rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(rsp._statu) << "\r\n";
for (auto &head : rsp._headers) {
rsp_str << head.first << ": " << head.second << "\r\n";
}
rsp_str << "\r\n";
rsp_str << rsp._body;
//3. 发送数据
conn->Send(rsp_str.str().c_str(), rsp_str.str().size());
}
// 判断是否是一个静态请求
bool IsFileHandler(const HttpRequest &req) {
// 1. 必须设置了静态资源根目录
if (_basedir.empty()) return false;
// 2. 请求方法,必须是GET / HEAD请求方法
if (req._method != "GET" && req._method != "HEAD") return false;
// 3. 请求的资源路径必须是一个合法路径
if (Util::ValidPath(req._path) == false) return false;
// 4. 请求的资源必须存在,且是一个普通文件
// 组织请求的路径
std::string req_path = _basedir + req._path;
// 如果请求的路径最后是/,默认加上一个index.html
if (req._path.back() == '/') req_path += "index.html";
if (Util::IsRegular(req_path) == false) return false;
return true;
}
//静态资源的请求处理 --- 将静态资源文件的数据读取出来,放到rsp的_body中, 并设置mime
void FileHandler(const HttpRequest &req, HttpResponse *rsp) {
std::string req_path = _basedir + req._path;
// 如果请求路径为/,则默认为index页面
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);
}
// 如果找不到指定的页面,则在响应中填充一个404页面
//功能性请求的分类处理
void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers) {
// 在对应请求方法的路由表中,查找是否含有对应资源请求的处理函数,有则调用,没有则发404
// 思想:路由表存储的时键值对 -- 正则表达式 & 处理函数
// 使用正则表达式,对请求的资源路径进行正则匹配,匹配成功就使用对应函数进行处理
// /numbers/(\d+) /numbers/12345
for (auto &handler : handlers) {
const std::regex &re = handler.first;
const Handler &functor = handler.second;
bool ret = std::regex_match(req._path, req._matches, re);
if (ret == false) continue;
return functor(req, rsp);//传入请求信息,和空的rsp,执行处理函数
}
// 如果都不是,设置为404,并将准备好的页面写入HttpResponse
rsp->_statu = 404;
std::string path = _basedir + "/404.html";
std::string body;
Util::ReadFile(path, &body);
rsp->SetContent(body);
}
// 进行业务处理,如果获取静态资源则直接进行获取,否则查询路由表执行相应处理
void Route(HttpRequest &req, HttpResponse *rsp) {
//1. 对请求进行分辨,是一个静态资源请求,还是一个功能性请求
// 静态资源请求,则进行静态资源的处理
// 功能性请求,则需要通过几个请求路由表来确定是否有处理函数
// 既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405
if (IsFileHandler(req) == true) return FileHandler(req, rsp);
if (req._method == "GET" || req._method == "HEAD") return Dispatcher(req, rsp, _get_route);
if (req._method == "POST") return Dispatcher(req, rsp, _post_route);
if (req._method == "PUT") return Dispatcher(req, rsp, _put_route);
if (req._method == "DELETE") return Dispatcher(req, rsp, _delete_route);
rsp->_statu = 405;// Method Not Allowed
}
// 给新连接设置上下文
void OnConnected(const PtrConnection &conn) {
conn->SetContext(HttpContext());
DBG_LOG("new connection %p", conn.get());
}
//缓冲区数据解析+处理
void OnMessage(const PtrConnection &conn, Buffer *buffer) {
while(buffer->ReadAbleSize() > 0){
// 1. 获取上下文
HttpContext *context = std::any_cast<HttpContext>(conn->GetContext());
// 2. 通过上下文对缓冲区数据进行解析,得到HttpRequest对象
context->RecvHttpRequest(buffer);
HttpRequest &req = context->Request();
HttpResponse rsp(context->RespStatu());
if (context->RespStatu() >= 400) {
//进行错误响应,关闭连接
ErrorHandler(req, &rsp);//填充一个错误显示页面数据到rsp中`
WriteReponse(conn, req, rsp);//组织响应发送给客户端
context->ReSet();
buffer->Clear();//出错了就把缓冲区数据清空
conn->Shutdown();//关闭连接
return;
}
// 当前请求还没有接收完整,则退出,等新数据到来再重新继续处理
if (context->RecvStatu() != RECV_HTTP_OVER) return;
// 3. 请求路由 + 业务处理
Route(req, &rsp);
// 4. 对HttpResponse进行组织发送
WriteReponse(conn, req, rsp);
// 5. 重置上下文
context->ReSet();
// 6. 根据长短连接判断是否关闭连接或者继续处理
if (rsp.Close() == true) conn->Shutdown();//短链接则直接关闭
}
}
public:
HttpServer(int port, int timeout = DEFALT_TIMEOUT):_server(port) {
_server.EnableInactiveRelease(timeout);
_server.SetConnectedCallback(std::bind(&HttpServer::OnConnected, this, std::placeholders::_1));
_server.SetMessageCallback(std::bind(&HttpServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));
}
// 设置基础目录
void SetBaseDir(const std::string &path) {
assert(Util::IsDirectory(path) == true);
_basedir = path;
}
/*设置/添加,请求(请求的正则表达)与处理函数的映射关系*/
void Get(const std::string &pattern, const Handler &handler) { _get_route.push_back(std::make_pair(std::regex(pattern), handler)); }
void Post(const std::string &pattern, const Handler &handler) { _post_route.push_back(std::make_pair(std::regex(pattern), handler)); }
void Put(const std::string &pattern, const Handler &handler) { _put_route.push_back(std::make_pair(std::regex(pattern), handler)); }
void Delete(const std::string &pattern, const Handler &handler) { _delete_route.push_back(std::make_pair(std::regex(pattern), handler)); }
// 设置子线程数
void SetThreadCount(int count) { _server.SetThreadCount(count); }
// 开始监听
void Listen() { _server.Start(); }
};
#undef DEFALT_TIMEOUT
10. 基于 HttpServer 搭建 Http 服务器
cpp
#include "http/HttpServer.hpp"
#define WWWROOT "./wwwroot/"
// 将请求组织称字符串
std::string RequestStr(const HttpRequest &req) {
std::stringstream ss;
ss << req._method << " " << req._path << " " << req._version << "\r\n";
for (auto &it : req._params) {
ss << it.first << ": " << it.second << "\r\n";
}
for (auto &it : req._headers) {
ss << it.first << ": " << it.second << "\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(8888);
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;
}
11. 功能测试
11.1 使用Postman进行基本功能测试

11.2 长连接连续请求测试
一个连接每隔3s向服务器发送一个请求,查看是否会收到响应。
预期结果:可以正常进行长连接的通信。
cpp
/*长连接测试1:创建一个客户端持续给服务器发送数据,直到超过超时时间看看是否正常*/
#include "../source/server.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8888, "127.0.0.1");
std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
while(1) {
assert(cli_sock.Send((void*)req.c_str(), req.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
sleep(3);
}
cli_sock.Close();
return 0;
}

11.3 超时连接释放测试1
创建一个客户端,连接上服务器后,不进行消息发送,等待看超时后,连接是否会自动释放(当前默认设置超时时间为10s)。
预期结果:10s后连接被释放。
cpp
#include "../source/server.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8888, "127.0.0.1");
std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
while(1) {
assert(cli_sock.Send((void *)req.c_str(), req.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
sleep(15);
}
cli_sock.Close();
return 0;
}

11.4 超时连接释放测试2
连接服务器,告诉服务器要发送100字节正文数据给服务器,但是实际上发送数据不足1024字节,然后看服务器处理情况。
预期结果:服务器第一次接收请求不完整,会将后边的请求当作第一次请求的正文进行处理。最终对剩下的数据处理的时候处理出错,关闭连接。
cpp
#include "../source/server.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8888, "127.0.0.1");
std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\n123456";
while(1) {
assert(cli_sock.Send((void*)req.c_str(), req.size()) != -1);
assert(cli_sock.Send((void*)req.c_str(), req.size()) != -1);
assert(cli_sock.Send((void*)req.c_str(), req.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
sleep(3);
}
cli_sock.Close();
return 0;
}

上面红色框的第一个部分是对我们发送的第一个报文的响应,由于我们发送的第一个请求内容为100,所以服务器把后面两个请求的部分内容当成了第一个报文的正文,而我们第一个报文请求的是/hello,在服务器中用GET方法请求/hello,服务器就会把收到的请求重新组织成字符串发送回来。
而第二个部分就是服务器读取了缓冲区中剩余的内容,发现是一个错误的请求,然后返回了错误页面,并关闭了连接。
11.5 超时连接释放测试3
接收请求的数据,但是业务处理的时间过长,超过了设置的超时销毁时间(服务器性能达到瓶颈),观察服务端的处理。
预期结果:在一次业务处理中耗费太长时间,导致其他连接被连累超时,导致其他的连接有可能会超时释放。
假设有 12345 描述符就绪了, 在处理1的时候花费了30s处理完,超时了,导致2345描述符因为长时间没有刷新活跃度,则存在两种可能处理结果:
- 如果接下来的2345描述符都是通信连接描述符,恰好本次也都就绪了事件,则并不影响,因为等1处理完了,接下来就会进行处理并刷新活跃度。
- 如果接下来的2号描述符是定时器事件描述符,定时器触发超时,执行定时任务,就会将345描述符给释放掉,这时候⼀旦345描述符对应的连接被释放,接下来在处理345事件的时候就会导致程序崩溃(内存访问错误),因此,在任意的事件处理中,都不应该直接对连接进行释放,而应该将释放操作压入到任务池中,等所有连接事件处理完了,然后执行任务池中的任务的时候再去进行释放。
服务端:
cpp
/*业务处理中sleep 15s,超过服务器设置的超时时间*/
#include "../source/http/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 &it : req._params) {
ss << it.first << ": " << it.second << "\r\n";
}
for (auto &it : req._headers) {
ss << it.first << ": " << it.second << "\r\n";
}
ss << "\r\n";
ss << req._body;
return ss.str();
}
void Hello(const HttpRequest &req, HttpResponse *rsp)
{
rsp->SetContent(RequestStr(req), "text/plain");
sleep(15);
}
int main()
{
HttpServer server(8888);
server.SetThreadCount(3);
server.SetBaseDir(WWWROOT);
server.Get("/hello", Hello);
server.Listen();
return 0;
}
客户端:
cpp
#include <signal.h>
#include "../source/server.hpp"
int main()
{
signal(SIGCHLD, SIG_IGN);
for (int i = 0; i < 10; i++) {
pid_t pid = fork();
if (pid < 0) {
DBG_LOG("FORK ERROR");
return -1;
}else if (pid == 0) {
Socket cli_sock;
cli_sock.CreateClient(8888, "127.0.0.1");
std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
while(1) {
assert(cli_sock.Send((void*)req.c_str(), req.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
}
cli_sock.Close();
exit(0);
}
}
while(1) sleep(1);
return 0;
}
服务端信息:

客户端信息:

11.6 数据中多条请求处理测试
给服务器发送的一条数据中包含有多个HTTP请求,观察服务器的处理。
预期结果:每一条请求都有对应的响应。
cpp
#include "../source/server.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8888, "127.0.0.1");
std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
req += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
req += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
while(1) {
assert(cli_sock.Send((void*)req.c_str(), req.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
sleep(3);
}
cli_sock.Close();
return 0;
}

可以从打印结果看到,多个响应也被放到缓冲区中同时进行发送了。因此这边一次是接收了所有的响应,一次是在连接超时关闭后接收为空。
11.7 PUT大文件上传测试
使用put请求上传一个大文件进进行保存,大文件数据的接收会被分在多次请求中接收,然后计算源文件和上传后保存的文件的MD5值,判断请求的接收处理是否存在问题。(这里主要观察的是上下文的处理过程是否正常。)
服务端:
cpp
#include "../source/http/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 &it : req._params) {
ss << it.first << ": " << it.second << "\r\n";
}
for (auto &it : req._headers) {
ss << it.first << ": " << it.second << "\r\n";
}
ss << "\r\n";
ss << req._body;
return ss.str();
}
void PutFile(const HttpRequest &req, HttpResponse *rsp)
{
std::string pathname = WWWROOT + req._path;
Util::WriteFile(pathname, req._body);
}
int main()
{
HttpServer server(8888);
server.SetThreadCount(3);
server.SetBaseDir(WWWROOT);
server.Put("/1234.txt", PutFile);
server.Listen();
return 0;
}
客户端:
cpp
#include "../source/http/http.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8888, "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(cli_sock.Send((void*)req.c_str(), req.size()) != -1);
assert(cli_sock.Send((void*)body.c_str(), body.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
sleep(3);
cli_sock.Close();
return 0;
}
大文件创建(创建一个300MB的文件):
shell
czxyv@czxyv:~/Data/reactor_server_v2/test/http_test$ dd if=/dev/zero of=./hello.txt bs=1M count=300
300+0 records in
300+0 records out
314572800 bytes (315 MB, 300 MiB) copied, 0.767874 s, 410 MB/s
创建出来的文件的MD5值:
shell
czxyv@czxyv:~/Data/reactor_server_v2/test/http_test$ md5sum hello.txt
0d97a9cd8bbd7ce75a2a76bb06258915 hello.txt
服务端接收到的文件的MD5值:
czxyv@czxyv:~/Data/reactor_server_v2/test/http_test/wwwroot$ md5sum 1234.txt
0d97a9cd8bbd7ce75a2a76bb06258915 1234.txt
MD5值相同,文件内容相同。
12. 性能测试
采用webbench进行服务器性能测试。
12.1 测试环境
服务器环境:4核4G云服务器ubuntu24.04LTS,服务器采用1主3从reactor模式。
客户端环境:同上。
12.2 测试500个客户端连接的情况
shell
czxyv@czxyv:~/webbench-1.5$ ./webbench -c 500 -t 60 http://127.0.0.1:8888/
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
Benchmarking: GET http://127.0.0.1:8888/
500 clients, running 60 sec.
Speed=199640 pages/min, 3918350 bytes/sec.
Requests: 199640 susceed, 0 failed.
12.2 测试5000个客户端连接的情况
czxyv@czxyv:~/webbench-1.5$ ./webbench -c 500 -t 60 http://127.0.0.1:8888/
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
Benchmarking: GET http://127.0.0.1:8888/
500 clients, running 60 sec.
Speed=183003 pages/min, 3581257 bytes/sec.
Requests: 182998 susceed, 0 failed.
以上测试中,使用浏览器访问服务器,均能流畅获取请求的页面。但是根据测试结果能够看出,虽然并发量⼀直在提搞,但是总的请求服务器的数量并没有增加,反而有所降低,侧⾯反馈了处理所耗时间更多了,基本上可以根据35w/min左右的请求量计算出5000并发量时服务器的极限了,但是这个测试其实意义不大,因为测试客户端和服务器都在同一台机器上,传输的速度更快,但同时抢占cpu也影响了处理,最好的方式就是在两台不同的机器上进行测试,这里只是通过这个方法告诉大家该如何对服务器进行性能测试。
目前受限于设备环境配置,尚未进行更多并发量的测试。