生活总是让我们遍体鳞伤,
但到后来,
那些受伤的地方一定会变成我们最强壮的地方。
-- 《老人与海》--
高并发服务器项目总结
模块关系图

项目工具模块
缓冲区模块
底层使用字符容器储存数据,通过两个指针:读偏移和写偏移管理空间:
- 对于写入:确保空间足够,在写偏移后拷贝数据,写偏移向后移动
- 对于读取:确保可读数据足够,在读偏移后读取数据,读偏移向后移动
- 空间不足时及时扩容
通用类型模块
想要实现的效果:
cpp
Any a;
a = 123;
a = "123"
a = vector<int>(n , 0);
为了实现这种效果,首先Any类肯定不能带有模版参数,不然就不能自由转换了!
- 可以在内部设计一个内部类placeholder(继承holder父类),它是一个模板类,是实际储存数据的对象;
- 当我们创建了一个Any对象时,会创建一个父类指针,管理placeholder对象
- 通过模版函数,我们可以实现对placeholder对象赋值。
- 可以通过交换placeholder对象管理的资源实现自由赋值。
套接字socket模块
这个模块对外提供一键创建服务端套接字和客户端套接字的方法(是对内部接口的一个封装):
- 创建套接字接口:以数据流的方式创建一个套接字文件
- 绑定端口信息
- 进入监听模式
- 发起连接:组织目标端口信息,进行申请连接
- 启动地址重用:可以快速重启服务,跳过系统释放端口的时间;允许多个 socket 监听相同的地址和端口;避免地址已在使用错误。
- 设置非阻塞读取
对外提供的接口
- 构建服务端套接字:创建套接字,设置为非阻塞,将地址与端口设置为可重用,绑定地址信息,进行监听。
- 构建客户端套接字:创建套接字,连接服务器
- 发送数据
- 接收数据
信道Channel模块
该模块是用来管理一个文件描述符的,对该描述符的事件进行监控,触发事件就调用对应的回调函数:
- 当前需要监控的事件集
- 当前连接触发的事件集
- 管理该文件描述符的Reactor模型(EventLoop)
- 可读事件触发的回调函数
- 可写事件触发的回调函数
- 错误事件触发的回调函数
- 断开事件触发的回调函数
- 任意事件触发的回调函数
提供一系列接口帮助我们管理对该文件描述符的监控。触发事件通过HandleEvent()函数进行处理。
多路转接Poller模块
该模块是对epoll接口的二次封装,管理一系列的文件描述符。内部储存文件描述符与其对应的信道Channel;
可以通过信道对epoll中监控的文件描述符进行更新监控事件集
提供一个开始监控接口Poll,该函数中阻塞式等待事件就绪。得到就绪事件就进行将就绪事件更新到对应的信道中,然后将信道放入活跃队列中等待上层处理。
Reactor模块
时间轮TimeWheel模块
时间轮模块是用来执行定时任务的,项目中可以用来管理超时连接,及时进行释放操作。
为了可以实现定时执行任务,首先需要对封装出一个任务对象Timer:
- 任务ID uint64_t _id :用来标识任务
- 超时时间 uint32_t _timeout;
- 是否被取消:bool _canceled; 这样取消任务时,通过哈希表找到对应任务,设置该标志位就好了,效率高!
- 回调任务 _task_cb void()类型
- 释放操作函数 _release :用于删除TimeWheel保存的定时器对象信息
- 任务释放时执行回调函数
时间轮TimeWheel的本质是一个数组,一个指针定时移动,到哪里就释放该位置的所有任务Timer。
cpp
class TimeWheel
{
using TaskPtr = std::shared_ptr<Timer>;
using WeakPtr = std::weak_ptr<Timer>; // 辅助shared_ptr 不会增加引用计数
private:
int _capacity; // 最大容量 表盘最大数量(默认60秒)
int _tick; // 移动表针
std::vector<std::vector<TaskPtr>> _wheel; // 时间轮
std::unordered_map<uint64_t, WeakPtr> _timers; // 定时任务对象哈希表
int _timerfd; // 定时器fd
EventLoop *_loop; // 对应的Eventfd
std::unique_ptr<Channel> _timer_channel; // 管理_timerfd的channel类!
//...
}
这里使用智能指针进行管理,方便操作。引用计数归零时自动释放。设置一个timefd,系统每1s都会想其中写入一个1。读取到数据,就移动指针,释放该时间的定时任务指针。
EventLoop模块
该模块是真正的Reactor模型,管理一下数据:
cpp
std::thread::id _event_id; // 线程ID
int _eventfd; // eventfd 用于通知事件
std::unique_ptr<Channel> _event_channel; // 管理Event事件的Channel对象
Poller _poller; // epoll模型
std::vector<Functor> _tasks; // 任务池
std::mutex _mtx; // 互斥锁保护线程
TimeWheel _timer_wheel; // 时间轮
该模块的执行逻辑为:
- 通过EventLoop模块通过的接口可以添加管理的信道(文件描述符)
- 启动监控后,对epoll中的文件描述符进行事件监控。
- Poller模块Poll函数中返回就绪的活跃信道集。
- 对每个活跃信道执行handleEvent处理就绪事件
- 对于外部设置的任务,需要保证是在该eventloop所在线程内部执行,如果不是放入任务池中等待执行。
EventLoop模块与TimeWheel模块整合
Reactor模型内部加入一个时间轮,负责执行超时连接销毁任务,对外提供接口:
- Void TimerAdd(uint64_t id, int delay, Task_t cb):加入定时任务
- void TimerRefresh(uint64_t id):刷新定时任务
- void TimerCancel(uint64_t id):取消定时任务
- bool HasTimer(uint64_t id):判断是否有该任务
连接Connection模块
连接模块是对上面模块的整体整合,具有以下参数:
cpp
uint64_t _conn_id; // connection连接ID
Socket _socket; // 管理的套接字
int _sockfd; // 套接字fd
EventLoop *_loop; // connection连接关联的EventLoop对象
Any _context; // 上下文数据
Channel _channel; // 管理连接事件
Buffer _in_buffer; // 输入缓冲区 存放Socket中读取的数据
Buffer _out_buffer; // 输出缓冲区 存放要发送给对端的数据
bool _enable_active_release; // 是否开启超时销毁 默认是false
ConnStatu _statu; // Connection连接状态
// 5 个 回调函数 --- 注意使用智能指针 防止在执行任务之前Connection销毁
using ConnectedCallBack = std::function<void(const PtrConn &)>; // 连接时进行的回调函数
using MessageCallBack = std::function<void(const PtrConn &, Buffer *)>; // 处理数据时的回调函数
using ClosedCallBack = std::function<void(const PtrConn &)>; // 关闭连接时的回调函数
using AnyEventCallBack = std::function<void(const PtrConn &)>; // 处理任意事件时的回调函数
ConnectedCallBack _conn_cb; // 连接回调函数类型
MessageCallBack _message_cb; // 处理时回调函数
ClosedCallBack _closed_cb; // 关闭阶段的回调
AnyEventCallBack _event_cb; // 任意事件触发的回调
// 还需要组件内的连接关闭回调 因为服务器组件内会把所有的连接管理起来 一旦某个连接关闭 就应该从管理的地方移除自己的信息!
ClosedCallBack _event_closed_cb;
- 该连接管理的底层套接字
- 管理该套接字的信道:用于处理新连接事件
- 连接关联的EventLoop模块,用于事件监控
- 通用类型上下文
- 输入/输出缓冲区
- 新连接/处理消息/关闭连接/任意事件 回调函数
- 连接内部连接关闭函数
内部提供的函数:
- 首先是该连接内部遇到事件的函数,作为信道的回调函数
- 读事件回调函数
- 写事件回调函数
- 关闭事件回调
- 错误事件回调
- 线程内发送数据函数
- 线程内关闭连接函数
- 线程内启动超时管理函数:将该连接的释放函数Release放入EventLoop的时间轮中
- 线程内取消超时管理函数
- 线程内更新回调函数(协议)
- 线程内释放函数ReleaseInLoop(真正的释放函数):修改连接状态,取消定时任务,执行用户设置的关闭连接回调函数,通过内部释放回调函数释放在连接在服务器内的资源。
对外提供的接口:
- 基础接口:返回连接id,套接字fd...
- 设置回调函数接口
- 发送数据接口:在EventLoop中加入发送任务
- 关闭连接接口:在EventLoop中加入关闭连接任务
- 启动超时销毁接口:在EventLoop中加入启动超时销毁任务
- 取消超时销毁接口:在EventLoop中加入取消超时销毁任务
- 切换协议接口:在EventLoop中加入取消切换协议任务
Accepter模块
该模块是对监听套接字进行管理的:
cpp
Socket _socket; // 套接字对象
EventLoop *_loop; // 对监听套接字进行事件监控
Channel _channel; // 用于对管理监听套接字的事件
using AcceptCallBack = std::function<void(int)>;
AcceptCallBack _accept_callback;
不同于不同的套接字文件,监听套接字只需要读取事件的回调函数,从监听套接字中读取出新连接的fd,然后对该fd执行获取连接的回调函数。
线程池模块
我们先对线程进行封装,使其包含EventLoop模块。线程中需要执行的函数就是创建一个EventLoop,然后执行Start监控。
线程池对若干个线程进行管理:
cpp
int _thread_size; // 从属线程数量
int _next_idx; // 线程索引
std::vector<LoopThread *> _threads; // 管理从属线程
std::vector<EventLoop *> _loops; // 管理从属Reactor
EventLoop *_baseloop; // 主Reactor
每次取出一个线程对外提供时,只需提供线程对应的EventLoop即可
TcpServer模块
这个模块就是最终的服务器模块。主Reactor对监听套接字的进行管理,获取到新连接后就将连接交给一个线程中的从属Reactor进行监控。进而实现高并发的服务器!
该模块管理的成员变量:
cpp
uint64_t _conn_id; // 自增长的连接ID;
int _port; // 绑定的端口号
EventLoop _baseloop; // 主Reactor模型
Acceptor _acceptor; // 监听套接字
LoopThreadPool _pool; // 从属线程池
std::unordered_map<uint64_t, PtrConn> _conns; // 管理连接的哈希表
int _timeout; // 超时时间
bool _enable_active_release; // 是否开启超时销毁 默认是false
// 4种 回调函数 --- 注意使用智能指针 防止在执行任务之前Connection销毁
using ConnectedCallBack = std::function<void(const PtrConn &)>; // 连接时进行的回调函数
using MessageCallBack = std::function<void(const PtrConn &, Buffer *)>; // 处理数据时的回调函数
using ClosedCallBack = std::function<void(const PtrConn &)>; // 关闭连接时的回调函数
using AnyEventCallBack = std::function<void(const PtrConn &)>; // 处理任意事件时的回调函数
ConnectedCallBack _conn_cb; // 连接回调函数类型
MessageCallBack _message_cb; // 处理时回调函数
ClosedCallBack _closed_cb; // 关闭阶段的回调
AnyEventCallBack _event_cb; // 任意事件触发的回调
- 提供给Accepterd的回调函数NewConnection,在该函数中对连接赋予一个id,并设置连接的回调函数,将该连接交给线程池中的一个线程进行监管,启动对应的超时销毁任务。
- 提供连接的内部释放函数RemoveConnection,销毁连接在服务器中的资源
- 提供给上层的设置回调函数接口
- 服务器开始运行:启动服务器套接字的监听,创建线程池,开启主Reactor的监控。