【聊天室后端服务器开发】文件存储子服务

概述

关系总结

  • 客户端请求、网关处理:客户端向RPC服务器发起请求,指定操作类型(具体实现了获取单个或者多个文件、上传单个或者多个文件)
  • RPC服务器:FIleServer接收请求然后调用FIleServerImpl中的方法进行文件操作
  • 文件操作模块:FIleServerImpl 类实现文件读写操作,文件被存储在指定的目录中
  • 服务注册模块:Registry 将文件存储服务注册到服务注册中心,从而使得其他服务或者客户端可以发现该服务并调用

设计思路

实现流程

  • 客户端文件上传:将文件数据以及对应的文件元信息上传到服务器
  • 服务器生成ID:服务器针对于上传的文件生成一个唯一ID,然后将这个唯一ID作为文件名对文件数据进行存储
  • 服务器返回文件ID:文件数据持久化到磁盘后,将磁盘中标识该数据的文件ID返回给客户端

文件存储子服务提供的功能

  • 文件上传
    • 单个文件上传,收到文件消息将文件数据转发给文件子服务进行持久化存储
    • 多个文件上传
  • 文件下载
    • 单个文件下载,后台用户获取用户头像文件数据,客户端获取文件语音图片等消息
    • 多个文件下载

主要模块

  • 参数与配置文件解析:flages框架实现
  • 日志:spdlog框架实现
  • 服务注册:etcd的二次封装直接使用
  • RPC服务模块:brpc框架搭建RPC服务器
  • 文件操作模块:自制常用工具模块 

具体实现

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 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;//请求ID
    bool success = 2; //是否成功
    string errmsg = 3;//出错原因
    FileMessageInfo file_info = 4; //返回了文件组织的元信息
}

单个文件获取:根据文件ID组织FileDownLoadDate的结构类型

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
}

file_server.hpp

单个与多个文件上传

  • 接收GetSingleFileReq请求,然后返回GetSingleFileRsp响应
  • 通过文件ID获取对应文件内容
    • 如果是上传多个文件则逐个读取文件内容,然后将文件数据填充到响应file_data的map中
  • 如果文件读取失败则返回错误信息
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 FileDownloadData {
    string file_id = 1;
    bytes file_content = 2;
}
            void GetSingleFile(google::protobuf::RpcController* controller,
                    const ::mag::GetSingleFileReq* request,
                    ::mag::GetSingleFileRsp* response,
                    ::google::protobuf::Closure* done)
                    {
                        brpc::ClosureGuard rpc_guard(done);
                        response->set_request_id(request->request_id());
                        //1. 取出请求中的文件ID
                        std::string fid = request->file_id();
                        std::string filename = _storage_path + fid;
                        //2. 将文件ID作为文件名,读取文件数据
                        std::string body;
                        bool ret = readFile(filename, body);
                        if (ret == false) 
                        {
                            response->set_success(false);
                            response->set_errmsg("读取文件数据失败!");
                            LOG_ERROR("{} 读取文件数据失败!", request->request_id());
                            return;
                        }
                        //3. 组织响应发送
                        response->set_success(true);
                        response->mutable_file_data()->set_file_id(fid);
                        response->mutable_file_data()->set_file_content(body);
                    }

            void GetMultiFile(google::protobuf::RpcController* controller,
                    const ::mag::GetMultiFileReq* request,
                    ::mag::GetMultiFileRsp* response,
                    ::google::protobuf::Closure* done)
                    {
                        brpc::ClosureGuard rpc_guard(done);
                        response->set_request_id(request->request_id());
                        for (int i = 0; i < request->file_id_list_size(); i++) 
                        {
                            std::string fid = request->file_id_list(i);
                            std::string filename = _storage_path + fid;
                            std::string body;
                            bool ret = readFile(filename, body);
                            if (ret == false) {
                                response->set_success(false);
                                response->set_errmsg("读取文件数据失败!");
                                LOG_ERROR("{} 读取文件数据失败!", request->request_id());
                                return;
                            }
                            FileDownloadData data;
                            data.set_file_id(fid);
                            data.set_file_content(body);
                            response->mutable_file_data()->insert({fid, data});
                        }
                        response->set_success(true);
                    }

