目录
还记得我们整个项目的服务架构吗?

当语音识别子服务启动后,其首要任务是与注册中心(etcd服务器)建立连接并完成服务注册。
这一过程涉及以下几个关键环节:
-
向注册中心注册 :子服务实例将自身提供的RPC服务信息 (包括服务名称、版本、协议、对外访问地址和端口)**通过API写入etcd。**为了确保注册信息的准确性和实时性,通常会设置一个租约(lease)并定期续约,这样一旦服务实例崩溃或网络分区,etcd能够自动将过期实例从注册表中移除。注册完成后,该服务实例便成为服务发现体系中的一个可用节点。
-
健康检查与保活:子服务后台会启动一个心跳任务,周期性地向etcd刷新租约,告知注册中心自己依然存活。同时,etcd可以配置健康检查机制,主动探测服务实例的状态,进一步增强系统的可靠性。
-
网关的服务发现:客户端发送语音识别请求到网关,网关作为统一的入口,需要知道应该将请求转发给哪个具体的服务实例。为此,网关通常会缓存一份服务注册表,或者实时向etcd查询可用的语音识别服务实例列表。当有多个实例时,网关可以根据负载均衡策略(如轮询、一致性哈希、最少连接数等)选择一个目标实例。
-
RPC调用与结果返回:网关确定目标实例后,会发起RPC调用(基于brpc框架),将客户端的原始请求转发给文件存储子服务。子服务接收到请求后,将语音数据保存到指定的存储介质中,并生成对应的文件标识符。随后,子服务将存储结果封装成RPC响应返回给网关。网关再将响应转换为客户端期望的格式(如HTTP JSON或WebSocket消息)返回给客户端。
一.功能概述
文件服务主要负责系统中各类文件资源的上传与下载操作,支持单文件与多文件两种处理方式,适用于后台管理端及客户端的不同业务场景。
其核心职责包括接收文件数据、调用存储子服务进行持久化处理,以及根据文件标识获取并返回文件内容。
功能详细说明
- 文件上传
- **a. 单个文件上传:**该接口主要用于后台管理场景,例如用户上传头像、发送图片或文件消息等。系统接收到文件数据后,将其转发至文件存储子服务进行保存,并生成对应的文件标识供后续访问使用。
- **b. 多个文件上传:**适用于后台批量处理场景,例如一次性上传多张图片或多份文档。系统将循环处理每一个文件,依次调用存储子服务完成保存,最终统一返回所有文件的处理结果。
- 文件下载
- **a. 单个文件下载:**用于后台获取特定资源(如用户头像)以及客户端获取消息中的图片、语音或文件内容。系统根据请求中携带的文件标识查找对应文件,读取并返回其二进制数据。
- **b. 多个文件下载:**适用于后台批量获取用户头像(如加载联系人列表时)或前端批量下载文件等场景。系统依次处理多个文件标识,读取对应文件数据后统一返回,便于前端进行压缩打包或逐一下载。
接口实现流程
- 单个文件上传流程
-
解析请求,获取待上传文件的元数据,包括文件名、文件大小、文件内容等。
-
为当前文件生成全局唯一的文件ID,作为该文件在存储系统中的标识。
-
以文件ID为名称创建文件(或对象),并将文件内容写入存储系统。
-
文件写入完成后,组织包含文件ID、文件大小、上传状态等信息的响应数据并返回给调用方。
- 多个文件上传流程
-
从请求中解析出多个文件的元数据列表。
-
遍历文件列表,对每一个文件执行以下操作:
-
生成唯一的文件ID。
-
将文件内容写入存储系统(以文件ID命名)。
-
-
所有文件处理完毕后,汇总每个文件的处理结果(如文件ID、上传成功/失败状态),构建统一的响应体返回给客户端。
- 单个文件下载流程
-
解析请求参数,获取所需下载文件的文件ID。
-
根据文件ID在存储系统中定位对应的文件。
-
打开文件,获取其大小并读取全部或指定范围的二进制数据。
-
设置合适的响应头(如Content-Type、Content-Disposition等),将文件数据返回给请求方。
- 多个文件下载流程
-
从请求中获取多个文件ID的列表。
-
依次遍历每个文件ID:
-
根据文件ID定位并读取对应的文件数据。
-
收集每个文件的元数据及内容。
-
-
当所有文件数据准备就绪后,组织响应数据。若为前端批量下载场景,可考虑将多个文件打包为ZIP压缩包后再返回;若为后台批量获取场景,则以结构化数据形式(如JSON)返回文件内容列表。
二.实现
我们整个文件存储子服务的模块划分如下图所示:

