概述
关系总结
- 客户端请求、网关处理:客户端向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 ®_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 ®_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();
}