[项目深挖]仿muduo库的并发服务器的解析与优化方案
标题:[项目深挖]仿muduo库的并发服务器的优化方案
@水墨不写bug
文章目录
- 一、buffer 模块
- (1)线性缓冲区+直接扩容---->环形缓冲区+定时扩容(只会扩容一次)
- (2)使用双缓冲(Double Buffering)
- (3)数据丢弃策略
- 为什么视频传输选择不可靠的UDP协议?
- (4)零拷贝
- 为什么零拷贝重要?
- 零拷贝的典型场景
- 传统数据传输的过程
- 零拷贝的过程
- 实现零拷贝的技术
- 1. `sendfile` 系统调用
- 2. `mmap` + `write`
- 3. `splice` 系统调用
- 二、EventLoop 模块
- 如何理解RunInLoop这一类的函数?
- 为什么需要 runInLoop?
- runInLoop 的优点
- (1)减少锁的使用----无锁队列
- (2)优化定时器处理----时间轮代替最小堆
- 为什么 TimerWheel 需要 weak_ptr?
- 三. ThreadPool 模块
- 四、Acceptor 模块
- 2. 关键组件
- 3. 实现步骤
- 3.1. 定义负载均衡器
- 3.2. 改造 `Acceptor` 模块
- 3.3. 改造 `EventLoop` 模块
- 五、定时器模块
- 六、日志模块
- 七、 错误处理与监控
- 1. 设计原则
- 2. 实现步骤
- 2.1. 定义异常基类
- 2.2. 定义派生异常类
- 2.3. 在核心模块中抛出异常
- 2.4. 全局捕获异常
- 3. 优化与扩展
- 细化异常信息
- 结合日志记录
- 异常恢复机制
- 支持自定义异常类型
一、buffer 模块
(1)线性缓冲区+直接扩容---->环形缓冲区+定时扩容(只会扩容一次)
如果缓冲区满了超过一定时间(10s)仍然处于高使用率(>=90%)的状态,则扩容一次,增大环形缓冲区的大小,后续不再扩容。扩容一次之后,关闭定时器。
基于历史数据自适应扩容:历史上缓冲区如果负载较高,可以选择较大扩容幅度;如果负载较低,可以选择较小扩容幅度。通过prev使用率与cur使用率求变化率。
如果扩容后的缓冲区仍然一直处于高使用率状态,则计入日志文件。
合理性:
缓冲区负载偶发性:当缓冲区满的情况是偶发的,而不是长期的瓶颈。
内存资源敏感性:扩容是有限制的(只扩容一次),避免了动态扩容带来的过多内存消耗。
(2)使用双缓冲(Double Buffering)
使用两个缓冲区,当一个缓冲区满时切换到另一个缓冲区,避免阻塞。通过状态变量控制两个缓冲区的切换。
(3)数据丢弃策略
当缓冲区满时,直接丢弃新到达的数据或旧数据。
丢弃最旧数据:移除环形缓冲区中最早的数据(比如日志系统中)。
丢弃新数据:直接丢弃当前要写入的内容(比如视频帧流中)。
优点:
避免系统阻塞,保证系统运行流畅。
缺点:
数据丢失可能会影响系统的业务逻辑。
适用场景:
应用对数据完整性要求不高,如日志、视频流等场景。
为什么视频传输选择不可靠的UDP协议?
UDP------无连接,不可靠,低延迟,面向数据报。
实时性
使用 TCP 时,丢包会触发重传机制,可能导致延迟增加或卡顿,不适合实时性要求高的场景。
UDP 没有重传机制,即使丢包,视频播放也不会被阻塞,用户可能只会看到短暂的画质下降。
容忍丢包
视频流通常使用编码技术(如 H.264、H.265),具有一定的抗丢包能力。
即使部分数据丢失,解码器仍然可以通过冗余信息或插值技术恢复画面,保证用户体验。
高效性
UDP 的开销比 TCP 更小,因为它没有复杂的连接管理、流量控制和拥塞控制。
对于带宽有限的网络环境,减少协议开销意味着可以传输更多的视频数据。
乱序容忍
视频播放有一定的缓冲区,可以通过序列号等方式重新排序数据包,解决 UDP 的乱序问题。
即使部分数据包延迟到达,也可以选择丢弃,而不会影响整体流畅度。
视频传输选择 UDP 的原因主要是为了满足实时性、高效性和丢包容忍的需求。尽管 UDP 本身是不可靠的,但结合应用层协议(如 RTP)、纠错技术(如 FEC)和优化手段(如自适应比特率),可以弥补其不足,确保视频流的质量和流畅性。对于实时性要求低的场景(如视频文件下载),则可以选择更可靠的 TCP。
(4)零拷贝
零拷贝(Zero-Copy) 是一种优化技术,旨在在计算机系统中减少数据复制的次数,以提高数据传输或处理的效率,尤其是在文件或网络数据的高效传输中。零拷贝的核心理念是避免 CPU 将数据从一个位置复制到另一个位置,而是通过特定的硬件或内核支持,直接在数据的生产者和消费者之间传递数据。
为什么零拷贝重要?
- 减少 CPU 占用:
- 数据拷贝通常需要 CPU 介入,零拷贝通过减少拷贝次数,释放了 CPU 的计算资源。
- 提高数据传输效率:
- 数据直接从一个位置移动到目标位置,不经过中间缓冲,大幅减少传输延迟。
- 降低内存带宽压力:
- 传统的多次数据拷贝会占用宝贵的内存带宽,零拷贝减少了这一开销。
零拷贝的典型场景
- 文件传输(文件到网络):
- 将文件内容直接发送到网络(如通过
sendfile
系统调用)。
- 将文件内容直接发送到网络(如通过
- 网络数据传输:
- 数据直接从内核缓冲区发送到网卡,不经过用户态。
- 磁盘 I/O 优化:
- 在大文件读写中,避免数据在磁盘、内核缓冲区、用户态缓冲区之间反复拷贝。
传统数据传输的过程
以文件发送到网络为例,传统数据传输的步骤如下:
- 文件读取:
- 从磁盘读取文件内容到内核缓冲区。
- 复制到用户空间:
- 将内核缓冲区的数据复制到用户空间的缓冲区。
- 发送到内核:
- 用户空间的数据再复制回内核空间的网络缓冲区。
- 发送到网卡:
- 最后,网卡从内核网络缓冲区中读取数据并发送。
总共涉及 4 次数据拷贝,其中 CPU 负责完成至少 2 次数据复制。
零拷贝的过程
通过零拷贝技术,可以将上述过程优化为:
- 数据直接映射:
- 使用内核支持,直接将文件从磁盘的页缓存发送到网络缓冲区(不经过用户态)。
- 网卡直接读取:
- 网卡直接从内核缓冲区读取数据并发送,不需要额外的拷贝。
总共涉及 0 次用户态拷贝,CPU 只负责控制流程。
实现零拷贝的技术
以下是几种常见的零拷贝实现技术:
1. sendfile
系统调用
- 描述:
sendfile
是 Linux 提供的一种系统调用,用于将文件直接从内核页缓存发送到网络套接字。
- 工作原理:
- 文件数据从磁盘被读取到内核页缓存后,直接从内核页缓存发送到网卡,无需经过用户态。
- 适用场景:
- 文件服务器、Web 服务器等需要高效传输文件的场景。
- 示例代码:
int fd = open("file.txt", O_RDONLY); int sock = socket(...); sendfile(sock, fd, NULL, file_size);
2. mmap
+ write
- 描述:
- 使用
mmap
将文件映射到用户空间内存地址,然后直接调用write
将数据发送到套接字。
- 使用
- 工作原理:
- 避免了从磁盘读取到用户缓冲区的额外拷贝。
- 适用场景:
- 需要灵活访问文件内容,同时减少拷贝次数的场景。
- 示例代码:
int fd = open("file.txt", O_RDONLY); char* data = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0); write(sock, data, file_size);
3. splice
系统调用
- 描述:
splice
允许在两个文件描述符之间直接移动数据,减少拷贝。
- 工作原理:
- 数据通过内核缓冲区直接从一个管道移动到另一个目标,无需用户态干预。
- 适用场景:
- 网络数据流处理、文件复制等场景。
- 示例代码:
int pipefd[2]; pipe(pipefd); splice(file_fd, NULL, pipefd[1], NULL, file_size, SPLICE_F_MOVE); splice(pipefd[0], NULL, sock_fd, NULL, file_size, SPLICE_F_MOVE);
二、EventLoop 模块
EventLoop 是 Muduo 的核心模块之一,负责管理事件循环和分发。
如何理解RunInLoop这一类的函数?
在 Muduo 网络库中,runInLoop 这一类函数是为了在特定的线程(即事件循环线程)中执行某些任务而设计的。它们的主要用途是解决跨线程调用的问题,确保任务在正确的线程上下文中执行。
runInLoop 的主要作用是将一个任务(通常是回调函数)添加到当前的 EventLoop 中执行。如果调用 runInLoop 的线程不是事件循环的线程,那么任务会被放入事件循环的任务队列中,等待事件循环线程来执行。
为什么需要 runInLoop?
在多线程环境中,Muduo 的 EventLoop 是线程不安全的,即:不能直接从多个线程访问或修改同一个 EventLoop。
事件循环线程需要对事件进行处理,而其他线程可能需要向事件循环线程发起某些操作(例如注册回调函数、修改定时器等)。
问题: 如果直接在非事件循环线程中调用事件循环的操作,可能会导致数据竞争或崩溃。
解决: 使用 runInLoop 将任务安全地转移到事件循环线程中,确保任务在正确的线程上下文中被执行。 runInLoop 的实现原理
以下是 runInLoop 的主要工作机制:
1、判断线程上下文:
如果调用 runInLoop 的线程是当前 EventLoop 所属的线程,则直接执行任务。
如果调用线程不是事件循环线程,则将任务添加到任务队列中,等待事件循环线程来执行。
2、任务队列:
EventLoop 内部有一个任务队列(通常是 std::vector<std::function<void()>>),用于存储需要在事件循环线程中执行的任务。
3、唤醒机制:
如果任务是从其他线程添加的,EventLoop 需要被唤醒(通常通过向 wakeupFd 写入数据),以便尽快处理新增的任务。
runInLoop 的优点
线程安全:
确保所有任务都在事件循环线程中执行,避免数据竞争。
高效唤醒:
使用轻量级唤醒机制(如 eventfd 或 pipe)快速响应任务。
任务聚合:
通过任务队列,可以批量处理任务,减少上下文切换。
(1)减少锁的使用----无锁队列
问题:EventLoop 的跨线程操作(如 runInLoop 和 queueInLoop)使用了锁保护。
优化:
使用无锁队列(如基于 lock-free 的 CAS 算法)替代当前的 std::mutex。
在单线程场景下,完全移除锁。
无锁队列(Lock-Free Queue)是一种数据结构,在多线程环境中使用时,不需要依赖传统的互斥锁来同步线程间的访问,而是通过硬件支持的原子操作(如 CAS,Compare-And-Swap)来完成线程安全的操作。无锁队列通常具有更高的性能,因为它避免了锁的开销和可能的线程阻塞。
实现思路
使用 CAS 操作:
CAS(ptr, old, new):如果 *ptr == old,则将 *ptr 更新为 new,否则不更新,并返回是否成功。
记录队列头和尾:
使用原子变量指向队列的头部和尾部。
生产者操作(Enqueue):
找到当前尾节点并尝试将新节点插入到尾部。
消费者操作(Dequeue):
找到当前头节点并尝试移除它。
(2)优化定时器处理----时间轮代替最小堆
问题:EventLoop 的定时器使用了最小堆存储,复杂度为 O(log N),当定时任务量非常多时可能会产生性能瓶颈。
优化:
使用分层时间轮(TimerWheel)替代最小堆,降低复杂度到 O(1),尤其适合高频定时任务场景。
使用智能指针管理定时任务
在 TimerWheel 中使用 shared_ptr 和 weak_ptr 来管理定时任务是一种常见的设计,主要目的是解决 内存管理 和 资源生命周期控制 的问题。
shared_ptr 和 weak_ptr 的基本概念
shared_ptr:
一个智能指针,提供共享所有权。
当最后一个 shared_ptr 被销毁时,所管理的对象会自动释放。
使用 use_count() 方法可以查看当前有多少个 shared_ptr 在共享同一对象。
weak_ptr:
一个不影响引用计数的智能指针。
只能通过 lock() 方法访问所管理的对象。
当所指向的对象被销毁时,weak_ptr 会变为无效(即 expired() 返回 true)。
为什么 TimerWheel 需要 weak_ptr?
在 TimerWheel 中,一个定时任务可能需要被多个地方引用,例如:
TimerWheel 的槽位:每个槽位可能存储一组任务(通常是 shared_ptr < TimerTask > )。
用户代码:用户可能直接持有某个定时任务的引用,以便随时取消或修改任务。
如果只使用 shared_ptr,会导致循环引用的问题。例如:
定时任务本身持有引用,而 TimerWheel 的槽位又持有定时任务的 shared_ptr。
这种情况下,shared_ptr 的引用计数永远不会降为 0,导致内存泄漏。
为了解决这种问题,使用 weak_ptr 来打破循环引用:
TimerWheel 的槽位使用 weak_ptr 存储定时任务。
用户持有的 shared_ptr 决定了任务的生命周期。
weak_ptr 的作用
1.避免循环引用:
如果定时任务被 shared_ptr 引用,但槽位只保留了 weak_ptr,当用户的 shared_ptr 被销毁时,任务会自动释放,避免了内存泄漏。
2.弱引用机制:
TimerWheel 的槽位只需要一个弱引用来跟踪定时任务,而不需要管理其生命周期。
在执行定时任务时,可以通过 weak_ptr::lock() 检查任务是否仍然有效。如果任务已被用户取消或销毁,则无需执行。
3.任务销毁的灵活性:
用户可以随时销毁 shared_ptr,从而取消任务。
同时,TimerWheel 的槽位不会影响任务的生命周期。
三. ThreadPool 模块
ThreadPool
模块用于管理线程池,处理多线程任务。
- 任务窃取(Work Stealing)
- 问题:当前
ThreadPool
使用一个任务队列,可能导致某些线程处于繁忙状态,而其他线程空闲。 - 优化:
- 实现任务窃取机制,每个线程都有独立的任务队列,当线程空闲时可以从其他线程的队列中窃取任务。
- 提高任务分配的公平性和整体吞吐量。
- 问题:当前
四、Acceptor 模块
Acceptor
模块负责监听新连接并分发给 TcpConnection
。
- 多线程负载均衡
- 问题:
Acceptor
默认将新连接分配给单个线程,可能导致线程负载不均。 - 优化:
- 实现动态负载均衡算法(如基于线程负载或连接数),合理分配新连接。
- 为了在 Muduo 网络库的 Acceptor 模块中实现动态负载均衡算法(如基于线程负载或连接数的分配),需要在接受新连接时,动态地将连接分配给负载最轻的线程或事件循环(
EventLoop
)。
- 问题:
以下是实现动态负载均衡的设计方案和步骤:
- 动态分配:
- 当
Acceptor
接收到一个新的连接时,根据每个线程或EventLoop
的当前负载(如连接数或任务队列长度),将连接分配给负载最轻的线程。
- 当
- 动态监控:
- 持续跟踪每个线程的负载情况,确保负载均衡。
- 高效分发:
- 分配逻辑应尽量轻量化,避免增加额外的系统开销。
2. 关键组件
-
线程池或
EventLoopThreadPool
:- 管理多个
EventLoop
线程,每个线程处理一定数量的连接。 - 提供接口获取每个线程的负载信息。
- 管理多个
-
负载监控机制:
- 跟踪每个线程或
EventLoop
的当前负载(如连接数、任务队列长度)。 - 负载信息可以通过计数器或定时统计更新。
- 跟踪每个线程或
-
负载均衡算法:
- 基于负载信息动态选择最优的线程。
- 典型算法包括:
- 最少连接数优先:将新连接分配给连接数最少的线程。
- 任务队列长度优先:将新连接分配给任务队列最短的线程。
- 加权随机分配:根据线程的负载权重随机分配。
-
Acceptor 模块的改造:
- 在接受新连接时调用负载均衡器,分配连接到合适的线程。
3. 实现步骤
3.1. 定义负载均衡器
创建一个负载均衡器类,负责跟踪线程的负载信息并选择合适的线程。
#include <vector>
#include <memory>
#include <mutex>
#include <functional>class EventLoop; // 前向声明class LoadBalancer {
public:LoadBalancer() = default;// 添加一个线程的负载监控void addEventLoop(EventLoop* loop) {std::lock_guard<std::mutex> lock(mutex_);eventLoops_.emplace_back(loop, 0); // 初始负载为 0}// 更新线程的负载(比如连接数变化)void updateLoad(EventLoop* loop, int delta) {std::lock_guard<std::mutex> lock(mutex_);for (auto& [eventLoop, load] : eventLoops_) {if (eventLoop == loop) {load += delta;break;}}}// 获取负载最轻的线程EventLoop* getLeastLoadedEventLoop() {std::lock_guard<std::mutex> lock(mutex_);EventLoop* bestLoop = nullptr;int minLoad = INT_MAX;for (const auto& [eventLoop, load] : eventLoops_) {if (load < minLoad) {bestLoop = eventLoop;minLoad = load;}}return bestLoop;}private:std::vector<std::pair<EventLoop*, int>> eventLoops_; // 每个线程及其负载std::mutex mutex_; // 保护线程安全
};
3.2. 改造 Acceptor
模块
修改 Acceptor
,在接收到新连接时调用负载均衡器,选择最优线程。
#include "LoadBalancer.h"
#include "EventLoop.h"
#include <functional>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>class Acceptor {
public:Acceptor(EventLoop* baseLoop, int port, LoadBalancer& loadBalancer): baseLoop_(baseLoop), loadBalancer_(loadBalancer) {// 创建监听套接字listenFd_ = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);// 绑定地址和端口sockaddr_in addr{};addr.sin_family = AF_INET;addr.sin_addr.s_addr = INADDR_ANY;addr.sin_port = htons(port);::bind(listenFd_, (sockaddr*)&addr, sizeof(addr));// 开始监听::listen(listenFd_, SOMAXCONN);}~Acceptor() {::close(listenFd_);}// 开始接受连接void acceptConnections() {while (true) {sockaddr_in clientAddr{};socklen_t clientLen = sizeof(clientAddr);int connFd = ::accept4(listenFd_, (sockaddr*)&clientAddr, &clientLen, SOCK_NONBLOCK);if (connFd < 0) {if (errno == EAGAIN || errno == EWOULDBLOCK) {break; // 无更多连接} else {std::cerr << "Accept error: " << strerror(errno) << std::endl;break;}}// 使用负载均衡器选择最优线程EventLoop* targetLoop = loadBalancer_.getLeastLoadedEventLoop();if (targetLoop) {// 将新连接分配给目标线程targetLoop->runInLoop([connFd, targetLoop]() {targetLoop->addConnection(connFd);});// 更新负载信息loadBalancer_.updateLoad(targetLoop, 1);} else {std::cerr << "No available EventLoop to handle connection" << std::endl;::close(connFd);}}}private:EventLoop* baseLoop_; // 主线程的事件循环int listenFd_; // 监听套接字LoadBalancer& loadBalancer_; // 负载均衡器
};
3.3. 改造 EventLoop
模块
扩展 EventLoop
,支持添加和处理新连接。
#include <functional>
#include <vector>
#include <mutex>
#include <unistd.h>
#include <iostream>class EventLoop {
public:EventLoop() = default;void runInLoop(const std::function<void()>& task) {// 简化的事件循环任务执行std::lock_guard<std::mutex> lock(mutex_);tasks_.emplace_back(task);}void addConnection(int connFd) {std::cout << "Handling new connection on EventLoop: " << this << std::endl;connections_.emplace_back(connFd);}void processTasks() {std::vector<std::function<void()>> tasksCopy;{std::lock_guard<std::mutex> lock(mutex_);tasksCopy.swap(tasks_);}for (const auto& task : tasksCopy) {task();}}private:std::vector<std::function<void()>> tasks_; // 待处理任务std::mutex mutex_; // 保护任务队列std::vector<int> connections_; // 当前连接列表
};
- 连接限流
- 问题:在高并发场景下,可能会同时接收大量连接,导致系统资源耗尽。
- 优化:
- 设置连接速率限制(如每秒最多接收 100 个连接)。
- 在超过连接限制时,拒绝新连接。
五、定时器模块
TimerQueue 模块用于管理定时任务。
- 分层时间轮
问题:当前使用最小堆存储定时任务,当定时器数量较多时,插入和删除的复杂度较高。
优化:使用分层时间轮(TimerWheel)结构,降低复杂度到 O(1)。
分层时间轮的设计灵感来源于时钟的分层结构。例如:
时钟有秒针、分针、时针,每一层负责处理不同的时间粒度。
当秒针完成一圈(60 秒),会推动分针向前迈进一格。
类似的,分层时间轮将时间划分为多个层级,每一层是一个环形队列,队列中的每个槽(slot)存储对应时间间隔的定时任务。当时间轮的某一层转动一圈后,触发下一层的转动。
亮点:使用智能指针管理任务TimerTask
分层时间轮的结构
1、时间轮层级:
每一层是一个环形数组(类似时钟的轮盘)。
每个槽代表一个固定的时间间隔(时间粒度)。
第一层的时间粒度最小,越高层的时间粒度越大。
2、每个槽的内容:
每个槽存储定时任务的列表。
每个任务需要携带额外的元数据(例如任务到期时间)。
3、层级的关系:
第一层负责管理最小时间粒度的任务。
如果某个任务的时间超过当前层的最大时间范围,则被推进到下一级时间轮。
分层时间轮的工作原理
插入任务
根据任务的到期时间,计算任务所在的时间轮层级和槽位:
槽位索引 = ( 到期时间 , / , 时间粒度 ) ,
如果任务的到期时间超过当前时间轮层级的时间范围,则将任务递归推入到更高层的时间轮。
定时器轮转
时间轮以固定的时间间隔轮转一格(类似秒针转动)。
每次轮转到一个槽位时,触发该槽中的任务。
如果某个任务的到期时间未到,则将其重新分配到更高层时间轮的对应槽位。
任务触发
当轮盘转动到任务所在的槽位,并且任务的到期时间与当前时间匹配时,触发任务。
分层时间轮的优势
高性能:
插入、删除、触发的时间复杂度接近 (O(1))。
适用于大规模定时任务的场景(例如实时系统的事件调度)。
灵活性:
支持多层时间轮,适应不同的时间范围和粒度需求。
内存效率:
由于采用环形数组存储任务,内存占用较小。
分层时间轮的应用场景
网络服务器:
TCP 连接的超时管理。
应用于高性能网络库(如 Netty、Muduo 等)。
实时系统:
事件驱动的调度系统。
需要高效管理大量定时任务的场景。
分布式系统:
分布式任务调度(如分布式锁的过期时间管理)。
游戏引擎:
游戏中的倒计时、技能冷却等事件。
分层时间轮的局限性
时间粒度限制:
时间轮的粒度决定了定时器的精度,任务触发可能会有一定的延迟。
任务分层复杂性:
跨层任务需要递归推进,可能增加一定的实现复杂性。
非实时性:
高层时间轮的任务可能需要等待低层时间轮转动完成,导致延迟。
- 批量定时器
问题:大量定时任务可能触发频繁的上下文切换。
优化:
合并多个定时器事件,使用批量处理逻辑减少系统调用。
六、日志模块
Logging
模块是 Muduo 的日志系统。
-
异步日志优化
- 问题:当前异步日志可能会阻塞高优先级任务的处理。
- 优化:
- 使用双缓冲区实现异步日志,减少阻塞。
- 提供日志压缩功能,减少磁盘 I/O 开销。
-
日志分级
- 问题:日志系统不支持动态调整日志级别。
- 优化:
- 支持按模块或线程动态调整日志级别,提高调试效率。
七、 错误处理与监控
Muduo 缺乏对错误和性能的全面监控。
层级式异常管理
在 Muduo 库的基础上设计一个异常机制,可以通过引入基于 C++ 多态 的异常层次结构来分类和处理不同类型的异常。以下是具体的设计思路:
1. 设计原则
- 异常分层:
- 定义一个基类
MuduoException
,所有具体异常类型都从该基类派生。 - 使用 C++ 多态(基类引用捕获派生类异常)来实现统一的异常处理逻辑。
- 定义一个基类
- 异常分类:
- 根据 Muduo 的核心模块(如
EventLoop
,TcpConnection
,TimerQueue
等),定义具体的异常类型。例如:EventLoopException
:处理事件循环相关的异常。TcpConnectionException
:处理 TCP 连接相关的异常。TimerQueueException
:处理定时器相关的异常。
- 根据 Muduo 的核心模块(如
- 异常捕获:
- 在全局或模块级别捕获
MuduoException
类型的异常,并为不同的派生类提供具体的处理逻辑。
- 在全局或模块级别捕获
- 日志和反馈:
- 捕获异常后,记录日志或提供反馈信息,方便调试和问题追踪。
2. 实现步骤
2.1. 定义异常基类
创建一个通用的异常基类 MuduoException
,它继承自 std::exception
,并提供基本的异常信息接口。
#include <exception>
#include <string>class MuduoException : public std::exception {
public:explicit MuduoException(const std::string& message): message_(message) {}// 返回异常信息virtual const char* what() const noexcept override {return message_.c_str();}// 提供异常类型的标识virtual const char* type() const noexcept {return "MuduoException";}protected:std::string message_;
};
2.2. 定义派生异常类
为 Muduo 的核心模块定义具体的异常类型,这些类从 MuduoException
派生。
#include "MuduoException.h"// 事件循环相关异常
class EventLoopException : public MuduoException {
public:explicit EventLoopException(const std::string& message): MuduoException(message) {}virtual const char* type() const noexcept override {return "EventLoopException";}
};// TCP 连接相关异常
class TcpConnectionException : public MuduoException {
public:explicit TcpConnectionException(const std::string& message): MuduoException(message) {}virtual const char* type() const noexcept override {return "TcpConnectionException";}
};// 定时器相关异常
class TimerQueueException : public MuduoException {
public:explicit TimerQueueException(const std::string& message): MuduoException(message) {}virtual const char* type() const noexcept override {return "TimerQueueException";}
};
2.3. 在核心模块中抛出异常
在 Muduo 的核心模块中,当检测到错误情况时,抛出对应的异常。
示例:在 EventLoop
模块中抛出异常
#include "DerivedExceptions.h"
#include <iostream>class EventLoop {
public:void loop() {try {// 模拟事件循环错误throw EventLoopException("Event loop encountered an error!");} catch (const MuduoException& e) {handleException(e);}}private:void handleException(const MuduoException& e) {// 根据异常类型进行处理if (std::string(e.type()) == "EventLoopException") {std::cerr << "[EventLoop Error] " << e.what() << std::endl;// 执行特定的恢复逻辑} else {std::cerr << "[Unknown Error] " << e.what() << std::endl;}}
};
2.4. 全局捕获异常
可以在应用的入口函数中统一捕获所有的 MuduoException
类型异常。
#include "EventLoop.cpp"int main() {try {EventLoop loop;loop.loop();} catch (const MuduoException& e) {std::cerr << "Caught a MuduoException: " << e.type() << " - " << e.what() << std::endl;} catch (const std::exception& e) {std::cerr << "Caught a std::exception: " << e.what() << std::endl;} catch (...) {std::cerr << "Caught an unknown exception" << std::endl;}return 0;
}
3. 优化与扩展
细化异常信息
- 为每个异常类型添加更多上下文信息(如错误码、模块名称、操作步骤等)。
- 示例:
explicit TcpConnectionException(const std::string& message, int errorCode): MuduoException(message), errorCode_(errorCode) {}int errorCode() const { return errorCode_; }
结合日志记录
- 在捕获异常后,将异常信息写入日志,方便后续排查问题。
- 示例:
void logException(const MuduoException& e) {// 写入日志std::ofstream logFile("error.log", std::ios::app);logFile << "[" << e.type() << "] " << e.what() << std::endl; }
异常恢复机制
- 根据异常类型,尝试执行不同的恢复策略:
- 重启事件循环。
- 关闭并重建 TCP 连接。
- 重新注册定时器。
支持自定义异常类型
- 提供一个工厂函数或宏,方便用户定义新的异常类型。
#define DEFINE_EXCEPTION(name, base) \
class name : public base { \
public: \explicit name(const std::string& message) \: base(message) {} \virtual const char* type() const noexcept { \return #name; \} \
};
使用示例:
DEFINE_EXCEPTION(CustomException, MuduoException)
- 基于多态的异常机制:
- 使用基类
MuduoException
统一管理异常,派生类提供具体的异常类型。
- 使用基类
- 模块化异常分类:
- 根据 Muduo 的核心模块设计派生异常类,例如
EventLoopException
、TcpConnectionException
。
- 根据 Muduo 的核心模块设计派生异常类,例如
- 日志和恢复:
- 捕获异常时记录日志,并根据异常类型执行恢复策略。
- 扩展性和灵活性:
- 通过工厂函数或宏支持用户自定义异常类型。
通过这种设计,可以在 Muduo 的基础上实现一个灵活、可扩展的异常机制,既满足了错误检测的需求,又增强了代码的可维护性和健壮性。
性能监控
- 增加性能监控接口,统计每个模块的延迟、吞吐量和错误率。
等待进一步更新与更正~
相关文章:
[项目深挖]仿muduo库的并发服务器的解析与优化方案
标题:[项目深挖]仿muduo库的并发服务器的优化方案 水墨不写bug 文章目录 一、buffer 模块(1)线性缓冲区直接扩容---->环形缓冲区定时扩容(只会扩容一次)(2)使用双缓冲(Double Buf…...
(独家)SAP CO模块中 销售发票对应的Cost Document中的PSG对象是什么东东??
背景: 在销售发票生成的凭证中,控制凭证有两个字段:对象类型、对应编码;那这个PSG到底是什么东东?网上一直没人解释,可能没人研究过这个问题。 官方解释: 按我的理解,PSG profile …...
流程编辑器Bpmn与LogicFlow学习
工作流技术如何与用户交互结合(如动态表单、任务分配)处理过 XML 与 JSON 的转换自定义过 bpmn.js 的样式(如修改节点颜色、形状、图标)扩展过上下文菜单(Palette)或属性面板(Properties Panel&…...
群晖NAS部署PlaylistDL音乐下载器结合cpolar搭建私有云音乐库
文章目录 前言1.关于PlaylistDL音乐下载器2.Docker部署3.PlaylistDL简单使用4.群晖安装Cpolar工具5.创建PlaylistDL音乐下载器的公网地址6.配置固定公网地址总结 前言 各位小伙伴们,你们是不是经常为了听几首歌而开通各种平台的VIP?或者为了下载无损音质…...
Unity光照笔记
问题 在做项目中遇到了播放中切换场景后地面阴影是纯黑的问题,不得不研究一下光照。先放出官方文档。 Lighting 窗口 - Unity 手册 播放中切换场景后地面阴影是纯黑 只有投到地面的阴影是纯黑的。且跳转到使用相同Terrain的场景没有问题。 相关文章:…...
【ROS2】编译Qt实现的库,然后链接该库时,报错:/usr/bin/ld: XXX undefined reference to `vtable for
1、问题描述 在ROS2工程中,编译使用Qt实现的库,在其它ROS2包链接该库时,报错: /usr/bin/ld: XXX undefined reference to `vtable for2、原因分析 查看链接失败的几个函数接口都是,信号函数(signals 标记的函数)。因为信号函数都只有定义,没有实现,在执行ROS2 colc…...
deepseek讲解如何快速解决内存泄露,内存溢出问题
Java内存泄漏与内存溢出解决方案及预防措施 作为Java架构师,处理内存泄漏和内存溢出问题需要系统性的方法。以下是一份完整的解决方案和预防建议: 一、问题诊断阶段 1. 确认内存泄漏现象 监控GC日志,观察老年代使用率是否持续增长使用jst…...
双系统重装ubuntu
双系统ubuntu20.04重装(详细版)_ubuntu20.04安装教程-CSDN博客...
图形语言中间层:重构 AI 编程的未来之路
在软件开发的历史长河中,每一次技术革新都伴随着对效率与可控性的重新定义。当 ChatGPT、GitHub Copilot 等 AI 工具以自然语言生成代码的惊艳表现叩响编程世界的大门时,人们曾满怀憧憬地期待一个 “无代码” 的黄金时代 —— 只需用日常语言描述需求&am…...
Ubuntu操作合集
UFWUncomplicated Firewall 查看状态和规则: 1查看状态sudo ufw status, 2查看详细信息sudo ufw status verbose, 默认策略配置: 1拒绝所有入站sudo ufw default deny incoming 2允许所有出战sudo ufw default allow outgoing …...
张量与Python标量:核心区别与计算图断开解析
张量与Python标量的核心区别 张量(Tensor) 是PyTorch中的核心数据结构,类似于多维数组: 支持GPU加速计算跟踪计算历史(用于自动求导)可以包含多个元素Python标量(int/float) 是普通的Python数值类型: 不支持GPU加速没有计算历史记录单个独立数值计算图断开的原因 Py…...
U9C与钉钉审批流对接完整过程
U9C 功能强大,然而在移动办公和审批流方面存在一定不足。为了弥补这一缺陷,不少企业在使用 U9C 的同时,会选择开通钉钉这类 OA 管理系统。不过,两套系统并行使用时,数据同步问题便随之而来。目前,常见的做法…...
双重差分模型学习笔记4(理论)
【DID最全总结】90分钟带你速通双重差分!_哔哩哔哩_bilibili 目录 总结:双重差分法(DID)在社会科学中的应用:理论、发展与前沿分析 一、DID的基本原理与核心思想 二、经典DID:标准模型与应用案例 三、…...
【Pandas】pandas DataFrame diff
Pandas2.2 DataFrame Computations descriptive stats 方法描述DataFrame.abs()用于返回 DataFrame 中每个元素的绝对值DataFrame.all([axis, bool_only, skipna])用于判断 DataFrame 中是否所有元素在指定轴上都为 TrueDataFrame.any(*[, axis, bool_only, skipna])用于判断…...
什么是Agentic AI(代理型人工智能)?
什么是Agentic AI(代理型人工智能)? 一、概述 Agentic AI(代理型人工智能)是一类具备自主决策、目标导向性与持续行动能力的人工智能系统。与传统AI系统依赖外部输入和显式命令不同,Agentic AI在设定目标…...
记录算法笔记(2025.5.15)二叉树的层序遍历
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。 示例 1: 输入:root [3,9,20,null,null,15,7] 输出:[[3],[9,20],[15,7]] 示例 2: 输入…...
2025 Java 微信小程序根据code获取openid,二次code获取手机号【工具类】拿来就用
一、controller调用 /*** 登录** author jiaketao* since 2024-04-10*/ RestController RequestMapping("/login") public class LoginController {/*** 【小程序】登录获取session_key和openid** param code 前端传code* return*/GetMapping("/getWXSessionKe…...
2021-10-25 C++三的倍数含五
缘由含数字五且是三的倍数-编程语言-CSDN问答 void 三的倍数含五() {//缘由https://ask.csdn.net/questions/7544132?spm1005.2025.3001.5141int a 3, aa a;while (a < 10000){if (aa)if (aa % 10 5)std::cout << a << std::ends, aa a 3; else aa / 10;…...
编程日志5.8
二叉树练习题 1.965. 单值二叉树 - 力扣(LeetCode) /** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) :…...
Vue.js---避免无限递归循环 调度执行
4.4 避免无限递归循环 什么情况下会无限递归? 01 const data { foo: 1 } 02 const obj new Proxy(data, { /*...*/ }) 03 04 effect(() > obj.foo)例如这种情况,它会反复设置添加一直到栈溢出 首先读取obj.foo 的值,这会触发 track 操…...
AI大模型学习二十四、实践QEMU-KVM 虚拟化:ubuntu server 25.04 下云镜像创建Ubuntu 虚拟机
一、说明 虽然说大部分的场合,docker都能解决问题,但是有些大型的软件安装时如果修改配置会很麻烦,比方说前面遇到的code-server和dify 默认都是80和443端口要使用,安装在一起就会端口冲突,通过该端口来解决问题&#…...
Lovart:首个AI设计智能体
今天介绍一款AI设计智能体——Lovart,能调用各种绘画API和视频API,也能调用LibLib上的Flux和LoRA,并且智能体的编排效果确实很好,产出效果比豆包和ChatGPT都好,可以说没有竞品。视频为效果演示,官网有更多案…...
Trae 插件 Builder 模式:从 0 到 1 开发天气查询小程序,解锁 AI 编程新体验
在软件开发领域,效率与创新始终是开发者追求的核心目标。Trae 插件(原 MarsCode 编程助手)Builder 模式的全面上线,无疑为开发者带来了全新的解决方案。它不仅同时支持 VS Code、JetBrains IDEs 等主流开发环境,还能让…...
解决ubuntu20中tracker占用过多cpu,引起的风扇狂转
track是linux中的文件索引工具,ubuntu18之前是默认不安装的,所以在升级到20后会默认安装,它是和桌面程序gnome绑定的,甚至还有很多依赖项,导致无法删除,一旦删除很多依赖项都不能运行,禁用也很难…...
解码生命语言:深度学习模型TranslationAI揭示RNA翻译新规则
RNA翻译是基因表达的核心环节,其精确调控依赖于翻译起始位点(TIS)和终止位点(TTS)的准确识别。传统方法依赖于简单的经验规则(如Kozak序列或最长开放阅读框ORF),但忽略了RNA结构、顺…...
20250515测试飞凌的OK3588-C的核心板在Linux R4下适配以太网RTL8211F-CG时跑iperf3的极速
20250515测试飞凌的OK3588-C的核心板在Linux R4下适配以太网RTL8211F-CG时跑iperf3的极速 2025/5/15 14:47 缘起:让飞凌的OK3588-C的核心板在Linux R4下,想看看以太网RTL8211F-CG的极速。 于是在飞凌的OK3588-C的核心板上,iperf3的收发一起跑…...
在Linux内安装虚拟机安装vmnet.tar 报错
编译报错如下: /usr/lib/vmware/modules/source/vmnet-only/userif.c: 在函数‘VNetCsumCopyDatagram’中: /usr/lib/vmware/modules/source/vmnet-only/userif.c:88:39: 错误:‘skb_frag_t {或称 const struct bio_vec}’ has no member named ‘page_offset’; di…...
CodeBuddy编程新范式
不会写?不想写? 腾讯推出的CodeBuddy彻底解放双手。 示例 以下是我对CodeBuddy的一个小体验。 我只用一行文字对CodeBuddy说明了一下我的需求,剩下的全部就交给了CodeBuddy,我需要做的就是验收结果即可。 1.首先CodeBuddy会对任…...
ESP32简介及相关使用
乐鑫官网: 无线通信 SoC、软件、云和 AIoT 方案|乐鑫科技 (espressif.com) 简介 ESP32 是由 乐鑫科技(Espressif Systems) 推出的一款高性能、低功耗的 Wi-Fi & 蓝牙双模物联网(IoT)芯片,广…...
全志F10c200开发笔记——移植uboot
相关资料: (二)uboot移植--从零开始自制linux掌上电脑(F1C200S)<嵌入式项目>-CSDN博客 F1C200S挖坑日记(3)——Uboot编译篇_f1c200s uboot-CSDN博客 一、安装编译器 Linaro Rele…...
解密企业级大模型智能体Agentic AI 关键技术:MCP、A2A、Reasoning LLMs- Manus解密
解密企业级大模型智能体Agentic AI 关键技术:MCP、A2A、Reasoning LLMs- Manus解密 那你当前这个步骤执行完成之后,这边说了一个非常重要的点?每次迭代只选择一个工具,这个可能对大家感觉有点反直觉,可能大家立即选择分…...
理解c++中关键字友元friend的作用
理解c中关键字友元friend的作用 friend 关键字在 C 中用于声明一个函数或类为另一个类的友元。 友元函数或友元类可以访问该类的私有(private)和保护(protected)成员。 友元函数 作用: 允许非成员函数访问私有成员&…...
【学习心得】2025年Docker Desktop安装记录
1、docker的官方网站,已进入就可以看到下载按钮,无脑点击下载!英特尔的CPU所以选择AMD64 2、双击安装,默认的勾选不用改 Docker Desktop 4.40.0安装过程中的配置选项窗口 Use WSL 2 instead of Hyper-V (recommended)(…...
数据结构——例题2
1.在线性表中,除了开始元素外,每个元素(A) A.只有唯一的前驱元素 B.只有唯一的后继元素 C.有多个前驱元素 D.有多个后继元素 2.在一个长度为n的顺序表中删除第i个元素(1<i<n)时,需向前…...
python开发api平台雏形
api平台雏形 一、Django基本配置 1.1使用pycherm创建项目 1.2 运行项目 1.3 创建app python.exe .\manage.py startapp cmdb1.4 settings.py添加app 1.5 settings.py设置数据库 DATABASES {default: {ENGINE: django.db.backends.mysql,NAME: devopsapi,USER: root,PASSWO…...
Android Development Roadmap
🔧 Android Development Roadmap (Practical First → Theory Later) Here’s a lean, real-world roadmap tailored to the mindset — build-first, theory-when-needed: 🟢 Stage 1: Core Setup & Workflow (Done ✅) ✅ Install Android Studio…...
将.pt文件执行图像比对
目录 1. 加载模型 2. 图像预处理 3. 提取图像特征 4. 计算相似度 调用API或封装函数即可实现端到端比对 使用.pt文件进行图像比对通常涉及以下步骤: 1. 加载模型 python import torch# 假设模型是PyTorch保存的权重文件 model YourModelClass() # 需与保存时…...
西门子S7-1200 MC卡使用方法及故障现象分析
一、S7 1200 MC卡 S7 1200系列PLC使用的存储卡为SD卡,也被称为MC卡 ,支持4M、12M、24M、256M、2G以及32G等不同容量规格。它可作为程序卡、传送卡,还能用于更新硬件及解除密码,在S7 1200的系统运行和维护中扮演着重要角色。 二…...
5.重建大师数据管理模块介绍
摘要:本文主要介绍重建大师数据管理模块,包含:照片、点云数据可视化管理工具。 数据管理界面主要包含工具栏、可视化界面和照片组列表三部分。 图 数据管理界面 1.工具栏 工具栏包含以下功能按钮,包含添加照片、视频、点云、控制…...
BUUCTF——shrine
BUUCTF——shrine 进入靶场 只有一串代码 import flask import os app flask.Flask(__name__) app.config[FLAG] os.environ.pop(FLAG) #程序从环境变量 FLAG 读取一个敏感值,并存储在 app.config[FLAG] 中。 #安全问题:如果攻击者能访问 app.con…...
基于FPGA的车速检测系统仿真设计与实现
标题:基于FPGA的车速检测系统仿真设计与实现 内容:1.摘要 本文旨在设计并实现基于FPGA的车速检测系统仿真。随着汽车行业的快速发展,精确的车速检测对于车辆的安全性和性能评估至关重要。本研究采用FPGA作为核心处理单元,结合传感器数据采集与处理技术进…...
STM32 DMA技术深度解析:从原理到实战应用讲解
知识点1【DMA的介绍】 直接内存访问(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输,无需CPU的干预,数据可以通过DMA快速地移动,这就节省了CPU的资源,来做其他的操作——提高CPU的效…...
模仿学习笔记
模仿学习总共分两类: 行为克隆:BC,Dagger逆强化学习:又分为 2.1基于最大边际逆强化学习 (无法主要歧义问题):学徒学习 2.2 基于最大熵逆强化学习 (主要解决歧义问题):GAIL 学徒学习 基于最大熵…...
论文学习_Directed Greybox Fuzzing
摘要:现有的灰盒模糊测试工具(Greybox Fuzzers,简称GF)在测试引导性方面存在明显不足,比如难以有效地将测试引导至特定的高风险变更或补丁、关键系统调用、危险代码位置,或是试图重现漏洞时涉及的堆栈追踪中…...
《MySQL:MySQL视图特性》
视图是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含一系列带有名称的列和行数据。视图的数据变化会影响基表,基表的数据变化也会影响视图。 创建视图 create view 视图名 as select语句; 删除视图 drop view 视图名; 视图规则与限…...
Flutter——数据库Drift开发详细教程(六)
目录 1.视图2.视图中列的可空性3.DAO4.流查询5.高级用途6.注意事项 1.视图 也可以将SQL 视图定义 为 Dart 类。为此,请编写一个抽象类来扩展View。此示例声明了一个视图,用于读取示例中架构中某个类别中添加的待办事项数量: abstract class C…...
PCIe数据采集系统
PCIe数据采集系统 一、模块功能划分与职责 1. 时钟管理模块 (clock_manager) 核心功能: 生成系统所需的多时钟信号(100MHz 系统时钟、125MHz PCIe 时钟、200MHz DDR3 时钟)。 关键接口: 输入:系统主时钟 sys_clk、P…...
mac docker弹窗提示Docker 启动没有响应
一、原因分析 这台笔记电脑是Mac M3操作系统,安装Docker之后,Docker应用程序一直启动不起来。 二、解决办法 sudo rm /Library/PrivilegedHelperTools/com.docker.vmnetd sudo cp /Applications/Docker.app/Contents/Library/LaunchServices/com.docker.vmnetd /Library/Pri…...
TVS管用万用表测量方法详解(含二极管档使用指南)
点击下面图片带您领略全新的嵌入式学习路线 🔥爆款热榜 88万阅读 1.6万收藏 TVS管(瞬态抑制二极管)是一种用于保护电路免受瞬态高电压冲击的半导体器件。其核心功能是通过快速导通将过压钳位在安全范围内。本文将重点介绍如何用万用表测量…...
当下流行的智能体通信协议:MCP、A2A、ANP 分别是什么?
在当前人工智能(AI)智能体生态系统中,智能体之间的有效沟通至关重要。为了让AI智能体能够高效、安全地协同工作,业界提出了多种通信协议。其中,MCP、A2A 和 ANP 代表了三个关键层级的通信协议,各自应对不同…...