上传单个文件和多个文件 

  • 接收PutSingleFileReq请求,然后返回PutSingleFileRsp响应
  • 给文件生成一个唯一的ID,然后将文件内容写入指定路径
    • 如果是多个文件,则逐一对每个文件生成一个ID,然后将文件内容写入到指定路径
  • 如果文件写入失败,那么就返回错误消息;否则就返回所有文件的元数据
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;//请求ID
    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;
}

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;
}

        // 上传单个文件的业务处理
        void PutSingleFile(google::protobuf::RpcController* controller,
                    const mag::PutSingleFileReq* request,
                    mag::PutSingleFileRsp* response,
                    google::protobuf::Closure* done) {
            brpc::ClosureGuard rpc_guard(done);
            response->set_request_id(request->request_id());

            // 1. 为文件生成唯一的文件ID(即 UUID)作为文件名
            std::string fid = uuid();
            std::string filename = _storage_path + fid;

            // 2. 获取请求中的文件数据并写入文件
            bool ret = writeFile(filename, request->file_data().file_content());
            if (!ret) {
                response->set_success(false);
                response->set_errmsg("写入文件数据失败!");
                LOG_ERROR("{} 写入文件数据失败!", request->request_id());
                return;
            }

            // 3. 返回文件信息
            response->set_success(true);
            response->mutable_file_info()->set_file_id(fid);
            response->mutable_file_info()->set_file_size(request->file_data().file_size());
            response->mutable_file_info()->set_file_name(request->file_data().file_name());
        }

        // 上传多个文件的业务处理
        void PutMultiFile(google::protobuf::RpcController* controller,
                    const mag::PutMultiFileReq* request,
                    mag::PutMultiFileRsp* response,
                    google::protobuf::Closure* done) {
            brpc::ClosureGuard rpc_guard(done);
            response->set_request_id(request->request_id());

            // 循环处理请求中的多个文件数据
            for (int i = 0; i < request->file_data_size(); i++) {
                std::string fid = uuid();
                std::string filename = _storage_path + fid;

                bool ret = writeFile(filename, request->file_data(i).file_content());
                if (!ret) {
                    response->set_success(false);
                    response->set_errmsg("写入文件数据失败!");
                    LOG_ERROR("{} 写入文件数据失败!", request->request_id());
                    return;
                }

                // 添加文件信息到响应
                mag::FileMessageInfo *info = response->add_file_info();
                info->set_file_id(fid);
                info->set_file_size(request->file_data(i).file_size());
                info->set_file_name(request->file_data(i).file_name());
            }

            // 返回成功响应
            response->set_success(true);
        }

FIleServer类 

 启动并管理文件存储服务的 RPC 服务器

class FileServer {
    public:
        using ptr = std::shared_ptr<FileServer>;

        // 构造函数,接收服务注册客户端和 RPC 服务器
        FileServer(const Registry::ptr &reg_client,
            const std::shared_ptr<brpc::Server> &server):
            _reg_client(reg_client),
            _rpc_server(server) {}

        ~FileServer() {}

        // 启动 RPC 服务器,直到请求停止
        void start() {
            _rpc_server->RunUntilAskedToQuit();
        }

    private:
        Registry::ptr _reg_client;       // 服务注册客户端
        std::shared_ptr<brpc::Server> _rpc_server;  // RPC 服务器
};

 构建者类与语音子服务相同

// 文件存储服务构建者,用于构建文件存储服务
class FileServerBuilder {
    public:
        // 初始化服务注册客户端
        void make_reg_object(const std::string &reg_host,
            const std::string &service_name,
            const std::string &access_host) {
            _reg_client = std::make_shared<Registry>(reg_host);
            _reg_client->registry(service_name, access_host);
        }

        // 构建 RPC 服务器
        void make_rpc_server(uint16_t port, int32_t timeout, 
            uint8_t num_threads, const std::string &path = "./data/") {
            _rpc_server = std::make_shared<brpc::Server>();
            FileServiceImpl *file_service = new FileServiceImpl(path);

            // 注册文件服务到 RPC 服务器
            int ret = _rpc_server->AddService(file_service, 
                brpc::ServiceOwnership::SERVER_OWNS_SERVICE);
            if (ret == -1) {
                LOG_ERROR("添加Rpc服务失败!");
                abort();
            }

            brpc::ServerOptions options;
            options.idle_timeout_sec = timeout;
            options.num_threads = num_threads;

            // 启动 RPC 服务器
            ret = _rpc_server->Start(port, &options);
            if (ret == -1) {
                LOG_ERROR("服务启动失败!");
                abort();
            }
        }

