微服务即时通讯系统(服务端)——文件存储模块全链路设计与实现(3)

文章目录

  • 微服务即时聊天系统:文件存储模块全链路设计与实现
    • 一、模块定位与核心需求
      • [1. 核心功能需求](#1. 核心功能需求)
      • [2. 技术栈选型](#2. 技术栈选型)
    • 二、服务端核心设计与实现
      • [1. 接口定义:基于 Protobuf 的契约设计](#1. 接口定义:基于 Protobuf 的契约设计)
        • [1.1 核心数据结构](#1.1 核心数据结构)
        • [1.2 服务接口(FileService)](#1.2 服务接口(FileService))
      • [2. 服务实现层:FileServiceImpl 核心逻辑](#2. 服务实现层:FileServiceImpl 核心逻辑)
        • [2.1 初始化:存储目录检查与创建](#2.1 初始化:存储目录检查与创建)
        • [2.2 单个文件上传(PutSingleFile):校验 + UUID 命名](#2.2 单个文件上传(PutSingleFile):校验 + UUID 命名)
        • [2.3 批量文件上传(PutMultiFile):原子性保障](#2.3 批量文件上传(PutMultiFile):原子性保障)
        • [2.4 文件下载(GetSingleFile/GetMultiFile):快速定位与读取](#2.4 文件下载(GetSingleFile/GetMultiFile):快速定位与读取)
      • [3. 服务封装与构建:FileServer 与 FileServerBuilder](#3. 服务封装与构建:FileServer 与 FileServerBuilder)
        • [3.1 FileServer:服务运行封装](#3.1 FileServer:服务运行封装)
        • [3.2 FileServerBuilder:构建器模式实现](#3.2 FileServerBuilder:构建器模式实现)
      • [4. 服务启动入口:main 函数与配置解析](#4. 服务启动入口:main 函数与配置解析)
    • [三、测试端设计:基于 GTest 的自动化测试](#三、测试端设计:基于 GTest 的自动化测试)
      • [1. 测试环境初始化](#1. 测试环境初始化)
      • [2. 核心测试用例设计](#2. 核心测试用例设计)
        • [2.1 单文件上传测试(put_file, put_single_file)](#2.1 单文件上传测试(put_file, put_single_file))
        • [2.2 多文件上传测试(put_file, put_multi_file)](#2.2 多文件上传测试(put_file, put_multi_file))
        • [2.3 单文件下载测试(get_file, get_single_file)](#2.3 单文件下载测试(get_file, get_single_file))
        • [2.4 多文件下载测试(get_file, get_multi_file)](#2.4 多文件下载测试(get_file, get_multi_file))
      • [3. 测试执行与结果验证](#3. 测试执行与结果验证)
    • 四、关键设计亮点与扩展方向
      • [1. 核心设计](#1. 核心设计)
        • 一、CMake设计逻辑:模块化与自动化的结合
        • 二、CMake核心流程解析
          • [1. 基础配置:指定 CMake 版本与工程名称](#1. 基础配置:指定 CMake 版本与工程名称)
          • [2. Protobuf 代码自动生成:核心依赖处理](#2. Protobuf 代码自动生成:核心依赖处理)
          • 关键实现步骤:
          • 设计亮点:
        • [3. CMake源码收集:业务代码与测试代码分离](#3. CMake源码收集:业务代码与测试代码分离)
        • [4. CMake目标定义与编译:关联源码与依赖](#4. CMake目标定义与编译:关联源码与依赖)
          • [4.1 定义可执行目标](#4.1 定义可执行目标)
          • [4.2 头文件搜索路径配置](#4.2 头文件搜索路径配置)
          • [4.3 链接第三方库](#4.3 链接第三方库)
        • [5. CMake安装部署:标准化输出路径](#5. CMake安装部署:标准化输出路径)
      • [2. 扩展方向](#2. 扩展方向)
    • 五、总结

微服务即时聊天系统:文件存储模块全链路设计与实现

在即时聊天系统中,文件存储模块是支撑图片、文档、语音等非文本消息流转的核心基础设施。本文将从需求拆解出发,详细讲解文件存储模块从服务端架构设计、核心逻辑实现到自动化测试验证的全流程设计思路,为微服务场景下的文件管理提供可落地的解决方案。

gittee码云完整代码链接

一、模块定位与核心需求

文件存储模块(File Service)作为即时聊天系统的独立微服务,主要承担文件上传、文件下载、服务注册与发现三大核心职责,需满足高可用、数据一致性、可扩展性三大需求。

1. 核心功能需求

  • 文件操作:支持单个 / 批量文件上传(Put)、单个 / 批量文件下载(Get),覆盖聊天场景中常见的文件交互场景。
  • 服务治理:集成 Etcd 实现服务注册与发现,确保客户端能动态感知服务节点,支持服务水平扩展。
  • 数据安全:上传文件时校验大小一致性,避免损坏文件存入;采用临时文件 + 原子重命名机制,保证文件写入的原子性。
  • 可配置化:支持日志模式(调试 / 发布)、存储路径、RPC 线程数、超时时间等参数通过命令行配置,提升部署灵活性。

2. 技术栈选型

技术领域 选型 核心作用
通信框架 brpc 高性能 RPC 通信,支撑服务间同步调用
服务注册发现 Etcd 实现服务节点动态注册与客户端发现,支持服务扩容
数据序列化 Protobuf 定义文件服务接口与数据结构,保证跨语言兼容性
日志系统 spdlog + butil-log 支持调试 / 发布双模式日志,便于问题排查
测试框架 Google Test(GTest) 实现自动化单元测试,覆盖核心接口场景

二、服务端核心设计与实现

服务端采用分层设计,分为「服务实现层(FileServiceImpl)」「服务封装层(FileServer)」「构建器层(FileServerBuilder)」,通过依赖注入和模块化拆分降低耦合,提升可维护性。

1. 接口定义:基于 Protobuf 的契约设计

首先通过 Protobuf 定义服务接口与数据结构,明确客户端与服务端的通信契约,这是微服务解耦的基础。

1.1 核心数据结构
  • FileUploadData:上传文件元信息,包含文件名、文件大小、文件二进制内容。
  • FileDownloadData:下载文件数据,包含文件 ID、文件二进制内容。
  • FileMessageInfo:文件元信息(用于上传后返回),包含文件 ID、大小、文件名,供客户端后续下载使用。
protobuf 复制代码
message FileDownloadData {
    string file_id = 1;
    bytes file_content = 2;
}

message FileUploadData {
    string file_name = 1;   //文件名称
    int64 file_size = 2;    //文件大小
    bytes file_content = 3; //文件数据
}
message FileMessageInfo {
    optional string file_id = 1;//文件id,客户端发送的时候不用设置
    optional int64 file_size = 2;//文件大小
    optional string file_name = 3;//文件名称
    //文件数据,在ES中存储消息的时候只要id和元信息,不要文件数据, 服务端转发的时候也不需要填充
    optional bytes file_contents = 4;
}
1.2 服务接口(FileService)

定义 4 个核心 RPC 方法,覆盖单文件 / 多文件的上传与下载:

protobuf 复制代码
service FileService {
    // 单个文件下载
    rpc GetSingleFile(GetSingleFileReq) returns (GetSingleFileRsp);
    // 批量文件下载
    rpc GetMultiFile(GetMultiFileReq) returns (GetMultiFileRsp);
    // 单个文件上传
    rpc PutSingleFile(PutSingleFileReq) returns (PutSingleFileRsp);
    // 批量文件上传
    rpc PutMultiFile(PutMultiFileReq) returns (PutMultiFileRsp);
}
  • 请求(Req) :均包含request_id(唯一标识请求,便于链路追踪)、可选的user_id/session_id(预留用户权限校验扩展点)。
  • 响应(Rsp) :包含success(执行结果)、errmsg(错误信息)、业务数据(如文件元信息 / 二进制内容),便于客户端快速判断结果。
protobuf 复制代码
message GetSingleFileReq {
    string request_id = 1;
    string file_id = 2;
    optional string user_id = 3;
    optional string session_id = 4;
}
message GetSingleFileRsp {
    string request_id = 1;
    bool success = 2;
    string errmsg = 3; 
    optional FileDownloadData file_data = 4;
}

message GetMultiFileReq {
    string request_id = 1;
    optional string user_id = 2;
    optional string session_id = 3;
    repeated string file_id_list = 4;
}
message GetMultiFileRsp {
    string request_id = 1;
    bool success = 2;
    string errmsg = 3; 
    map<string, FileDownloadData> file_data = 4;//文件ID与文件数据的映射map
}

message PutSingleFileReq {
    string request_id = 1; //请求ID,作为处理流程唯一标识
    optional string user_id = 2;
    optional string session_id = 3;
    FileUploadData file_data = 4;
}
message PutSingleFileRsp {
    string request_id = 1;
    bool success = 2;
    string errmsg = 3;
    FileMessageInfo file_info = 4; //返回了文件组织的元信息
}

message PutMultiFileReq {
    string request_id = 1;
    optional string user_id = 2;
    optional string session_id = 3;
    repeated FileUploadData file_data = 4;
}
message PutMultiFileRsp {
    string request_id = 1;
    bool success = 2;
    string errmsg = 3; 
    repeated FileMessageInfo file_info = 4;
}

2. 服务实现层:FileServiceImpl 核心逻辑

FileServiceImpl 是文件服务的核心,负责具体的文件 IO 操作与业务逻辑处理,关键设计集中在文件上传的原子性批量操作的失败处理

2.1 初始化:存储目录检查与创建

构造函数中完成存储目录的初始化,确保服务启动前目录可用,避免运行时文件写入失败:

cpp 复制代码
FileServiceImpl(const std::string& storage_path) : storage_path_(storage_path) {
    umask(0); // 重置文件权限掩码,确保创建的目录权限生效
    // 目录不存在则创建,忽略"目录已存在"错误
    if (mkdir(storage_path.c_str(), 0775) != 0 && errno != EEXIST) {
        LOG_ERROR("创建存储目录{}失败: {}", storage_path, strerror(errno));
        throw std::runtime_error("初始化存储目录失败"); // 终止服务启动
    }
    // 确保路径以"/"结尾,避免后续拼接文件名时出现路径错误
    if(!storage_path_.empty() && storage_path_.back() != '/')
        storage_path_.push_back('/');
}
2.2 单个文件上传(PutSingleFile):校验 + UUID 命名

核心逻辑:生成唯一文件 ID → 校验文件大小 → 写入文件 → 返回元信息。

  • 唯一文件 ID :通过bite_im::uuid()生成全局唯一 ID,避免文件名冲突(如不同用户上传同名文件)。
  • 大小校验 :对比file_sizefile_content.size(),确保客户端上传的文件未损坏。
  • 文件写入 :调用工具函数WriteFile将二进制内容写入存储目录,失败则返回错误信息。
cpp 复制代码
std::string uuid() 
{
    //生成一个由16位随机字符组成的字符串作为唯一ID
    // 1. 生成6个0~255之间的随机数字(1字节-转换为16进制字符)--生成12位16进制字符

    //实例化设备随机数对象-用于生成设备随机数
    std::random_device rd;
    //以设备随机数为种子,实例化伪随机数对象
    std::mt19937 generator(rd());
    //限定数据范围
    std::uniform_int_distribution<int> distribution(0,255); 

    std::stringstream ss;
    for (int i = 0; i < 6; i++) 
    {
        if (i == 3) ss << "-";
        ss << std::setw(2) << std::setfill('0') << std::hex << distribution(generator);
    }
    ss << "-";
    // 2. 通过一个静态变量生成一个2字节的编号数字--生成4位16进制数字字符
    static std::atomic<short> idx(0);
    short tmp = idx.fetch_add(1);
    ss << std::setw(4) << std::setfill('0') << std::hex << tmp;
    return ss.str();
}

std::string vcode()
{
    std::random_device rd;
    std::mt19937 mt(rd());
    std::uniform_int_distribution<int> dis(0,9);

    std::stringstream ss;
    for(int i = 0;i < 4; i++)
    {
        ss << dis(mt);
    }
    return ss.str();
}

// 检查文件是否存在且为常规文件
bool FileExists(const std::string& path) 
{
    struct stat st;
    if (stat(path.c_str(), &st) != 0) return false;
    return S_ISREG(st.st_mode); // 仅承认常规文件
}

bool ReadFile(const std::string& filepath, std::string& body)
{
    try
    {
        if(!FileExists(filepath))
        {
            LOG_ERROR("文件-{}-不存在, 不能读取!", filepath);
            return false;
        }
        std::ifstream in(filepath, std::ios::binary | std::ios::in);
        if(!in.is_open())
        {
            LOG_ERROR("打开文件{}失败", filepath);
            return false;
        }
        in.seekg(0, std::ios::end);
        size_t flen = in.tellg();
        body.resize(flen);
        in.seekg(std::ios::beg);
        in.read(&body[0], flen);
        if(!in.good())
        {
            LOG_ERROR("读取文件{}数据失败", filepath);
            return false;
        }
        return true;
    }
    catch (const std::exception& e)
    {
        LOG_ERROR("读取文件{}失败,原因:{}", filepath, e.what());
    }
    return false;
}

// 新增:写入临时文件后原子重命名(用于批量操作)
bool WriteFileAtomically(const std::string& filepath, const std::string& body)
{
    try
    {
        std::string tmp_path = filepath + ".tmp";
        std::ofstream out(tmp_path, std::ios::binary | std::ios::out | std::ios::trunc);
        if(!out.is_open())
        {
            LOG_ERROR("打开临时文件{}失败", tmp_path);
            return false;
        }
        out.write(&body[0], body.size());
        if(!out.good())
        {
            LOG_ERROR("写入临时文件{}失败", tmp_path);
            std::remove(tmp_path.c_str());
            return false;
        }
        out.close();

        // 原子重命名(保证文件完整性)
        if (std::rename(tmp_path.c_str(), filepath.c_str()) != 0)
        {
            LOG_ERROR("重命名文件{}->{}失败", tmp_path, filepath);
            std::remove(tmp_path.c_str());
            return false;
        }
        return true;
    }
    catch (const std::exception& e)
    {
        LOG_ERROR("写入文件{}失败,原因:{}", filepath, e.what());
    }
    return false;
}

bool WriteFile(const std::string& filepath, const std::string& body)
{
    return WriteFileAtomically(filepath, body);
}
2.3 批量文件上传(PutMultiFile):原子性保障

批量上传需保证 "要么全部成功,要么全部失败",避免部分文件上传成功导致的数据不一致,核心设计是临时文件 + 原子重命名

  1. 临时文件写入 :每个文件先写入.tmp后缀的临时文件(如xxx.tmp),避免未写完的文件被客户端读取。
  2. 批量重命名 :所有临时文件写入成功后,通过std::rename.tmp文件原子重命名为最终文件(如xxx)。
  3. 失败回滚:若任一文件写入或重命名失败,删除已创建的临时文件和已重命名的文件,确保数据一致性。
2.4 文件下载(GetSingleFile/GetMultiFile):快速定位与读取
  • 单文件下载 :通过 "存储路径 + 文件 ID" 拼接文件路径,调用ReadFile读取二进制内容,不存在则返回 "文件不存在" 错误。
  • 批量文件下载 :遍历file_id_list,逐个读取文件;若任一文件失败,立即返回错误,避免客户端等待过长时间。

3. 服务封装与构建:FileServer 与 FileServerBuilder

为简化服务初始化流程、提升可配置性,采用Builder 模式封装服务创建过程,将 "服务注册""RPC 服务启动""参数配置" 解耦。

3.1 FileServer:服务运行封装

负责启动 RPC 服务并阻塞等待退出信号,内部持有 Etcd 注册器(regs_)和 brpc 服务器(brpcServer_)的指针,屏蔽底层细节:

cpp 复制代码
void start() {
    brpcServer_->RunUntilAskedToQuit(); // 阻塞运行,直到收到退出信号(如Ctrl+C)
}
3.2 FileServerBuilder:构建器模式实现

提供链式调用接口,支持配置存储路径、Etcd TTL 等参数,分步骤初始化服务:

  1. 参数配置 :通过set_storage_pathset_etcd_ttl设置核心参数,默认 Etcd TTL 为 10 秒。
  2. Etcd 注册(make_regs_object) :将服务地址(access_host)注册到 Etcd,失败则终止服务(std::abort())。
  3. RPC 服务初始化(make_rpc_object) :创建FileServiceImpl实例 → 添加到 brpc 服务器 → 启动 RPC 监听端口。
  4. 服务构建(build) :校验 Etcd 注册器和 RPC 服务是否初始化完成,返回FileServer实例。

Builder 模式的优势:

  • 隐藏复杂的初始化流程,客户端只需链式调用即可创建服务。
  • 便于扩展新参数(如后续添加 SSL 配置),无需修改已有代码。
cpp 复制代码
class FileServerBuilder
{
public:
    FileServerBuilder() = default;

    // 新增:设置存储路径
    FileServerBuilder& set_storage_path(const std::string& path)
    {
        storage_path_ = path;
        return *this;
    }

    // 新增:设置Etcd TTL
    FileServerBuilder& set_etcd_ttl(int ttl)
    {
        etcd_ttl_ = ttl;
        return *this;
    }

    void make_regs_object(const std::string& reghost
                          , const std::string &service_name
                          , const std::string &access_host)
    {
        regs_ = std::make_shared<Etcd_Tool::RegisterEtcd>(reghost);
        Etcd_Tool::resinfo_t resinfo;
        resinfo.service_name_ = service_name;
        resinfo.service_addr_ = access_host;
        resinfo.ttl_ = etcd_ttl_; // 使用配置的TTL

        // 检查注册结果
        if (!regs_->Register(resinfo))
        {
            LOG_ERROR("Etcd服务注册失败");
            std::abort();
        }
    }

    void make_rpc_object(in_port_t port
                         , int timeout_sec
                         , size_t threadnums)
    {
        if (storage_path_.empty())
        {
            LOG_ERROR("未设置存储路径");
            std::abort();
        }

        brpcServer_ = std::make_shared<brpc::Server>();
        brpc::ServerOptions op;
        op.idle_timeout_sec = timeout_sec;
        op.num_threads = threadnums;

        // 使用配置的存储路径
        FileServiceImpl *File_service = new FileServiceImpl(storage_path_);
        if (brpcServer_->AddService(File_service, brpc::ServiceOwnership::SERVER_OWNS_SERVICE) != 0) 
        {
            LOG_ERROR("添加Rpc服务失败!");
            delete File_service; // 避免泄漏
            std::abort();
        }

        if (brpcServer_->Start(port, &op) != 0) 
        {
            LOG_ERROR("启动服务失败!");
            std::abort();
        }
    }

    std::shared_ptr<FileServer> build()
    {
        if(!regs_)
        {
            LOG_ERROR("Etcd注册器未初始化!");
            std::abort();
        }
        if(!brpcServer_)
        {
            LOG_ERROR("brpc服务未初始化!");
            std::abort();
        }

        return std::make_shared<FileServer>(regs_, brpcServer_);
    }
private:
    std::shared_ptr<Etcd_Tool::RegisterEtcd> regs_;
    std::shared_ptr<brpc::Server> brpcServer_;
    std::string storage_path_; // 存储路径配置
    int etcd_ttl_ = 10; // 默认TTL
};

4. 服务启动入口:main 函数与配置解析

main 函数负责解析命令行参数、初始化日志、创建服务并启动,核心流程如下:

  1. 参数解析:使用 gflags 解析命令行参数(如 Etcd 地址、RPC 端口、日志模式)。
  2. 日志初始化 :调用LogModule::Log::Init初始化日志,支持调试模式(控制台输出)和发布模式(文件输出)。
  3. 服务创建:通过 Builder 模式配置存储路径、注册 Etcd、启动 RPC 服务。
  4. 服务运行 :调用server->start()阻塞运行,直到收到退出信号。
cpp 复制代码
int main(int argc, char* argv[])
{
    gflags::ParseCommandLineFlags(&argc, &argv, true);

    logging::SetMinLogLevel(4);

    LogModule::Log::Init(FLAGS_log_mode, FLAGS_log_file, FLAGS_log_level);

    bite_im::FileServerBuilder builder;
    builder.set_storage_path("/home/person/study/Project1/MicrCommSys/filestorage");
    builder.make_regs_object(FLAGS_registry_host
        , FLAGS_base_service + FLAGS_instance_name
        , FLAGS_access_host);
    builder.make_rpc_object(FLAGS_rpc_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads);

    std::shared_ptr<bite_im::FileServer> server = builder.build();

    server->start();
    return 0;
}

关键配置参数示例:

cpp 复制代码
// Etcd配置
DEFINE_string(registry_host, "http://127.0.0.1:2379", "etcd服务注册中心地址");
// RPC配置
DEFINE_int32(rpc_listen_port, 10002, "Rpc服务器监听端口");
DEFINE_int32(rpc_threads, 1, "Rpc的IO线程数量");
// 日志配置
DEFINE_bool(log_mode, false, "日志模式: false(调试模式), true(发布模式)");

三、测试端设计:基于 GTest 的自动化测试

为确保文件服务的正确性,设计覆盖核心接口的自动化测试用例,采用客户端视角模拟真实调用场景,验证上传、下载功能的正确性。

1. 测试环境初始化

测试前需完成 Etcd 服务发现、RPC 信道管理、日志初始化,确保测试用例能正常调用服务端:

  1. Etcd 监视器 :通过Etcd_Tool::MonitorEtcd监听文件服务节点变化,当服务节点上线 / 下线时,更新客户端的 RPC 信道列表。
  2. RPC 信道管理brpcChannelTool::ServiceManager管理 RPC 信道,通过Choose方法随机选择一个可用的服务节点,实现负载均衡。
  3. 日志初始化:与服务端一致,支持调试 / 发布模式,便于测试过程中的问题排查。

初始化核心代码:

cpp 复制代码
// 初始化Etcd监视器,监听文件服务节点变化
Etcd_Tool::monitor_t info;
info.monitor_path_ = FLAGS_file_host; // 文件服务在Etcd中的路径
info.put_on_ = std::bind(&brpcChannelTool::ServiceManager::Online, manager.get(), std::placeholders::_1, std::placeholders::_2);
info.put_off_ = std::bind(&brpcChannelTool::ServiceManager::Offline, manager.get(), std::placeholders::_1, std::placeholders::_2);
m.PushMonitor(info);
// 等待初始服务发现完成,确保测试用例有可用节点
if (!m.WaitForInitialDiscovery()) {
    LOG_ERROR("服务发现初始化失败");
    return -1;
}

2. 核心测试用例设计

采用 GTest 的TEST宏定义测试用例,覆盖 "单文件上传""多文件上传""单文件下载""多文件下载" 四大场景,每个用例包含 "调用前准备→发起 RPC 调用→结果断言" 三步。

2.1 单文件上传测试(put_file, put_single_file)
  • 测试步骤
    1. 读取本地文件(如Makefile)作为上传内容。
    2. 构造PutSingleFileReq,设置request_id、文件元信息与内容。
    3. 发起 RPC 调用,断言响应successtrue,并记录返回的file_id(供后续下载测试使用)。
  • 关键断言
    • 响应的request_id与请求一致(确保请求未被混淆)。
    • 响应successtrue(上传成功)。
    • 返回的file_id非空(生成了唯一文件 ID)。
2.2 多文件上传测试(put_file, put_multi_file)
  • 测试步骤
    1. 读取多个本地文件(如base.pb.hbase.pb.cc)。
    2. 构造PutMultiFileReq,添加多个FileUploadData
    3. 发起 RPC 调用,断言响应successtrue,并记录所有file_id
  • 关键断言
    • 响应的file_info_size与上传文件数量一致(所有文件均上传成功)。
2.3 单文件下载测试(get_file, get_single_file)
  • 测试步骤:
    1. 使用单文件上传测试中记录的file_id,构造GetSingleFileReq
    2. 发起 RPC 调用,断言响应successtrue
    3. 将下载的file_content写入本地文件(如3333.txt),验证文件内容正确性(可选)。
2.4 多文件下载测试(get_file, get_multi_file)
  • 测试步骤
    1. 使用多文件上传测试中记录的file_id列表,构造GetMultiFileReq
    2. 发起 RPC 调用,断言响应successtrue
    3. 遍历下载的文件内容,分别写入本地文件(如4444.txt14444.txt2)。
  • 关键断言
    • 响应的file_data大小与请求的file_id_list大小一致(所有文件均下载成功)。
cpp 复制代码
//测试单个上传文件
TEST(put_file, put_single_file)
{
    //选择信道
    brpcChannelTool::channelptr ptr = manager->Choose(FLAGS_file_host);
    ASSERT_TRUE(ptr != NULL);
    //实例化文件rpc服务
    bite_im::FileService_Stub stub(ptr.get());
    //请求
    bite_im::PutSingleFileReq req;
    std::string rqid = "1111";
    req.set_request_id(rqid); //请求id
    req.mutable_file_data()->set_file_name("Makefile");  //请求文件名
    std::string data;
    ASSERT_TRUE(bite_im::ReadFile("./Makefile", data));
    req.mutable_file_data()->set_file_size(data.size()); //文件大小
    req.mutable_file_data()->set_file_content(data.c_str());  //文件内容
    //响应
    bite_im::PutSingleFileRsp rsp;
    //发起rpc远程调用
    brpc::Controller ctl;
    stub.PutSingleFile(&ctl, &req, &rsp, nullptr);
    ASSERT_EQ(rsp.success(), true);
    ASSERT_EQ(rsp.request_id(), rqid);
    LOG_INFO("单个文件上传请求调用成功,请求id:{}", rsp.request_id());
    fileid.push_back(rsp.file_info().file_id());
}

//测试多个上传文件
TEST(put_file, put_multi_file)
{
    //选择信道
    brpcChannelTool::channelptr ptr = manager->Choose(FLAGS_file_host);
    ASSERT_TRUE(ptr != NULL);
    //实例化文件rpc服务
    bite_im::FileService_Stub stub(ptr.get());
    //请求
    bite_im::PutMultiFileReq req;
    std::string rqid = "2222";
    req.set_request_id(rqid); //请求id

    //复数请求设置
    bite_im::FileUploadData* fupdata = req.add_file_data();
    std::string data;
    ASSERT_TRUE(bite_im::ReadFile("base.pb.h", data));
    fupdata->set_file_size(data.size()); //文件大小
    fupdata->set_file_content(data.c_str());  //文件内容
    
    bite_im::FileUploadData* fupdata2 = req.add_file_data();
    std::string data2;
    ASSERT_TRUE(bite_im::ReadFile("base.pb.cc", data2));
    fupdata2->set_file_size(data2.size()); //文件大小
    fupdata2->set_file_content(data2.c_str());  //文件内容

    //响应
    bite_im::PutMultiFileRsp rsp;
    //发起rpc远程调用
    brpc::Controller ctl;
    stub.PutMultiFile(&ctl, &req, &rsp, nullptr);
    ASSERT_EQ(rsp.success(), true);
    ASSERT_EQ(rsp.request_id(), rqid);
    LOG_INFO("复数文件上传请求调用成功,请求id:{}", rsp.request_id());

    for (size_t i = 0; i < rsp.file_info_size(); i++)
    {
        fileid.push_back(rsp.file_info(i).file_id());
    }
}

TEST(get_file, get_single_file)
{
    brpcChannelTool::channelptr ptr = manager->Choose(FLAGS_file_host);
    ASSERT_TRUE(ptr != nullptr);
    bite_im::FileService_Stub stub(ptr.get());

    //获取单个文件的请求
    bite_im::GetSingleFileReq req;
    ASSERT_TRUE(fileid.empty() != true);
    req.set_file_id(fileid[0]);
    std::string rqid = "3333";
    req.set_request_id(rqid);
    //回复
    bite_im::GetSingleFileRsp rsp;
    brpc::Controller ctl;

    stub.GetSingleFile(&ctl, &req, &rsp, nullptr);
    ASSERT_TRUE(ctl.Failed() == false);
    ASSERT_TRUE(rsp.success());

    ASSERT_TRUE(bite_im::WriteFile("./3333.txt", rsp.file_data().file_content()));
    LOG_INFO("单个文件获取请求调用成功,请求id:{}", rsp.request_id());
}

TEST(get_file, get_multi_file)
{
    brpcChannelTool::channelptr ptr = manager->Choose(FLAGS_file_host);
    ASSERT_TRUE(ptr != nullptr);
    bite_im::FileService_Stub stub(ptr.get());

    //获取单个文件的请求
    bite_im::GetMultiFileReq req;
    ASSERT_TRUE(fileid.empty() != true);
    std::string rqid = "4444";
    req.set_request_id(rqid);
    ASSERT_TRUE(fileid.size() >= 3);
    req.add_file_id_list(fileid[1]);
    req.add_file_id_list(fileid[2]);
    //回复
    bite_im::GetMultiFileRsp rsp;
    brpc::Controller ctl;

    stub.GetMultiFile(&ctl, &req, &rsp, nullptr);
    ASSERT_TRUE(ctl.Failed() == false);
    ASSERT_TRUE(rsp.success());

    int i = 1;
    for (auto it = rsp.mutable_file_data()->begin();
        it != rsp.mutable_file_data()->end();
        it++)
    {
        ASSERT_TRUE(bite_im::WriteFile("./4444.txt" + to_string(i), it->second.file_content()));
        i++;
    }
    LOG_INFO("单个文件获取请求调用成功,请求id:{}", rsp.request_id());
}

3. 测试执行与结果验证

运行测试程序时,GTest 会自动执行所有测试用例,并输出每个用例的执行结果(成功 / 失败)。若用例失败,可通过日志查看具体错误信息(如 "文件不存在""RPC 调用超时"),快速定位问题。

测试执行命令示例:

bash 复制代码
./testClient --registry_host=http://127.0.0.1:2379 --file_host=/service/file_service

四、关键设计亮点与扩展方向

1. 核心设计

  • 原子性保障:批量上传采用 "临时文件 + 原子重命名",避免部分文件上传成功导致的数据不一致。
  • 可配置化与可扩展性 :通过 Builder 模式和 gflags 支持参数配置,便于不同环境部署;预留user_id/session_id字段,支持后续添加权限校验。
  • 服务治理:集成 Etcd 实现服务注册与发现,支持服务水平扩展,提升系统可用性。
  • CMakeLists.txt:快速编译和链接,CMakeLists.txt 的设计是构建流程的核心,它负责将源代码、第三方依赖、Protobuf 自动生成代码等要素串联起来,最终生成可执行程序。以下从设计逻辑、核心流程和关键技术点三个维度,详细解析该 CMakeLists.txt 的设计思路。
一、CMake设计逻辑:模块化与自动化的结合

该 CMakeLists 的核心设计逻辑是 "模块化拆分 + 自动化处理" ,通过明确的步骤将 "代码生成""源码收集""编译链接""安装部署" 拆分为独立环节,同时通过 CMake 的自动化指令(如add_custom_command)减少人工干预,确保构建过程的可靠性和可扩展性。

具体表现为:

  1. 先处理依赖生成 :优先处理 Protobuf 代码生成,确保业务代码编译时能引用到最新的.pb.h.pb.cc文件。
  2. 再整合源码与依赖:将手动编写的业务源码、自动生成的 Protobuf 源码、第三方库依赖统一管理,避免编译时出现 "找不到文件" 或 "链接失败" 的问题。
  3. 最后标准化部署 :通过INSTALL指令定义可执行程序的安装路径,确保部署流程一致。
二、CMake核心流程解析
1. 基础配置:指定 CMake 版本与工程名称
cmake 复制代码
cmake_minimum_required(VERSION 3.1.3)  # 声明最低支持的CMake版本,确保兼容性
project(fileServer)                     # 定义工程名称,用于后续目标命名和日志输出
  • 作用:CMake 版本决定了可用的指令集(如add_custom_commandPRE_BUILD参数在低版本可能不支持),工程名称则作为构建过程的标识。
2. Protobuf 代码自动生成:核心依赖处理

Protobuf 是微服务通信的基础(定义接口和数据结构),但.proto文件需要编译为 C++ 代码才能被业务逻辑引用。这一步是整个 CMakeLists 的核心难点,需要确保:

  • .proto文件更新时,自动重新生成.pb.h.pb.cc
  • 生成的代码能被后续编译流程正确引用。
关键实现步骤:
cmake 复制代码
# 1. 定义proto文件路径和文件名
set(proto_path ${CMAKE_CURRENT_SOURCE_DIR}/../proto)  # .proto文件所在目录
set(proto_files file.proto base.proto)                # 需要处理的proto文件列表

# 2. 初始化变量,用于收集生成的.cc文件
set(proto_srcs "")

# 3. 遍历每个proto文件,生成对应的C++代码
foreach(proto_file ${proto_files})
    # 生成对应的.h和.cc文件名(如file.proto → file.pb.h、file.pb.cc)
    string(REPLACE ".proto" ".pb.h" proto_h ${proto_file})
    string(REPLACE ".proto" ".pb.cc" proto_cc ${proto_file})
    
    # 若目标文件不存在,或.proto文件更新过,则触发生成指令
    if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc})
        add_custom_command(
            PRE_BUILD  # 在构建目标前执行(确保代码生成在编译前完成)
            COMMAND protoc  # 调用protoc编译器
            ARGS 
                --cpp_out=${CMAKE_CURRENT_BINARY_DIR}  # 生成代码的输出目录(build目录)
                -I ${proto_path}                       # 指定proto文件的搜索目录
                --experimental_allow_proto3_optional    # 支持proto3的optional字段(兼容旧版本protoc)
                ${proto_path}/${proto_file}             # 输入的proto文件路径
            DEPENDS ${proto_path}/${proto_file}        # 依赖:当.proto文件修改时,重新生成
            OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc}  # 生成的目标文件
            COMMENT "生成proto框架代码文件: ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc}"  # 构建日志
        )
    endif()
    
    # 将生成的.cc文件加入源码列表,供后续编译
    list(APPEND proto_srcs ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc})
endforeach()
设计亮点:
  • 条件生成 :通过if (NOT EXISTS ...)判断是否需要生成代码,避免重复执行protoc,提升构建效率。
  • 依赖追踪DEPENDS指令确保.proto文件修改后自动重新生成代码,解决 "代码与 proto 不同步" 的问题。
  • 路径规范 :生成的代码统一输出到CMAKE_CURRENT_BINARY_DIR(构建目录),避免污染源码目录。
3. CMake源码收集:业务代码与测试代码分离

为区分 "服务端程序" 和 "测试程序",分别收集对应的源码文件:

cmake 复制代码
# 收集服务端业务源码(source目录下的所有.cpp文件)
set(src_files "")
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/source src_files)

# 收集测试代码(test目录下的所有.cpp文件)
set(test_src_files "")
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/test test_src_files)
  • aux_source_directory:自动递归收集指定目录下的所有源文件,避免手动列举文件名(减少维护成本)。
  • 分离的好处:服务端程序和测试程序可独立编译,测试代码的修改不会影响服务端程序的构建。
4. CMake目标定义与编译:关联源码与依赖
4.1 定义可执行目标
cmake 复制代码
set(target "fileServer")           # 服务端程序目标名
set(test_client "testClient")      # 测试程序目标名

# 生成服务端程序:依赖业务源码和proto生成的代码
add_executable(${target} ${src_files} ${proto_srcs})
# 生成测试程序:依赖测试源码和proto生成的代码
add_executable(${test_client} ${test_src_files} ${proto_srcs})
  • 核心:将手动编写的src_files和自动生成的proto_srcs关联到目标,确保编译时能找到所有代码。
4.2 头文件搜索路径配置

源码中存在大量#include语句(如引用etcdTool.hppfile.pb.h),需要告诉 CMake 头文件的位置:

cmake 复制代码
include_directories(
    ${CMAKE_CURRENT_SOURCE_DIR}/source  # 服务端源码的头文件目录
    ${CMAKE_CURRENT_BINARY_DIR}         # proto生成的.pb.h文件目录(构建目录)
    ${CMAKE_CURRENT_SOURCE_DIR}/../common  # 公共工具类的头文件目录(如etcdTool.hpp)
)
  • 作用:解决编译时 "找不到头文件" 的错误,确保#include "file.pb.h"等语句能正确定位文件。
4.3 链接第三方库

文件存储模块依赖大量第三方库(如 brpc、Protobuf、Etcd 客户端等),需要在链接阶段指定:

cmake 复制代码
# 服务端程序链接的库
target_link_libraries(${target} 
    -lprotobuf  # Protobuf库(序列化/反序列化)
    -lgtest     # 测试框架(服务端可能包含内部测试)
    -lgflags    # 命令行参数解析
    -lspdlog    # 日志库
    -lbrpc      # RPC通信框架
    -lpthread   # 多线程支持
    -lssl -lcrypto  # 加密库(brpc依赖)
    -letcd-cpp-api  # Etcd客户端库
    -lcpprest   # HTTP客户端(Etcd依赖)
    -lcurl      # 网络请求库
    -lfmt       # 格式化库
    -ljsoncpp   # JSON解析库
    -L/usr/local/lib  # 第三方库的安装路径(如brpc、etcd-cpp-api通常安装在这里)
)

# 测试程序链接的库(与服务端相同,因为测试也需要调用RPC和Etcd)
target_link_libraries(${test_client} ...)  # 同上
  • 设计逻辑:根据模块依赖的技术栈(见前文 "技术栈选型"),逐一列出所需库,确保链接时能找到对应的.so.a文件。
  • 注意:-L/usr/local/lib指定了非系统默认路径的库位置,避免因库安装路径特殊导致链接失败。
5. CMake安装部署:标准化输出路径

通过INSTALL指令定义程序安装位置,便于后续部署和运维:

cmake 复制代码
INSTALL(TARGETS ${target} RUNTIME DESTINATION bin)         # 服务端程序安装到bin目录
INSTALL(TARGETS ${test_client} RUNTIME DESTINATION bin)    # 测试程序安装到bin目录
  • 作用:执行make install时,可将生成的fileServertestClient复制到系统的bin目录(如/usr/local/bin),便于全局调用。

2. 扩展方向

  • 文件分片上传:当前设计仅支持小文件(依赖 brpc 单次请求大小限制),后续可添加分片上传功能,支持 GB 级大文件。
  • 文件过期清理:添加定时任务,根据文件元信息中的过期时间(需扩展 Protobuf 字段)清理过期文件,释放存储空间。
  • 权限校验 :基于user_id/session_id添加用户权限校验,确保用户只能下载自己有权访问的文件。
  • 分布式存储:当前使用本地文件系统存储,后续可接入 MinIO、S3 等分布式存储,提升存储容量和可用性。

五、总结

文件存储模块作为即时聊天系统的核心微服务,通过 "Protobuf 接口定义→分层服务实现→Builder 模式封装→GTest 自动化测试" 的全链路设计,确保了功能正确性、可扩展性和可维护性。本文的设计思路不仅适用于即时聊天系统,也可迁移到其他需要文件管理的微服务场景,为同类系统的开发提供参考。

相关推荐
威桑4 小时前
C++ Linux 环境下内存泄露检测方式
linux·c++
交换机路由器测试之路4 小时前
交换机路由器基础(二)-运营商网络架构和接入网
网络·架构
开发者如是说4 小时前
Compose 开发桌面程序的一些问题
前端·架构
wdfk_prog5 小时前
[Linux]学习笔记系列 -- [kernel][time]tick
linux·笔记·学习
凯歌的博客5 小时前
python虚拟环境应用
linux·开发语言·python
我在人间贩卖青春6 小时前
Linux基础
linux
大聪明-PLUS7 小时前
从 C 到 C++20 协程编写方法的演变。第一部分:函数 + 宏 = 协程
linux·嵌入式·arm·smarc
ZHANG13HAO7 小时前
OK3568 Android11 实现 App 独占隔离 CPU 核心完整指
linux·运维·服务器
山猪打不过家猪7 小时前
【无标题】
微服务