实现本地文件通过客户端上传到网络服务器主机中,客户端也可以实现通过请求服务器查看上传的文件,可以下载云备份的文件,如果下载过程中客户端与服务器终端,还有断点续传功能,就是可以接着中断前的进度下载.
按照功能划分:服务端要支持客户端文件上传,备份文件列表查看,文件下载,断点续传,热点文件识别和管理功能,当文件长期不被访问时,对备份文件压缩存储.
按照模块划分:有数据管理模块,负责备份数据的信息管理,网络通信模块,负责与客户端建立通信,业务处理模块,负责文件长传,列表查看,文件下载功能,热点管理模块,负责对
按照功能划分:客户端要支持指定目录/文件夹的文件检测,如果目录中有文件就将文件获取上传,判断文件是否需要备份,最新一次的上传而且是修改过的文件,并且经过一段时间间隔仍然没有修改的文件就备份.将需要备份的文件上传到服务器.
按照模块儿划分,有数据管理模块,管理备份过的文件信息,文件检测模块,监控指定目录是否有文件,判断文件是否需要备份.文件上传模块.
sudo yum install -y centos-release-scl-rh centos-release-scl命令获取源信息
sudo yum install -y devtool-7-gcc devtool-7-gcc-c++命令安装7.3版本的gcc和g++
source /opt/rh/devtoolset-7/enable 临时环境配置
echo “source /opt/rh/devtoolset-7/enable” >> ~/.bashrc 将临时环境配置信息追加重定向到.bashrc文件完成永久配置.
使用g++ -v gcc -v验证安装成功
sudo yum install epel-release
sudo yum install jsoncpp-devel安装jsonc库
ls /usr/include/jsoncpp/json/json.h验证是否安装成功
sudo yum install -y git 安装git工具
git clone https://github.com/r-lyeh-archived/bundle.git 从github获取bundle压缩包
git clone https://github.com/yhirose/cpp-httplib.git 从github获取httplib压缩包
bundle库是嵌入式库,就是不用包含头文件,直接将源码拿过来使用的
json是一种数据组织格式,
json 数据类型:对象,数组,字符串,数字
对象:使用花括号 {} 括起来的表示一个对象。
数组:使用中括号 [] 括起来的表示一个数组。
字符串:使用常规双引号 “” 括起来的表示一个字符串
数字:包括整形和浮点型,直接使用。
class Json::Value{
Value &operator=(const Value &other); //Value重载了[]和=,因此所有的赋值和获取数据都可以通过
Value& operator[](const std::string& key);//简单的方式完成 val["姓名"] = "小明";
Value& operator[](const char* key);
Value removeMember(const char* key);//移除元素
const Value& operator[](ArrayIndex index) const; //val["成绩"][0]
Value& append(const Value& value);//添加数组元素val["成绩"].append(88);
ArrayIndex size() const;//获取数组元素个数 val["成绩"].size();
std::string asString() const;//转string string name = val["name"].asString();
const char* asCString() const;//转char* char *name = val["name"].asCString();
Int asInt() const;//转int int age = val["age"].asInt();
float asFloat() const;//转float
bool asBool() const;//转 bool
};
//json序列化类,低版本用这个更简单
class JSON_API Writer {
virtual std::string write(const Value& root) = 0;
}
class JSON_API FastWriter : public Writer {
virtual std::string write(const Value& root);
}
class JSON_API StyledWriter : public Writer {
virtual std::string write(const Value& root);
}
//json序列化类,高版本推荐,如果用低版本的接口可能会有警告
class JSON_API StreamWriter {
virtual int write(Value const& root, std::ostream* sout) = 0;
}
class JSON_API StreamWriterBuilder : public StreamWriter::Factory {
virtual StreamWriter* newStreamWriter() const;
}
//json反序列化类,低版本用起来更简单
class JSON_API Reader {
bool parse(const std::string& document, Value& root, bool collectComments = true);
}
//json反序列化类,高版本更推荐
class JSON_API CharReader {
virtual bool parse(char const* beginDoc, char const* endDoc,
Value* root, std::string* errs) = 0;
}
class JSON_API CharReaderBuilder : public CharReader::Factory {
virtual CharReader* newCharReader() const;
}
Json库的序列化和反序列化类有新老两个版本,推荐是用最新版charReader 反序列化类,和StreamWriter序列化类.,这两个类的实例化对象需要用CharReaderBuilder类和StreamWriterBuilder类的成员函数创建.,其中writer函数需要用到ostringstream类进行序列化操作.
序列化和反序列化都要用到Json的Value类,被序列化和反序列化的数据首先要存储在Value对象中格式化保存,Value类中重载了[]参数const char* 返回值Value类型,还重载了[]参数int index,返回值Value.以及类型转换函数,asString(),asInt (),asFloat()等分别会将Value内的成员转换为字符串型数据,整型数据,浮点型数据.
序列化
#include
#include
#include
#include
#include
int main()
{
const char*name="张三";
int age = 15;
float score[]={89,99,55.7};
Json::Value root;
root["姓名"]=name;
root["年龄"]= age;
root["成绩"].append(score[0]);
root["成绩"].append(score[1]);
root["成绩"].append(score[2]);
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::ostringstream os;
sw->write(root,&os);
std::string str=os.str();
std::cout<<str<<std::endl;
return 0;
}
反序列化
#include
#include
#include
#include
#include
int main()
{
std::string str=R"({"姓名":"张三","年龄":15,"成绩":[89.1,88,133]})";
Json::Value root;
Json::CharReaderBuilder crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err;
cr->parse(str.c_str(),str.c_str()+str.size(),&root,&err);
std::cout<<root["姓名"].asString()<<std::endl;
std::cout<<root["年龄"].asInt()<<std::endl;
int size=root["成绩"].size();
for(int i=0;i<size;i++)
{
std::cout<<root["成绩"][i].asFloat()<<std::endl;
}
}
bundle库是嵌入式库,可以直接将源码拿过来使用,使用时可以将源文件拷贝过来复用.
压缩
在读取文件的时候,因为要一次读取整个文件,所以要通过获取文件指针偏移量的方式来获取文件大小,以便设置接收文件内容的缓冲区的大小.
文件的binary打开模式,会自动创建不存在的文件.
fstream的读写功能参数只涉及到缓冲区地址和大小.
pack参数一个事压缩文件的类型,一个是要压缩的内容.
#include
#include
#include"bundle.h"
int main(int argc,char* argv[])
{
//接收压缩文件
std::string compressfile=argv[1];
std::string newfile=argv[2];
//打开压缩文件
std::ifstream ifs(compressfile,std::ios::binary);
//将文件指针偏移到文件末尾
ifs.seekg(0,std::ios::end);
//获取文件偏移量(文件字节大小)
int filesize=ifs.tellg();
//将文件指针偏移到文件起始
ifs.seekg(0,std::ios::beg);
//接收文件内容
std::string content;
content.resize(filesize);
ifs.read(&content[0],filesize);
ifs.close();
//获取文件大小
std::string packa=bundle::pack(bundle::LZIP,content);
//打开存储文件
std::ofstream ofs(newfile,std::ios::binary);
//将pack文件写入文件
ofs.write(&packa[0],packa.size());
//关闭文件
ifs.close();
return 0;
}
解压缩
#include
#include
#include"bundle.h"
int main(int argc,char* argv[])
{
//接收文件名
std::string compressfile=argv[1];
std::string newfile=argv[2];
//打开压缩文件
std::ifstream ifs(compressfile,std::ios::binary);
//将文件指针移动至末尾
ifs.seekg(0,std::ios::end);
//获取文件大小
size_t filesize=ifs.tellg();
//将文件指针移动至起始
ifs.seekg(0,std::ios::beg);
//设置接收字符串大小
std::string body;
body.resize(filesize);
//读取压缩文件内容
ifs.read(&body[0],filesize);
//解压内容
std::string packa=bundle::unpack(body);
//打开解压文件
std::ofstream ofs(newfile,std::ios::binary);
//将解压内容写入文件
ofs.write(&packa[0],packa.size());
//关闭两个文件
ifs.close();
ofs.close();
return 0;
}
最后可以用md5sum 文件名 命令来验证压缩后再解压缩是不是与源文件相同,md5sum是将文件内容和进行哈希映射值.
Request类认识
作用:客户端将用户请求格式化为http请求相关信息,最终组织http请求发送
服务端收到http之后,解析请求,将解析的数据保存在Resquest类中等待处理.
namespace httplib{
struct MultipartFormData {
std::string name;
std::string content;
std::string filename;
std::string content_type;
};
using MultipartFormDataItems = std::vector<MultipartFormData>;
struct Request {
std::string method;//请求方法
std::string path;//资源路径
Headers headers;//头部内容
std::string body;//主体内容
// for server
std::string version; //服务器版本
Params params; //用户输入关键字
MultipartFormDataMap files; //保存客户端上床的文件信息
Ranges ranges; //断点续传,文件内容区间
bool has_header(const char *key) const; //查询头部中有无某个字段
std::string get_header_value(const char *key, size_t id = 0) const;//获取头部值
void set_header(const char *key, const char *val);//设置头部的值
bool has_file(const char *key) const;//是否含有某个文件
MultipartFormData get_file_value(const char *key) const;//获取文件的值
};
Response类认识
格式化响应,组织响应
struct Response {
std::string version;//服务器版本号
int status = -1; //状态码
std::string reason;//原因
Headers headers;//头部信息
std::string body;//主体信息
std::string location; // 重定向的地址
void set_header(const char *key, const char *val);//设置头部信息
void set_content(const std::string &s, const char *content_type);
//设置内容
};
server类认识
用于搭建服务器
class Server {
//请求处理回调函数类型
using Handler = std::function<void(const Request &, Response &)>;
//请求路由表类型,regex:正则表达式,用于匹配http资源路径,handler回调函数
using Handlers = std::vector<std::pair<std::regex, Handler>>;
//线程池,处理响应的资源请求任务,一个线程一个任务
std::function<TaskQueue *(void)> new_task_queue;
//向表中添加映射关系
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);
//搭建服务器,接收链接请求.
bool listen(const char *host, int port, int socket_flags = 0);
};
client类认识
搭建客户端
class Client {
//接收IP和端口号
Client(const std::string &host, int port);
//向服务器发送GET请求
Result Get(const char *path, const Headers &headers);
//向服务器发送多区域数据请求
Result Post(const char *path, const char *body, size_t content_length,const char *content_type);
Result Post(const char *path, const MultipartFormDataItems &items);
}
}
http::Server原理:当启动Server对象时,会创建并绑定套接字,设置监听,循环接收客户端链接请求,接收到客户端请求时,会创建新的线程,新线程从找到用户传递的回调处理函数,解析客户端请求字段,包括请求方法,URL,版本号,请求包头,主体.设置响应字段.
#include
#include"httplib.h"
#include
int main()
{
httplib::Server sr;
//请求方法为GET,请求内容为纯文本
sr.Get("/hi",[&](const httplib::Request& req,httplib::Response& resp)->void{
resp.set_content("hello world","text/plain");
resp.status=200;
});
//请求方法为GET,请求内容为纯文本,(\d+)为正则表达式,\d表示匹配一个数字,+表示重复匹配一个或多个,()表示捕获数字
sr.Get(R"(/numbers/(\d+))",[&](const httplib::Request& req,httplib::Response& resp)->void{
//下标为0的内容是这个path,后面捕获的内容
auto number=req.matches[1];
//设置响应内容
resp.set_content(number,"text/plain");
//设置状态码
resp.status=200;
});
//请求方法是post,请求内容是文件请求
sr.Post("/multipart",[&](const httplib::Request& req,httplib::Response& resp)->void{
//获取文件个数
size_t size=req.files.size();
//判断是否存在指定文件
bool ret=req.has_file("name1");
if(ret)
{
std::cout<<"文件获取失败"<<std::endl;
}
//获取文件内容
httplib::MultipartFormData file= req.get_file_value("name1");
//设置响应主体为文件内容
resp.body=file.filename;
resp.body="\n";
resp.body=file.content;
//设置报头
resp.set_header("Content-Type","text/plain");
//状态码
resp.status=200;
});
sr.listen("0.0.0.0",9999);
return 0;
}
#include"httplib.h"
int main()
{
//启动客户端
httplib::Client cl("127.0.0.1",9999);
//发送请求
httplib::Result res=cl.Get("/hi");
std::cout<<res->status<<std::endl;
std::cout<<res->body<<std::endl;
httplib::Result res=cl.Get("/numbers/678");
std::cout<<res->status<<std::endl;
std::cout<<res->body<<std::endl;
httplib::MultipartFormDataItems items = {
{ "file1", "this is file content", "hello.txt", "text/plain" },
};
httplib::Result res=cl.Post("/upload",items);
std::cout<<res->status<<std::endl;
std::cout<<res->body<<std::endl;
return 0;
}
成员变量:文件名
成员函数:获取文件大小,获取文件最后修改时间,获取文件最后访问时间,获取文件名,设置文件内容,获取文件内容,获取文件指定位置的指定长度的内容,获取指定目录中所有文件信息,判断文件是否存在,创建目录,文件压缩,文件解压缩;
#include
#include
#include
#include
#include
#include
#include"bundle.h"
namespace cloud
{
class FileUtil
{
public:
// 获取文件名
bool Getfilename(const std::string &path)
{
// 从后往前查找第一个'/'
size_t pos = path.find_last_of("/");
if (pos == std::string::npos)
{
std::cout << "get file name fail" << std::endl;
return false;
}
// 提取文件名
_filename = path.substr(pos + 1);
return true;
}
// 获取文件大小
size_t GetFileSize()
{
// 填充文件属性结构体stat
struct stat sta;
int ret = stat(_filename.c_str(), &sta);
// 判断填充是否成功
if (ret == -1)
{
std::cout << "get file access time fail,error code: " << errno << "error info: " << strerror(errno) << std::endl;
return -1;
}
// 提取文件最后访问时间属性
return sta.st_size;
}
// 获取文件最后一次访问时间
time_t GetFileATime()
{
// 填充文件属性结构体stat
struct stat sta;
int ret = stat(_filename.c_str(), &sta);
// 判断填充是否成功
if (ret == -1)
{
std::cout << "get file access time fail,error code: " << errno << "error info: " << strerror(errno) << std::endl;
return -1;
}
// 提取文件最后访问时间属性
return sta.st_atime;
}
// 获取文件最后一次修改时间
time_t GetFileMTime()
{
// 填充文件属性结构体stat
struct stat sta;
int ret = stat(_filename.c_str(), &sta);
// 判断填充是否成功
if (ret == -1)
{
std::cout << "get file access time fail,error code: " << errno << "error info: " << strerror(errno) << std::endl;
return -1;
}
// 提取文件最后修改时间属性
return sta.st_mtime;
}
// 设置文件内容
bool SetFileContent(const std::string &Content)
{
// 以写方式打开文件
std::ofstream ofs;
ofs.open(_filename, std::ios::binary);
// 写文件
ofs.write(Content.c_str(), Content.size());
// 判断流是否错误
if (!ofs.good())
{
std::cout << "file write" << _filename << "fail" << std::endl;
return false;
}
ofs.close();
return true;
}
// 获取文件指定位置指定长度内容
bool GetFileSpecifiedContent(std::string &content, const int pos, const int len)
{
// 以读方式打开文件
std::ifstream ifs(_filename, std::ios::binary);
// 设置文件下一个字符访问位置
ifs.seekg(pos, std::ios::beg);
// 设置接收缓冲区大小
char buffer[len];
// 读取文件
ifs.read(buffer, len);
// 判断流是否错误
if (!ifs.good())
{
std::cout << "file write specified content " << _filename << "fail" << std::endl;
return false;
}
// 提取文件内容
content = buffer;
ifs.close();
return true;
}
// 获取文件内容
bool GetFileContent(std::string &content)
{
size_t size=this->GetFileSize();
return GetFileSpecifiedContent(content,0,size);
}
// 文件压缩
bool Compress(const std::string &Comfilename)
{
std::string content;
if(!GetFileContent(content))
{
return false;
}
// 获取压缩文件内容
std::string package = bundle::pack(bundle::LZIP, content);
// 以写方式打开压缩内容存储文件
std::ofstream ofs(Comfilename, std::ios::binary);
// 写文件
ofs.write(package.c_str(), package.size());
// 判断流是否错误
if (!ofs.good())
{
std::cout << "file" << _filename << "compress fail" << std::endl;
return false;
}
// 关闭文件
ofs.close();
return true;
}
// 文件解压缩
bool Uncompress(const std::string uncomfilename)
{
std::string content;
if(!GetFileContent(content))
{
return false;
}
// 解压内容
std::string package=bundle::unpack(content);
// 打开解压内容存储文件
std::ofstream ofs(uncomfilename, std::ios::binary);
// 写入文件
ofs.write(&package[0], package.size());
// 判断流是否错误
if (!ofs.good())
{
std::cout << "file" << _filename << "uncompress fail" << std::endl;
return false;
}
// 关闭文件
ofs.close();
return true;
}
public:
std::string _filename;
};
}
RLE(run length encoding 运行长度编码)
将连续的重复字符替换为字符+重复次数进行压缩.
HUFFMAN(哈佛曼算法)算法
1.统计字符出现频率
2.将字符用二进制从0,01,11,等开始依次编码,编码顺序由字符出现频率从高到低开始编码.
3.构建哈夫曼二叉树,字符和频率作为节点创建最小堆,取出频率最小的两个堆将他们将的字符合并,频率相加组成新节点插入堆中,依次归并,最终得到所以字符频率之和的新节点结束.
4.从根节点遍历哈夫曼树,路径往左标记0,往右标记1,遍历到叶子节点,记录路径上所有标记形成编码,将叶子节点的字符与编码组成映射关系记录到表中
5.将对应的字符转化编码进行压缩.
DEFLATE算法
1.使用RLE算法去重
2.使用哈夫曼算法堆字符编码
#pragma once
#include
#include
#include
#include
namespace cloud
{
class JsonUtil
{
public:
//序列化
static bool Serialize(const Json::Value& root,std::string* str)
{
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::ostringstream oss;
sw->write(root,&oss);
*str=oss.str();
return true;
}
//反序列化
static bool Deserialize(Json::Value* root,const std::string& str)
{
Json::CharReaderBuilder crb;
std::string err;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
bool ret=cr->parse(&str[0],&str[0]+str.size(),root,&err);
if(!ret)
{
std::cout<<err<<std::endl;
return false;
}
return true;
}
};
}
程序各个模块需要使用到热点判断时间,文件下载前缀名,文件压缩包后缀名,上传文件存放路径,文件压缩存放路径,备份信息存储文件,IP 地址,端口这个样的固定的关键信息,使用一个类统一管理这些信息,方便管理,便于后续维护.又因为需要不止一个模块,需要访问这些信息,而且使用的时机也不同,所以需要这个类是一个全局唯一对象,单例模式的类管理信息,用文件存储配置信息.在创建对象的时候读取配置文件,将最新的配置信息加载到程序中,使程序每次启动使用的是最新的信息.
#pragma once
#include "fileutil.hpp"
#include "json.hpp"
#include
#define CNFFILE_NAME "./config.txt"
namespace cloud
{
class Config
{
private:
// 全局静态对象
static Config *cnf;
static std::mutex _mutex;
public:
static Config *Instance()
{
if (cnf == nullptr)
{
_mutex.lock();
if (cnf == nullptr)
{
cnf = new Config;
}
}
_mutex.unlock();
return cnf;
}
Config()
{
Readfile(CNFFILE_NAME);
}
bool Readfile(const std::string &filename)
{
// 获取配置文件内容
cloud::FileUtil fu(filename);
std::string content;
fu.GetFileContent(content);
// 反序列化文件内容
Json::Value root;
cloud::JsonUtil::Deserialize(&root, content);
_hot_time = root["hot_time"].asInt();
_server_ip = root["server_ip"].asString();
_server_port = root["server_port"].asInt();
_url_prefix = root["url_prefix"].asString();
_arc_suffix = root["arc_suffix"].asString();
_back_dir = root["back_dir"].asString();
_pack_dir = root["pack_dir"].asString();
_manager_file = root["manager_file"].asString();
}
public:
// 热点中断时间
time_t _hot_time;
// 文件下载URL前缀路径
std::string _url_prefix;
// 压缩包后缀名称
std::string _arc_suffix;
// 上传文件存放路径
std::string _back_dir;
// 压缩文件存放路径
std::string _pack_dir;
// 服务端备份信息存放文件
std::string _manager_file;
// 服务器访问 IP 地址
std::string _server_ip;
// 服务器访问端口
uint16_t _server_port;
};
Config* Config::cnf = nullptr;
std::mutex Config::_mutex;
}
对客户端上传的文件进行热点管理,将长时间没有被访问的文件进行压缩存储,需要用到文件的服务器本地存储路径,服务器本地压缩路径,文件最后一次访问时间,文件最后一次修改时间,文件压缩标志;客户端下载文件,需要用到文件索引信息URL,浏览备份文件列表,需要用向用户展示文件大小.为了方便维护管理,所以需要用一个单独的模块封装设计.
**功能设计:**文件上传涉及到信息插入,文件列表展示以及热点管理涉及到信息查询,所以类功能设计主要是插入和查询功能.如果一直有用户的上传的文件需要维护,文件的信息就一直需要维护,所以考虑用文件实现信息的持久化存储,每次数据更新就将数据持久化存储.服务器启动时需要读取加载文件信息到程序中,所以还需要一个初始化模块,用户加载文件信息到程序中.以供使用.
管理结构:考虑到热点管理模块对与查询文件信息的操作比较频繁,用哈希表结构管理文件信息,提高查询效率.
数据会被服务器,热点管理多个线程访问,服务器模块本身使用的是一个线程池,处理应用程序发来的请求,请求中有查询和插入操作,热点管理只是查询操作,所以对于数据的访问是读多写少,使用读写锁加锁可以提高并发性能.
#pragma once
#include "fileutil.hpp"
#include "config.hpp"
#include "json.hpp"
#include
#include
namespace cloud
{
struct BackUpInfo
{
// 文件最后一次访问时间
Json::UInt64 last_a_time = 0;
// 文件最后一次修改时间
Json::UInt64 last_m_time = 0;
// 文件大小
Json::UInt64 file_size = 0;
// 文件压缩标志
bool pack_flag = false;
// 文件实际存储路径,backdir
std::string real_path;
// 文件压缩存储路径 packdir
std::string pack_path;
// 用户请求
std::string url;
// 填充结构信息
bool NewBackUpInfo(const std::string &realpath)
{
// 获取文件名
FileUtil fu(realpath);
// 如果文件不存在就不处理
if (fu.Exists() == false)
{
std::cout << "file not exists" << std::endl;
return false;
}
// 填充文件大小
file_size = fu.GetFileSize();
// 填充文件最后访问时间
last_a_time = fu.GetFileATime();
// 填充文件最后修改时间
last_m_time = fu.GetFileMTime();
// 填充文件实际存储路径
real_path = realpath;
// 填充文件压缩存储路径
pack_path = Config::Instance()->GetPackDir() + fu.FileName() + Config::Instance()->GetPackSuffix();
// 用户请求路径
url = Config::Instance()->GetDownLoadPreFix() + fu.FileName();
// 填充文件压缩标志
pack_flag = false;
}
};
// 数据管理类
class InfoManager
{
public:
// 构造
InfoManager()
{
// 初始化读写锁
pthread_rwlock_init(&_rwlock, nullptr);
// 获取上传文件信息管理文件
_backup_file = Config::Instance()->GetBackUpFile();
// 初始化加载
InitLoad();
}
// 初始化加载
void InitLoad()
{
// 读文件
std::string str;
_backup_file.GetFileContent(str);
// 反序列化
Json::Value root;
JsonUtil::Deserialize(&root, str);
// 填充BackUpInfo结构体信息字段
for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
{
BackUpInfo info;
info.file_size = root[i]["file_size"].asUInt64();
info.last_a_time = root[i]["last_a_time"].asUInt64();
info.last_m_time = root[i]["last_m_time"].asUInt64();
info.real_path = root[i]["real_path"].asString();
info.pack_flag = root[i]["pack_flag"].asBool();
info.pack_path = root[i]["pack_path"].asString();
info.url = root[i]["url"].asString();
// 将BackUpInfo对象插入哈希表中
Insert(info);
}
}
// 持久存储
bool Storge()
{
std::vector<BackUpInfo> arr;
if (!GetAll(&arr))
{
std::cout<<"get all file backup info fail"<<std::endl;
return false;
}
Json::Value root;
for (size_t i = 0; i < arr.size(); i++)
{
// 添加到Json::Value
Json::Value item;
item["file_size"] = arr[i].file_size;
item["last_a_time"] = arr[i].last_a_time;
item["last_m_time"] = arr[i].last_m_time;
item["real_path"] = arr[i].real_path;
item["pack_flag"] = arr[i].pack_flag;
item["pack_path"] = arr[i].pack_path;
item["url"] = arr[i].url;
root.append(item);
// 序列化
std::string str;
JsonUtil::Serialize(root, &str);
// 将信息写入到上传文件信息备份文件中
_backup_file.SetFileContent(str);
}
return true;
}
// 向table插入数据
bool Insert(const BackUpInfo &info)
{
// 加锁
pthread_rwlock_wrlock(&_rwlock);
// 插入数据
_table.insert(std::make_pair(info.url, info));
// 解锁
pthread_rwlock_unlock(&_rwlock);
//持久化存储
Storge();
return true;
}
// 更新数据
bool Updata(const BackUpInfo &info)
{
// 加锁
pthread_rwlock_wrlock(&_rwlock);
// 插入数据
_table.insert(std::make_pair(info.url, info));
// 解锁
pthread_rwlock_unlock(&_rwlock);
//持久化存储
Storge();
return true;
}
// 通过url获取一个备份文件数据
bool GetOneByUrl(const std::string &url, BackUpInfo *info)
{
// 加锁
pthread_rwlock_wrlock(&_rwlock);
// 查找
if (_table.count(url) == false)
{
std::cout << "not find file info" << std::endl;
return false;
}
// 提取数据
*info = _table[url];
// 解锁
pthread_rwlock_unlock(&_rwlock);
return true;
}
// 通过实际存储路径获取一个备份文件数据
bool GetOneByRealPath(const std::string &realpath, BackUpInfo *info)
{
// 加锁
pthread_rwlock_wrlock(&_rwlock);
// 查找
for (auto it = _table.begin(); it != _table.end(); it++)
{
if (it->second.real_path == realpath)
{
*info = it->second;
return true;
}
}
pthread_rwlock_unlock(&_rwlock);
// 解锁
std::cout << "not find file info" << std::endl;
return false;
}
// 获取全部信息
bool GetAll(std::vector<BackUpInfo> *arr)
{
// 加锁
pthread_rwlock_wrlock(&_rwlock);
// 遍历获取数据
for (auto e : _table)
{
arr->push_back(e.second);
}
// 解锁
pthread_rwlock_unlock(&_rwlock);
return true;
}
~InfoManager()
{
pthread_rwlock_destroy(&_rwlock);
}
private:
// 文件操作工具
FileUtil _backup_file;
// 读写锁
pthread_rwlock_t _rwlock;
// hash表
std::unordered_map<std::string, BackUpInfo> _table;
};
}
对于客户端上传的文件,实时检测文件是否被经常访问,对于不经常被访问的文件,压缩存储可以减少服务器磁盘压力.
热点对象访问全局数据对象获取所有备份文件信息,遍历所有文件信息,利用数据中的文件访问时间与当前时间作比较,判断该文件是否为热点文件,将非热点文件压缩存储在新的路径下,删除源文件可以避免源文件被重复检测.
#pragma once
#include "DataManager.hpp"
#include "config.hpp"
#include "fileutil.hpp"
extern cloud::InfoManager *_data;
namespace cloud
{
class HotManager
{
public:
HotManager()
{
// 从配置文件获取成员信息
_backdir = Config::Instance()->GetBackDir();
_packdir = Config::Instance()->GetPackDir();
_hottime = Config::Instance()->GetHotTime();
_pack_suffix = Config::Instance()->GetPackSuffix();
// 目录不存在就创建目录
FileUtil(_backdir).CreateDirectory();
FileUtil(_packdir).CreateDirectory();
}
bool HotJudge(const std::string &filename)
{
// 获取最近访问时间与当前时间
time_t last_atime = FileUtil(filename).GetFileATime();
time_t cur_time = time(NULL);
// 非热点文件
if (cur_time - last_atime > _hottime)
{
return true;
}
// 热点文件
return false;
}
void RunModule()
{
while (1)
{
// 遍历目录获取文件名
std::vector<std::string> array;
FileUtil(_backdir).ScanDirectory(&array);
// 判断是否为热点文件
for (auto a : array)
{
// 热点文件不处理
if (HotJudge(a) == false)
{
continue;
}
// 获取文件备份信息
BackUpInfo info;
if (_data->GetOneByRealPath(a, &info) == false)
{
info.NewBackUpInfo(a);
}
// 压缩非热点文件7
// 删除源文件
FileUtil fu(a);
fu.Compress(info.pack_path);
fu.Remove();
// 修改备份文件信息
info.pack_flag = true;
_data->Updata(info);
}
}
}
private:
// 备份文件信息管理类对象
std::string _backdir;
std::string _packdir;
std::string _pack_suffix;
int _hottime;
};
}
服务端要能响应客户端的文件列表查看请求,文件下载请求,文件上传请求.
文件上传功能的实现:判断响应报文中是否设置了"file"(自定义头部键值),使用request对象获取文件内容,将内容写入上传目录中,更新全局数据对象结构中的数据.
断点续传功能的实现原理:
正常文件下载,客户端请求POST /download/filename HTTP/1.1+请求报头+主体;
服务端响应:HTTP/1.1 200 OK+请求报头(“Etag: asasdfjashdfoa(文件唯一标识)”,“Accept-Range: bytes”(告诉客户端服务端支持断点续传))+body(文件内容);
异常中断后,客户端请求断点续传,POST /download/filename HTTP/1.1+请求报头(Range: bytes=10-900)(If-Range: 服务器响应的唯一标识)+body;
首先服务端检测是否有If-Range头部字段,判断是否需要断点续传,在判断If-Range对应的etag文件唯一标识符是否与当前文件的唯一标识符一致,不一致说明文件在此之前已经被修改了,需要重新下载文件,否则就获取Range请求报头的区间值,Range值设置方式有Range: bytes 10-100或者Range: bytes 10-表示10-文件末尾,将区间内的文件内容设置到body中响应给客户端
服务器响应: HTTP/1.1 206 partial content+请求报头(“Etag: asasdfjashdfoa(文件唯一标识)”,“Accept-Range: bytes”(告诉客户端服务端支持断点续传),“Content-Range: bytes 10-900/891”)+body(文件内容);
#pragma once
#include "httplib.h"
#include "config.hpp"
#include "DataManager.hpp"
#include
#include
extern cloud::InfoManager *_data;
namespace cloud
{
class Service
{
public:
Service()
{
_Download_prefix = Config::Instance()->GetDownLoadPreFix();
_server_ip = Config::Instance()->GetServerIp();
_server_port = Config::Instance()->GetServerPort();
}
// 上传
static void UpLoad(const httplib::Request &req, httplib::Response &resp)
{
// 判断有没有文件上传区域
bool flag = req.has_file("file");
if (flag == false)
{
std::cout << "not file contant" << std::endl;
resp.status = 400;
return;
}
// 获取文件信息
auto file = req.get_file_value("file");
// 获取文件上传路径
std::string backdir = Config::Instance()->GetBackDir();
// 获取文件实际存储路径
std::string realpath = backdir + FileUtil(file.filename).FileName();
// 向文件写入数据
FileUtil(realpath).SetFileContent(file.content);
// 向数据管理模块添加信息
BackUpInfo info;
info.NewBackUpInfo(realpath);
_data->Insert(info);
}
static std::string GetETag(const BackUpInfo &info)
{
FileUtil fu(info.real_path);
std::string etag = fu.FileName();
etag += '-';
etag += std::to_string(info.file_size);
etag += '-';
etag += std::to_string(info.last_m_time);
return etag;
}
// 下载
static void Download(const httplib::Request &req, httplib::Response &resp)
{
// 根据url获取文件备份信息
BackUpInfo info;
_data->GetOneByUrl(req.path, &info);
// 判断文件是否被压缩,如果被压缩,要先解压缩
if (info.pack_flag == true)
{
// 将压缩包解压到上传路径下
FileUtil fu(info.pack_path);
fu.Uncompress(info.real_path);
// 删除压缩包
fu.Remove();
// 修改备份信息
info.pack_flag = false;
// 更新备份信息
_data->Updata(info);
}
bool resume_flag = false;
// 判断请求字段中是否有需要热点续传
if (req.has_header("If-Range"))
{
auto old_etag = req.get_header_value("If-Range");
if (old_etag == GetETag(info))
{
resume_flag = true;
}
}
// 读取文件信息将内容放入响应内容
FileUtil(info.real_path).GetFileContent(resp.body);
if (resume_flag)
{
// 设置头部字段
resp.set_header("Accept-Ranges", "bytes");
// 热点续传标志
resp.set_header("ETag", GetETag(info));
// 响应文件类型
resp.set_header("Content-Type", "application/octet-stream");
resp.status = 206;
}
else
{
// 设置头部字段
resp.set_header("Accept-Ranges", "bytes");
// 热点续传标志
resp.set_header("ETag", GetETag(info));
// 响应文件类型
resp.set_header("Content-Type", "application/octet-stream");
resp.status = 200;
}
}
static const char *Gettime(time_t time)
{
return std::ctime(&time);
}
// 列表显示
static void ListShow(const httplib::Request &req, httplib::Response &resp)
{
(void)req;
// 获取所有文件信息
std::vector<BackUpInfo> array;
_data->GetAll(&array);
std::stringstream ss;
ss << "Download ";
ss << "Download
";
ss << "";
for (auto a : array)
{
ss << "";
FileUtil fu(a.real_path);
ss << "" << fu.FileName() << " ";
ss << "" << Gettime(a.last_m_time) << " ";
ss << "" << a.file_size / 1024 << "k ";
ss << " ";
}
ss << "
";
// 设置响应展示页面
resp.body = ss.str();
resp.set_header("Content-Type", "text/html");
resp.status = 200;
}
// 运行
void RunModule()
{
// 创建服务器
// 接收上传请求
_server.Post("/upload", UpLoad);
// 接收下载请求
std::string str = Config::Instance()->GetDownLoadPreFix() + "(.*)";
_server.Get(str.c_str(), Download);
// 接收列表展示请求
_server.Get("/listshow", ListShow);
_server.Get("/", ListShow);
// 启动服务器
_server.listen(_server_ip.c_str(), _server_port);
}
private:
std::string _server_ip;
uint16_t _server_port;
std::string _Download_prefix;
httplib::Server _server;
};
}
客户端要实现的功能是对指定文件夹中的文件自动进行备份上传。但是并不是所有的文件每次都需要上传,需要判断哪些文件需要上传哪些不需要,因此需要将备份的文件信息给管理起来,作为下一次文件是否需要备份的判断。因此需要被管理的信息包含以下:文件路径名称,文件唯一标识:由文件名,最后一次修改时间,文件大小组成的一串信息
#pragma once
#include
#include
#include
#include"fileutil.hpp"
namespace cloud
{
class DataManager
{
public:
DataManager(const std::string& back_file):_back_file(back_file)
{
InitLoad();
}
//持久化存储
bool Storage()
{
std::stringstream ss;
//遍历获取表中的信息
for (auto a : _table)
{
ss << a.first << " " << a.second << "\n";
}
//向管理信息文件写入信息
FileUtil fu(_back_file);
fu.SetFileContent(ss.str());
return true;
}
bool Split(const std::string& str, const std::string& sep, std::vector<std::string>* array)
{
//分隔符位置,开始查找位置
size_t pos = 0, indx = 0;
while (1)
{
//从indx位置查找
pos=str.find(sep, indx);
//没找到
if (pos == std::string::npos)
{
return false;
}
//将string转化为stringstream插入array
std::string tmp = str.substr(indx, pos - indx);
array->push_back(tmp);
//更新下次查找开始位置
indx = pos + sep.size();
}
if (indx < str.size())
{
array->push_back(str.substr(indx));
}
return true;
}
//初始化加载
bool InitLoad()
{
//读取文件信息
std::string str;
FileUtil fu(_back_file);
fu.GetFileContent(str);
//按照格式分割文件内容
std::vector<std::string> array;
Split(str, "\n", &array);
//将格式化信息插入表中
for (auto a : array)
{
std::stringstream ss(a);
std::string filename, flag;
ss >> filename>>flag;
_table.insert(std::make_pair(filename, flag));
}
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;
}
//通过key值获取一个数据
bool GetOneByKey(const std::string& key, std::string* value)
{
auto it = _table.count(key);
if (it == false)
{
return false;
}
*value = _table[key];
return true;
}
private:
//数据管理表
std::unordered_map<std::string, std::string> _table;
//持久化存储文件
std::string _back_file;
};
}
循环检测指定目录中的文件是否需要将上传,被更改的,或者是没有上床过的文件都需要上传,上传的同时将文件路径名作为key值,文件名+文件大小+最后一次修改时间组成的文件唯一标识符作为value存储在哈希表中,并持久化存储.
#pragma once
#include"data.h"
#include"httplib.h"
#include
#define BACKDIR "./backdir/"
#define BACKFILE "backfile.dat"
#define SERVER_IP "170.106.107.74"
#define SERVER_PORT 9090
namespace cloud
{
class BackUp
{
public:
BackUp(const std::string& back_dir= BACKDIR) :_back_dir(back_dir)
{
_data = new DataManager(BACKFILE);
}
std::string GetFileIdentifier(const std::string& filename)
{
FileUtil fu(filename);
std::string size = std::to_string(fu.GetFileSize());
std::string mtime = std::to_string(fu.GetFileMTime());
std::stringstream ss;
ss << filename << "-" << size << "-" << mtime;
return ss.str();
}
//判断是否需要上传
bool IsUpLoad(const std::string& filename)
{
FileUtil fu(filename);
std::string id;
bool ret=_data->GetOneByKey(filename, &id);
if ((ret&& GetFileIdentifier(filename) == id)
|| (time(NULL) - fu.GetFileMTime() < 3))
{
return false;
}
return true;
}
//上传
bool UpLoad(const std::string& filename)
{
//获取文件内容
FileUtil fu(filename);
std::string body;
fu.GetFileContent(body);
//创建Client对象
httplib::Client client(SERVER_IP, SERVER_PORT);
//设置文件上传格式
httplib::MultipartFormData item;
item.filename = fu.FileName();
item.content = body;
item.content_type = "application/octet-stream";
item.name = "file";
httplib::MultipartFormDataItems items;
items.push_back(item);
//发送上传请求
auto ret=client.Post("/upload", items);
//判断是否成功
if (!ret || ret->status != 200)
{
return false;
}
return true;
}
void RunModule()
{
while (1)
{
//遍历获取目录文件名称
FileUtil fu(_back_dir);
std::vector<std::string> array;
fu.ScanDirectory(&array);
for (auto a : array)
{
//判断文件是否需要上传
if (IsUpLoad(a) == true)
{
//文件上传成功就更新数据
if (UpLoad(a) == true)
{
_data->Insert(a, GetFileIdentifier(a));
}
}
}
Sleep(1);
}
}
private:
std::string _back_dir;
DataManager* _data;
};
}