        // 构建并返回文件服务器对象
        FileServer::ptr build() {
            if (!_reg_client) {
                LOG_ERROR("还未初始化服务注册模块!");
                abort();
            }
            if (!_rpc_server) {
                LOG_ERROR("还未初始化RPC服务器模块!");
                abort();
            }

            // 返回构建好的文件服务器对象
            return std::make_shared<FileServer>(_reg_client, _rpc_server);
        }

    private:
        Registry::ptr _reg_client;       // 服务注册客户端
        std::shared_ptr<brpc::Server> _rpc_server;  // RPC 服务器
};

服务器启动和RPC绑定问题

  • FileServer类
    • 管理整个文件服务,其中包括启动RPC服务器和进行服务注册
    • 通过构造函数传入服务注册客户端和RPC服务器,确保服务器可以正常运行
  • FileServerBuilder类
    • 构建FIleServer类
    • make_reg_object()方法用于创建服务注册的客户端,然后将服务注册到注册中心
    • make_rpc_server()方法用于创建并配置RPC服务器,其中包括文件存储路径、配置服务等

file_server.cc

#include "file_server.hpp"


DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

DEFINE_string(registry_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(instance_name, "/file_service/instance", "当前实例名称");
DEFINE_string(access_host, "127.0.0.1:10002", "当前实例的外部访问地址");

DEFINE_string(storage_path, "./data/", "当前实例的外部访问地址");

DEFINE_int32(listen_port, 10002, "Rpc服务器监听端口");
DEFINE_int32(rpc_timeout, -1, "Rpc调用超时时间");
DEFINE_int32(rpc_threads, 1, "Rpc的IO线程数量");

int main(int argc, char *argv[])
{
    google::ParseCommandLineFlags(&argc, &argv, true);
    init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);

    mag::FileServerBuilder fsb;
    fsb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads, FLAGS_storage_path);
    fsb.make_reg_object(FLAGS_registry_host, FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host);
    auto server = fsb.build();
    server->start();
    return 0;
}

测试

gtest测试

#include <gflags/gflags.h>
#include <gtest/gtest.h>
#include <thread>
#include "etcd.hpp"
#include "channel.hpp"
#include "logger.hpp"
#include "file.pb.h"
#include "base.pb.h"
#include "../source/utils.hpp"


DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

