基于Reactor的文件管理服务开发笔记

第一次提交:搭建框架

1.CMake 的模块化构建设计

CMake 负责"读你的构建规则 → 生成 Makefile"

Make 负责"读 Makefile → 调用 g++ 编译 + 链接"

复制代码
cmake ..    # "照我的 CMakeLists.txt,生成这个项目的构建脚本"
make        # "执行构建脚本,编译全部文件 + 链接成可执行程序"

两级cmake分工

根目录:CMakeLists.txtC++ 项目的构建配置文件,它的作用是告诉 CMake 工具如何编译和链接你的项目。

比如说我的项目就是告诉编译器,我的项目名,用C++11标准编译,提供了两种模式,DEBUG,RELEASE,我的头文件在根目录,要链接的库,哪些源文件需要进行编译,要不要优化代码?优化就是release模式,删除了调试信息,不优化就是debug版本。

子目录:每个模块的 CMakeLists.txt 只声明自己的编译规则自己依赖谁。如果全部写在根文件里,当模块数量从现在的 10 个膨胀到 50 个时,根文件会变成上千行的巨无霸,谁都改不动。

对比

文件 一句话职责
CMakeLists.txt 全局配置 + 声明"有哪些子模块"
common/CMakeLists.txt 编译 libcommon.a(logger + 公共类型)
threadpool/CMakeLists.txt 编译 libthreadpool.a,链接 common
protocol/CMakeLists.txt 编译 libprotocol.a(目前只有头文件占位)
test/CMakeLists.txt 编译可执行文件 fileserver_test,链接全部库
... 其他 6 个 同上,预留的模块骨架

2.第一次的测试

<>代表去系统路径找

""代表先在项目目录找

日志测试

复制代码
// 测试日志级别过滤
    //把日志级别调到 kWarning。Logger::Instance() 获取日志单例——全局只有一个 Logger 对象,不管在哪里调用拿到的都是同一个
    //级别从低到高:DEBUG → INFO → WARNING → ERROR → FATAL
    Logger::Instance().SetLevel(LogLevel::kWarning);
    LOG_DEBUG("这条调试信息不应该出现");
    LOG_WARNING("这条警告信息应该出现");
    LOG_ERROR("这条错误信息应该出现");
复制代码
std::cout << "\n=== 测试: 日志模块 ===\n" << std::endl;
​
    LOG_DEBUG("这是一条调试信息");//宏,预处理会把LOG_DEBUG替换为Logger::Instance().Log(LogLevel::kDebug, "test/main.cpp", 16, "TestLogger", "这是一条调试信息");
    LOG_INFO("这是一条普通信息");
    LOG_WARNING("这是一条警告信息");
    LOG_ERROR("这是一条错误信息");

线程池测试

复制代码
// 创建一个包含 4 个工作线程的线程池
    ThreadPool pool(4);
    std::cout << "工作线程数: " << pool.WorkerCount() << std::endl;
​
    // 提交几个简单任务
    auto future1 = pool.Submit([]() -> int {
        LOG_INFO("任务1 在工作线程上运行");// 这里的 LOG_INFO 宏会把日志输出到控制台,显示任务1在工作线程上运行
        return 100;
    });
​
    auto future2 = pool.Submit([](int a, int b) -> int {
        LOG_INFO("任务2 在工作线程上运行: " + std::to_string(a) + " + " + std::to_string(b));
        return a + b;
    }, 3, 7);
​
    auto future3 = pool.Submit([]() -> std::string {
        LOG_INFO("任务3 在工作线程上运行");
        return "hello from threadpool";
    });
​
    // 等待并验证结果
    assert(future1.get() == 100);
    assert(future2.get() == 10);
    assert(future3.get() == "hello from threadpool");

Submit 内部做的事:

  1. std::packaged_task 把 lambda 包装成可调用对象

  2. 加互斥锁,把任务塞进 tasks_ 队列

  3. cond_.notify_one() --- 唤醒一个正在 wait 的工作线程

  4. 返回 future

复制代码
 // 批量提交任务,测试并发执行
    const int kNumTasks = 20;
    std::vector<std::future<int>> futures;
    for (int i = 0; i < kNumTasks; ++i) {
        futures.push_back(pool.Submit([i]() -> int {
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            return i * i;
        }));
    }
    
for (int i = 0; i < kNumTasks; ++i) {
        assert(futures[i].get() == i * i);//// 这里的 get() 会阻塞,直到对应的任务完成并返回结果
    }

循环 20 次,每次 Submit 一个任务。关键细节:

  • [i] --- lambda 的捕获列表里写了 i,表示按值捕获 ,把循环变量 i 的值复制一份给 lambda 内部使用。如果不捕获,lambda 里面访问不到外面定义的 i

  • sleep_for --- 每个任务睡 10 毫秒,模拟真实业务中的耗时操作(如数据库查询)

  • 4 个线程并发处理 20 个任务 → 每个线程大约处理 5 个

  • Submit 之后不等待,直接进入下一次循环 → 20 个任务几乎同时入队

错误码测试

复制代码
auto printCode = [](ErrorCode code, const char* name) {
        std::cout << "  " << name << " = " << static_cast<int32_t>(code) << std::endl;
    };
    
printCode(ErrorCode::kOk,          "kOk         (成功)");
    printCode(ErrorCode::kUnknown,     "kUnknown    (未知错误)");
    printCode(ErrorCode::kAuthFailed,  "kAuthFailed (认证失败)");
    printCode(ErrorCode::kNotFound,    "kNotFound   (未找到)");

复用lambda

