文件管理子服务,主要用于管理用户的头像,以及消息中的文件存储,因此需要提供以下接口:
-
文件上传:
- 单个文件的上传:这个接口基本用于后台部分,收到文件消息后将文件数据转发给文件子服务进行存储
- 多个文件的上传:这个接口基本用于后台部分,收到文件消息后将文件数据转发给文件子服务进行存储
-
文件下载:
- 单个文件的下载:在后台用于获取用户头像文件数据,以及客户端用于获取文件/语音/图片消息的文件数据
- 多个文件的下载:在后台用于大批量获取用户头像数据(比如获取用户列表的时候),以及前端的批量文件下载
模块划分
-
参数/配置文件解析模块:基于
gflags框架直接使用进行参数/配置文件解析。 -
日志模块:基于
spdlog框架封装的模块直接使用进行日志输出。 -
服务注册模块:基于
etcd框架封装的注册模块直接使用进行语音识别子服务的服务注册。 -
rpc服务模块:基于brpc框架搭建rpc服务器。 -
文件操作模块:基于标准库的文件流操作实现文件读写的封装。
文件存储子服务搭建流程
-
单个文件的上传:
- 获取文件元数据(大小、文件名、文件内容)
- 为文件分配文件
ID - 以文件
ID为文件名打开文件,并写入数据。 - 组织响应进行返回
-
多个文件的上传:多文件上传,其实相较于单文件上传,就是将处理的过程循环进行了而已
- 从请求中获取文件元数据
- 为文件分配文件
ID - 以文件
ID为文件名打开文件,并写入数据 - 回到第一步进行下一个文件的处理
- 当所有文件数据存储完毕,组织响应进行返回
-
单个文件的下载:
- 从请求中获取文件
ID - 以文件
ID作为文件名打开文件,获取文件大小,并从中读取文件数据。 - 组织响应进行返回
- 从请求中获取文件
-
多个文件的下载:多文件下载,其实相较于单文件下载,就是将处理的过程循环进行了而已
- 从请求中获取文件
ID - 以文件
ID作为文件名打开文件,获取文件大小,并从中读取文件数据 - 回到第一步进行下一个文件的处理
- 当所有文件数据获取完毕,组织响应进行返回
- 从请求中获取文件
file_server.hpp 文件
#include <brpc/server.h>
#include <butil/logging.h>
#include "etcd.hpp" // 服务注册模块封装,负责将服务注册到注册中心(如 etcd)
#include "logger.hpp" // 日志模块封装,用于记录调试和错误日志
#include "utils.hpp" // 工具类封装,包含读写文件、生成 UUID 等常用函数
#include "base.pb.h" // 基础 protobuf 定义(如通用数据结构)
#include "file.pb.h" // 文件操作相关的 protobuf 消息定义
namespace liren
{
// FileServiceImpl 类实现了文件操作 RPC 服务接口,
// 主要处理文件的上传与下载业务逻辑
class FileServiceImpl : public liren::FileService
{
public:
// 构造函数:接收文件存储目录路径,
// 创建该目录(如果不存在),并确保路径以 '/' 结尾,方便后续拼接文件名
FileServiceImpl(const std::string &storage_path)
: _storage_path(storage_path)
{
// 设置 umask 为 0,确保创建目录的权限不受当前进程 umask 限制
umask(0);
// 创建目录,权限设置为 0775(所有者可读写执行,组用户可读写执行,其他用户可读执行)
mkdir(storage_path.c_str(), 0775);
if (_storage_path.back() != '/') _storage_path.push_back('/');
}
~FileServiceImpl(){}
// 下载单个文件
// 业务流程:
// 1. 从请求中提取文件 ID(文件名)
// 2. 拼接出完整的文件路径,并读取文件内容
// 3. 根据读取结果组织响应,成功返回文件数据,失败则返回错误信息
void GetSingleFile(google::protobuf::RpcController* controller,
const ::liren::GetSingleFileReq* request,
::liren::GetSingleFileRsp* response,
::google::protobuf::Closure* done)
{
// 利用 ClosureGuard 确保 RPC 回调最终被调用,防止资源泄漏
brpc::ClosureGuard rpc_guard(done);
// 设置响应中的请求 ID,方便请求跟踪
response->set_request_id(request->request_id());
// 1. 获取文件 ID(在这里即文件名)
std::string fid = request->file_id();
std::string filename = _storage_path + fid; // 拼接完整的文件路径
// 2. 读取文件内容到字符串 body
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);
}
// 下载多个文件
// 业务流程:
// 遍历请求中的文件 ID 列表,依次读取每个文件的数据,并将其存入响应中的映射表中
void GetMultiFile(google::protobuf::RpcController* controller,
const ::liren::GetMultiFileReq* request,
::liren::GetMultiFileRsp* response,
::google::protobuf::Closure* done)
{
brpc::ClosureGuard rpc_guard(done);
response->set_request_id(request->request_id());
// 遍历请求中的所有文件 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;
}
// 构造文件下载数据对象,并设置文件 ID 和内容
FileDownloadData data;
data.set_file_id(fid);
data.set_file_content(body);
// 将该数据插入到响应中的映射 map 中,键为文件 ID
response->mutable_file_data()->insert({fid, data});
}
response->set_success(true);
}
// 上传单个文件
// 业务流程:
// 1. 生成唯一的文件 ID(UUID)作为文件名及标识符
// 2. 将请求中的文件数据写入到指定文件中
// 3. 组织响应,返回文件的元信息(如文件大小、文件名等)
void PutSingleFile(google::protobuf::RpcController* controller,
const ::liren::PutSingleFileReq* request,
::liren::PutSingleFileRsp* response,
::google::protobuf::Closure* done)
{
brpc::ClosureGuard rpc_guard(done);
response->set_request_id(request->request_id());
// 1. 生成唯一的文件 ID,并拼接生成文件路径
std::string fid = uuid();
std::string filename = _storage_path + fid;
// 2. 从请求中取出文件数据,并写入到磁盘中
bool ret = writeFile(filename, request->file_data().file_content());
if (ret == false) {
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());
}
// 上传多个文件
// 业务流程:
// 遍历每个上传文件数据,为每个文件生成唯一 ID,
// 写入磁盘,并将对应的文件元信息添加到响应中
void PutMultiFile(google::protobuf::RpcController* controller,
const ::liren::PutMultiFileReq* request,
::liren::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++)
{
// 为每个文件生成唯一文件 ID
std::string fid = uuid();
std::string filename = _storage_path + fid;
// 写入文件内容到磁盘
bool ret = writeFile(filename, request->file_data(i).file_content());
if (ret == false) {
response->set_success(false);
response->set_errmsg("读取文件数据失败!");
LOG_ERROR("{} 写入文件数据失败!", request->request_id());
return;
}
// 将文件的元信息添加到响应中
liren::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);
}
private:
std::string _storage_path; // 文件存储目录路径,所有文件将存储在此目录下
};
// FileServer 类封装了文件存储子服务的 RPC 服务器功能
class FileServer
{
public:
using ptr = std::shared_ptr<FileServer>;
FileServer(const Registry::ptr ®_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; // brpc 服务器对象,负责处理 RPC 请求
};
// FileServerBuilder 类用于构造和配置文件存储子服务
// 包括服务注册和 RPC 服务器的初始化,确保各个模块正确组装
class FileServerBuilder
{
public:
// 构造服务注册客户端对象,并在注册中心中注册文件服务
// 参数:
// - reg_host: 注册中心地址
// - service_name: 服务名称(例如 "/service/file")
// - access_host: 当前服务的访问地址
void make_reg_object(const std::string ®_host,
const std::string &service_name,
const std::string &access_host)
{
_reg_client = std::make_shared<Registry>(reg_host);
_reg_client->regiter(service_name, access_host);
}
// 构造 RPC 服务器对象,并启动服务
// 参数:
// - port: 监听端口
// - timeout: 空闲连接超时时间(秒)
// - num_threads: 服务器工作线程数
// - path: 文件存储目录,默认为 "./data/"(可以自定义目录)
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 实例,传入文件存储路径
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;
ret = _rpc_server->Start(port, &options);
if (ret == -1) {
LOG_ERROR("服务启动失败!");
abort();
}
}
// 构建 FileServer 对象,前提是注册模块和 RPC 服务器都已初始化
FileServer::ptr build()
{
if (!_reg_client) {
LOG_ERROR("还未初始化服务注册模块!");
abort();
}
if (!_rpc_server) {
LOG_ERROR("还未初始化RPC服务器模块!");
abort();
}
FileServer::ptr server = std::make_shared<FileServer>(_reg_client, _rpc_server);
return server;
}
private:
Registry::ptr _reg_client; // 服务注册客户端对象
std::shared_ptr<brpc::Server> _rpc_server; // brpc 服务器对象
};
}file_server.cc
// 按照流程完成服务器的搭建
// 1. 参数解析
// 2. 日志初始化
// 3. 构造服务器对象,启动服务器
#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);
liren::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
liren::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;
}file.proto
/*
文件操作服务器的子服务注册信息: /service/file/instance_id
服务名称:/service/file
实例 ID: instance_id 每个能够提供用户操作服务的子服务器唯一 ID
当服务发现的时候,通过 /service/file 进行服务发现,就可以发现所有的能够提供用户操作的实例信息了
*/
syntax = "proto3";
package liren;
import "base.proto";
option cc_generic_services = true;
// 获取单个文件
message GetSingleFileReq {
string request_id = 1; // 请求ID,作为处理流程唯一标识
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;
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;
}
// 定义文件操作服务,提供文件的上传和下载功能
service FileService {
rpc GetSingleFile(GetSingleFileReq) returns (GetSingleFileRsp);
rpc GetMultiFile(GetMultiFileReq) returns (GetMultiFileRsp);
rpc PutSingleFile(PutSingleFileReq) returns (PutSingleFileRsp);
rpc PutMultiFile(PutMultiFileReq) returns (PutMultiFileRsp);
}utils.hpp
// 实现项目中一些公共的工具类接口
// 1. 生成一个唯一ID的接口
// 2. 文件的读写操作接口
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <atomic>
#include <random>
#include <iomanip>
#include "logger.hpp"
namespace liren
{
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 == 2) 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 generator(rd()); // 以设备随机数为种子,实例化伪随机数对象
std::uniform_int_distribution<int> distribution(0,9); // 限定数据范围
std::stringstream ss;
for (int i = 0; i < 4; i++) {
ss << distribution(generator);
}
return ss.str();
}
bool readFile(const std::string &filename, std::string &body)
{
// 实现读取一个文件的所有数据,放入body中
std::ifstream ifs(filename, std::ios::binary | std::ios::in);
if (ifs.is_open() == false) {
LOG_ERROR("打开文件 {} 失败!", filename);
return false;
}
ifs.seekg(0, std::ios::end); // 跳转到文件末尾
size_t flen = ifs.tellg(); // 获取当前偏移量-- 文件大小
ifs.seekg(0, std::ios::beg); // 跳转到文件起始
body.resize(flen);
ifs.read(&body[0], flen);
if (ifs.good() == false) {
LOG_ERROR("读取文件 {} 数据失败!", filename);
ifs.close();
return false;
}
ifs.close();
return true;
}
bool writeFile(const std::string &filename, const std::string &body)
{
// 实现将body中的数据,写入filename对应的文件中
std::ofstream ofs(filename, std::ios::out | std::ios::binary | std::ios::trunc);
if (ofs.is_open() == false) {
LOG_ERROR("打开文件 {} 失败!", filename);
return false;
}
ofs.write(body.c_str(), body.size());
if (ofs.good() == false) {
LOG_ERROR("读取文件 {} 数据失败!", filename);
ofs.close();
return false;
}
ofs.close();
return true;
}
}CMakeLists.txt
该子服务的目录结构如下所示:
# 1. 添加cmake版本说明
cmake_minimum_required(VERSION 3.1.3)
# 2. 声明工程名称
project(file_server)
set(target "file_server")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
# 3. 检测并生成ODB框架代码
# 1. 添加所需的proto映射代码文件名称
set(proto_path ${CMAKE_CURRENT_SOURCE_DIR}/../proto)
set(proto_files file.proto base.proto)
# 2. 检测框架代码文件是否已经生成
set(proto_hxx "")
set(proto_cxx "")
set(proto_srcs "")
foreach(proto_file ${proto_files})
# 3. 如果没有生成,则预定义生成指令 -- 用于在构建项目之间先生成框架代码
string(REPLACE ".proto" ".pb.cc" proto_cc ${proto_file})
string(REPLACE ".proto" ".pb.h" proto_hh ${proto_file})
if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}${proto_cc})
add_custom_command(
PRE_BUILD
COMMAND protoc
ARGS --cpp_out=${CMAKE_CURRENT_BINARY_DIR} -I ${proto_path} --experimental_allow_proto3_optional ${proto_path}/${proto_file}
DEPENDS ${proto_path}/${proto_file}
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc}
COMMENT "生成Protobuf框架代码文件:" ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc}
)
endif()
list(APPEND proto_srcs ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc})
endforeach()
# 4. 获取源码目录下的所有源码文件
set(src_files "")
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/source src_files)
# 5. 声明目标及依赖
add_executable(${target} ${src_files} ${proto_srcs})
# 6. 设置需要连接的库
target_link_libraries(${target} -lgflags -lspdlog -lfmt -lbrpc -lssl -lcrypto -lprotobuf -lleveldb -letcd-cpp-api -lcpprest -lcurl /usr/lib/x86_64-linux-gnu/libjsoncpp.so.19)
set(test_client "file_client")
set(test_files "")
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/test test_files)
add_executable(${test_client} ${test_files} ${proto_srcs})
target_link_libraries(${test_client} -lgtest -lgflags -lspdlog -lfmt -lbrpc -lssl -lcrypto -lprotobuf -lleveldb -letcd-cpp-api -lcpprest -lcurl /usr/lib/x86_64-linux-gnu/libjsoncpp.so.19)
# 7. 设置头文件默认搜索路径
include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../header)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../third/include)
# 8. 设置安装路径
INSTALL(TARGETS ${target} ${test_client} RUNTIME DESTINATION bin)file_client.cc(测试代码)
/*
* 文件客户端单元测试程序
* 作用:对文件存储子服务的四个业务接口(单文件/多文件上传和下载)进行单元测试
* 测试使用 Google Test 框架进行断言,使用 gflags 管理命令行参数
*/
#include <gflags/gflags.h> // 命令行参数解析库
#include <gtest/gtest.h> // Google Test 单元测试框架
#include <thread> // 用于线程操作,测试过程中可能需要等待
#include "etcd.hpp" // 服务注册模块封装,负责服务注册和发现
#include "channel.hpp" // RPC 信道封装,提供 RPC 通信通道
#include "logger.hpp" // 日志模块封装,用于记录调试和错误日志
#include "file.pb.h" // 文件服务相关的 protobuf 消息定义
#include "base.pb.h" // 基础数据类型的 protobuf 定义
#include "utils.hpp" // 工具类封装,包含读写文件、生成 UUID 等常用函数
// 定义命令行参数
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", "文件服务的监控路径");
liren::ServiceManager::channel_ptr channel; // RPC 信道,用于后续调用文件服务的接口
std::string single_file_id; // 保存单个文件上传返回的文件ID,供后续下载测试使用
// 上传单个文件接口测试
TEST(put_test, single_file)
{
// 1. 读取当前目录下的 "Makefile" 文件数据
std::string body;
ASSERT_TRUE(liren::readFile("./Makefile", body));
// 2. 构造文件服务的 RPC 客户端对象(Stub),通过全局信道channel获取
liren::FileService_Stub stub(channel.get());
// 构造上传单个文件的请求消息
liren::PutSingleFileReq req;
req.set_request_id("1111"); // 设置请求ID
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); // 设置文件内容
// 3. 构造 brpc Controller、响应对象,并发起 RPC 调用
brpc::Controller *cntl = new brpc::Controller();
liren::PutSingleFileRsp *rsp = new liren::PutSingleFileRsp();
stub.PutSingleFile(cntl, &req, rsp, nullptr);
// 4. 断言 RPC 调用未失败,并检查上传是否成功
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");
// 保存返回的文件ID,用于后续下载单文件测试
single_file_id = rsp->file_info().file_id();
LOG_DEBUG("文件ID:{}", rsp->file_info().file_id());
}
// 下载单个文件接口测试
TEST(get_test, single_file)
{
// 1. 构造 RPC 客户端对象
liren::FileService_Stub stub(channel.get());
liren::GetSingleFileReq req;
liren::GetSingleFileRsp *rsp;
req.set_request_id("2222"); // 设置请求ID
req.set_file_id(single_file_id); // 设置要下载的文件ID(之前上传返回的ID)
// 2. 构造 Controller、响应对象,发起 RPC 调用
brpc::Controller *cntl = new brpc::Controller();
rsp = new liren::GetSingleFileRsp();
stub.GetSingleFile(cntl, &req, rsp, nullptr);
// 3. 检查 RPC 调用状态和响应内容
ASSERT_FALSE(cntl->Failed());
ASSERT_TRUE(rsp->success());
ASSERT_EQ(single_file_id, rsp->file_data().file_id());
// 4. 将下载的文件数据写入到本地文件 "make_file_download"
liren::writeFile("make_file_download", rsp->file_data().file_content());
}
// 保存上传多个文件返回的文件ID列表,用于后续下载测试
std::vector<std::string> multi_file_id;
// 单元测试:上传多个文件接口测试
TEST(put_test, multi_file)
{
// 1. 分别读取当前目录下的 "base.pb.h" 和 "file.pb.h" 文件数据
std::string body1;
ASSERT_TRUE(liren::readFile("./base.pb.h", body1));
std::string body2;
ASSERT_TRUE(liren::readFile("./file.pb.h", body2));
// 2. 构造 RPC 客户端对象,构造上传多个文件请求消息
liren::FileService_Stub stub(channel.get());
liren::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);
// 3. 构造 Controller、响应对象,并发起 RPC 调用
brpc::Controller *cntl = new brpc::Controller();
liren::PutMultiFileRsp *rsp = new liren::PutMultiFileRsp();
stub.PutMultiFile(cntl, &req, rsp, nullptr);
ASSERT_FALSE(cntl->Failed());
// 4. 检查响应,断言上传成功,同时保存返回的文件ID列表
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)
{
// 1. 构造 RPC 客户端对象,构造下载多个文件请求消息
liren::FileService_Stub stub(channel.get());
liren::GetMultiFileReq req;
liren::GetMultiFileRsp *rsp;
req.set_request_id("4444");
// 添加之前上传的多个文件ID
req.add_file_id_list(multi_file_id[0]);
req.add_file_id_list(multi_file_id[1]);
// 2. 构造 Controller、响应对象,发起 RPC 调用
brpc::Controller *cntl = new brpc::Controller();
rsp = new liren::GetMultiFileRsp();
stub.GetMultiFile(cntl, &req, rsp, nullptr);
ASSERT_FALSE(cntl->Failed());
ASSERT_TRUE(rsp->success());
// 3. 检查响应 map 中是否包含对应的文件数据
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());
// 4. 从响应 map 中提取文件数据,并将内容写入到本地文件中
auto map = rsp->file_data();
auto file_data1 = map[multi_file_id[0]];
liren::writeFile("base_download_file1", file_data1.file_content());
auto file_data2 = map[multi_file_id[1]];
liren::writeFile("file_download_file2", file_data2.file_content());
}
int main(int argc, char *argv[])
{
// 初始化
testing::InitGoogleTest(&argc, argv);
google::ParseCommandLineFlags(&argc, &argv, true);
liren::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 1. 构造 RPC 信道管理对象 ServiceManager
auto sm = std::make_shared<liren::ServiceManager>();
sm->declared(FLAGS_file_service);
// 设置服务上线和下线的回调函数
auto put_cb = std::bind(&liren::ServiceManager::online, sm.get(), std::placeholders::_1, std::placeholders::_2);
auto del_cb = std::bind(&liren::ServiceManager::offline, sm.get(), std::placeholders::_1, std::placeholders::_2);
// 2. 构造服务发现对象,使用 etcd 作为注册中心
liren::Discovery::ptr dclient = std::make_shared<liren::Discovery>(FLAGS_etcd_host, FLAGS_base_service, put_cb, del_cb);
// 3. 通过 ServiceManager 获取对应文件服务的 RPC 信道
channel = sm->getChannel(FLAGS_file_service);
if (!channel) {
// 如果未获取到信道,等待1秒后退出(这里简化处理,实际应重试或报错)
std::this_thread::sleep_for(std::chrono::seconds(1));
return -1;
}
// 4. 运行所有单元测试
return RUN_ALL_TESTS();
}