那么我们慢慢来实现吧
2.1.RPC服务的定义
2.1.1.base.proto文件的编写
其实在我们的文件存储子服务里,我们不管是什么类型的文件,我们都可以使用二进制流来表示。
因此,我们在这个base.proto里面定义了下面这2个字段,这2个字段专门用于通信过程中存储我们的文件的数据。
cpp
// 文件下载数据,用于响应文件下载请求
message FileDownloadData {
string file_id = 1; // 文件ID
bytes file_content = 2; // 文件的二进制数据
}
// 文件上传数据,用于客户端上传文件
message FileUploadData {
string file_name = 1; // 文件名称
int64 file_size = 2; // 文件大小(字节)
bytes file_content = 3; // 文件的二进制数据
}
那为什么把这2个字段单独放到这里来编写?
- 代码复用
- 多个服务或模块可能都需要使用相同的消息结构。例如,除了文件服务外,聊天服务、用户服务也可能需要传输文件元数据或文件内容。将这些基础消息定义在 base.proto 中,其他 .proto 文件只需通过 import 导入即可直接使用,避免了在每个文件中重复定义相同的内容。
- 当基础数据结构需要修改时(如增加字段),只需修改一处(base.proto),所有依赖它的地方都会自动同步,降低维护成本。
- 模块化与解耦
- 将通用基础类型与具体业务接口分离,使得项目结构更清晰,职责单一。file.proto 专注于定义文件服务的 RPC 接口,而 base.proto 专注于定义跨服务共享的数据模型。这种解耦有助于后续扩展:如果新增一个音视频服务,同样可以复用 base.proto 中的文件相关消息。
2.1.2.file.proto文件的编写
有了上面这个base.proto,那么我们就能专注于我们的文件存储子服务的file.proto文件的编写了
我们file.proto就是为了将我们的RPC服务给定义出来,那么我们究竟需要提供什么RPC服务呢?
我们在上面已经说明了,就是下面这些
- 文件上传
- **a. 单个文件上传:**该接口主要用于后台管理场景,例如用户上传头像、发送图片或文件消息等。系统接收到文件数据后,将其转发至文件存储子服务进行保存,并生成对应的文件标识供后续访问使用。
- **b. 多个文件上传:**适用于后台批量处理场景,例如一次性上传多张图片或多份文档。系统将循环处理每一个文件,依次调用存储子服务完成保存,最终统一返回所有文件的处理结果。
- 文件下载
- **a. 单个文件下载:**用于后台获取特定资源(如用户头像)以及客户端获取消息中的图片、语音或文件内容。系统根据请求中携带的文件标识查找对应文件,读取并返回其二进制数据。
- **b. 多个文件下载:**适用于后台批量获取用户头像(如加载联系人列表时)或前端批量下载文件等场景。系统依次处理多个文件标识,读取对应文件数据后统一返回,便于前端进行压缩打包或逐一下载。
那么我们一个一个来。注意:file.proto是基于base.proto的。
我们这里需要特别强调一下:我们的请求ID和响应ID是必须是一样的
- 下载单个文件
cpp
// 下载单个文件的请求消息
message GetSingleFileReq {
string request_id = 1; // 请求ID,用于唯一标识本次请求
string file_id = 2; // 要下载的文件ID
optional string user_id = 3; // 用户ID(可选),用于鉴权或记录,optional表示可选
optional string session_id = 4;// 会话ID(可选),用于标识所属会话
}
// 下载单个文件的响应消息
message GetSingleFileRsp {
string request_id = 1; // 请求ID,与请求对应
bool success = 2; // 操作是否成功
string errmsg = 3; // 错误信息(如果失败)
optional FileDownloadData file_data = 4; // 文件下载数据(成功时返回),optional表示可选
}
// 文件服务,提供单个/多个文件的获取和上传功能
service FileService {
rpc GetSingleFile(GetSingleFileReq) returns (GetSingleFileRsp); // 下载单个文件
}
我们仔细看看这个响应的FileDownloadData,我们可能懵了,这个其实就是我们base.proto里面定义好的
cpp
// 文件下载数据,用于响应文件下载请求
message FileDownloadData {
string file_id = 1; // 文件ID
bytes file_content = 2; // 文件的二进制数据
}
这里就存放着我们下载的文件的二进制数据
- 下载多个文件
cpp
// 下载多个文件的请求消息
message GetMultiFileReq {
string request_id = 1; // 请求ID
optional string user_id = 2; // 用户ID(可选)
optional string session_id = 3; // 会话ID(可选)
repeated string file_id_list = 4; // 要下载的文件ID列表
}
// 下载多个文件的响应消息
message GetMultiFileRsp {
string request_id = 1; // 请求ID
bool success = 2; // 是否成功
string errmsg = 3; // 错误信息
map<string, FileDownloadData> file_data = 4; // 文件ID到文件下载数据的映射
}
// 文件服务,提供单个/多个文件的下载和上传功能
service FileService {
rpc GetMultiFile(GetMultiFileReq) returns (GetMultiFileRsp); // 下载多个文件
}
这个和下载单个文件的核心区别:文件数量变多了。
我们仔细看一下这个其实和我们下载单个文件的过程是差不多的,只不过这个我们文件数量上去了,所以我们只能使用哈希表<文件ID,文件下载数据>来将它们进行一一对应起来。
- 上传单个文件
cpp
// 上传单个文件的请求消息
message PutSingleFileReq {
string request_id = 1; // 请求ID
optional string user_id = 2; // 用户ID(可选)
optional string session_id = 3; // 会话ID(可选)
FileUploadData file_data = 4; // 文件上传数据
}
// 上传单个文件的响应消息
message PutSingleFileRsp {
string request_id = 1; // 请求ID
bool success = 2; // 是否成功
string errmsg = 3; // 错误信息
FileMessageInfo file_info = 4; // 文件元信息(成功时返回)
}
// 文件服务,提供单个/多个文件的下载和上传功能
service FileService {
rpc PutSingleFile(PutSingleFileReq) returns (PutSingleFileRsp); // 上传单个文件
}
有的人可能懵了,这个FileUploadData是个啥?其实就是我们在base.proto里面定义好的东西
cpp
// 文件上传数据,用于客户端上传文件
message FileUploadData {
string file_name = 1; // 文件名称
int64 file_size = 2; // 文件大小(字节)
bytes file_content = 3; // 文件的二进制数据
}
里面承载着我们客户端发送过来的数据。
那么FileMessageInf又是个啥?
cpp
// 消息结构,表示一条完整的聊天消息
message MessageInfo {
string message_id = 1; // 消息ID,全局唯一
string chat_session_id = 2; // 消息所属聊天会话ID
int64 timestamp = 3; // 消息产生的时间戳(Unix时间戳,秒)
UserInfo sender = 4; // 消息发送者的用户信息
MessageContent message = 5; // 消息内容体
}
至于想要了解更多细节,可以去上面base.proto里面看看
- 上传多个文件
cpp
// 上传多个文件的请求消息
message PutMultiFileReq {
string request_id = 1; // 请求ID
optional string user_id = 2; // 用户ID(可选)
optional string session_id = 3; // 会话ID(可选)
repeated FileUploadData file_data = 4; // 文件上传数据列表
}
// 上传多个文件的响应消息
message PutMultiFileRsp {
string request_id = 1; // 请求ID
bool success = 2; // 是否成功
string errmsg = 3; // 错误信息
repeated FileMessageInfo file_info = 4; // 文件元信息列表
}
// 文件服务,提供单个/多个文件的下载和上传功能
service FileService {
rpc PutMultiFile(PutMultiFileReq) returns (PutMultiFileRsp); // 上传多个文件
}
这个和单个的核心思想没啥区别,就是使用了数组来存储而已
file.proto完整定义
cpp
syntax = "proto3"; // 指定使用 proto3 语法
package IMS; // 定义包名,用于避免命名冲突
import "base.proto"; // 导入基础消息定义,其中包含 FileDownloadData、FileUploadData、FileMessageInfo 等类型
option cc_generic_services = true; // 启用 C++ 通用服务接口生成
// 下载单个文件的请求消息
message GetSingleFileReq {
string request_id = 1; // 请求ID,用于唯一标识本次请求
string file_id = 2; // 要下载的文件ID
optional string user_id = 3; // 用户ID(可选),用于鉴权或记录,optional表示可选
optional string session_id = 4;// 会话ID(可选),用于标识所属会话
}
// 下载单个文件的响应消息
message GetSingleFileRsp {
string request_id = 1; // 请求ID,与请求对应
bool success = 2; // 操作是否成功
string errmsg = 3; // 错误信息(如果失败)
optional FileDownloadData file_data = 4; // 文件下载数据(成功时返回),optional表示可选
}
// 下载多个文件的请求消息
message GetMultiFileReq {
string request_id = 1; // 请求ID
optional string user_id = 2; // 用户ID(可选)
optional string session_id = 3; // 会话ID(可选)
repeated string file_id_list = 4; // 要下载的文件ID列表
}
// 下载多个文件的响应消息
message GetMultiFileRsp {
string request_id = 1; // 请求ID
bool success = 2; // 是否成功
string errmsg = 3; // 错误信息
map<string, FileDownloadData> file_data = 4; // 文件ID到文件下载数据的映射
}
// 上传单个文件的请求消息
message PutSingleFileReq {
string request_id = 1; // 请求ID
optional string user_id = 2; // 用户ID(可选)
optional string session_id = 3; // 会话ID(可选)
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; // 请求ID
optional string user_id = 2; // 用户ID(可选)
optional string session_id = 3; // 会话ID(可选)
repeated FileUploadData file_data = 4; // 文件上传数据列表
}
// 上传多个文件的响应消息
message PutMultiFileRsp {
string request_id = 1; // 请求ID
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); // 上传多个文件
}
2.1.3.RPC服务的定义
那么我们上面在file.proto里面定义好了我们的RPC服务,我们还需要进行重写虚函数来完全实现我们的RPC服务。
首先我们需要明确的知道,我们的RPC服务核心就是 文件上传/下载 服务。
那么我们的文件上传/下载需要什么东西??
其实很简单,就是我文件上传到哪里?我的文件从哪里下载??
那么我们就需要一个目录,这个目录专门用于存储用户上传的文件,用户想要下载文件,也是基于这个目录来
cpp
// 文件RPC服务实现类,继承自protobuf生成的FileService接口
class FileServiceImpl : public IMS::FileService
{
public:
// 构造函数:接收文件存储路径,并确保目录存在
FileServiceImpl(const std::string &storage_path):
_storage_path(storage_path)
{
umask(0); // 设置文件创建掩码,确保权限不受影响
mkdir(storage_path.c_str(), 0775); // 创建存储目录(如果不存在),权限为rwxrwxr-x
if (_storage_path.back() != '/') _storage_path.push_back('/'); // 保证路径以'/'结尾
}
~FileServiceImpl(){} // 析构函数(空实现)
......
private:
std::string _storage_path; // 文件存储的根目录路径
};
整个项目就变得很清晰明了起来。
我们就基于这个基础目录来完成下面四个RPC服务的定义
- 处理单个文件下载的RPC请求
- 处理多个文件批量下载的RPC请求
- 处理单个文件上传的RPC请求
- 处理多个文件批量上传的RPC请求
其实这个都很简单的
处理单个文件下载的RPC请求
特别需要注意,我们这里设计的是请求ID必须和响应ID是一致的
其他的倒是没什么好说的了
cpp
// 处理单个文件下载的RPC请求
void GetSingleFile(google::protobuf::RpcController* controller,
const ::IMS::GetSingleFileReq* request,//我们设定的函数参数
::IMS::GetSingleFileRsp* response,//我们设定的返回值
::google::protobuf::Closure* done)
{
brpc::ClosureGuard rpc_guard(done); // 确保done->Run()在函数退出时被调用
response->set_request_id(request->request_id()); // 回填请求ID,我们这里设计的是请求ID必须和响应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. 组织成功响应,填充文件ID和内容
response->set_success(true);
response->mutable_file_data()->set_file_id(fid);//mutable_开头的才能去进行写操作
response->mutable_file_data()->set_file_content(body);
}
该RPC服务的功能是处理客户端的单文件下载请求,主要包含以下步骤:
-
接收请求
客户端发送下载请求,其中包含要下载的文件的唯一标识(如文件名或ID)。
-
定位文件
根据文件标识在服务器端找到对应的文件路径。
-
读取文件内容
尝试读取该文件的数据。如果读取成功,则将文件内容准备好;若失败(如文件不存在、无权限等),则记录错误。
-
返回响应
-
成功时:将文件内容封装进响应,并标记下载成功。
-
失败时:返回失败状态,并附带错误信息(如"读取文件数据失败")。
-
-
请求-响应关联
每个响应都会携带对应的请求ID,确保客户端能够匹配请求与响应。
处理多个文件批量下载的RPC请求
cpp
// 处理多个文件批量下载的RPC请求
void GetMultiFile(google::protobuf::RpcController* controller,
const ::IMS::GetMultiFileReq* request,
::IMS::GetMultiFileRsp* response,
::google::protobuf::Closure* done)
{
brpc::ClosureGuard rpc_guard(done); // 确保done->Run()在函数退出时被调用
response->set_request_id(request->request_id());// 回填请求ID,我们这里设计的是请求ID必须和响应ID必须是一致的
// 遍历请求中的每个文件ID
for (int i = 0; i < request->file_id_list_size(); i++)
{
std::string fid = request->file_id_list(i);//获取文件ID
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;
}
// 构建单个文件下载数据,并插入响应的map中
FileDownloadData data;
data.set_file_id(fid);
data.set_file_content(body);
response->mutable_file_data()->insert({fid, data});
}
response->set_success(true);
}
该RPC服务用于处理客户端的批量文件下载请求,主要流程如下:
-
接收请求
客户端发送批量下载请求,其中包含一个文件标识列表(例如多个文件名或ID)。
-
遍历并读取每个文件
服务端依次根据每个文件标识找到对应的文件,并尝试读取其内容。
-
原子性处理
-
如果任意一个文件读取失败(如文件不存在、无权限等),则整个批量请求判定为失败,立即返回错误信息,不再继续处理后续文件。
-
只有当所有文件都成功读取时,才会继续下一步。
-
-
组装成功响应
将每个成功读取的文件内容与其标识对应起来(例如使用键值对结构),统一封装到响应中,并标记请求成功。
-
请求-响应关联
每个响应都会携带对应的请求ID,确保客户端能准确匹配请求与响应。
简言之,该服务实现了**"要么全部成功下载,要么一个都不返回"**的批量文件下载逻辑。
特别需要注意的是:文件ID里面存储的其实就是文件名
处理单个文件上传的RPC请求
cpp
// 处理单个文件上传的RPC请求
void PutSingleFile(google::protobuf::RpcController* controller,
const ::IMS::PutSingleFileReq* request,
::IMS::PutSingleFileRsp* response,
::google::protobuf::Closure* done)
{
brpc::ClosureGuard rpc_guard(done);
response->set_request_id(request->request_id());// 回填请求ID,我们这里设计的是请求ID必须和响应ID必须是一致的
// 1. 生成唯一UUID作为文件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. 组织成功响应,返回文件ID、大小和原始文件名
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());
}
该RPC服务用于处理客户端的单文件上传请求,主要流程如下:
-
接收上传内容
客户端将待上传的文件数据(包括文件内容、原始文件名、文件大小等)发送给服务端。
-
生成唯一标识
服务端为上传的文件生成一个全局唯一的ID(如UUID),该ID将作为文件在服务器上的存储名称,确保不会与其他文件冲突。
-
保存文件
将接收到的文件内容写入服务器的存储目录中,文件以生成的唯一ID命名。
-
返回上传结果
-
成功时:返回成功状态,并附带该文件的唯一ID、原始文件名和文件大小,方便客户端后续通过该ID进行下载或管理。
-
失败时:返回失败状态和错误信息(例如"写入文件数据失败")。
-
-
请求-响应关联
每个响应都会携带对应的请求ID,确保客户端能准确匹配请求与响应。
简言之,该服务实现了**"接收文件 → 分配唯一ID → 存储 → 告知结果"**的完整上传逻辑,是文件存储系统的基础写入接口。
特别需要注意的是:文件ID里面存储的其实就是文件名
处理多个文件批量上传的RPC请求
cpp
// 处理多个文件批量上传的RPC请求
void PutMultiFile(google::protobuf::RpcController* controller,
const ::IMS::PutMultiFileReq* request,
::IMS::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(); // 每个文件生成独立ID
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;
}
// 在响应中添加一个文件信息对象
IMS::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);
}
该RPC服务用于处理客户端的批量文件上传请求,主要流程如下:
-
接收多个文件
客户端在单个请求中携带多个文件的数据,每个文件包含文件内容、原始文件名、文件大小等信息。
-
逐个处理文件
服务端遍历请求中的每个文件:
-
为当前文件生成一个全局唯一的ID(如UUID),作为该文件在服务器上的存储名称。
-
将文件内容写入服务器的存储目录,以生成的唯一ID命名文件。
-
-
原子性保障
-
如果任何一个文件写入失败(例如磁盘空间不足、权限问题等),则整个批量上传任务判定为失败,立即停止处理并返回错误信息,已写入的文件不会保留(逻辑上回滚,具体实现可能需额外处理)。
-
只有所有文件均成功写入,才视为上传成功。
-
-
返回上传结果
-
成功时:返回成功状态,并为每个上传的文件提供其对应的唯一ID、原始文件名和文件大小,方便客户端后续进行下载或管理。
-
失败时:返回失败状态和错误信息。
-
-
请求-响应关联
每个响应都会携带对应的请求ID,确保客户端能准确匹配请求与响应。
简言之,该服务实现了**"要么全部成功上传,要么一个都不保存"**的批量文件上传逻辑,是文件存储系统的高效写入接口。
为什么上传的时候需要创建一个新的文件ID来给新文件,而下载则是提取请求里面的文件ID来找文件?
上传和下载是两个角色完全相反的操作,它们处理的是信息的不同阶段。
可以这样理解:
- 上传:创建新资源,需要一个"身份证"
-
核心任务 :客户端要把一个全新的文件交给服务器保管。
-
关键问题:全世界有无数个文件叫"我的照片.jpg",如果直接用它当名字存,服务器上早就乱套了------别人的同名文件会把你覆盖掉。
-
文件ID的作用 :服务器作为保管方,需要给这个新文件颁发一个全世界独一无二的"身份证号"。这个号码(文件ID)完美地解决了重名和冲突问题,确保你的文件是唯一的。这就是为什么上传时一定要"生成"一个新ID。
- 下载:访问已有资源,需要出示"身份证号"
-
核心任务:客户端想把之前存好的文件拿回来。
-
关键问题:服务器怎么知道你要拿哪一个?服务器硬盘里可能有几亿个文件。
-
为什么不用生成 :这时候再生成一个新的文件ID 就完全搞错了方向。下载的关键是**"寻址"** 而不是"创建"。客户端必须告诉服务器:"请把
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx这个号码对应的文件给我。" -
文件ID的用途 :此时,之前生成的文件ID 就成了查找文件的钥匙。服务器只需要拿着这个ID去仓库(存储路径)里找就行了。
总结一下
把整个过程想象成去自助储物柜存东西:
-
上传(存包):
-
你拿了一个包(文件)过去。
-
系统不会问你的包叫什么名字,它会直接给你一个唯一的号码牌(文件ID),然后把你包放进对应号码的柜子里。
-
这里必须生成新号码,因为所有号码牌必须是唯一的,不能重复。
-
-
下载(取包):
-
你拿着那个**唯一的号码牌(文件ID)**回来。
-
系统看到号码牌,就知道去几号柜子把包取出来给你。
-
这里不需要生成新号码,你手里必须有之前那个号码牌才能取回东西。如果你现场再要一个新号码,那系统就糊涂了,不知道你到底要取哪一个。
-
我们的客户端必须保存着那个<文件名,文件ID>的对应关系。
在我们这里设计的情况是
处理下载请求:
cpp
// 1. 取出请求中的文件ID(即文件名)
std::string fid = request->file_id();//文件ID就是文件名
std::string filename = _storage_path + fid; // 拼接完整文件路径
处理上传请求
cpp
// 1. 生成唯一UUID作为文件ID(同时也是文件名)
std::string fid = uuid();//uuid是会生成独一无二的一个编号的。我们拿uuid来生成文件ID.
std::string filename = _storage_path + fid;
可以看到,
- 在上传文件的过程中,我们的文件ID是由uuid()生成的,并且这个文件ID就是作为我们的这个文件的名字。
- 在下载文件的过程中,我们也是那这个文件ID去找这个文件的
2.2.FileServer类
这个类是我们文件存储子服务的核心模块。
在整个文件存储子服务中,FileServer 扮演着服务实例的整合者与启动入口的角色。它的主要作用如下:
-
聚合核心组件
FileServer内部持有了两个关键对象:-
RPC 服务器(负责接收并处理客户端的网络请求)
-
服务注册客户端 (负责将当前服务的信息注册到注册中心,如 etcd,以便客户端发现)
它将这两个组件组合在一起,形成了一个完整的、可对外提供文件上传下载功能的服务实例。
-
-
统一生命周期管理
通过
start()方法启动 RPC 服务器后,服务便开始持续运行,直到收到退出信号。这一封装简化了服务启动的流程,调用者只需调用start()即可让服务正常工作,无需关心底层网络细节。 -
作为构建过程的产物
FileServer通常由FileServerBuilder创建,建造者模式负责分别配置注册客户端和 RPC 服务器,最后生成一个配置完毕的FileServer对象。这种设计保证了服务创建的灵活性和正确性。 -
代表一个独立运行的服务单元
每个
FileServer实例对应着一个具体的文件存储服务节点,它拥有自己的存储路径、监听地址和注册信息。多个这样的节点可以共同组成一个分布式的文件存储集群。
简而言之,FileServer 是整个文件存储子服务的服务端封装 ,它将 RPC 网络层、服务发现与业务逻辑(FileServiceImpl)粘合在一起,对外提供稳定、可被发现的文件上传下载能力。
cpp
// 文件存储子服务的服务器类,封装RPC服务器和服务注册客户端
class FileServer {
public:
using ptr = std::shared_ptr<FileServer>; // 智能指针类型别名
// 构造函数:接收注册客户端和RPC服务器对象
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; // 服务注册客户端(用于向etcd注册服务)
std::shared_ptr<brpc::Server> _rpc_server; // brpc服务器对象
};
brpc::Server 的 RunUntilAskedToQuit() 方法用于阻塞当前线程,启动服务器的服务循环,使服务器持续处理 RPC 请求,直到收到明确的退出指令(如调用 Stop() 或接收到中断信号)。
简单来说,它让服务器开始运行并等待停止,通常在主线程中调用,以保证进程不会立即退出。
2.3.FileServerBuilder类
这个类就是辅助构建这个FileServer类
cpp
// 构造函数:接收注册客户端和RPC服务器对象
FileServer(const Registry::ptr ®_client,
const std::shared_ptr<brpc::Server> &server):
_reg_client(reg_client),
_rpc_server(server){}
我们发现这个FileServer类的构造函数可是使用了拷贝构造函数。那么我们就需要提前构建好一个
Registry::ptr对象和brpc::Server对象。
那么我们闭着眼都知道这个成员变量应该是啥了吧
cpp
// FileServer的构建者类,采用建造者模式简化对象创建
class FileServerBuilder
{
public:
......
private:
Registry::ptr _reg_client; // 服务注册客户端(由make_reg_object创建)
std::shared_ptr<brpc::Server> _rpc_server; // RPC服务器(由make_rpc_server创建)
};
2.3.1.创建Registry::ptr对象
cpp
// 构造服务注册客户端对象,并执行注册操作
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); // 创建Registry实例
_reg_client->registry(service_name, access_host); // 向etcd注册服务
}
这个的思路和我们语音识别子服务可以说是一模一样的。
可以看到,我们这里指定了注册中心(etcd服务器)的IP地址创建了一个服务注册者,然后我们借助这个服务注册者来往我们的注册中心注册了服务名以及这个服务所在IP地址和端口号。
那么我们也可以去看看这个注册者内部的代码
cpp
// 服务注册客户端类
// 功能:向etcd注册服务实例信息,并通过租约保活机制维护服务在线状态
class Registry
{
public:
using ptr = std::shared_ptr<Registry>; // 智能指针类型别名
// 构造函数,初始化etcd客户端并创建租约保活对象
// host: etcd服务器地址(例如 "http://127.0.0.1:2379")
Registry(const std::string &host) : _client(std::make_shared<etcd::Client>(host)), // 创建etcd客户端
_keep_alive(_client->leasekeepalive(3).get()), // 创建租约保活对象,租约TTL为3秒,这里采用leasekeepalive,会自动续约
_lease_id(_keep_alive->Lease())
{
} // 获取租约ID
// 析构函数,取消租约保活,释放资源
~Registry()
{
_keep_alive->Cancel(); // 释放租约
}
// 注册服务信息到etcd
// key: 服务实例的键(通常包含服务名和实例标识,如 "/services/user/192.168.1.1:8080")
// val: 服务实例的值(如IP地址、端口等元数据)
// 返回值:注册成功返回true,失败返回false
bool registry(const std::string &key, const std::string &val)
{
// 向etcd写入键值对,并关联租约ID(租约到期后自动删除)
auto resp = _client->put(key, val, _lease_id).get(); // .get()同步等待结果,只有等到这个键值对真正写入这个etcd服务器后才会自动返回
if (resp.is_ok() == false)
{
LOG_ERROR("注册数据失败:{}", resp.error_message()); // 日志记录错误
return false;
}
return true;
}
private:
std::shared_ptr<etcd::Client> _client; // etcd客户端对象
std::shared_ptr<etcd::KeepAlive> _keep_alive; // 租约保活对象,负责自动续期,注意我们的_keep_alive的构造是依赖于_client,所以放在这个位置
uint64_t _lease_id; // 租约ID,用于关联注册的数据,注意我们的_lease_id的构造是依赖于_keep_alive,所以放在这个位置
};
可以看到注册服务本质就是往注册中心(etcd服务器)写入一个键值对
- 键:服务名称
- 值:服务的IP地址:端口号
这个就很明显了
2.3.2.构造brpc::Server对象
brpc::Server对象有啥作用?
- brpc::Server 启动后持续监听指定端口 ,接收客户端发来的 RPC 请求。
- 它首先解析请求内容,提取出目标服务名、方法名以及序列化的请求数据。
- 随后,Server 依据内部维护的服务路由表(通过 AddService 注册的服务实例)快速定位到对应的服务实现对象(如 FileServiceImpl),并调用该对象中与请求方法名匹配的业务处理函数。
- 最终,请求数据被反序列化后传递给用户定义的逻辑,而响应结果则经 Server 封装后返回给客户端。
整个过程对开发者透明,只需专注于服务实现本身。
cpp
// 构造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>(); // 创建brpc Server对象
FileServiceImpl *file_service = new FileServiceImpl(path); // 创建服务实现实例,注意FileServiceImpl是上面我们自己定义的
// 将服务添加到服务器,指定服务器拥有该服务对象所有权
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();
}
}
注意:我们这里设定我们文件上传/下载的目录默认是./data/
2.3.3.构造FileServer对象
我们发现这个FileServer类的构造函数可是使用了拷贝构造函数。那么我们就需要提前构建好一个
Registry::ptr对象和brpc::Server对象。
那么现在我们构造好了Registry::ptr对象和brpc::Server对象,我们现在就可以来构造FileServer对象了
cpp
// 构建并返回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;
}
还是很清晰明了的。
2.4.完整代码
file_server.hpp
cpp
// 包含brpc服务器库和日志库
#include <brpc/server.h>
#include <butil/logging.h>
// 包含服务注册、日志、工具函数和protobuf生成的头文件
#include "etcd.hpp" // 服务注册模块封装
#include "logger.hpp" // 日志模块封装
#include "utils.hpp" // 工具函数(如uuid, readFile, writeFile)
#include "base.pb.h" // 基础消息类型
#include "file.pb.h" // 文件服务的RPC接口定义
// 定义命名空间IMS,用于封装当前项目的所有组件
namespace IMS
{
// 文件RPC服务实现类,继承自protobuf生成的FileService接口
class FileServiceImpl : public IMS::FileService
{
public:
// 构造函数:接收文件存储路径,并确保目录存在
FileServiceImpl(const std::string &storage_path):
_storage_path(storage_path)
{
umask(0); // 设置文件创建掩码,确保权限不受影响
mkdir(storage_path.c_str(), 0775); // 创建存储目录(如果不存在),权限为rwxrwxr-x
if (_storage_path.back() != '/') _storage_path.push_back('/'); // 保证路径以'/'结尾
}
~FileServiceImpl(){} // 析构函数(空实现)
// 处理单个文件下载的RPC请求
void GetSingleFile(google::protobuf::RpcController* controller,
const ::IMS::GetSingleFileReq* request,//我们设定的函数参数
::IMS::GetSingleFileRsp* response,//我们设定的返回值
::google::protobuf::Closure* done)
{
brpc::ClosureGuard rpc_guard(done); // 确保done->Run()在函数退出时被调用
response->set_request_id(request->request_id()); // 回填请求ID,我们这里设计的是请求ID必须和响应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. 组织成功响应,填充文件ID和内容
response->set_success(true);
response->mutable_file_data()->set_file_id(fid);//mutable_开头的才能去进行写操作
response->mutable_file_data()->set_file_content(body);
}
// 处理多个文件批量下载的RPC请求
void GetMultiFile(google::protobuf::RpcController* controller,
const ::IMS::GetMultiFileReq* request,
::IMS::GetMultiFileRsp* response,
::google::protobuf::Closure* done)
{
brpc::ClosureGuard rpc_guard(done); // 确保done->Run()在函数退出时被调用
response->set_request_id(request->request_id());// 回填请求ID,我们这里设计的是请求ID必须和响应ID必须是一致的
// 遍历请求中的每个文件ID
for (int i = 0; i < request->file_id_list_size(); i++)
{
std::string fid = request->file_id_list(i);//获取文件ID
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;
}
// 构建单个文件下载数据,并插入响应的map中
FileDownloadData data;
data.set_file_id(fid);
data.set_file_content(body);
response->mutable_file_data()->insert({fid, data});
}
response->set_success(true);
}
// 处理单个文件上传的RPC请求
void PutSingleFile(google::protobuf::RpcController* controller,
const ::IMS::PutSingleFileReq* request,
::IMS::PutSingleFileRsp* response,
::google::protobuf::Closure* done)
{
brpc::ClosureGuard rpc_guard(done);
response->set_request_id(request->request_id());// 回填请求ID,我们这里设计的是请求ID必须和响应ID必须是一致的
// 1. 生成唯一UUID作为文件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. 组织成功响应,返回文件ID、大小和原始文件名
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());
}
// 处理多个文件批量上传的RPC请求
void PutMultiFile(google::protobuf::RpcController* controller,
const ::IMS::PutMultiFileReq* request,
::IMS::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(); // 每个文件生成独立ID
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;
}
// 在响应中添加一个文件信息对象
IMS::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; // 文件存储的根目录路径
};
// 文件存储子服务的服务器类,封装RPC服务器和服务注册客户端
class FileServer {
public:
using ptr = std::shared_ptr<FileServer>; // 智能指针类型别名
// 构造函数:接收注册客户端和RPC服务器对象
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; // 服务注册客户端(用于向etcd注册服务)
std::shared_ptr<brpc::Server> _rpc_server; // brpc服务器对象
};
// FileServer的构建者类,采用建造者模式简化对象创建
class FileServerBuilder
{
public:
// 构造服务注册客户端对象,并执行注册操作
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); // 创建Registry实例
_reg_client->registry(service_name, access_host); // 向etcd注册服务
}
// 构造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>(); // 创建brpc Server对象
FileServiceImpl *file_service = new FileServiceImpl(path); // 创建服务实现实例,注意FileServiceImpl是上面我们自己定义的
// 将服务添加到服务器,指定服务器拥有该服务对象所有权
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; // 服务注册客户端(由make_reg_object创建)
std::shared_ptr<brpc::Server> _rpc_server; // RPC服务器(由make_rpc_server创建)
};
} // namespace IMS
三.文件存储子服务的实现
基于上面这些准备工作,我们终于可以搭建出我们的文件存储子服务了。
file_server.cc
cpp
// 包含文件服务器的头文件
#include "file_server.hpp"
// 定义命令行参数:运行模式(调试/发布)
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
// 定义命令行参数:日志输出文件(发布模式下有效)
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
// 定义命令行参数:日志输出等级(发布模式下有效)
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");
// 定义命令行参数:服务注册中心地址(etcd地址)
DEFINE_string(registry_host, "http://127.0.0.1:2379", "服务注册中心地址");
// 定义命令行参数:服务监控根目录(用于服务发现)
DEFINE_string(base_service, "/service", "服务监控根目录");
// 定义命令行参数:当前实例在注册中心的名称
DEFINE_string(instance_name, "/file_service/instance", "当前实例名称");
// 定义命令行参数:当前实例的外部访问地址(IP:端口)
DEFINE_string(access_host, "127.0.0.1:10002", "当前实例的外部访问地址");
// 定义命令行参数:文件存储的根路径
DEFINE_string(storage_path, "./data/", "当前实例的外部访问地址");
// 定义命令行参数:RPC服务器监听端口
DEFINE_int32(listen_port, 10002, "Rpc服务器监听端口");
// 定义命令行参数:RPC调用超时时间(-1表示不超时)
DEFINE_int32(rpc_timeout, -1, "Rpc调用超时时间");
// 定义命令行参数:RPC服务器的IO线程数量
DEFINE_int32(rpc_threads, 1, "Rpc的IO线程数量");
int main(int argc, char *argv[])
{
// 1. 解析命令行参数,将参数值赋给对应的FLAG变量
google::ParseCommandLineFlags(&argc, &argv, true);
// 2. 根据解析出的运行模式和日志配置,初始化日志系统
IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 3. 创建文件服务器构建器对象,用于组装服务器组件
IMS::FileServerBuilder fsb;
// 4. 构建RPC服务器模块:指定监听端口、超时时间、IO线程数和文件存储路径
fsb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads, FLAGS_storage_path);
// 5. 构建服务注册模块:指定注册中心地址、服务名称(根目录+实例名)和本机访问地址
fsb.make_reg_object(FLAGS_registry_host, FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host);
// 6. 使用构建器生成最终的FileServer对象(整合了RPC服务器和注册客户端)
auto server = fsb.build();
// 7. 启动服务器(阻塞运行,直到收到退出信号)
server->start();
// 8. 程序正常退出
return 0;
}
特别需要注意:我们这里设定的是,
- 我们这里往注册中心注册的键值对默认是</service/file_service/instance,127.0.0.1:10002>
- 文件存储子服务运行在127.0.0.1:10002上
- 设定我们文件上传/下载的目录默认是./data/
四.测试
我们这里测试是采用了Gtest来对我们的文件存储子服务进行测试。
我们就采用一个文件来进行我们的测试
file_client.cc
cpp
// 编写一个file客户端程序,对文件存储子服务进行单元测试
// 包含gflags命令行参数解析库
#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 "utils.hpp"
// 定义命令行参数:运行模式(调试/发布)
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
// 定义命令行参数:日志输出文件(发布模式下有效)
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
// 定义命令行参数:日志输出等级(发布模式下有效)
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");
// 定义命令行参数:etcd服务注册中心地址
DEFINE_string(etcd_host, "http://127.0.0.1:2379", "服务注册中心地址");
// 定义命令行参数:服务监控根目录(用于服务发现)
DEFINE_string(base_service, "/service", "服务监控根目录");
// 定义命令行参数:文件存储子服务的名称(用于服务发现)
DEFINE_string(file_service, "/service/file_service", "服务监控根目录");
// 全局RPC信道指针,用于所有测试用例中的RPC调用
IMS::ServiceChannel::ChannelPtr channel;
......中间的测试代码
// 主函数
int main(int argc, char *argv[])
{
// 初始化Google Test框架,解析命令行参数(包括gtest自己的参数)
testing::InitGoogleTest(&argc, argv);
// 解析gflags命令行参数
google::ParseCommandLineFlags(&argc, &argv, true);
// 根据解析出的运行模式和日志配置,初始化日志系统
IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 1. 先构造Rpc信道管理对象(用于维护多个服务实例的信道)
auto sm = std::make_shared<IMS::ServiceManager>();
// 声明要关注的服务名称(文件存储子服务)
sm->declared(FLAGS_file_service);//声明我们关注的服务是/service/file_service,只有我们关注的服务在服务上下线时才会去调用我们的服务上线和下线时的回调函数
// 定义服务上线和下线时的回调函数(绑定到ServiceManager的对应方法)
auto put_cb = std::bind(&IMS::ServiceManager::onServiceOnline, sm.get(), std::placeholders::_1, std::placeholders::_2);
auto del_cb = std::bind(&IMS::ServiceManager::onServiceOffline, sm.get(), std::placeholders::_1, std::placeholders::_2);
// 2. 构造服务发现对象,连接etcd,监控服务变化
IMS::Discovery::ptr dclient = std::make_shared<IMS::Discovery>(FLAGS_etcd_host, FLAGS_base_service, put_cb, del_cb);
// 3. 通过Rpc信道管理对象,获取提供文件服务的信道(选择一个可用的服务实例)
channel = sm->choose(FLAGS_file_service);
if (!channel) {
// 如果没有获取到信道(服务未就绪),休眠1秒后返回错误
std::this_thread::sleep_for(std::chrono::seconds(1));
return -1;
}
// 4. 运行所有Google Test测试用例
return RUN_ALL_TESTS();
}
注意我们的客户端设定的是
- 监控的服务是:/service
- 关注的服务是:/service/file_service
只有关注的服务在上下线的时候才会调用对应的服务上下线处理回调函数。
那么接下来我们就来补充我们的测试代码
4.1.单文件上传/下载功能测试
在测试之前,我们先搞清楚一件事情:
上传和下载是两个角色完全相反的操作,它们处理的是信息的不同阶段。
可以这样理解:
- 上传:创建新资源,需要一个"身份证"
-
核心任务 :客户端要把一个全新的文件交给服务器保管。
-
关键问题:全世界有无数个文件叫"我的照片.jpg",如果直接用它当名字存,服务器上早就乱套了------别人的同名文件会把你覆盖掉。
-
文件ID的作用 :服务器作为保管方,需要给这个新文件颁发一个全世界独一无二的"身份证号"。这个号码(文件ID)完美地解决了重名和冲突问题,确保你的文件是唯一的。这就是为什么上传时一定要"生成"一个新ID。
- 下载:访问已有资源,需要出示"身份证号"
-
核心任务:客户端想把之前存好的文件拿回来。
-
关键问题:服务器怎么知道你要拿哪一个?服务器硬盘里可能有几亿个文件。
-
为什么不用生成 :这时候再生成一个新的文件ID 就完全搞错了方向。下载的关键是**"寻址"** 而不是"创建"。客户端必须告诉服务器:"请把
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx这个号码对应的文件给我。" -
文件ID的用途 :此时,之前生成的文件ID 就成了查找文件的钥匙。服务器只需要拿着这个ID去仓库(存储路径)里找就行了。
总结一下
把整个过程想象成去自助储物柜存东西:
-
上传(存包):
-
你拿了一个包(文件)过去。
-
系统不会问你的包叫什么名字,它会直接给你一个唯一的号码牌(文件ID),然后把你包放进对应号码的柜子里。
-
这里必须生成新号码,因为所有号码牌必须是唯一的,不能重复。
-
-
下载(取包):
-
你拿着那个**唯一的号码牌(文件ID)**回来。
-
系统看到号码牌,就知道去几号柜子把包取出来给你。
-
这里不需要生成新号码,你手里必须有之前那个号码牌才能取回东西。如果你现场再要一个新号码,那系统就糊涂了,不知道你到底要取哪一个。
-
我们的客户端必须保存着那个<文件名,文件ID>的对应关系。
由于我们这里测试的过程中,我们上传/下载其实是同一个文件,所以我们只需要简单的保存一下我们的文件ID即可。
我们先准备一个全局变量来保存我们的文件ID
cpp
// 全局变量,保存单文件上传后返回的文件ID,供下载测试使用
std::string single_file_id;
单文件上传功能测试
cpp
// 测试用例:单文件上传
TEST(put_test, single_file)
{
// 1. 读取当前目录下的Makefile文件数据
std::string body;
ASSERT_TRUE(IMS::readFile("./Makefile", body));
// 2. 实例化RPC调用客户端对象,通过全局信道发起RPC调用
IMS::FileService_Stub stub(channel.get());
// 构造单文件上传请求
IMS::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); // 文件内容
// 创建RPC控制器和响应对象
brpc::Controller *cntl = new brpc::Controller();
IMS::PutSingleFileRsp *rsp = new IMS::PutSingleFileRsp();
// 发起单文件上传RPC调用
stub.PutSingleFile(cntl, &req, rsp, nullptr);
// 断言RPC调用成功(无网络错误)
ASSERT_FALSE(cntl->Failed());
// 3. 检测返回值中上传是否成功
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(); // 保存返回的文件ID
LOG_DEBUG("文件ID:{}", rsp->file_info().file_id()); // 打印日志
}
这个函数是一个单元测试用例,用于验证单文件上传功能的正确性。它的执行过程如下:
-
准备测试数据
从本地当前目录下读取一个名为
Makefile的文件(这个文件需要我们自己去准备,提前放到这个客户端程序所在目录里,不过我们使用的是CMake,所以它会自动生成这个Makefile文件,我们就不必去自己准备了),获取其内容,作为待上传的文件数据。 -
构造上传请求
创建一个文件上传请求,其中包含:
-
一个唯一的请求标识(用于追踪)
-
原始文件名(
Makefile) -
文件大小
-
完整的文件内容
-
-
发起RPC调用
通过RPC客户端将请求发送给文件存储服务,调用对应的单文件上传接口。
-
验证响应
-
检查RPC调用本身是否成功(无网络错误)。
-
检查业务返回结果:上传操作是否成功标志为真。
-
验证返回的文件大小是否与上传前一致。
-
验证返回的原始文件名是否正确。
-
-
记录结果
从响应中提取服务端为文件生成的唯一ID,并保存下来(供后续下载测试使用),同时打印日志记录该ID。
简而言之,这个测试用例模拟了客户端上传一个文件的全过程,并断言服务端正确接收并存储了文件,且返回了符合预期的信息。
正常的运行结果就是我们服务端程序所在目录里面的./data/里面多了一个Makefile文件
单文件下载功能测试
cpp
// 测试用例:单文件下载
TEST(get_test, single_file)
{
// 发起RPC调用,进行文件下载
IMS::FileService_Stub stub(channel.get());
// 构造单文件下载请求
IMS::GetSingleFileReq req;
IMS::GetSingleFileRsp *rsp;
req.set_request_id("2222"); // 设置请求ID
req.set_file_id(single_file_id); // 设置要下载的文件ID(来自上传测试)
// 创建RPC控制器和响应对象
brpc::Controller *cntl = new brpc::Controller();
rsp = new IMS::GetSingleFileRsp();
// 发起单文件下载RPC调用
stub.GetSingleFile(cntl, &req, rsp, nullptr);
// 断言RPC调用成功
ASSERT_FALSE(cntl->Failed());
// 断言业务返回成功
ASSERT_TRUE(rsp->success());
// 验证返回的文件ID与请求的一致
ASSERT_EQ(single_file_id, rsp->file_data().file_id());
// 将下载的文件数据写入本地文件(保存为make_file_download)
IMS::writeFile("make_file_download", rsp->file_data().file_content());
}
这个函数是一个单元测试用例,用于验证单文件下载功能的正确性。它的执行过程如下:
-
构造下载请求
创建一个文件下载请求,其中包含:
-
一个唯一的请求标识(用于追踪)
-
要下载的文件的唯一ID(该ID来自之前单文件上传测试中保存的结果,说明我们想要下载的文件和原来那个我们上传的文件是一模一样的,也就是之前上传的Makefile,同时也是./data/里面的那个文件)
-
-
发起RPC调用
通过RPC客户端将请求发送给文件存储服务,调用对应的单文件下载接口,尝试获取文件内容。
-
验证响应
-
检查RPC调用本身是否成功(无网络错误)。
-
检查业务返回结果:下载操作是否成功标志为真。
-
验证返回的文件ID是否与请求时传入的ID一致,确保下载的是正确的文件。
-
-
保存下载结果
将下载得到的文件内容写入本地磁盘,文件名为
make_file_download,以便后续可以人工检查文件内容是否与原始上传文件一致。
简而言之,这个测试用例模拟了客户端根据文件ID下载一个文件的全过程,并断言服务端能够正确返回文件内容,且返回的信息符合预期。
正常的运行结果就是我们的客户端程序所在目录里面多了一个叫make_file_download的文件,这个文件和
4.2.批量文件上传/下线功能测试
在测试之前,我们先搞清楚一件事情:
上传和下载是两个角色完全相反的操作,它们处理的是信息的不同阶段。
可以这样理解:
- 上传:创建新资源,需要一个"身份证"
-
核心任务 :客户端要把一个全新的文件交给服务器保管。
-
关键问题:全世界有无数个文件叫"我的照片.jpg",如果直接用它当名字存,服务器上早就乱套了------别人的同名文件会把你覆盖掉。
-
文件ID的作用 :服务器作为保管方,需要给这个新文件颁发一个全世界独一无二的"身份证号"。这个号码(文件ID)完美地解决了重名和冲突问题,确保你的文件是唯一的。这就是为什么上传时一定要"生成"一个新ID。
- 下载:访问已有资源,需要出示"身份证号"
-
核心任务:客户端想把之前存好的文件拿回来。
-
关键问题:服务器怎么知道你要拿哪一个?服务器硬盘里可能有几亿个文件。
-
为什么不用生成 :这时候再生成一个新的文件ID 就完全搞错了方向。下载的关键是**"寻址"** 而不是"创建"。客户端必须告诉服务器:"请把
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx这个号码对应的文件给我。" -
文件ID的用途 :此时,之前生成的文件ID 就成了查找文件的钥匙。服务器只需要拿着这个ID去仓库(存储路径)里找就行了。
总结一下
把整个过程想象成去自助储物柜存东西:
-
上传(存包):
-
你拿了一个包(文件)过去。
-
系统不会问你的包叫什么名字,它会直接给你一个唯一的号码牌(文件ID),然后把你包放进对应号码的柜子里。
-
这里必须生成新号码,因为所有号码牌必须是唯一的,不能重复。
-
-
下载(取包):
-
你拿着那个**唯一的号码牌(文件ID)**回来。
-
系统看到号码牌,就知道去几号柜子把包取出来给你。
-
这里不需要生成新号码,你手里必须有之前那个号码牌才能取回东西。如果你现场再要一个新号码,那系统就糊涂了,不知道你到底要取哪一个。
-
我们的客户端必须保存着那个<文件名,文件ID>的对应关系。
由于我们这里是批量文件上传/下载的测试,所以我们就需要一个数组来保存<文件名,文件ID>的对应关系
我们先准备一个变量来存储我们的文件ID
cpp
// 全局向量,保存多文件上传后返回的文件ID列表,供批量下载测试使用
std::vector<std::string> multi_file_id;
批量文件上传功能测试
cpp
// 测试用例:批量文件上传
TEST(put_test, multi_file)
{
// 1. 读取当前目录下的两个文件:base.pb.h 和 file.pb.h
std::string body1;
ASSERT_TRUE(IMS::readFile("./base.pb.h", body1));
std::string body2;
ASSERT_TRUE(IMS::readFile("./file.pb.h", body2));
// 2. 实例化RPC调用客户端对象
IMS::FileService_Stub stub(channel.get());
// 构造批量文件上传请求
IMS::PutMultiFileReq req;
req.set_request_id("3333"); // 设置请求ID
// 添加第一个文件数据
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);
// 创建RPC控制器和响应对象
brpc::Controller *cntl = new brpc::Controller();
IMS::PutMultiFileRsp *rsp = new IMS::PutMultiFileRsp();
// 发起批量文件上传RPC调用
stub.PutMultiFile(cntl, &req, rsp, nullptr);
// 断言RPC调用成功
ASSERT_FALSE(cntl->Failed());
// 3. 检测返回值中上传是否成功
ASSERT_TRUE(rsp->success());
// 遍历返回的文件信息,将每个文件的ID保存到全局向量中
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]); // 打印日志
}
}
这个函数是一个单元测试用例,用于验证批量文件上传功能的正确性。它的执行过程如下:
-
准备测试数据
从本地当前目录下读取两个文件(
base.pb.h和 file.pb.h,这两个文件在我们的构建过程中都会自动生成的),获取它们的内容,作为待上传的文件集合。 -
构造批量上传请求
创建一个批量上传请求,其中包含:
-
一个唯一的请求标识(用于追踪)
-
两个文件的详细信息:每个文件的原始文件名、文件大小和完整的文件内容
-
-
发起RPC调用
通过RPC客户端将批量请求发送给文件存储服务,调用对应的批量文件上传接口。
-
验证响应
-
检查RPC调用本身是否成功(无网络错误)
-
检查业务返回结果:批量上传操作是否成功标志为真(表示所有文件均上传成功)
-
遍历响应中返回的每个文件信息
-
-
记录结果
从响应中提取服务端为每个上传文件生成的唯一ID,将这些ID保存到一个列表中(供后续批量下载测试使用),同时打印日志记录这些ID。
简而言之,这个测试用例模拟了客户端同时上传多个文件的全过程,并断言服务端正确接收并存储了所有文件,且返回了每个文件对应的唯一标识信息。
批量文件下载功能测试
cpp
// 测试用例:批量文件下载
TEST(get_test, multi_file)
{
// 发起RPC调用,进行批量文件下载
IMS::FileService_Stub stub(channel.get());
// 构造批量文件下载请求
IMS::GetMultiFileReq req;
IMS::GetMultiFileRsp *rsp;
req.set_request_id("4444"); // 设置请求ID
// 添加要下载的两个文件ID(来自批量上传测试)
req.add_file_id_list(multi_file_id[0]);
req.add_file_id_list(multi_file_id[1]);
// 创建RPC控制器和响应对象
brpc::Controller *cntl = new brpc::Controller();
rsp = new IMS::GetMultiFileRsp();
// 发起批量文件下载RPC调用
stub.GetMultiFile(cntl, &req, rsp, nullptr);
// 断言RPC调用成功
ASSERT_FALSE(cntl->Failed());
// 断言业务返回成功
ASSERT_TRUE(rsp->success());
// 验证返回的map中包含两个文件的ID
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());
// 从返回的map中取出两个文件的数据并写入本地文件
auto map = rsp->file_data();
auto file_data1 = map[multi_file_id[0]];
IMS::writeFile("base_download_file1",file_data1.file_content()); // 保存第一个文件
auto file_data2 = map[multi_file_id[1]];
IMS::writeFile("file_download_file2", file_data2.file_content()); // 保存第二个文件
}
这个函数是一个单元测试用例,用于验证批量文件下载功能的正确性。它的执行过程如下:
-
构造批量下载请求
创建一个下载请求,其中包含:
-
一个唯一的请求标识(用于追踪)
-
两个要下载的文件的ID(这些ID来自之前批量上传测试中保存的结果**,说明我们想要下载的文件和原来那个我们上传的那些文件是一模一样的,也就是之前上传的base.pb.h 和 file.pb.h,同时也是./data/里面的那些个文件**)
-
-
发起RPC调用
通过RPC客户端将批量下载请求发送给文件存储服务,调用对应的批量文件下载接口,尝试一次性获取两个文件的内容。
-
验证响应
-
检查RPC调用本身是否成功(无网络错误)
-
检查业务返回结果:下载操作是否成功标志为真
-
验证返回的数据中确实包含了请求的那两个文件(通过文件ID确认)
-
-
保存下载结果
从响应中提取两个文件的内容,分别写入本地磁盘,文件名分别为
base_download_file1和file_download_file2,以便后续可以人工检查下载的文件内容是否与原始上传文件一致。
简而言之,这个测试用例模拟了客户端根据多个文件ID同时下载多个文件的全过程,并断言服务端能够正确返回所有请求的文件内容,且返回的信息符合预期。
4.3.完整测试代码
cpp
// 编写一个file客户端程序,对文件存储子服务进行单元测试
// 包含gflags命令行参数解析库
#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 "utils.hpp"
// 定义命令行参数:运行模式(调试/发布)
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
// 定义命令行参数:日志输出文件(发布模式下有效)
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
// 定义命令行参数:日志输出等级(发布模式下有效)
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");
// 定义命令行参数:etcd服务注册中心地址
DEFINE_string(etcd_host, "http://127.0.0.1:2379", "服务注册中心地址");
// 定义命令行参数:服务监控根目录(用于服务发现)
DEFINE_string(base_service, "/service", "服务监控根目录");
// 定义命令行参数:文件存储子服务的名称(用于服务发现)
DEFINE_string(file_service, "/service/file_service", "服务监控根目录");
// 全局RPC信道指针,用于所有测试用例中的RPC调用
IMS::ServiceChannel::ChannelPtr channel;
// 全局变量,保存单文件上传后返回的文件ID,供下载测试使用
std::string single_file_id;
// 测试用例:单文件上传
TEST(put_test, single_file)
{
// 1. 读取当前目录下的Makefile文件数据
std::string body;
ASSERT_TRUE(IMS::readFile("./Makefile", body));
// 2. 实例化RPC调用客户端对象,通过全局信道发起RPC调用
IMS::FileService_Stub stub(channel.get());
// 构造单文件上传请求
IMS::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); // 文件内容
// 创建RPC控制器和响应对象
brpc::Controller *cntl = new brpc::Controller();
IMS::PutSingleFileRsp *rsp = new IMS::PutSingleFileRsp();
// 发起单文件上传RPC调用
stub.PutSingleFile(cntl, &req, rsp, nullptr);
// 断言RPC调用成功(无网络错误)
ASSERT_FALSE(cntl->Failed());
// 3. 检测返回值中上传是否成功
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(); // 保存返回的文件ID
LOG_DEBUG("文件ID:{}", rsp->file_info().file_id()); // 打印日志
}
// 测试用例:单文件下载
TEST(get_test, single_file)
{
// 发起RPC调用,进行文件下载
IMS::FileService_Stub stub(channel.get());
// 构造单文件下载请求
IMS::GetSingleFileReq req;
IMS::GetSingleFileRsp *rsp;
req.set_request_id("2222"); // 设置请求ID
req.set_file_id(single_file_id); // 设置要下载的文件ID(来自上传测试)
// 创建RPC控制器和响应对象
brpc::Controller *cntl = new brpc::Controller();
rsp = new IMS::GetSingleFileRsp();
// 发起单文件下载RPC调用
stub.GetSingleFile(cntl, &req, rsp, nullptr);
// 断言RPC调用成功
ASSERT_FALSE(cntl->Failed());
// 断言业务返回成功
ASSERT_TRUE(rsp->success());
// 验证返回的文件ID与请求的一致
ASSERT_EQ(single_file_id, rsp->file_data().file_id());
// 将下载的文件数据写入本地文件(保存为make_file_download)
IMS::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(IMS::readFile("./base.pb.h", body1));
std::string body2;
ASSERT_TRUE(IMS::readFile("./file.pb.h", body2));
// 2. 实例化RPC调用客户端对象
IMS::FileService_Stub stub(channel.get());
// 构造批量文件上传请求
IMS::PutMultiFileReq req;
req.set_request_id("3333"); // 设置请求ID
// 添加第一个文件数据
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);
// 创建RPC控制器和响应对象
brpc::Controller *cntl = new brpc::Controller();
IMS::PutMultiFileRsp *rsp = new IMS::PutMultiFileRsp();
// 发起批量文件上传RPC调用
stub.PutMultiFile(cntl, &req, rsp, nullptr);
// 断言RPC调用成功
ASSERT_FALSE(cntl->Failed());
// 3. 检测返回值中上传是否成功
ASSERT_TRUE(rsp->success());
// 遍历返回的文件信息,将每个文件的ID保存到全局向量中
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调用,进行批量文件下载
IMS::FileService_Stub stub(channel.get());
// 构造批量文件下载请求
IMS::GetMultiFileReq req;
IMS::GetMultiFileRsp *rsp;
req.set_request_id("4444"); // 设置请求ID
// 添加要下载的两个文件ID(来自批量上传测试)
req.add_file_id_list(multi_file_id[0]);
req.add_file_id_list(multi_file_id[1]);
// 创建RPC控制器和响应对象
brpc::Controller *cntl = new brpc::Controller();
rsp = new IMS::GetMultiFileRsp();
// 发起批量文件下载RPC调用
stub.GetMultiFile(cntl, &req, rsp, nullptr);
// 断言RPC调用成功
ASSERT_FALSE(cntl->Failed());
// 断言业务返回成功
ASSERT_TRUE(rsp->success());
// 验证返回的map中包含两个文件的ID
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());
// 从返回的map中取出两个文件的数据并写入本地文件
auto map = rsp->file_data();
auto file_data1 = map[multi_file_id[0]];
IMS::writeFile("base_download_file1",file_data1.file_content()); // 保存第一个文件
auto file_data2 = map[multi_file_id[1]];
IMS::writeFile("file_download_file2", file_data2.file_content()); // 保存第二个文件
}
// 主函数
int main(int argc, char *argv[])
{
// 初始化Google Test框架,解析命令行参数(包括gtest自己的参数)
testing::InitGoogleTest(&argc, argv);
// 解析gflags命令行参数
google::ParseCommandLineFlags(&argc, &argv, true);
// 根据解析出的运行模式和日志配置,初始化日志系统
IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 1. 先构造Rpc信道管理对象(用于维护多个服务实例的信道)
auto sm = std::make_shared<IMS::ServiceManager>();
// 声明要关注的服务名称(文件存储子服务)
sm->declared(FLAGS_file_service);//声明我们关注的服务是/service/file_service,只有我们关注的服务在服务上下线时才会去调用我们的服务上线和下线时的回调函数
// 定义服务上线和下线时的回调函数(绑定到ServiceManager的对应方法)
auto put_cb = std::bind(&IMS::ServiceManager::onServiceOnline, sm.get(), std::placeholders::_1, std::placeholders::_2);
auto del_cb = std::bind(&IMS::ServiceManager::onServiceOffline, sm.get(), std::placeholders::_1, std::placeholders::_2);
// 2. 构造服务发现对象,连接etcd,监控服务变化
IMS::Discovery::ptr dclient = std::make_shared<IMS::Discovery>(FLAGS_etcd_host, FLAGS_base_service, put_cb, del_cb);
// 3. 通过Rpc信道管理对象,获取提供文件服务的信道(选择一个可用的服务实例)
channel = sm->choose(FLAGS_file_service);
if (!channel) {
// 如果没有获取到信道(服务未就绪),休眠1秒后返回错误
std::this_thread::sleep_for(std::chrono::seconds(1));
return -1;
}
// 4. 运行所有Google Test测试用例
return RUN_ALL_TESTS();
}
4.4.编译运行
预期效果:
首先我们的客户端程序所在目录里面必须有下面这3个文件

