目录
[1. 服务端程序](#1. 服务端程序)
[1.1 功能细分](#1.1 功能细分)
[1.2 模块划分](#1.2 模块划分)
[2. 客户端程序](#2. 客户端程序)
[2.1 客户端功能细分](#2.1 客户端功能细分)
[2.2 客户端模块](#2.2 客户端模块)
[1. json库](#1. json库)
[1.1 介绍](#1.1 介绍)
[1.2 使用方式](#1.2 使用方式)
[1.2.1 json的数据类型](#1.2.1 json的数据类型)
[1.2.2 json的增删查改](#1.2.2 json的增删查改)
[1.3 示例](#1.3 示例)
[2. bundle库](#2. bundle库)
[2.1 介绍](#2.1 介绍)
[2.2 使用方式](#2.2 使用方式)
[2.3 示例](#2.3 示例)
[3. httplib库](#3. httplib库)
[3.1 介绍](#3.1 介绍)
[3.2 使用方式](#3.2 使用方式)
[3.2.1 基本了解](#3.2.1 基本了解)
[3.3 示例](#3.3 示例)
[4.1. 工具类](#4.1. 工具类)
[4.1.1 文件操作工具类](#4.1.1 文件操作工具类)
[4.1.2 序列化工具类](#4.1.2 序列化工具类)
[4.2. 配置文件类](#4.2. 配置文件类)
[4.3. 数据管理模块](#4.3. 数据管理模块)
[4.4. 热点管理模块](#4.4. 热点管理模块)
[4.5. 业务处理模块](#4.5. 业务处理模块)
[4.5.1 上传](#4.5.1 上传)
[4.5.2 页面展示](#4.5.2 页面展示)
[4.5.3 下载](#4.5.3 下载)
[4.5.4 总代码](#4.5.4 总代码)
[4.6 服务器主函数](#4.6 服务器主函数)
[5.1 工具类](#5.1 工具类)
[5.2 数据管理类](#5.2 数据管理类)
[5.3 业务处理类](#5.3 业务处理类)
[5.4 客户端主函数](#5.4 客户端主函数)
一、项目概述
我们要做的项目是云备份项目,云备份项目的功能是将客户端的指定目录下的所有文件进行备份管理,将文件上传到服务端进行备份,并且对备份的文件进行管理。
该项目的核心技术是要了解掌握有关于http网络协议等知识,该项目采用C++语言进行开发,客户端的主要功能就是将目录下的文件通过http协议,上传到服务端 ,并且我们需要一个简单的可视化界面,能够看到已经上传的文件的部分基本信息,并且还可以通过该界面去将上传备份的文件进行本地的下载,客户端的开发环境采用Windows下的VStudio2022(要求至少支持C++17的版本),服务端的业务主要就是支持客户端上传 和下载文件 ,并且将文件进行管理 ,其中还涉及到热点管理,也就是当判断某个文件长时间未被访问,则为了节省空间,我们会对文件进行压缩处理,服务端的开发选择在Linux环境下采用VSCode远程连接云服务器的环境。
本篇博客会从项目准备、环境搭建、第三方库的认识和简单使用、整个项目开发逻辑和各个板块的开发思路以及具体实现,一步步整理总结该项目的内容。

二、实现目标
这个云备份项⽬需要我们实现两端程序,在客户端上的客户端程序需要实现将文件上传到服务端的功能,在服务器上的服务端程序需要实现对上传文件的存储、管理和下载等功能,两端联合实现云服务器的功能
1. 服务端程序
1.1 功能细分
(1)支持客户端文件上传的功能
(2)支持客户端文件备份列表的查看功能
(3)支持客户端文件下载功能(断点续传)
(4)热点文件管理功能(对长时间无访问的文件进行压缩存储)
1.2 模块划分
(1)数据管理模块(管理备份数据的信息,以便随时获取)
(2)网络通信模块(实现客户端和服务端的网络通信)
(3)业务处理模块(上传、查看、下载(断点续传))
(4)热点管理模块(对长时间无访问的文件进行压缩存储)
2. 客户端程序
2.1 客户端功能细分
(1)对指定的文件夹中的文件进行检测(获取文件夹中有什么文件)
(2)判断指定的文件是否需要备份
(3)将需要备份的文件上传备份到服务器中
2.2 客户端模块
(1)数据管理模块(备份文件的数据)
(2)文件检测模块(检测文件夹内需要进行备份的数据)
(3)文件备份模块(将文件上传备份到服务端,也就是网络通信的部分)
三、第三方库的认识
1. json库
1.1 介绍
json库是用于进行序列化和反序列化操作的一个库 ,其中提供的接口能够按照相应的格式,将我们的数据进行序列化和反序列化,而序列化就是把结构化的数据转化成字符串的意思 ,这便于我们在网络中进行信息的传输,而反序列化则是接收方接受到数据后,将序列化后的信息解析还原成原来的样子 ,并且能够提取出来,就叫反序列化,json库提供的接口帮我们完成的就是序列化和反序列的工作。
1.2 使用方式
1.2.1 json的数据类型
cpp
// 原结构化数据
char name = "小明";
int age = 18;
float score[3] = {88.5,99,58};
转换到json内的存储格式如下
cpp
{
"name" : "小明",
"age" : 18,
"score" : [88.5,99,58]
}
1.2.2 json的增删查改
要使用json库,首先要包含其头文件,然后就是定义出一个json对象
cpp
json::Value root; // json对象
将数据放入json对象的方式如下:
对于一个字符串或者是整形数据而言
cpp
root["姓名"] = "小明";
root["年龄"] = 18;
//如此在json对象内存储的格式可以认为是如下:
{
"姓名" : "小明",
"年龄" : 18
}
对于一个数组而言
cpp
//假设当前数据score为{10,20,30}
float score[] = {10,20,30};
root["成绩"].appent(score[0]);
root["成绩"].appent(score[1]);
root["成绩"].appent(score[2]);
//在json中的格式:
{
"成绩" : [10,20,30]
}
将数据成功放到json对象后,通过json提供的接口进行序列化
json提供的接口中有说明,需要序列化 ,首先要构建一个StreamWriteBuilder类型的对象 ,然后再通过这个对象提供的方法,使用智能指针的方式去构建SteamWrite类型的对象 ,再使用里面的write方法去实现序列化,write内的参数,一个是存着数据的json对象,第二个是流类型
cpp
Json::StreamWriterBulider swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::StringStream ss;
sw->write(root,&ss);
而反序列化的步骤类似,反序列化的场景是我接受到了一段json格式的字符串,现在需要将这个字符串内的数据读取出来,重新构建出一个json对象,然后能够让我依次拿到json对象里我需要的数据
所以还是先构建Json对象,然后通过CharReaderBuilder对象提供的接口去创建ChaeReader对象,然后使用其提供的parse接口去反序列化
cpp
parse(json序列化后的char*类型的首地址,末尾地址,要将数据写入的json对象,错误描述字符串);
cpp
// 假如str就是接受到的json格式的字符串
std::string str = R"({"姓名":"小黑","年龄":19,"成绩":[60,65,88.5]})";
//反序列化
Json::Value root;
Json::CharReaderBulider crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err;
cr->parse(str.cstr(),str.cstr()+str.size(),&root,&err);
//从Value类型的对象中读取到数据
root["姓名"].asString();
root["年龄"].asInt();//字符也是这样读
root["成绩"][0]; // 可以使用for循环去读数组类型
root["成绩"][1];
root["成绩"][2];
1.3 示例
序列化
cpp
#include <iostream>
#include <sstream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
const char* name = "小明";
int age = 18;
float score[] = {77.5,88,60};
// 将数据放到Value中
Json::Value root;
root["姓名"] = name;
root["年龄"] = age;
for(int i = 0;i < sizeof(score); i++)
{
root["成绩"].appent(score[i]);
}
// 序列化
Json::StreamWriterBuilder swb;
std::unique<Json::StreamWriter> sw(swb.newStreamWriter());
std::StringStream ss;
sw->write(root,&ss);
std::cout << ss.str() << std::endl;
return 0;
}
反序列化
cpp
#include <iostream>
#include <sstream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
//假如str就是接受到的json格式的字符串
std::string str = R"({"姓名":"小黑","年龄":19,"成绩":[60,65,88.5]})";
//反序列化
Json::Value root;
Json::CharReaderBulider crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err;
cr->parse(str.cstr(),str.cstr()+str.size(),&root,&err);
//从Value类型的对象中读取到数据
root["姓名"].asString();
root["年龄"].asInt();//字符也是这样读
root["成绩"][0]; // 可以使用for循环去读数组类型
root["成绩"][1];
root["成绩"][2];
return 0;
}
2. bundle库
2.1 介绍
bundle库是一个嵌入式压缩库 ,嵌入式压缩库可以简单理解就是,将库下载后,需要拿到对应的.cpp文件和.hpp文件,将文件拷贝到工程项目中直接使用,这样的使用方式就是嵌入式库,该库的主要作用是用来将文件进行压缩和解压处理的,其中支持23种压缩算法和2种存档格式。
2.2 使用方式
这部分的知识点主要是关于文件读写的操作,我们将文件内容从原文件里读出来,然后对内容进行压缩,库里面直接提供了pack接口,选择压缩的格式,然后是需要压缩的内容 ,最后会得到压缩好的结果,解压则是unpack
2.3 示例
压缩
cpp
#include<iostream>
#include<string>
#include<fstream>
#include"bundle.h"
int main(int argc,char* argv[])
{
if(argc != 3) return -1;
std::string ifilename = argv[1];
std::string ofilename = argv[2];
//获取文件大小
std::ifstream ifs;
ifs.open(ifilename,std::ios::binary); //以二进制方式打开
ifs.seekg(0,std::ios::end); //跳转到文件结尾
size_t fsize = ifs.tellg(); //获取末尾偏移量 -> 文件大小
ifs.seekg(0,std::ios::beg); //将光标重新放到文件开始
//将文件内容读出来,然后进行压缩
std::string body;
body.resize(fsize);//调整大小
ifs.read(&body[0],fsize);//读取文件内容,并放到body中
std::string packed = bundle::pack(bundle::LZIP,body);
//将压缩好后的数据放到ofilename中
std::ofstream ofs;
ofs.open(ofilename,std::ios::binary);
ofs.write(&packed[0],packed.size());
ifs.close();
ofs.close();
return 0;
}
解压
cpp
#include<iostream>
#include<fstream>
#include<string>
#include"bundle.h"
int main(int argc,char* argv[])
{
if(agrc != 3) return -1;
std::string ifilename = argv[1]; //压缩名路径
std::string ofilename = argv[2]; //解压后的文件名
// 将压缩包内容读出来
std::ifstream ifs;
ifs.open(ifilename,std::ios::binary);
ifs.seekg(0,std::ios::beg);
size_t fsize = ifs.tellg();//获得文件大小
ifs.seekg(0,std::ios::beg);
std::string body;
body.resize(fsize);
ifs.read(&body[0],fsize);
ifs.close();
//解压
std::string unpacked = bundle::unpack(body);
//将解压后的内容放到ofilename文件中
std::ofstream ofs;
ofs.open(ofilename,std::ios::binary);
ofs.write(&unpacked[0],unpacked.size());
ofs.close();
return 0;
}
3. httplib库
3.1 介绍
httplib库是一个C++11支持的、支持跨平台Http/Https协议编写的库,只需要将头文件httplib.h包含在代码中即可使用。它帮助我们快速的能够搭建Http协议的服务器,能够让我们把更多的时间花在具体的业务处理上,提高开放的效能。
3.2 使用方式
3.2.1 基本了解
首先要先对Http有基本的了解,Http协议是帮助我们实现网络通信的协议,它有其规定的格式,在Http协议中的request和response的格式如下

通常在编写服务器的时候,我们会将request和response结构化,也就是写成一个类,其中包含重要的字段内容,以及相关的接口,然后通常由客户端构建好request后,将request按上面的格式进行序列化发送到服务器去等待服务器处理并且返回响应response,而httplib库就提供了已经写好的request类和response类
request类
cpp
struct Request
{
//成员
std::string method;//请求方法
std::string path;//资源路径
Headers headers;//头部字段
std::string body; //正文
std::string version;//协议版本
params params;//查询字符串
MultipartFormDataMap files;//保存的是客户端上传的文件信息
Ranges ranges;//用于实现断点续传的,表示请求文件的区间
//接口
//检查头部字段是否存在
bool has_header(const char* key) const;
//获取key值对应的value
std::string get_header_value(const char* key,size_t id = 0) const;
//设置Request中的头部字段,用于构建Request
void set_header(const char*key,const char* val);
//检查是否包含特定的文件上传字段
bool has_file(const char* key) const;
//获取指定文件上传字段的相关信息
MultipartFormData get_file_value(const char* key)const;
};
// Headers就是用来描述头部字段的结构
struct Headers
{
//成员
std::map<std::string, std::string> fields;
//接口
// 添加头部字段
void set(const std::string& key, const std::string& value);
// 获取头部字段值
std::string get(const std::string& key) const;
// 判断是否存在指定头部字段
bool has(const std::string& key) const;
};
// MultipartFormData是用于储存文件相关数据的结构体,而MultipartFormDataMap则是文件数组的感觉
response类
cpp
struct Response
{
std::string version;//协议版本
int status = -1;//响应状态码
Headers headers; //头部字段
std::string body;//响应给客户端的正文
//接口
//设置头部字段
void set_header(const char* key,const char* val);
//设置正文
void set_content(const std::string& s,const char* content_type);
}
然后就是在httplib中,也已经写好了服务器类的接口和客户端接口,可以直接使用这两个类提供的接口去搭建自己的服务器
Server类
cpp
class Server
{
//函数指针类型 void func(const Request&,Response&)
using Handler = std::function<void(const Request&,Response&)>;
using Handlers = std::vector<std::pair<std::regex,Handler>>;
std::function<TaskQueue* (void)> new_task_queue;//线程池
// 用于建立Get方法下,pattern路径对应的handler方法的映射
Server& Get(const std::string& pattern,Handler handler);
// 用于建立Post方法下,pattern路径对应的handler方法的映射
Server& Post(const std::string& pattern,Handler handler);
// 搭建并启动服务器,其中host为主机IP,post为端口
bool Listen(const char* host,int post,int socket_flag = 0);
//...
};
Handler:函数指针类型,定义了一个httplib请求处理问题回调函数的固定格式,即
void (const Request&,Response&);
Handlers: 请求路由数组,可以将其理解成一张表,映射了请求路径与对应的处理函数,当服务器解析到request后,会根据资源请求方法+路径去找到对应匹配的相关方法,这个handlers表的作用就是建立起路由与方法的映射关系。
Client类
cpp
class Client
{
Cilent(const std::string& host,int port);// 传入服务器IP和端口
Result Get(const char* path,const Headers& headers);// 向服务器发送Get请求
Result Post(const char* path,const char* body,size_t content_len,const char* content_type);//向服务器提交Post请求
//Post请求提交多区域数据,常用于提交多文件上传
Result Post(const char* path,const MultiportFormDataltems& items);
}
3.3 示例
搭建一个简单的服务器
cpp
#include<iostream>
#include<httplib.h> // 相关头文件
void Hello(const httplib::Request& req,httplib::Response& rsp)
{
rsp.set_content("Hello World","text/plain");
rsp.status = 200;
}
void Numbers(const httplib::Request& req,httplib::Response& rsp)
{
auto num = req.matches[1];
rsp.set_content(num,"text/plain");
rsp.status = 200;
}
void Multipart(const httplib::Request& req,httplib::Response& rsp)
{
auto ret = req.has_file("file");
if(ret == false)
{
std::cout << "not file upload\n";
rsp.status = 400;
return;
}
const auto& file = req.get_file_value("file");
rsp.body.clear();
rsp.body = file.filename;
rsp.body += "\n";
rsp.body += file.content;
rsp.set_header("Content-Type","text/plain");
rsp.status = 200;
return;
}
int main()
{
httplib::Server server;
server.Get("/hi",Hello);
server.Get(R"(/numbers/(\d+))",Numbers);
server.Post("/multipart",Multipart);
server.listen("0.0.0.0",9090);
return 0;
}
客户端,用于上传多个文件的简单样例
cpp
// 搭建一个简单客户端示例
#include "httplib.h"
#include <iostream>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 80
int main()
{
httplib::Client client(SERVER_IP,SERVER_PORT);
httplib::MultipartFormData item;
item.filename = "hello.txt";
item.content = "Hello World";
item.content_type = "text/plain";
httplib::MultipartFormDataItems items;
items.push_back(item);
auto res = client.Post("/multipart",items);
std::cout << res->status << std::endl;
std::cout << res->body << std::endl;
return 0;
}
这部分主要考查关于对http的了解,我们在这个项目中,我们采用现有的http去搭建服务器,但不代表我们不能够自己搭建,我们应该要清晰掌握http的底层相关逻辑,在项目中我们采用http库是为了减少开发的负担,将更多的精力放到具体的业务开发中。
四、服务器
首先,我们先对服务端进行开发,服务端这边主要的功能模块就是要实现上传、下载、页面展示的业务,并且要管理好上传的文件,所以这个过程会涉及大量的文件操作,所以最开始实现的模块是工具类,关于可能用到的文件操作先封装成接口,然后还有关于序列化的内容,然后是关于基本的配置信息,我们用配置文件去处理,设计单例模式的类进行管理,然后就是数据管理类,热点管理类以及最后的业务处理。
4.1. 工具类
4.1.1 文件操作工具类
首先我们先实现文件操作相关的类,文件操作包括:
cpp
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <experimental/filesystem>
#include <sys/stat.h>
#include <jsoncpp/json/json.h>
#include "bundle.h"
namespace CH{
namespace fs = std::experimental::filesystem;
//文件通用类
class FileUtil{
private:
std::string _filename; //文件名
public:
//构造函数
FileUtil(const std::string &filename):_filename(filename){}
//删除文件
bool Remove(){
if (this->Exists()== false) {
return true;
}
remove(_filename.c_str());
return true;
}
//返回文件大小
int64_t FileSize(){
struct stat st;
//获取文件的信息放入st中
if (stat(_filename.c_str(), &st) < 0) {
std::cout << "get file size failed!\n";
return -1;
}
return st.st_size;
}
//文件最后修改时间
time_t LastMTime(){
struct stat st;
//获取文件的信息放入st中
if (stat(_filename.c_str(), &st) < 0) {
std::cout << "get file size failed!\n";
return -1;
}
return st.st_mtime;
}
//文件最后访问时间
time_t LastATime() {
struct stat st;
//获取文件的信息放入st中
if (stat(_filename.c_str(), &st) < 0) {
std::cout << "get file size failed!\n";
return -1;
}
return st.st_atime;
}
//返回文件名字
std::string FileName(){
// ./abc/test.txt
size_t pos = _filename.find_last_of("/");
if (pos == std::string::npos) {
return _filename;
}
return _filename.substr(pos+1);
}
//在文件中,从指定位置pos,获取len个字符,放入body中
bool GetPosLen(std::string *body, size_t pos, size_t len){
size_t fsize = this->FileSize();
//获取超出范围
if (pos + len > fsize){
std::cout << "get file len is error\n";
return false;
}
//输出流
std::ifstream ifs;
//以二进制形式打开文件
ifs.open(_filename, std::ios::binary);
if (ifs.is_open() == false) {
std::cout << "read open file failed!\n";
return false;
}
//将光标从开头,偏移pos个字符
ifs.seekg(pos, std::ios::beg);
body->resize(len);
//读取字符放入body中
ifs.read(&(*body)[0], len);
if (ifs.good() == false) {
std::cout << "get file content failed\n";
ifs.close();
return false;
}
ifs.close();
return true;
}
//将文件的内容写入body中
bool GetContent(std::string *body) {
size_t fsize = this->FileSize();
return GetPosLen(body, 0, fsize);
}
//将body的内容设置进文件
bool SetContent(const std::string &body) {
//输入流
std::ofstream ofs;
ofs.open(_filename, std::ios::binary);
if (ofs.is_open() == false) {
std::cout << "write open file failed!\n";
return false;
}
//将字符串的数据写入文件
ofs.write(&body[0], body.size());
if (ofs.good() == false) {
std::cout << "write file content failed!\n";
ofs.close();
return false;
}
ofs.close();
return true;
}
//压缩文件,给一个压缩后的名字
bool Compress(const std::string &packname){
//1. 获取源文件数据
std::string body;
if (this->GetContent(&body) == false){
std::cout << "compress get file content failed!\n";
return false;
}
//2. 对数据进行压缩
std::string packed = bundle::pack(bundle::LZIP, body);
//3. 将压缩的数据存储到压缩包文件中
FileUtil fu(packname);
if (fu.SetContent(packed) == false){
std::cout << "compress write packed data failed!\n";
return false;
}
return true;
}
//解压文件,给一个解压后的文件名
bool UnCompress(const std::string &filename) {
//将当前压缩包数据读取出来
std::string body;
if (this->GetContent(&body) == false){
std::cout << "uncompress get file content failed!\n";
return false;
}
//对压缩的数据进行解压缩
std::string unpacked = bundle::unpack(body);
//将解压缩的数据写入到新文件
FileUtil fu(filename);
if (fu.SetContent(unpacked) == false){
std::cout << "uncompress write packed data failed!\n";
return false;
}
return true;
}
//判断文件是否存在
bool Exists() {
return fs::exists(_filename);
}
//创建目录
bool CreateDirectory() {
if (this->Exists()) return true;
return fs::create_directories(_filename);
}
//遍历目录,并且将该目录的文件名放入数组中
bool ScanDirectory(std::vector<std::string> *arry) {
//directory_iterator 是 C++ 文件系统库中的一个迭代器类,用于遍历目录中的文件和子目录。
for(auto& p: fs::directory_iterator(_filename)) {
//是文件夹则跳过
if (fs::is_directory(p) == true){
continue;
}
//relative_path 带有路径的文件名
arry->push_back(fs::path(p).relative_path().string());//返回相对路径,并且将其转化为string
}
return true;
}
};
}
获取文件的基本信息(文件大小、最后一次修改时间、最后一次访问时间、文件名称、文件内容),对文件内容的操作(向文件写入数据,压缩和解压),检测是否存在该文件,关于目录遍历的操作(这个是为客户端遍历目录下所有文件,并获得文件名的操作做准备)
4.1.2 序列化工具类
序列化的操作我们借用Json库去完成,而Json库提供的使用方法较为繁琐麻烦,所以我们将序列化操作和反序列操作进行一个封装,接口的设计和实现如下:
cpp
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <experimental/filesystem>
#include <sys/stat.h>
#include <jsoncpp/json/json.h>
#include "bundle.h"
namespace CH{
//序列化通用类
class JsonUtil{
public:
//序列化,将对象的数据(Value)转化为字符串
static bool Serialize(const Json::Value &root, std::string *str){
Json::StreamWriterBuilder swb;//这是一个建造者,专门用来创建StreamWriter
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
//将对象的数据写入字符串
if (sw->write(root, &ss) != 0) {
std::cout << "json write failed!\n";
return false;
}
*str = ss.str();
return true;
}
//反序列化,将字符串转化为对象(Value)
static bool UnSerialize(const std::string &str, Json::Value *root){
Json::CharReaderBuilder crb;//这是一个建造者,专门用来创建CharReader
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err;
//将字符串转化为对象(Value),err告诉错误原因
bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), root, &err);
if (ret == false) {
std::cout << "parse error: " << err << std::endl;
return false;
}
return true;
}
};
}
那序列化举例,序列化需要先创建一个StreamWriterBuilder对象,然后用智能指针的方式,用StreamWriterBuilder对象内提供的方法去创建一个StreamWriter对象,然后用该对象内提供的方法进行序列化,同样的,反序列化同样如此。
关于对命名的理解,个人理解是因为序列化的操作是我在网络上需要对你进行信息的传递,就是说我需要向你发出某个信息,那我就是Writer(写者),而我要在网络上传输,则需要先将我的信息进行序列化(转化成字符串),而接收方作为Reader(读者),需要将信息进行反序列化才能拿到我原本想要传达的内容。
4.2. 配置文件类
配置文件类是用于管理配置文件内的数据 ,而配置文件内的数据通常是一些不涉及核心业务逻辑代码,主要是为了代码的运行提供必要的环境设置和资源定位,例如,服务器监听的端口号就是一种运行环境的设置,这些非代码逻辑相关的信息放在配置文件中,与代码解耦合,可以使得代码更加清晰,一旦需要修改则不要在所有出现过的地方都进行调整修改,而只需要改变配置文件 ,而且对应一些安全要求的保护,也可以对配置文件的信息进行专门的保护,以下是该项目的配置文件信息:
cpp
{
"hot_time" : 30,
"server_port" : 8080,
"server_ip" : "0.0.0.0",
"download_prefix" : "/download/",
"packfile_suffix" : ".lz",
"pack_dir" : "./packdir/",
"back_dir" : "./backdir/",
"backup_file" : "./cloud.dat"
}
上面涉及到了:热点管理的时间,端口号,IP地址,下载请求的path路径前缀,压缩后缀,压缩文件存放的文件夹,备份文件存放的文件夹路径,持久存储信息文件。
配置文件采用了Json的格式,方便我们在代码中获取时,可以使用Json的接口去读取到相关的数据,具体的文件配置类如下:
cpp
#pragma once
#include <mutex>
#include "util.hpp"
namespace CH{
#define CONFIG_FILE "./cloud.conf"
class Config{
private:
Config(){
ReadConfigFile();//都取配置文件信息
}
//以单例模式构建这个类,由于配置文件的数据是通用的,所以只需要创建一次即可
//static修饰的成员变量只能在类外定义
static Config *_instance;
//单例模式在多线程情况下,是不安全的
static std::mutex _mutex;
private:
int _hot_time; //热点管理时间
int _server_port; //服务器端口号
std::string _server_ip; //服务器ip地址
std::string _download_prefix; //下载路径前缀
std::string _packfile_suffix; //压缩后缀
std::string _pack_dir; //压缩文件存放的文件夹
std::string _back_dir; //备份文件存放的文件夹
std::string _backup_file; //持久存储信息文件
//将配置文件的信息全部读取到类中
bool ReadConfigFile() {
//打开配置文件,将配置文件的读到字符串中
FileUtil fu(CONFIG_FILE);
std::string body;
if(fu.GetContent(&body) == false){
std::cout << "load config file failed!\n";
return false;
}
//由于配置文件是按序列化后存储的方式,所以我们可以将其反序列化,把数据放入root对象中
Json::Value root;
if (JsonUtil::UnSerialize(body, &root) == false){
std::cout << "parse config file failed!\n";
return false;
}
//从root对象中,取出对应的信息
_hot_time = root["hot_time"].asInt();
_server_port = root["server_port"].asInt();
_server_ip = root["server_ip"].asString();
_download_prefix = root["download_prefix"].asString();
_packfile_suffix = root["packfile_suffix"].asString();
_pack_dir = root["pack_dir"].asString();
_back_dir = root["back_dir"].asString();
_backup_file = root["backup_file"].asString();
return true;
}
public:
//返回Config对象
static Config *GetInstance() {
if (_instance == NULL){
_mutex.lock();
if (_instance == NULL) {
_instance = new Config();
}
_mutex.unlock();
}
return _instance;
}
//返回热点管理时间
int GetHotTime() {
return _hot_time;
}
//返回服务器端口号
int GetServerPort() {
return _server_port;
}
//返回服务器ip地址
std::string GetServerIp() {
return _server_ip;
}
//返回下载路径前缀
std::string GetDownloadPrefix() {
return _download_prefix;
}
//返回压缩后缀
std::string GetPackFileSuffix() {
return _packfile_suffix;
}
//返回压缩文件存放的文件夹
std::string GetPackDir() {
return _pack_dir;
}
//返回备份文件存放的文件夹
std::string GetBackDir() {
return _back_dir;
}
//返回持久存储信息文件
std::string GetBackupFile() {
return _backup_file;
}
};
Config *Config::_instance = NULL;
std::mutex Config::_mutex;
}
4.3. 数据管理模块
数据管理模块是为了将上传备份到服务器上的备份文件统一管理起来,根据**"先描述,再组织"**的原则,我们先选择需要哪些文件属性去更好的帮助我们在该项目中,去描述一个备份的文件
cpp
#pragma once
#include <unordered_map>
#include <pthread.h>
#include "util.hpp"
#include "config.hpp"
namespace CH{
//单个文件属性的数据结构体
typedef struct BackupInfo{
bool pack_flag; //判断文件是否压缩
size_t fsize; //文件的大小
time_t mtime; //文件最后修改时间
time_t atime; //文件最后访问时间
std::string real_path; //实际文件存储路径
std::string pack_path; //压缩文件存储路径
std::string url; //url下载路径
//根据文件的实际路径,获取文件的属性,并且将其放入结构体中
bool NewBackupInfo(const std::string &realpath){
//判断文件是否存在
FileUtil fu(realpath);
if (fu.Exists() == false) {
std::cout << "new backupinfo: file not exists!\n";
return false;
}
//通过创建配置文件类对象,来获取配置文件的信息
Config *config = Config::GetInstance();
std::string packdir = config->GetPackDir(); //压缩目录
std::string packsuffix = config->GetPackFileSuffix(); //压缩后缀
std::string download_prefix = config->GetDownloadPrefix(); //下载目录
//刚刚添加进来的文件都是没有被压缩的
this->pack_flag = false;
this->fsize = fu.FileSize();
this->mtime = fu.LastMTime();
this->atime = fu.LastATime();
this->real_path = realpath;
// ./backdir/a.txt -> ./packdir/a.txt.lz
this->pack_path = packdir + fu.FileName() + packsuffix; //文件压缩路径和名字
// ./backdir/a.txt -> /download/a.txt
this->url = download_prefix + fu.FileName(); //文件下载路径和名字
return true;
}
}BackupInfo;
1. 判断该备份文件是否被压缩,这在客户端下载时,是否需要先解压起到关键信息的作用
2. 文件大小,最后一次访问时间,最后一次修改时间,这些基本信息在后续写热点管理模块时候,以及断点续传模块时需要用到
3. 实际的文件储存路径,压缩后的压缩包存放路径,url的下载路径,这三个信息能够让我们找到文件内容存放的位置
然后就是提供一个接口,只需要给文件名称,就可以帮我们自动获取到这些基本信息,并且完成初始化,得到一个该文件的文件属性信息当描述好改文件信息结构体后,我们就需要将这个文件信息块给组织起来,我们要采用每个容器去管理这些信息,而我们考虑到在每次下载某个文件时,需要从大量文件中,找到我们需要的文件,所以,自然用哈希表的方式去存储是最合适的,我们通过每个文件的url下载路径去和文件属性块关联映射哈希表的方式,进行数据管理,然后还需要考虑到这个表有可能存在并发访问的问题,因为有可能多个进程同时发出下载请求,所以我们需要加上一把读写锁去保证安全 ,最后就是可持久化存储的考虑,我们需要将表的信息用文件的方式去存储起来,这样在服务器被意外关闭等情况下,不会将数据丢失,也就是我们需要设计成员函数,首先是保证每次表修改后,实时更新到表的备份信息文件中,然后就是需要再每次数据库重启时,将表的数据重新加载,大体如下:
cpp
//管理数据
class DataManager{
private:
std::string _backup_file; //持久存储信息文件
pthread_rwlock_t _rwlock; //读写锁
std::unordered_map<std::string, BackupInfo> _table; //哈希表
public:
//
DataManager() {
_backup_file = Config::GetInstance()->GetBackupFile();
pthread_rwlock_init(&_rwlock, NULL);//初始化读写锁
InitLoad();
}
~DataManager() {
pthread_rwlock_destroy(&_rwlock);//销毁读写锁
}
//将文件信息插入哈希表中
bool Insert(const BackupInfo &info){
pthread_rwlock_wrlock(&_rwlock);
_table[info.url] = info;
pthread_rwlock_unlock(&_rwlock);
Storage();//将新插入的数据,存入持久存储信息文件中
return true;
}
//将原来保存在哈希表中的该文件信息,进行更新
bool Update(const BackupInfo &info) {
pthread_rwlock_wrlock(&_rwlock);
_table[info.url] = info;
pthread_rwlock_unlock(&_rwlock);
Storage();//将更新后的数据,存入持久存储信息文件中
return true;
}
//根据url路径,在哈希表中查找对应的文件信息
bool GetOneByURL(const std::string &url, BackupInfo *info) {
pthread_rwlock_wrlock(&_rwlock);
//因为url是key值,所以直接通过find进行查找
auto it = _table.find(url);
if (it == _table.end()) {
pthread_rwlock_unlock(&_rwlock);
return false;
}
*info = it->second;
pthread_rwlock_unlock(&_rwlock);
return true;
}
//根据文件的实际路径,在哈希表中查找对应的文件信息
bool GetOneByRealPath(const std::string &realpath, BackupInfo *info) {
pthread_rwlock_wrlock(&_rwlock);
auto it = _table.begin();
for (; it != _table.end(); ++it){
if (it->second.real_path == realpath) {
*info = it->second;
pthread_rwlock_unlock(&_rwlock);
return true;
}
}
pthread_rwlock_unlock(&_rwlock);
return false;
}
//将哈希表中所有的文件信息全部存入arry数组中
bool GetAll(std::vector<BackupInfo> *arry) {
pthread_rwlock_wrlock(&_rwlock);
auto it = _table.begin();
for (; it != _table.end(); ++it){
arry->push_back(it->second);
}
pthread_rwlock_unlock(&_rwlock);
return true;
}
//将哈希表中的数据写入持久存储信息文件
bool Storage(){
//1. 获取所有数据
std::vector<BackupInfo> arry;
this->GetAll(&arry);
//2. 添加到Json::Value
Json::Value root;
for (int i = 0; i < arry.size(); i++){
Json::Value item;
item["pack_flag"] = arry[i].pack_flag;
item["fsize"] = (Json::Int64)arry[i].fsize;
item["atime"] = (Json::Int64)arry[i].atime;
item["mtime"] = (Json::Int64)arry[i].mtime;
item["real_path"] = arry[i].real_path;
item["pack_path"] = arry[i].pack_path;
item["url"] = arry[i].url;
root.append(item);//添加数组元素
}
//3. 对Json::Value序列化
std::string body;
JsonUtil::Serialize(root, &body);
//4. 写文件
FileUtil fu(_backup_file);
fu.SetContent(body);
return true;
}
//将持久存储信息文件中的数据,读出来放到哈希表中
bool InitLoad(){
//1. 将数据文件中的数据读取出来
FileUtil fu(_backup_file);
if (fu.Exists() == false){
return true;
}
std::string body;
fu.GetContent(&body);
//2. 反序列化
Json::Value root;
JsonUtil::UnSerialize(body, &root);
//3. 将反序列化得到的Json::Value中的数据添加到table中
for (int i = 0; i < root.size(); i++) {
BackupInfo info;
info.pack_flag = root[i]["pack_flag"].asBool();
info.fsize = root[i]["fsize"].asInt64();
info.atime = root[i]["atime"].asInt64();
info.mtime = root[i]["mtime"].asInt64();
info.pack_path = root[i]["pack_path"].asString();
info.real_path = root[i]["real_path"].asString();
info.url = root[i]["url"].asString();
Insert(info);
}
return true;
}
};
}
4.4. 热点管理模块
热点管理模块的任务是将备份文件所在目录下的文件进行一个轮询,不断地检测其中的文件是否为热点文件,如果检测到非热点文件,则将文件进行压缩打包到压缩包文件存放的目录下,设计该模块的目的是为了节省服务器的空间资源,这个模块会单独用一个线程去运行
设计思路:
首先我们需要用到的成员有备份文件所在的路径,压缩包文件所在的路径,压缩后的后缀,热点时间的临界值,这些可以设置为类成员,然后主要的业务逻辑是:
1. 首先获取到备份文件目录下的所有文件名称
2. 遍历每一个文件,去根据最后一次文件访问时间判断是否属于热点文件
3. 对非热点文件进行压缩处理,压缩好后需要将原先文件进行删除,并且修改备份文件数据库里的内容,例如是否被压缩的标志位得修改。
4. 将上述操作不断循环执行,时刻检查着是否有热点文件变成非热点文件
这里附上源码辅助理解
cpp
#pragma once
#include <unistd.h>
#include "data.hpp"
extern CH::DataManager *_data;
namespace CH{
//热点管理模块
class HotManager{
private:
std::string _back_dir; //备份文件目录
std::string _pack_dir; //压缩文件目录
std::string _pack_suffix; //压缩文件后缀
int _hot_time; //热点时间
private:
//非热点文件-返回真;热点文件-返回假
bool HotJudge(const std::string &filename){
FileUtil fu(filename);
time_t last_atime = fu.LastATime();
time_t cur_time = time(NULL);
//当前时间 - 最后修改时间 > 热点时间
if (cur_time - last_atime > _hot_time){
return true;
}
return false;
}
public:
HotManager() {
//构建配置文件类对象,将数据放入成员变量
Config *config = Config::GetInstance();
_back_dir = config->GetBackDir();
_pack_dir = config->GetPackDir();
_pack_suffix = config->GetPackFileSuffix();
_hot_time = config->GetHotTime();
//创建目录
FileUtil tmp1(_back_dir);
FileUtil tmp2(_pack_dir);
tmp1.CreateDirectory();
tmp2.CreateDirectory();
}
//运行模块
bool RunModule() {
while(true){
//1. 遍历备份目录,获取所有文件名
FileUtil fu(_back_dir);
std::vector<std::string> arry;
fu.ScanDirectory(&arry);
//2. 遍历判断文件是否是非热点文件
for (auto &a : arry) {
if (HotJudge(a) == false){
continue;//热点文件则不需要特别处理
}
//3. 获取文件的备份信息
BackupInfo bi;
if (_data->GetOneByRealPath(a, &bi) == false){
//现在有一个文件存在,但是没有备份信息
bi.NewBackupInfo(a);//设置一个新的备份信息出来
}
//3. 对非热点文件进行压缩处理
FileUtil tmp(a);
tmp.Compress(bi.pack_path);
//4. 删除源文件,修改备份信息
tmp.Remove();
bi.pack_flag = true;
_data->Update(bi);
}
usleep(1000);//避免空目录循环遍历,消耗cpu资源过高
}
return true;
}
};
}
4.5. 业务处理模块
业务处理这部分主要是服务端和客户端用http协议进行网络通信,并且服务端提供上传、页面展示、下载的核心业务,我们http协议的通信直接借用http库完成即可。
4.5.1 上传
我们需要提供客户端将文件上传的接口,这里面涉及到对http库的应用,http库中有个叫MultipartFormData的结构体,该结构是http协议中对文件的描述,其中成员包含:
**content :**文件内容
**content_type:**文件类型
**name:**结构体标识符,这个name更多是这个结构体对象的一个标识,而不是文件
**filename:**文件名称
基于http协议的网络通信,文件就是用该结构进行组织传输的,在服务端,我们通过接口获取到文件信息,把文件内容拿到后,建立一个文件去将内容存到指定的目录中,这个过程就是上传。
cpp
//上传
static void Upload(const httplib::Request &req, httplib::Response &rsp) {
//判断请求中有没有file这个字段,这个字段是和客户端约定好的
auto ret = req.has_file("file");//判断有没有上传的文件区域
if (ret == false){
rsp.status = 400;
return;
}
//拿到这个字段中的数据
const auto& file = req.get_file_value("file");
//file.filename:上传文件名称 file.content:上传文件数据
std::string back_dir = Config::GetInstance()->GetBackDir();
std::string realpath = back_dir + FileUtil(file.filename).FileName();
FileUtil fu(realpath);
fu.SetContent(file.content);//将数据写入文件中;
BackupInfo info;
info.NewBackupInfo(realpath);//组织备份的文件信息
_data->Insert(info);//向数据管理模块添加备份的文件信息
return ;
}
4.5.2 页面展示
页面展示就是把上传到服务端的备份文件,那个进行以前端的方式,展示出来,这部分是前端的知识,所以没有过多的了解,我们重点不在这部分,所以只是网上随便找了份简陋的页面,附上文件基本信息(文件名 文件备份时间 文件大小等等),这部分不能直接做个页面,因为文件信息是动态改变的,思路很简单,就是把文件和想要展示的信息,直接拿到然后按前端的开发格式去输入即可。
cpp
//将时间戳转化为年,月,日,时,分,秒的格式
static std::string TimetoStr(time_t t) {
std::string tmp = std::ctime(&t);
return tmp;
}
//显示文件
static void ListShow(const httplib::Request &req, httplib::Response &rsp){
//1. 获取所有的文件备份信息
std::vector<BackupInfo> arry;
_data->GetAll(&arry);
//2. 根据所有备份信息,组织html文件数据
std::stringstream ss;
ss << "<html><head><title>Download</title></head>";
ss << "<body><h1>Download</h1><table>";
for (auto &a : arry){
ss << "<tr>";
std::string filename = FileUtil(a.real_path).FileName();
ss << "<td><a href='" << a.url << "'>" << filename << "</a></td>";
ss << "<td align='right'>" << TimetoStr(a.mtime) << "</td>";
ss << "<td align='right'>" << a.fsize / 1024 << "k</td>";
ss << "</tr>";
}
ss << "</table></body></html>";
rsp.body = ss.str();
rsp.set_header("Content-Type", "text/html");//文件类型
rsp.status = 200;
return ;
}
4.5.3 下载
下载就是把服务端的文件内容,交给客户端,我们根据客户端request的唯一路径去找到指定的文件,然后由于我们存在热点管理,所以还需要判断是否需要解压,获取到文件内容后再将文件内容设置给response即可。
其中我们还有一个断点续传 的机制:
首先我们如果服务器支持断点续传的功能,我们需要在response中设置头部字段**"Accept-Ranges : bytes",表示支持。然后在第一次收到下载请求时,客户端的response的头部字段中还包含着Etag字段,表示该文件的唯一标识,其中Etag可以加上最后一次修改文件的时间作为标识一部分,然后再客户端受到response后,执行下载任务,如果此时因为一些异常原因,下载中断,则当再次发出请求时,会设置一个头部字段"If-Range : Etag",所以此时我在服务端只需要检测是否存在有"If-Range"的头部字段,就可以判断该下载请求是否需要断点续传,但是还需要考虑,如果在第二次申请下载任务期间,原先需要的文件修改了,则我们认为不能进行断点续传,而是需要完整的将新的版本全部重新下载,所以一般Etag的字段中带有最后一次修改时间,就方便在第二次发送下载请求到服务端的时候,进行比较判断文件是否修改过,当满足断点续传的条件后,我们根据第二次的response下载请求中,"Range"头部字段去得到我们这次需要下载的区间信息,然后再根据该期间信息将文件内容给到客户端,状态码设置成206**,由于http库中提供了断点续传的方法,也就是说,只要我们表明服务器支持断点续传,并且我们只需要做好判断是否需要且能够进行断点续传的情况,接下来获取**"Range"中的区间信息,并且将文件截取区间内容返回的部分在http**库内会处理,所以在代码上就会方便很多,但是其中原理需要知道。

参考代码如下:
cpp
//获取文件的唯一值
static std::string GetETag(const BackupInfo &info) {
// etg : filename-fsize-mtime
FileUtil fu(info.real_path);
std::string etag = fu.FileName();
etag += "-";
etag += std::to_string(info.fsize);
etag += "-";
etag += std::to_string(info.mtime);
return etag;
}
//下载文件
static void Download(const httplib::Request &req, httplib::Response &rsp){
//1. 获取客户端请求的资源路径path req.path
//2. 根据资源路径,获取文件备份信息
BackupInfo info;
_data->GetOneByURL(req.path, &info);
//3. 判断文件是否被压缩,如果被压缩,要先解压缩,
if (info.pack_flag == true){
FileUtil fu(info.pack_path);
fu.UnCompress(info.real_path);//将文件解压到备份目录下
//4. 删除压缩包,修改备份信息(已经没有被压缩)
fu.Remove();
info.pack_flag = false;
_data->Update(info);
}
bool retrans = false;//判断是否要断点续传
std::string old_etag;//文件原来的唯一值
//有这个则代表客户端请求断点续传,If-Range里面放的是文件的唯一值
if (req.has_header("If-Range")) {
old_etag = req.get_header_value("If-Range");
//有If-Range字段且,这个字段的值与请求文件的最新etag一致则符合断点续传
if (old_etag == GetETag(info)) {
retrans = true;
}
}
//4. 读取文件数据,放入rsp.body中
FileUtil fu(info.real_path);
if (retrans == false){
fu.GetContent(&rsp.body);
//5. 设置响应头部字段: ETag, Accept-Ranges: bytes
rsp.set_header("Accept-Ranges", "bytes");
rsp.set_header("ETag", GetETag(info));
rsp.set_header("Content-Type", "application/octet-stream");
rsp.status = 200;
}else {
//httplib内部实现了对于区间请求也就是断点续传请求的处理
//只需要我们用户将文件所有数据读取到rsp.body中,它内部会自动根据请求
//区间,从body中取出指定区间数据进行响应
// std::string range = req.get_header_val("Range"); bytes=start-end
fu.GetContent(&rsp.body);
rsp.set_header("Accept-Ranges", "bytes");
rsp.set_header("ETag", GetETag(info));
rsp.set_header("Content-Type", "application/octet-stream");
//rsp.set_header("Content-Range", "bytes start-end/fsize");
rsp.status = 206;//区间请求响应的是206
}
}
4.5.4 总代码
cpp
#pragma once
#include "data.hpp"
#include "httplib.h"
extern CH::DataManager *_data;
namespace CH {
class Service{
private:
int _server_port; //服务器端口号
std::string _server_ip; //服务器ip地址
std::string _download_prefix; //下载前缀
httplib::Server _server; //创建服务器对象
private:
//上传
static void Upload(const httplib::Request &req, httplib::Response &rsp) {
//判断请求中有没有file这个字段,这个字段是和客户端约定好的
auto ret = req.has_file("file");//判断有没有上传的文件区域
if (ret == false){
rsp.status = 400;
return;
}
//拿到这个字段中的数据
const auto& file = req.get_file_value("file");
//file.filename:上传文件名称 file.content:上传文件数据
std::string back_dir = Config::GetInstance()->GetBackDir();
std::string realpath = back_dir + FileUtil(file.filename).FileName();
FileUtil fu(realpath);
fu.SetContent(file.content);//将数据写入文件中;
BackupInfo info;
info.NewBackupInfo(realpath);//组织备份的文件信息
_data->Insert(info);//向数据管理模块添加备份的文件信息
return ;
}
//将时间戳转化为年,月,日,时,分,秒的格式
static std::string TimetoStr(time_t t) {
std::string tmp = std::ctime(&t);
return tmp;
}
//显示文件
static void ListShow(const httplib::Request &req, httplib::Response &rsp){
//1. 获取所有的文件备份信息
std::vector<BackupInfo> arry;
_data->GetAll(&arry);
//2. 根据所有备份信息,组织html文件数据
std::stringstream ss;
ss << "<html><head><title>Download</title></head>";
ss << "<body><h1>Download</h1><table>";
for (auto &a : arry){
ss << "<tr>";
std::string filename = FileUtil(a.real_path).FileName();
ss << "<td><a href='" << a.url << "'>" << filename << "</a></td>";
ss << "<td align='right'>" << TimetoStr(a.mtime) << "</td>";
ss << "<td align='right'>" << a.fsize / 1024 << "k</td>";
ss << "</tr>";
}
ss << "</table></body></html>";
rsp.body = ss.str();
rsp.set_header("Content-Type", "text/html");//文件类型
rsp.status = 200;
return ;
}
//获取文件的唯一值
static std::string GetETag(const BackupInfo &info) {
// etg : filename-fsize-mtime
FileUtil fu(info.real_path);
std::string etag = fu.FileName();
etag += "-";
etag += std::to_string(info.fsize);
etag += "-";
etag += std::to_string(info.mtime);
return etag;
}
//下载文件
static void Download(const httplib::Request &req, httplib::Response &rsp){
//1. 获取客户端请求的资源路径path req.path
//2. 根据资源路径,获取文件备份信息
BackupInfo info;
_data->GetOneByURL(req.path, &info);
//3. 判断文件是否被压缩,如果被压缩,要先解压缩,
if (info.pack_flag == true){
FileUtil fu(info.pack_path);
fu.UnCompress(info.real_path);//将文件解压到备份目录下
//4. 删除压缩包,修改备份信息(已经没有被压缩)
fu.Remove();
info.pack_flag = false;
_data->Update(info);
}
bool retrans = false;//判断是否要断点续传
std::string old_etag;//文件原来的唯一值
//有这个则代表客户端请求断点续传,If-Range里面放的是文件的唯一值
if (req.has_header("If-Range")) {
old_etag = req.get_header_value("If-Range");
//有If-Range字段且,这个字段的值与请求文件的最新etag一致则符合断点续传
if (old_etag == GetETag(info)) {
retrans = true;
}
}
//4. 读取文件数据,放入rsp.body中
FileUtil fu(info.real_path);
if (retrans == false){
fu.GetContent(&rsp.body);
//5. 设置响应头部字段: ETag, Accept-Ranges: bytes
rsp.set_header("Accept-Ranges", "bytes");
rsp.set_header("ETag", GetETag(info));
rsp.set_header("Content-Type", "application/octet-stream");
rsp.status = 200;
}else {
//httplib内部实现了对于区间请求也就是断点续传请求的处理
//只需要我们用户将文件所有数据读取到rsp.body中,它内部会自动根据请求
//区间,从body中取出指定区间数据进行响应
// std::string range = req.get_header_val("Range"); bytes=start-end
fu.GetContent(&rsp.body);
rsp.set_header("Accept-Ranges", "bytes");
rsp.set_header("ETag", GetETag(info));
rsp.set_header("Content-Type", "application/octet-stream");
//rsp.set_header("Content-Range", "bytes start-end/fsize");
rsp.status = 206;//区间请求响应的是206
}
}
public:
Service(){
//构造函数,对成员变量进行初始化
Config *config = Config::GetInstance();
_server_port = config->GetServerPort();
_server_ip = config->GetServerIp();
_download_prefix = config->GetDownloadPrefix();
}
//运行模块
bool RunModule() {
_server.Post("/upload", Upload);
_server.Get("/listshow", ListShow);
_server.Get("/", ListShow);
std::string download_url = _download_prefix + "(.*)";
_server.Get(download_url, Download);
_server.listen(_server_ip.c_str(), _server_port);
return true;
}
};
}
4.6 服务器主函数
cpp
#include "util.hpp"
#include "config.hpp"
#include "data.hpp"
#include "hot.hpp"
#include "service.hpp"
#include <thread>
void FileUtilTest(const std::string &filename)
{
/*
CH::FileUtil fu(filename);
std::cout << fu.FileSize() << std::endl;
std::cout << fu.LastMTime() << std::endl;
std::cout << fu.LastATime() << std::endl;
std::cout << fu.FileName() << std::endl;
CH::FileUtil fu(filename);
std::string body;
fu.GetContent(&body);
CH::FileUtil nfu("./hello.txt");
nfu.SetContent(body);
std::string packname = filename + ".lz";
CH::FileUtil fu(filename);
fu.Compress(packname);
CH::FileUtil pfu(packname);
pfu.UnCompress("./hello.txt");
*/
CH::FileUtil fu(filename);
fu.CreateDirectory();
std::vector<std::string> arry;
fu.ScanDirectory(&arry);
for (auto &a : arry){
std::cout << a << std::endl;
}
return;
}
void JsonUtilTest()
{
const char *name = "小明";
int age = 19;
float score[] = {85, 88.5, 99};
Json::Value root;
root["姓名"] = name;
root["年龄"] = age;
root["成绩"].append(score[0]);
root["成绩"].append(score[1]);
root["成绩"].append(score[2]);
std::string json_str;
CH::JsonUtil::Serialize(root, &json_str);
std::cout << json_str << std::endl;
Json::Value val;
CH::JsonUtil::UnSerialize(json_str, &val);
std::cout << val["姓名"].asString() << std::endl;
std::cout << val["年龄"].asInt() << std::endl;
for(int i = 0; i < val["成绩"].size(); i++) {
std::cout << val["成绩"][i].asFloat() << std::endl;
}
}
void ConfigTest()
{
CH::Config *config = CH::Config::GetInstance();
std::cout << config->GetHotTime() << std::endl;
std::cout << config->GetServerPort() << std::endl;
std::cout << config->GetServerIp() << std::endl;
std::cout << config->GetDownloadPrefix() << std::endl;
std::cout << config->GetPackFileSuffix() << std::endl;
std::cout << config->GetPackDir() << std::endl;
std::cout << config->GetBackDir() << std::endl;
std::cout << config->GetBackupFile() << std::endl;
}
void DataTest(const std::string &filename)
{
CH::DataManager data;
std::vector<CH::BackupInfo> arry;
data.GetAll(&arry);
for (auto &a : arry){
std::cout << a.pack_flag << std::endl;
std::cout << a.fsize<< std::endl;
std::cout << a.mtime<< std::endl;
std::cout << a.atime<< std::endl;
std::cout << a.real_path << std::endl;
std::cout << a.pack_path<< std::endl;
std::cout << a.url<< std::endl;
}
/*
CH::BackupInfo info;
info.NewBackupInfo(filename);
CH::DataManager data;
std::cout << "-----------insert and GetOneByURL--------\n";
data.Insert(info);
cloCHud::BackupInfo tmp;
data.GetOneByURL("/download/bundle.h", &tmp);
std::cout << tmp.pack_flag << std::endl;
std::cout << tmp.fsize<< std::endl;
std::cout << tmp.mtime<< std::endl;
std::cout << tmp.atime<< std::endl;
std::cout << tmp.real_path << std::endl;
std::cout << tmp.pack_path<< std::endl;
std::cout << tmp.url<< std::endl;
std::cout << "-----------update and getall--------\n";
info.pack_flag = true;
data.Update(info);
std::vector<CH::BackupInfo> arry;
data.GetAll(&arry);
for (auto &a : arry){
std::cout << a.pack_flag << std::endl;
std::cout << a.fsize<< std::endl;
std::cout << a.mtime<< std::endl;
std::cout << a.atime<< std::endl;
std::cout << a.real_path << std::endl;
std::cout << a.pack_path<< std::endl;
std::cout << a.url<< std::endl;
}
std::cout << "-----------GetOneByRealPath--------\n";
data.GetOneByRealPath(filename, &tmp);
std::cout << tmp.pack_flag << std::endl;
std::cout << tmp.fsize<< std::endl;
std::cout << tmp.mtime<< std::endl;
std::cout << tmp.atime<< std::endl;
std::cout << tmp.real_path << std::endl;
std::cout << tmp.pack_path<< std::endl;
std::cout << tmp.url<< std::endl;
*/
}
CH::DataManager *_data;
void HotTest(){
CH::HotManager hot;
hot.RunModule();
}
void ServiceTest() {
CH::Service srv;
srv.RunModule();
}
int main(int argc, char *argv[])
{
_data = new CH::DataManager();
//FileUtilTest(argv[1]);
//JsonUtilTest();
//ConfigTest();
//DataTest(argv[1]);
//HotTest();
//ServiceTest();
std::thread thread_hot_manager(HotTest);
std::thread thread_service(ServiceTest);
thread_hot_manager.join();
thread_service.join();
return 0;
}
五、客户端开发
客户端的开放,我们选择在Windows系统下开放,主要的业务就是将目录下的文件都进行备份,并且向服务端进行http协议的通信,上传文件等等。
5.1 工具类
工具类实际上也就是对于文件操作和Json序列化的操作,直接将服务端的工具类拷贝即可,当然也有地方要注意的是,在Windows系统下,文件分隔符是不一样的,在我们使用到文件分割的部分要注意,例如:
cpp
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <experimental/filesystem>
#include <sys/stat.h>
namespace CH{
namespace fs = std::experimental::filesystem;
class FileUtil{
private:
std::string _filename;
public:
FileUtil(const std::string &filename):_filename(filename){}
bool Remove(){
if (this->Exists()== false) {
return true;
}
remove(_filename.c_str());
return true;
}
int64_t FileSize(){
struct stat st;
if (stat(_filename.c_str(), &st) < 0) {
std::cout << "get file size failed!\n";
return -1;
}
return st.st_size;
}
time_t LastMTime(){
struct stat st;
if (stat(_filename.c_str(), &st) < 0) {
std::cout << "get file size failed!\n";
return -1;
}
return st.st_mtime;
}
time_t LastATime() {
struct stat st;
if (stat(_filename.c_str(), &st) < 0) {
std::cout << "get file size failed!\n";
return -1;
}
return st.st_atime;
}
std::string FileName(){
// ./abc/test.txt
size_t pos = _filename.rfind("\\");
if (pos == std::string::npos) {
return _filename;
}
return _filename.substr(pos+1);
}
bool GetPosLen(std::string *body, size_t pos, size_t len){
size_t fsize = this->FileSize();
if (pos + len > fsize){
std::cout << "get file len is error\n";
return false;
}
std::ifstream ifs;
ifs.open(_filename, std::ios::binary);
if (ifs.is_open() == false) {
std::cout << "read open file failed!\n";
return false;
}
ifs.seekg(pos, std::ios::beg);
body->resize(len);
ifs.read(&(*body)[0], len);
if (ifs.good() == false) {
std::cout << "get file content failed\n";
ifs.close();
return false;
}
ifs.close();
return true;
}
bool GetContent(std::string *body) {
size_t fsize = this->FileSize();
return GetPosLen(body, 0, fsize);
}
bool SetContent(const std::string &body) {
std::ofstream ofs;
ofs.open(_filename, std::ios::binary);
if (ofs.is_open() == false) {
std::cout << "write open file failed!\n";
return false;
}
ofs.write(&body[0], body.size());
if (ofs.good() == false) {
std::cout << "write file content failed!\n";
ofs.close();
return false;
}
ofs.close();
return true;
}
bool Exists() {
return fs::exists(_filename);
}
bool CreateDirectory() {
if (this->Exists()) return true;
return fs::create_directories(_filename);
}
bool ScanDirectory(std::vector<std::string> *arry) {
for(auto& p: fs::directory_iterator(_filename)) {
if (fs::is_directory(p) == true){
continue;
}
//relative_path 带有路径的文件名
arry->push_back(fs::path(p).relative_path().string());
}
return true;
}
};
}
5.2 数据管理类
数据管理部分主要管理的就是备份文件夹内的数据,由于客户端任务比较简单,所以我们不需要那么多文件属性,直接建立表格,将文件的真实路径和文件唯一标识建立哈希映射,文件标识中包含有文件最后一次修改的时间(这是为了方便在业务逻辑中判断该文件是否需要重新备份上传)
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include"util.hpp"
#include<unordered_map>
#include<sstream>
namespace CH
{
class DataManager
{
private:
std::string _back_file;//备份文件持久化存储文件
std::unordered_map<std::string, std::string> _table;//用来管理备份文件的数据,《文件名,唯一值》
public:
DataManager(const std::string& back_file)
:_back_file(back_file)
{
InitLoad();
}
//按特定的字符来分割字符串,放到数组中
void Split(const std::string& body, const std::string& sep, std::vector<std::string>* arry)
{
int start = 0,pos=0;
while (true)
{
pos = body.find(sep,start);
if (pos == std::string::npos)
{
break;
}
//从哪里开始,截取多少个
std::string tmp = body.substr(start,pos-start);
arry->push_back(tmp);
start = pos + sep.size();
}
if (pos < body.size())
{
arry->push_back(body.substr(pos));
}
}
//加载持久化存储文件,将持久化存储文件中的数据放入哈希表中进行管理
bool InitLoad()
{
//从文件中读取文件信息
FileUtil fu(_back_file);
std::string body;//将文件信息放到body中
fu.GetContent(&body);
//将数据解析,放入哈希表中进行管理
std::vector<std::string> arry;
//将持久化存储文件中一条条文件数据,存入数组中
Split(body, "\n", &arry);
//将文件的名字和唯一值放入哈西表中
for (auto& a : arry)
{
//a.txt a.txt-123456789
std::vector<std::string> tmp;
Split(a, " ", &tmp);
if (tmp.size() != 2){
continue;
}
_table[tmp[0]] = tmp[1];
}
return true;
}
//将哈希表中的数据放入持久化存储文件中
bool Storage()
{
//获取哈希表中的所有数据
std::stringstream ss;
auto it = _table.begin();
while (it != _table.end())
{
ss << it->first << " " << it->second <<"\n";;
it++;
}
//将其放入持久化存储文件中
FileUtil fu(_back_file);
fu.SetContent(ss.str());
return true;
}
//在哈希表中插入数据
bool Insert(const std::string& key,const std::string& value)
{
_table[key] = value;
//将新插入的数据,进行持久化存储文件的保存
Storage();
return true;
}
//更新该文件的唯一值
bool Update(const std::string& key, const std::string& value)
{
_table[key] = value;
//将新插入的数据,进行持久化存储文件的保存
Storage();
return true;
}
//根据文件名,找到唯一值
bool GetOneByKey(const std::string& key, std::string* value)
{
auto it = _table.find(key);
if (it == _table.end()){
return false;
}
*value = it->second;
return true;
}
};
}
5.3 业务处理类
业务处理部分的核心逻辑很简单:
获取到文件夹中的所有文件
将所有的文件进行检查,是否需要备份
在文件数据管理中没出现的新文件需要备份
被修改过的文件需要备份
当找到需要备份的文件,则对文件进行上传
添加文件数据管理信息
cpp
#pragma once
#include"data.hpp"
#include"util.hpp"
#include"httplib.h"
#include <windows.h>
namespace CH
{
#define SERVER_IP "115.159.87.192"
#define SERVER_PORT 8080
class Backup
{
private:
std::string _back_dir;//备份目录,也就是要上传文件的目录
DataManager* _data;//管理文件
public:
Backup(const std::string& back_dir,const std::string& back_up)
:_back_dir(back_dir)
{
_data = new DataManager(back_up);
}
bool RunModule()
{
while (true)
{
//遍历备份目录的文件,将其存入数组
FileUtil fu(_back_dir);
std::vector<std::string> arry;
fu.ScanDirectory(&arry);
//对于这些文件,逐个判断是否要上传
for (auto& a : arry)
{
if (IsNeedUpload(a) == false) {
continue;
}
//文件需要上传
if (Upload(a) == true) {
_data->Insert(a, GetFileETag(a));//新增文件备份信息
std::cout << a << " upload success!\n";
}
}
Sleep(2000);
}
}
//根据文件名,获取文件的唯一值
std::string GetFileETag(const std::string& filename)
{
// a.txt-fsize-mtime
FileUtil fu(filename);
std::stringstream ss;
ss << fu.FileName() << "-" << fu.FileSize() << "-" << fu.LastMTime();
return ss.str();
}
//判断文件是否要上传
bool IsNeedUpload(const std::string& filename)
{
// 需要上传的文件的判断条件:文件是新增的,不是新增但是被修改过
// 文件是新增的:看一下有没有历史备份信息
// 不是新增但是被修改过:有历史信息,但是历史的唯一标识与当前最新的唯一标识不一致
std::string etag;//保存旧的唯一值
if (_data->GetOneByKey(filename,&etag) != false)
{
//获取该文件新的唯一值
std::string new_etag = GetFileETag(filename);
if (etag == new_etag) {//相同,则表示没有被修改过
return false;
}
}
//一个文件比较大,正在徐徐的拷贝到这个目录下,拷贝需要一个过程,
//如果每次遍历则都会判断标识不一致需要上传一个几十G的文件会上传上百次
//因此应该判断一个文件一段时间都没有被修改过了,则才能上传
FileUtil fu(filename);
if (time(NULL) - fu.LastMTime() < 3) {//3秒钟之内刚修改过--认为文件还在修改中
return false;
}
std::cout << filename << " need upload!\n";
return true;
}
//上传文件
bool Upload(const std::string& filename)
{
//获取该文件的所有数据
FileUtil fu(filename);
std::string body;
fu.GetContent(&body);
//搭建客户端上传文件数据
httplib::Client client(SERVER_IP, SERVER_PORT); //创建客户端
httplib::MultipartFormData item; //创建字段
item.content = body;//设置该字段携带文件的内容
item.filename = fu.FileName();//设置携带文件名
item.name = "file"; //设置字段名,这是和服务器约定好的
item.content_type = "application/octet-stream";
httplib::MultipartFormDataItems items;
items.push_back(item);
auto res = client.Post("/upload", items);
if (!res || res->status != 200) {
return false;
}
return true;
}
};
}
5.4 客户端主函数
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING
#include"util.hpp"
#include"cloud.h"
#include"data.hpp"
#define BACKUP_FILE "./backup.txt"
#define BACKUP_DIR "./backup/"
int main()
{
CH::Backup backup(BACKUP_DIR, BACKUP_FILE);
backup.RunModule();
return 0;
}
六、项目总结
**项⽬名称:**云备份系统
项⽬功能:搭建云备份服务器与客⼾端,客⼾端程序运⾏在客⼾机上⾃动将指定⽬录下的⽂件备份到服务器,并且能够⽀持浏览器查看与下载,其中下载⽀持断点续传功能,并且服务器端对备份的⽂件进⾏热点管理,将⻓时间⽆访问⽂件进⾏压缩存储。开发环境: centos7.6/vim、g++、gdb、makefile 以及 windows10/vs2017
技术特点: http 客⼾端/服务器搭建, json 序列化,⽂件压缩,热点管理,断点续传,线程池, 读写锁,单例模式
项⽬模块:
1. 服务端:
**a. 数据管理模块:**内存中使⽤hash表存储提⾼访问效率,持久化使⽤⽂件存储管理备份数据
**b. 业务处理模块:**搭建 http 服务器与客⼾端进⾏通信处理客⼾端的上传,下载,查看请求,并⽀持断点续传
**c. 热点管理模块:**对备份的⽂件进⾏热点管理,将⻓时间⽆访问⽂件进⾏压缩存储,节省磁盘空 间。
2. 客⼾端**a. 数据管理模块:**内存中使⽤hash表存储提⾼访问效率,持久化使⽤⽂件存储管理备份数据
**b. ⽂件检索模块:**基于 c++17 ⽂件系统库,遍历获取指定⽂件夹下所有⽂件。
**c. ⽂件备份模块:**搭建 http 客⼾端上传备份⽂件。
七、项目拓展
给客⼾端开发⼀个好看的界⾯,让监控⽬录可以选择
内存中的管理的数据也可以采⽤热点管理
压缩模块也可以使⽤线程池实现
实现⽤⼾管理,不同的⽤⼾分⽂件夹存储以及查看
实现断点上传
客⼾端限速,收费则放开