DEFINE_string(etcd_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(file_service, "/service/file_service", "服务监控根目录");

mag::ServiceChannel::ChannelPtr channel;
std::string single_file_id;

std::vector<std::string> multi_file_id;

//
///测试:上传单个文件
/
TEST(put_test , single_file)
{
    //读取指定文件的数据到body
    std::string body;
    ASSERT_TRUE(mag::readFile("./Makefile",body));//判断是否成功读取
    //实例化RPC调用客户端对象然后发起RPC调用
    mag::FileService_Stub stub(channel.get());

    mag::PutSingleFileReq req;
    req.set_request_id("1111");
    req.mutable_file_data()->set_file_name("Makefile");
    req.mutable_file_data()->set_file_size(body.size());
    req.mutable_file_data()->set_file_content(body);

    brpc::Controller *cntl = new brpc::Controller();
    mag::PutSingleFileRsp *rsp = new mag::PutSingleFileRsp();
    stub.PutSingleFile(cntl,&req,rsp,nullptr);
    ASSERT_FALSE(cntl->Failed());

    //检测返回值,上传是否成功
    ASSERT_TRUE(rsp->success());
    ASSERT_EQ(rsp->file_info().file_size(), body.size());
    ASSERT_EQ(rsp->file_info().file_name(), "Makefile");
    single_file_id = rsp->file_info().file_id();
    LOG_DEBUG("文件ID:{}", rsp->file_info().file_id());
}

//
///测试:下载单个文件
/
TEST(get_test,single_file)
{
    //发起RPC调用,进行文件下载
    mag::FileService_Stub stub(channel.get());
    mag::GetSingleFileReq req;
    mag::GetSingleFileRsp *rsp;
    req.set_request_id("2222");
    req.set_file_id(single_file_id);

    brpc::Controller *cntl = new brpc::Controller();
    rsp = new mag::GetSingleFileRsp();
    stub.GetSingleFile(cntl, &req, rsp, nullptr);
    ASSERT_FALSE(cntl->Failed());
    ASSERT_TRUE(rsp->success());
    //将数据文件存储到文件中
    ASSERT_EQ(single_file_id,rsp->file_data().file_id());
    mag::writeFile("make_file_download", rsp->file_data().file_content());
}

//
///测试:上传多个文件
/
TEST(put_test, multi_file) {
    //1. 读取当前目录下的指定文件数据
    std::string body1;
    ASSERT_TRUE(mag::readFile("./base.pb.h", body1));
    std::string body2;
    ASSERT_TRUE(mag::readFile("./file.pb.h", body2));
    //2. 实例化rpc调用客户端对象,发起rpc调用
    mag::FileService_Stub stub(channel.get());

    mag::PutMultiFileReq req;
    req.set_request_id("3333");

    auto file_data = req.add_file_data();
    file_data->set_file_name("base.pb.h");
    file_data->set_file_size(body1.size());
    file_data->set_file_content(body1);

    file_data = req.add_file_data();
    file_data->set_file_name("file.pb.h");
    file_data->set_file_size(body2.size());
    file_data->set_file_content(body2);

    brpc::Controller *cntl = new brpc::Controller();
    mag::PutMultiFileRsp *rsp = new mag::PutMultiFileRsp();
    stub.PutMultiFile(cntl, &req, rsp, nullptr);
    ASSERT_FALSE(cntl->Failed());
    //3. 检测返回值中上传是否成功
    ASSERT_TRUE(rsp->success());
    for (int i = 0; i < rsp->file_info_size(); i++){
        multi_file_id.push_back(rsp->file_info(i).file_id());
        LOG_DEBUG("文件ID:{}", multi_file_id[i]);
    }
}

//
///测试:下载多个文件
/
TEST(get_test, multi_file) {
    //先发起Rpc调用,进行文件下载
    mag::FileService_Stub stub(channel.get());
    mag::GetMultiFileReq req;
    mag::GetMultiFileRsp *rsp;
    req.set_request_id("4444");
    req.add_file_id_list(multi_file_id[0]);
    req.add_file_id_list(multi_file_id[1]);

    brpc::Controller *cntl = new brpc::Controller();
    rsp = new mag::GetMultiFileRsp();
    stub.GetMultiFile(cntl, &req, rsp, nullptr);
    ASSERT_FALSE(cntl->Failed());
    ASSERT_TRUE(rsp->success());
    //将文件数据,存储到文件中
    ASSERT_TRUE(rsp->file_data().find(multi_file_id[0]) != rsp->file_data().end());
    ASSERT_TRUE(rsp->file_data().find(multi_file_id[1]) != rsp->file_data().end());
    auto map = rsp->file_data();
    auto file_data1 = map[multi_file_id[0]];
    mag::writeFile("base_download_file1",file_data1.file_content());
    auto file_data2 = map[multi_file_id[1]];
    mag::writeFile("file_download_file2", file_data2.file_content());
}


int main(int argc , char *argv[])
{
    testing::InitGoogleTest(&argc, argv);
    google::ParseCommandLineFlags(&argc, &argv, true);
    init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);

    //1. 先构造Rpc信道管理对象
    auto sm = std::make_shared<mag::ServiceManager>();
    sm->declared(FLAGS_file_service);
    auto put_cb = std::bind(&mag::ServiceManager::onServiceOnline, sm.get(), std::placeholders::_1, std::placeholders::_2);
    auto del_cb = std::bind(&mag::ServiceManager::onServiceOffline, sm.get(), std::placeholders::_1, std::placeholders::_2);
    //2. 构造服务发现对象
    mag::Discovery::ptr dclient = std::make_shared<mag::Discovery>(FLAGS_etcd_host, FLAGS_base_service, put_cb, del_cb);
    
    //3. 通过Rpc信道管理对象,获取提供Echo服务的信道
    channel = sm->choose(FLAGS_file_service);
    if (!channel) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        return -1;
    }

    return RUN_ALL_TESTS();
}



上传与下载一致性确认

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值