服务端程序所在目录里面生成一个./data目录,然后./data目录里面有3个文件,3个文件分别对应我们客户端程序所在目录里面的这些文件
- 单文件上传:Makefile
- 多文件上传:base.pb.h 和 file.pb.h
同时我们的客户端程序所在目录里面会多出3个文件
- 单文件下载:make_file_download(这个文件是下载了上面的./data目录对应那个Makefile文件的文件)
- 多文件下载:base_download_file1 和 file_download_file2(这2个文件是下载了上面的./data目录对应base.pb.h 和 file.pb.h文件的文件)
那么我们就来编译运行一下


我们去看看
可以看到客户端程序所在目录里面确实下载了3个文件,并且服务端程序所在目录也确实是生成了一个./data目录,data目录里面也确实是有3个文件

那么这些文件究竟是有没有关系呢?我们可以借助md5工具来验证一下
cpp
md5sum * data/*

我们眼睛尖锐的同学立马就能发现有3组的MD5值是相同的,并且每一组都有3个文件
组1(MD5:d46a26e4a127a3fe1e4a3aea945050fd)
- Makefile
- make_file_download
- data/dbff-2cdb498b-0000
组2(MD5:024ec50806c82e5056a28d756d6f85de)
- base.pb.h
- base_download_file1
- data/bca9-71edd370-0001
组3(MD5:ef5f5169e73e0fe35e94cea38cb67bf8)
- file.pb.h
- file_download_file2
- data/4faa-c21a8ee0-0002
这个和我们的实验预期是完全一样的。这就说明,我们的文件存储子服务是一点问题都没有的。