static_cast<int32_t>(code):是 C++ 的类型转换------枚举值不能直接当整数打印,需要显式转换。因为我们需要打印整数。

第二次提交:构建网络服务层

线程模型:搭建了主从 Reactor 架构

基于 muduo 实现了一个高并发 TCP 服务器

主线程进行事件循环,只做连接,有连接了分发连接到子线程(4个),每个子线程里面有也事件循环机制,监听socket,有任务了,调用合适的用户回调函数解析请求(读/写),再调用自定义回调函数,处理业务逻辑

代码分层:

  • 入口:启动事件循环,创建 Server(muduo服务器对象),进入事件循环

  • 封装:包 muduo TcpServer,设回调(OnMessage:拿到已经读好的 Buffer,以后去做业务处理),管理连接

  • 管理:管理Session (是连接档案,有连接ID,连接弱引用weak_ptr,客户端IP地址端口号,连接的建立时间,最后活跃的时间)增删查,线程安全

  • 会话:存连接信息(ID、地址、时间)

  • 底层:epoll、accept、socket 读写、IO 线程池

客户端发消息处理逻辑

  1. 客户端经过TCP三次握手建立连接,主线程拿到新的socket,通过轮询机制,分发给子线程,

  2. 子线程把socket注册到epoll,

  3. muduo创建tcp连接对象,通过共享智能指针管理对象,把连接信息记录到sessions中进行存储,调用连接回调

  4. 连接管理器对sessions进行管理,(接下来执行自定义的连接回调),返回,epoll继续等待

  5. 此时发送的数据信息到达了内核,数据可读

  6. muduo先调用read,把数据存到缓冲区buffer,再去调用OnMessage

  7. OnMessage:如果你有自定义的逻辑,优先用自定义处理业务,最后发送给客户端,如果没有,执行conn->send("hello") ,把 "hello" 追加到 TcpConnection 的 输出缓冲区

    send 不是直接调 write。它是先把数据放进内部的输出队列排队,等 epoll 说 socket 可写了再真正 write。这样如果网络慢、一次 write 不完,数据不会丢,下次继续写。

    复制代码
    你的代码                muduo 内部               内核
      │                       │                      │
    conn->send("hello")  →  追加到输出缓冲区           │
      │                       │                      │
      │                   epoll 说可写                  │
      │                       │                      │
      │                   write(fd, buf, len)  →  拷贝到发送缓冲区
      │                       │                      │
      │                       │                 网卡取走 → 网络

    所以你调 conn->send("hello") 是非阻塞的------调完立刻返回,不卡住,真正发数据是 epoll 在后台完成的。

  8. muduo 内部调 write() 缓冲区取出数据写入 socket内核发送缓冲区→ 数据发回客户端。返回,epoll继续等待

    复制代码
    缓冲区 [h][e][l][l][o]   ← 数据在这里面
             │
        write(fd, 缓冲区, 5)   ← 从缓冲区取数据,写入 socket
             │
             ▼
        socket 内核发送缓冲区 → 网卡 → 客户端
    ​
  9. 客户端收到数据,关闭客户端连接

  10. epoll检测到socket关闭,调用函数,关闭这个连接,遍历sessions删除这条连接信息,(再执行自定义断开连接回调)

  11. 释放连接,执行析构,删除连接对象。

muduo 负责:accept、epoll、read/write、创建/销毁 TcpConnection 你的代码负责:记录 Session、管理连接、决定收到数据后做什么

第二次测试

先开启服务端进行阻塞监听

开启客户端,并发连接100个客户端

结果

复制代码
============================================
  FileServer - 客户端并发测试
  目标: 127.0.0.1:8080
  并发客户端数: 100
============================================
​
​
============================================
  测试结果
============================================
  连接成功: 100/100
  连接失败: 0
  发送成功: 100/100
  发送失败: 0
  接收成功: 100/100
  接收失败: 0
  耗时: 50 ms
============================================
  全部测试通过!
============================================

这验证了主从 Reactor 模型的优势------4 个 Sub Reactor 线程通过 epoll 并发处理 100 个连接的 IO 事件,没有线程阻塞等待。如果换成单线程阻塞模型,100 个连接串行处理至少要几秒。

为什么没有线程阻塞等待?

操作系统:epoll层

调用epoll_wait(),一个线程等待100个socket的事件,他不会阻塞等待某一 个socket,而是谁就绪了处理谁

读和写操作不会阻塞,epoll_wait 会阻塞

多线程:有四个子线程。分摊100个连接,四个线程并行处理

相关推荐
疯狂打码的少年2 小时前
【操作系统】板块总结 + 下期预告(软件工程)
笔记·软件工程
李小小钦2 小时前
D. Storming Arasaka(Codeforces 2238)
c语言·开发语言·数据结构·c++·算法
卷无止境2 小时前
SFML 深度解读:一个教科书级 C++ 多媒体库的内功心法
c++·后端
咸甜适中3 小时前
rust语言学习笔记——Tokio库(异步编程)
笔记·学习·rust·tokio
YUS云生3 小时前
Python学习笔记·第29天:Git进阶——分支操作与合并冲突
笔记·python·学习
xiaoyuchidayuma3 小时前
已知 ud、uq 求解三相相电压并计算调制比
笔记·学习
B8017913Y3 小时前
上课记笔记写不完不会整理?2026学习笔记生成使用场景该怎么选
人工智能·笔记·学习
小徐不徐说4 小时前
Qt 线程迁移机制完整实战指南(moveToThread)
开发语言·c++·qt·程序设计
盐焗鹌鹑蛋4 小时前
【C++】set和map
c++