『 实战项目 』Cloud Backup System - 云备份
文章目录
- 云备份项目
- 服务端功能
- 服务端功能模块划分
- 客户端功能
- 客户端模块划分
- 项目条件
- Jsoncpp第三方库
- Bundle第三方库
- httplib第三方库
- Request类
- Response类
- Server类
- Client类
- 搭建简单服务器
- 搭建简单客户端
- 服务端工具类实现 - 文件实用工具类
- 服务器配置信息模块实现- 系统配置信息
- 服务端配置信息模块实现 - 单例文件配置类设计
- 服务端管理模块实现 - 需要管理的数据(后期需要用到的数据)
- 服务端管理模块设计
- 热点管理模块
- 网络通信模块和业务处理模块
- 网络通信接口设计
- 服务端业务处理类的设计
- 断点续传
- 客户端
- 数据管理模块
- 数据管理类设计
- 文件备份类设计
- 项目源码
云备份项目
将本地指定目录中需要备份的文件进行上传,将文件上传至服务器当中;
且随时能通过浏览器进行查看与下载;
下载过程中支持断点续传(暂停后继续下载);
服务器对所上传文件进行热点管理:
-
判定文件是否为热点文件
热点文件的判定为在一定时间或者条件内访问该文件的此处或者是文件最后一次访问的时间来判断是否为热点文件;
-
为热点文件
不处理
-
为非热点文件
将非热点文件进行压缩处理以节省云服务器的磁盘空间;
-
服务端功能
-
支持客户端文件的备份
-
支持客户端浏览器的查看与下载
-
支持客户端文件下载功能
断点续传;
-
热点文件管理
对服务器中长时间无访问的文件进行压缩存储存储;
服务端功能模块划分
-
数据管理模块
管理的备份文件信息,以便于随时获取;
-
网络通信模块
实现与客户端的网络通信;
-
业务处理模块
负责上传,展示列表,下载(断点续传)功能;
-
热点文件管理模块
对长时间无访问文件进行压缩存储;
客户端功能
-
指定文件夹中的文件检测
获取文件夹中的文件;
-
判断文件是否需要备份
新增的文件,备份在服务端但被修改过的文件;
除此之外间隔一段时间进行判断是否需要备份(如五分钟,十分钟);
-
将需要备份的文件上传到服务器上
客户端模块划分
-
数据管理模块
管理备份文件信息;
-
文件检测模块
检测/监控指定的目录/文件夹;
-
例:
当一个文件被加载至文件夹中, 而数据管理模块中备份文件信息并没有该文件的信息则表示该文件为新加入的文件,需要进行备份;
检测一个文件的信息与备份的文件信息是否一致,检测文件是否又被修改,若是文件被修改则需要进行备份;
-
-
文件备份模块(网络通信模块)
上传需要备份的文件数据,使得服务端存储所上传的数据;
项目条件
环境要求编译器gcc-c++ 7.3.1 以上支持;
-
环境搭建:
bundle数据压缩库,jsoncpp库,httplib第三方网络库;
Jsoncpp第三方库
数据交换格式,数据序列化与反序列化;
数据类型: 对象,数组,字符串,数字
-
对象
使用花括号
{}
括起来的表示为一个对象; -
数组
使用中括号
[]
括起来的表示一个数组; -
字符串
使用
""
常规双引号括起来表示一个字符串; -
数字
直接使用,包括整形和浮点型;
假设一组数据以cpp
的形式为:
/* Cpp */
char* name1 = "张三";
char* name2 = "李四";
int age1 = 18;
int age2 = 22;
float score1[3] = {88,89,90};
float score2[3] = {99,82.5,100}
对应json
的形式则为:
[
{
"姓名" : "张三",
"年龄" : 18,
"成绩" : [88,89,90]
},
{
"姓名" : "李四",
"年龄" : 22,
"成绩" : [99,82.5,100]
}
]
Bundle第三方库
对文件进行压缩与解压缩;
为嵌入式第三方库,嵌入式表示需要将头文件源文件嵌入至项目内(静态库.h
文件);
/* 使用例 */
#include <cassert>
#include "bundle.h"
int main() {
using namespace bundle;
using namespace std;
// 23 mb dataset
string original( "There's a lady who's sure all that glitters is gold" );
for (int i = 0; i < 18; ++i) original += original + string( i + 1, 32 + i );
// pack, unpack & verify all encoders
vector<unsigned> libs {
RAW, SHOCO, LZ4F, MINIZ, LZIP, LZMA20,
ZPAQ, LZ4, BROTLI9, ZSTD, LZMA25,
BSC, BROTLI11, SHRINKER, CSC20, BCM,
ZLING, MCM, TANGELO, ZMOLLY, CRUSH, LZJB
};
for( auto &lib : libs ) {
string packed = pack(lib, original);
string unpacked = unpack(packed);
cout << original.size() << " -> " << packed.size() << " bytes (" << name_of(lib) << ")" << endl;
assert( original == unpacked );
}
cout << "All ok." << endl;
}
其中主要的操作为pack()
与unpack
;
共有三个等级的API接口:
namespace bundle
{
// low level API (raw pointers)
bool is_packed( *ptr, len );
bool is_unpacked( *ptr, len );
unsigned type_of( *ptr, len );
size_t len( *ptr, len );
size_t zlen( *ptr, len );
const void *zptr( *ptr, len );
bool pack( unsigned Q, *in, len, *out, &zlen );
bool unpack( unsigned Q, *in, len, *out, &zlen );
// medium level API, templates (in-place)
bool is_packed( T );
bool is_unpacked( T );
unsigned type_of( T );
size_t len( T );
size_t zlen( T );
const void *zptr( T );
bool unpack( T &, T );
bool pack( unsigned Q, T &, T );
// high level API, templates (copy)
T pack( unsigned Q, T );
T unpack( T );
}
主要的操作流程为:
- 压缩
- 创建两个文件流(读文件流和写文件流)
- 用读文件流打开原文件(以二进制形式即
std::ios::binary
) - 用
seekg()
将文件流跳转至末尾并使用tellg()
获取当前文件流位置(本意是获取偏移量来计算文件大小)最后再次使用seekg()
将文件流跳回远处 - 创建一个
std::string
字符串对象并使用文件大小来初始化字符串大小 - 读文件流调用
read()
将文件数据写至字符串中 - 字符串调用
bundle::pack()
传入字符串进行压缩同时将返回一个字符串std::string
(Hight level API) - 用写文件流打开文件(以二进制形式即
std::ios::binary
) - 调用
write()
将压缩后的数据写入文件中作为压缩文件 - 关闭两个文件流
- 解压过程一致,只是调用接口不同,不进行赘述
httplib第三方库
第三方的网络库,一个C++11
单头文件的跨平台HTTP/HTTPS
库;
使用时包含httplib.h
头文件即可;
使用第三方库能够使得更加轻松的搭建一个http
服务器或者客户端从而提高开发效率;
Request类
Http请求格式通常为:
Request类为如下:
struct MultipartFormData {
std::string name; // 字段名称
std::string content; // 文件内容
std::string filename; // 文件名称
std::string content_type; // 正文类型
};
using MultipartFormDataItems = std::vector<MultipartFormData>;
struct Request {
// HTTP method and path
std::string method; // 请求方法
std::string path; // 资源路径
// Query parameters and headers
Params params; // 查询字符串
Headers headers; // 头部字段
// Request body
std::string body; // 正文部分
// Remote and local address information
std::string remote_addr;
int remote_port = -1;
std::string local_addr;
int local_port = -1;
// Server-specific attributes
std::string version; // 协议版本
MultipartFormDataMap files; // 所保存的客户端上传文件信息(见首行)
Ranges ranges; // 用于实现断点续传的请求文件区间
// 存在一个开始位置和结束位置
// Header-related methods
bool has_header(const std::string &key) const; // 查询header中是否有哪个头部字段
std::string get_header_value(const std::string &key, const char *def = "", size_t id = 0) const; // 获取头部字段的值
void set_header(const std::string &key, const std::string &val); // 设置头部字段的值
bool has_file(const std::string &key) const; // 是否包含某个文件
MultipartFormData get_file_value(const std::string &key) const; // 获取文件信息
private:
// ...
};
客户端保存的所有http
请求相关信息,将其组织成http
请求报文并发送给服务器;
服务器收到http
请求后进行解析,将解析的数据保存在Request
结构体中;
Response类
http响应格式为:
Response类通常存储响应信息;
Response类为如下:
struct Response {
std::string version; // 协议版本 - 不需要填充
int status = -1; // 响应状态码
Headers headers; // 头部字段
std::string body; // 响应给客户端的正文
void set_header(const std::string &key, const std::string &val); // 设置头部字段
void set_content(const std::string &s, const std::string &content_type); // 设置正文
// ...
};
-
功能:
用户将响应数据放到结构体中,httplib将会其中的数据按照httpResopnse格式组织成为一条响应,并发送给客户端;
Server类
Server类用于搭建http服务器:
class Server {
public:
using Handler = std::function<void(const Request &, Response &)>;
// 函数指针类型 - 定义了一个http请求处理回调函数格式
// httplib搭建的服务器收到请求后进行解析,得到一个Request结构体,其中包含请求数据
// 根据请求数据从而可以处理这个请求 处理函数定义的格式就是Handler
// Request参数保存请求数据 让用户能够根据请求数据进行业务处理
// Response参数需要用户在业务处理中填充数据,最终将数据通过响应的方式返回给客户端
// -----------
using Handlers = std::vector<std::pair<std::regex, Handler>>;
// Handlers 表示一个请求路由数组(请求与处理函数映射表)
// 其中regex 是一个正则表达式 用于匹配http请求资源路径
// Handler 为请求处理函数指针
// 可以理解为 Handlers 是一张表,映射了一个客户端请求的资源路径和一个处理函数(用户自定义的函数)
// 当服务器接收到了对应的请求后将会根据资源路径以及请求方法到这张表中查找是否有匹配的处理函数 若是没有匹配的处理函数则返回 404
// -----------
std::function<TaskQueue *(void)> new_task_queue;
// 线程池 - 用于处理请求
// 当httplib收到一个新的连接时 这个客户端连接将会被扔进线程池中;
/* 线程池中线程的工作:
1. 接收请求,解析请求,得到Request结构体
2. 在Handlers映射表中,根据请求信息查找处理函数,如果有则调用处理函数 void(const Request &, Response &)
3. 当处理函数调用完毕,根据函数返回的Response结构体中的数据组织http响应发回给客户端
*/
// -----------
bool listen(const std::string &host, int port, int socket_flags = 0);
// 搭建并启动 http 服务器
// -----------
// 下列函数为针对某种请求方法的
// 其中 handler 可调用对象参数为需要传入的函数指针
// 其中 pattern 表示资源路径 正则表达式的格式
Server &Get(const std::string &pattern, Handler handler);
Server &Post(const std::string &pattern, Handler handler);
Server &Put(const std::string &pattern, Handler handler);
Server &Patch(const std::string &pattern, Handler handler);
Server &Delete(const std::string &pattern, Handler handler);
Server &Options(const std::string &pattern, Handler handler);
};
Client类
struct MultipartFormData {
std::string name; // 字段名称
std::string content; // 文件内容
std::string filename; // 文件名称
std::string content_type; // 正文类型
};
using MultipartFormDataItems = std::vector<MultipartFormData>;
class Client {
public:
// Universal interface
explicit Client(const std::string &scheme_host_port); // 传入服务器的IP地址和端口
// 下列接口都为客户端向服务端发送对应请求
Result Get(const std::string &path, const Headers &headers); // 向服务端发送GET请求 参数分别传递请求路径和头部字段
Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type); // 向服务端发送 POST 请求, path 表示提交给哪个资源路径, body 表示正文数据, content_length 表示正文长度, content_type 表正文类型
Result Post(const std::string &path, const MultipartFormDataItems &items);
// 向服务端发送 POST 请求, path 表示资源提交路径, items 的类型为 MultipartFormDataItems, 实际上为一个vector数组;
// POST 请求提交多区域数据, 常用于多文件上传
};
搭建简单服务器
搭建服务器类最简单的方式就是使用httplib
第三方库实例化出一个对应的Server
类服务器对象server
;
通过httplib::Server::Get()
,httplib::Server::Post()
等成员函数将对应的请求与处理函数注册进服务器当中;
最后通过httplib::Server::listen()
函数传入需要监听的IP
和端口号,实例化并启动服务器;
#include <iostream>
#include <regex.h>
#include "httplib.h"
void Hello(const httplib::Request &req,httplib::Response &rsp){
rsp.set_content("Hello world","text/plain"); // 设置响应正文 其中数据类型为正文类型 "text/plain"
rsp.status = 200; // 设置状态码为 200
}
void Numbers(const httplib::Request &req, httplib::Response &rsp){
auto num = req.matches[1]; // 位置[0]中保存的是整体path,往后的下标中保存的都是捕捉的数据
rsp.set_content(num,"text/plain"); // 此处 num 获取上来仍为一个字符串 并未进行类型转移 因此直接以文本类型输出即可
rsp.status = 200;
}
void Multipart(const httplib::Request &req, httplib::Response &rsp){
// 来打酱油的 混个眼熟 在客户端中再配
auto ret = req.has_file("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(){
// std::cout<<"hello world"<<std::endl;
httplib::Server server; // 实例化一个Server对象
server.Get("/hi",Hello); // 注册一个针对"hi"的GET请求的处理函数映射关系
// server.Get(R"(/number/(\d+))",Numbers); // 这两行内容大致相同 其中采用 R 来去除字符串中特殊含义 如转义字符
server.Get("/number/(\\d+)",Numbers); // vimplus 中的报错可以不用卵
// ()用来捕捉数据
// 这里是注册一个针对"/number/[具体数字]"的GET请求的处理函数映射关系
// \d 是正则表达式 表示[0-9]的字符
// + 表示子表达式一次或多次 (>=1)
server.Post("/multipart",Multipart);
server.listen("0.0.0.0",8121); // 创建并使用服务器
return 0;
}
运行服务器后浏览器可以正常访问(防火墙已经开启);
搭建简单客户端
客户端的搭建与服务器的搭建别无二致;
客户端只需要实例化出一个httplib::Client
类型的客户端类并传入需要访问的服务器IP地址与端口号即可;
无需调用listen
,当程序运行时即表示对服务器进行访问;
在客户端中可以直接调用对应的成员函数,如httplib::Client::Get()
,httplib::Client::Post()
函数来进行对应的操作,如上传某个资源或者获取某个资源;
#include <iostream>
#include "httplib.h" // 引入 httplib 库的头文件,用于 HTTP 请求处理
#include <vector> // 引入 vector 容器,用于存储 multipart 文件条目
// 定义服务器地址和端口号
static const char* SERVER_IP = "114.55.52.91"; // 目标服务器的 IP 地址
static const uint16_t SERVER_PORT = 8121; // 目标服务器对应的端口号
int main() {
// 创建一个 HTTP 客户端,设置目标服务器的 IP 和端口
httplib::Client client(SERVER_IP, SERVER_PORT);
// 定义一个 MultipartFormData 对象,用于存储文件字段信息
httplib::MultipartFormData item;
item.name = "file"; // 字段名称为 file,对应表单中的 key 值
item.filename = "hello.txt"; // 设置文件名称为 hello.txt
item.content = "Hello world"; // 设置文件内容为 Hello world
item.content_type = "text/plain"; // 指定文件的 MIME 类型为纯文本类型 (text/plain)
// 创建一个列表(容器),用于存放多个 MultipartFormData 对象
httplib::MultipartFormDataItems items;
items.push_back(item); // 将上述文件数据结构添加到列表中
// 向服务器发送 POST 请求到路径 /multipart,并附加 multipart 文件数据
auto res = client.Post("/multipart", items);
// 打印服务器返回的 HTTP 状态码和响应内容
if (res) { // 检查请求是否成功 (res 是否为 nullptr)
std::cout << "HTTP Status Code: "<< res->status << std::endl; // 输出状态码
std::cout << "Response Body: "<< res->body << std::endl; // 输出响应主体
} else { // 如果请求失败
std::cerr << "Error: Failed to connect to server! "<< std::endl;
}
return 0;
}
该案例中使用了httplib::MultipartFormData
来定义一个文件数据结构并上传对应的文件信息;
结合上文服务器构建;
运行程序结果如下:
服务端工具类实现 - 文件实用工具类
设计一个封装文件操作类,用于简化后续任意模块对文件的操作;
-
结构大致如下:
class { private: std::string _filename; // 文件名(包含路径) public: size_t FileSize(); // 获取文件大小 time_t LastMTime(); // 获取文件最后一次修改时间 time_t LastATime(); // 获取文件最后一次访问时间 (通过访问时间判断是否为热点文件) std::string FileName(); // 获取文件路径名中的文件名称 bool SetContent(const std::string &body); // 向文件内写数据(write) bool GetContent(std::string *body); // 从文件中读取数据(read) bool GetPoslen(std::string *body, size_t pos, size_t len); // 获取文件指定位置 指定长度的数据 (断点续传) bool Exists(); // 判断文件(或目录)是否存在 bool CreateDirectory(); // 创建目录 bool GetDirectory(std::vector<std::string> *arry); // 遍历目录(目录也是文件) 获取目录中所有文件的文件名(包括路径) bool Compress(const std::string &packname); // 进行热点管理时的压缩 bool UnCompress(const std::string &packname); // 解压缩 };
服务器配置信息模块实现- 系统配置信息
-
热点判断时间
判断多长时间没有访问的文件为非热点文件;
-
文件下载的URL前缀路径
采用Web根目录的方式,如
wwwroot
的方式配置web
根目录使得避免客户端可通过路径直接访问到服务器的其他非web
资源;同时通过区别前缀路径判别不同请求,如:
http://127.0.0.1:8080/download/downloadfile.txt
其中
/download
这个前置路径表示这是一个下载请求,下载的文件是downloadfile.txt
文件; -
压缩包后缀名称
在对一个文件进行压缩后,这个文件压缩后的文件名为原文件名+对应的压缩格式;
如在压缩模块中采用
LZIP
形式进行压缩;假设对一个为
downloadfile.txt
文件进行压缩,压缩后的文件名则为downloadfile.txt.lz
; -
上传文件存放路径
一个文件上传后在云服务器中的对应位置;
在上面提到以
wwwroot
作为web
根目录,上传文件放在哪一个区域; -
压缩文件存放路径
与上传文件相似,当服务器检测到一个文件由热点文件变为非热点文件状态后将要对文件进行压缩,压缩后将要与未压缩文件进行区分,将其单独放在一个列表中进行管理;
-
服务端备份信息存放文件
当客户端上传一个文件后服务端需要对上传的文件信息进行备份,这些信息包括文件大小,文件名,最后修改时间,最后访问时间等等;
这些信息将单独存放在一个文件中方便进行查找;
-
服务器的访问IP地址
-
服务端的访问端口
服务端配置信息模块实现 - 单例文件配置类设计
class Config{
public:
static COnfig *GetInstance(); // 获取单例
private:
Config(){} // 构造函数私有化 并且在构造函数中读取配置文件
static std::mutex _mutex; // 互斥锁 - 搞定同步问题
static Config *_instance; // 单例实例
private:
int _hot_time; // 热点时间判断
int _server_port; // 服务器监听端口
std::string _download_prefix; // 下载url前缀路径
std::string _packfile_suffix; // 压缩包后缀名称
std::string _back_dir; // 备份文件存放目录
std::string _pack_dir; // 压缩包文件存放目录
std::string _server_ip; // IP地址
std::string _backup_file; // 数据信息存放文件
public:
int GetHotTime(); // 获取热点时间
int GetServerPort(); // 获取服务器监听端口
std::string GetDownloadPrefix(); // 获取前置路径
std::string GetPackFileSuffix(); // 获取压缩包
std::string GetBackDir(); // 获取备份文件目录
std::string GetPackDir(); // 获取压缩包文件目录
std::string GetServerIP(); // 获取IP地址
std::string GetBackupFile(); // 获取数据信息存放文件
};
服务端管理模块实现 - 需要管理的数据(后期需要用到的数据)
-
下载相关
-
文件的实际存储路径
当客户端需要下载文件时需要从哪个路径哪个文件中读取数据响应下载;
-
文件压缩包存放路径
被判为非热点文件的文件将被压缩存储,届时被查看后需要进行解压,解压位置在哪个位置;
对应的当客户端进行下载时也同样要把文件进行解压并传输,压缩文件路径在哪;
-
文件是否被压缩标志位
判断文件是否被压缩;
-
-
文件属性相关
-
文件大小
-
文件最后一次修改时间
-
文件最后一次访问时间
-
文件访问URL中资源路径
path
如:
/download/a.txt
-
-
如何管理数据
-
用于数据信息访问
使用哈希表在内存中进行数据管理,以
url
的path
作为key
值; -
持久化存存储管理
使用
json
序列化将所有数据信息保存在文件中;
-
服务端管理模块设计
-
数据信息结构体
typedef struct BackupInfo_t{ bool pack_flag;// 判断文件是否被压缩 size_t fsize; // 文件大小 time_t atime; // 最后一次访问时间 time_t mtime; // 最后一次修改时间 std::string real_path; // 文件实际存储路径名称 std::string pack_path; // 压缩包存储路径名称 std::string url_path; // 请求资源路径 }BackupInfo;
-
数据管理类
class DataManager{ private: DataManager(); std::string _backup_file; // 持久化存储文件 std::unordered_map<std::string, BackupInfo> _table; // 数据信息结构体在内存中以 hash 表存储 pthread_rwlock_t _rwlock; // 读写锁 引入线程池后使得多个线程能够一起读 写时互斥 bool Storage(); // 持久化存储 - 每当数据新增或者修改时都要重新持久化存储以避免数据丢失 bool InitLoad(); // 初始化加载, 在每次系统重启后都要加载以前的数据 bool Insert(const BackupInfo &info); // 新增 - 将BackupInfo存储进哈希表中 以 k-v 的形式存储 bool Update(const BackupInfo &info); // 当一个文件被压缩后可能需要修改某些信息 如:bool pack_flag; bool GetOneByUrl(const std::string &url, BackupInfo *info); // 当客户端发起下载请求时需要获取对应的文件信息 bool GetOneByRealpath(const std::string &path, BackupInfo *info); // 根据真实路径来获取文件信息 判断其是否为一个非热点文件等信息 bool GetAll(std::vector<BackupInfo> *arry); // 获取所有信息 };
-
获取信息函数
void NewBackupInfo(const std::string realpath, BackupInfo *info); // 可作为 BackupInfo 的成员函数
通过所传入的路径,将路径对应的文件中的信息在函数中填充至
info
中;
热点管理模块
对服务器上备份的文件进行检测,检测哪些文件长时间没有被访问则认为是非热点文件进行压缩存储从而节省磁盘空间;
实现思路为:
遍历所有的文件,检测文件最后一次访问时间,与当前时间进行相减得到差值;
这个差值如果大于设定好的非热点判断时间则认为是非热点文件;
-
遍历所有文件
- 从数据管理模块中遍历所有备份文件信息
- 遍历备份文件夹,获取所有文件进行属性获取从而判断
选择遍历备份文件夹从而获取最新的数据;
数据管理模块中的数据不一定是最新的,并且遍历文件夹同时可以解决数据信息缺漏的问题;
- 遍历备份目录 获取所有文件路径名称
- 逐个文件获取最后一次访问时间与当前系统时间进行比较判断
- 对非热点文件进行压缩处理并删除源文件
- 修改数据管理模块对应的文件信息(压缩标志 ->
true
)
流程为如下:
- 获取备份目录下的所有文件
- 逐个判断文件是否为非热点文件
- 非热点文件压缩处理
- 删除源文件 修改备份信息
extern DataManager* _data; // 数据管理模块
class HotManager{
private:
std::string _back_dir; // 备份文件路径
std::string _pack_dir; // 压缩文件路径
std::string _pack_suffix; //压缩包后缀名
int _hot_time; // 热点判断时间
public:
HotManager();
bool RunModule(); // 模块启动
};
网络通信模块和业务处理模块
网络模块可以直接使用httplib
实现,因此具体只需要处理业务处理即可;
-
搭建网络通信服务器
借助
httplib
完成; -
业务请求处理
-
文件上传请求
备份客户端上传的文件,响应上传成功;
-
文件列表请求:
客户端浏览器请求一个备份文件的展示页面,响应页面;
-
文件下载请求
通过展示页面,点击下载,响应客户端要下载的文件数据;
-
网络通信接口设计
约定好客户端发送什么样的请求,服务端返回什么样的响应;
-
请求
- 文件上传
- 展示页面
- 文件下载
-
接口设计
接口设计为上述三种请求对应的响应接口;
-
文件上传
当客户端发送了一个
POST
方法的"/upload"
请求时表示是一个上传请求;解析请求得到文件,将数据写入到文件中;
并返回一个对应的响应:
上传成功:
HTTP/1.0 200 OK
上传失败例:
HTTP/1.1 500 Bad Request
-
页面展示
当客户端发送了一个
GET
方法的"/listshow"
请求时表示是一个页面请求;对应的响应可能为:
HTTP/1.1 200 OK Content-Length: 头部信息... <html>...</html>正文信息 <!-- 页面信息 -->
-
文件下载
当客户端发送了一个
GET
方法的"/download/filename"
请求时表示为下载一个名为filename
文件;对应的响应可能为:
HTTP/1.1 200 OK Content-Length: 文件长度等头部信息 正文数据(文件数据)
-
服务端业务处理类的设计
class Service{
// 搭建一个http服务器并且进行业务处理
private:
int _server_port; // 服务器端口 - 可从配置文件获取
std::string _server_ip; // 服务器IP - 可从配置文件获取
std::string _download_prefix; // 下载前缀 - 可从配置文件获取
httplib:Server _server; // 构建服务器
private:
void Upload(const httplib::Request &req, httplib::Response &rsp); // 上传业务处理
void ListShow(const httplib::Request &req, httplib::Response &rsp); // 页面展示业务处理
void Download(const httplib::Request &req, httplib::Response &rsp); // 下载请求业务处理
public:
Server(); // 构造函数 初始化服务器所需资源
bool RunModule(); // 运行服务器 - 业务处理
};
-
文件上传
在进行文件上传时对应的客户端将以约定好的表单字段
"file"
判断是否为一个文件;将文件使用
FileUtil
工具写入至back_dir
备份路径中;并调用
NewBackupInfo()
填充BackupInfo
信息插入至_data
中; -
页面展示
需要展示一个
html
界面;获取所有的文件信息
BackupInfo
,组织前端信息为字符串;返回字符串的前端信息;
-
文件下载
文件下载的思路为采用
http
协议的ETag
字段(存储一个资源的唯一标识);客户端第一次下载的时候会收到这个响应信息,第二次下载的时候将会把信息发送给服务器,想要让服务器根据这个唯一标识判断资源是否又被修改,如果未被修改则直接使用原先缓存的数据,无需重新下载;
此处的
ETag
使用"文件名-文件大小-最后修改时间"组成(ETag
字段信息是什么http
协议并不关心,服务端能够自己标识即可);
断点续传
当文件下载过程中因为某种异常导致中断,如果再次从头下载将会降低效率;
因为已经传输过的数据需要再传输一次;
断点续传是在上次下载断开的位置继续下载,已经传输过的数据将不需要重新传输;
-
目的
提高文件重新传输效率;
-
思想
客户端在下载文件时需要时刻记录当前下载数据量;
当异常下载中断时下次断点续传需要将重新下载的数据区间(起始位置与当前位置)发送给服务器;
服务器接收后仅回传客户端剩余数据;
异常下载中断后的重新下载需要判断源文件是否有被修改而判断是否需要重新下载;
如果源文件已经被修改那就需要重新下载,因为已经下载下来的数据可能已经被修改了是作废数据;
在客户端发起第一次下载请求时,服务端向客户端返回响应时需要返回对应的ETag字段;
当出现异常后客户端再次向服务端发起下载请求,此时的下载请求将会携带 If-Range
头部字段;
If-Range
头部字段为服务端在下载时响应的etag
字段;
同样断点续传的下载请求还会携带头部字段Range: bytes [N]-[M]
;
这个表示客户端需要的数据区间为[N]-[M]
;
-
服务端动作
判断
If-Range
和ETag
字段是否相同,不同则从头将文件进行传输;相同则返回
[N]-[M]
的数据;当进行断点续传时,即
If-Range
和ETag
字段相同时对应的返回状态码不再是200
,而应该是206
,表示服务端处理了部分GET
请求(Partial Content
);-
响应
HTTP/1.1 206 Partial Content ETag: "xxxxxxxxxx" Content-Range: bytes [N]-[M]/文件大小 [数据正文]正文即为对应区间数据
其中
Range:
字段有几种形式:bytes start-end # 从头到结束 bytes 100-200 # 从100byte-200byte bytes 100- # 从100byte到结束
-
客户端
自动对指定文件夹进行备份;
-
模块划分
-
数据管理模块
管理备份文件信息;
-
目录遍历
获取指定文件夹中的所有文件路径名;
-
文件备份模块
将需要备份的文件上传至服务器;
-
开发环境为Windows11
,采用2017
以上版本的VisualStudio
作为集成开发环境(支持C++17
);
-
同样采用与服务器相同的
FileUtil
将服务端中设计的
util.hpp
文件进行拷贝至当前项目目录中;由于在客户端中未使用
jsoncpp
第三方库,因此可以将对应JsonUtil
所封装的内容进行删除;同时客户端无需操作压缩
Compress
和解压UnCompress
,应对应功能进行删除;所使用的
VisualStudio
可能提示已经在C++17
中摒弃了#include <experimental/filesystem>
需要将该头文件改成#include <filesystem>
;可以使用下列
#define
进行定义以防止编译时的报错:#define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING 1
数据管理模块
数据管理模块用于管理备份文件信息;
判断一个文件是否需要进行备份(或重新备份);
- 文件是否为新增
- 若是文件不为新增则判断该文件是否为已备份文件的修改版
主要管理的数据为 “文件路径名” 与 “文件唯一标识” ;
-
实现思想
-
内存存储
高效率访问,使用哈希表(
unordered_map
); -
持久化存储(文件存储)
文件存储涉及到数据序列化与反序列化(不采用
jsoncpp
,自定义序列化格式);key value\n
key
为文件路径名,value
为文件唯一标识(判断文件上传后是否被修改);
-
数据管理类设计
客户端的数据管理类与服务端大致相同(本质上都为数据管理);
class DataManager {
private:
std::unordered_map<std::string, std::string> _table; // 用于快速访问
std::string _backup_file; // 备份信息文件
private:
bool InitLoad(); // 读取已有备份信息
public:
DataManager(std::string);
bool Insert(const std::string& , const std::string&); // 插入新的备份信息至哈希表
bool Update(const std::string&, const std::string&); // 更新哈希表内的某个文件备份信息
bool Storage(); // 持久化存储
bool GetOneByFname(const std::string&, std::string*); // 获取一个文件的唯一标识符
int Split(const std::string&, const std::string&, std::vector<std::string>*); // 用于进行反序列化的字符串分割
}; // calss DataManager
文件备份类设计
自动将指定文件夹中的文件备份到服务器;
- 遍历指定文件夹获取文件信息
- 注意判断文件是否需要被备份
- 需要备份的文件进行上传至服务器备份
#define SERVER_ADDR "xxx.xxx.xxx.xxx" /* IP地址 */
#define SERVER_PORT 8888 /* 端口号 */
class Backup{
private:
std::string _back_dir; // 需要备份的文件夹
DataManager *_data; // 用于获取文件信息 判断文件是否需要被备份
std::string GetFileIDentifier(const std::string &filename); // 计算获取文件唯一标识
bool Upload(); // 当判断文件需要进行备份上传时则调用Upload进行备份上传
public:
Backup(std::string &backdir, std::string &backup_file); //构造函数(需要传入指定文件夹作为备份文件夹)
bool RunModule(); // 用来运行文件备份模块的主要功能(上述功能)
bool IsNeedUpload(const std::string &filename);
// 判断文件是否需要被上传 (判断文件是否有被修改/文件上一次的修改时间是否过近 如果修改时间过近则可能表示文件正在实时拷贝当中 不适合上传)
};
项目源码
-
gitee
Gitee - 半介莽夫/CloudSystem