文章目录
- @[TOC](文章目录)
- 一、项目框架定义
- 1.项目需求
- 2. 服务端框架搭建思想
- 3.客户端框架搭建思想
- 二、环境搭建
- 1.gcc编译器升级
- 2.安装第三方库
- 三、认识第三方库
- 1.json库使用
- 2.bundle库使用
- 3.httplib库使用
- 4.简单服务器搭建
- 5.简单客户端搭建
- 四、文件使用工具类设计
- 1.类的功能结构
- 2.功能实现
- 3.常见压缩算法
- 五、json工具类实现
- 六、服务端配置信息模块
- 1.设计思想
- 2.实现
- 七、服务端数据管理模块
- 1.设计思想
- 2.实现
- 八、服务端热点管理模块
- 1.设计思想
- 2.代码
- 九、服务端业务处理模块
- 1.实现思想
- 2.代码
- 十、客户端数据模块
- 1.实现思想
- 2.代码
- 十一、客户端业务处理模块
- 十二、项目类关系图
- 在这里插入图片描述
文章目录
- @[TOC](文章目录)
- 一、项目框架定义
- 1.项目需求
- 2. 服务端框架搭建思想
- 3.客户端框架搭建思想
- 二、环境搭建
- 1.gcc编译器升级
- 2.安装第三方库
- 三、认识第三方库
- 1.json库使用
- 2.bundle库使用
- 3.httplib库使用
- 4.简单服务器搭建
- 5.简单客户端搭建
- 四、文件使用工具类设计
- 1.类的功能结构
- 2.功能实现
- 3.常见压缩算法
- 五、json工具类实现
- 六、服务端配置信息模块
- 1.设计思想
- 2.实现
- 七、服务端数据管理模块
- 1.设计思想
- 2.实现
- 八、服务端热点管理模块
- 1.设计思想
- 2.代码
- 九、服务端业务处理模块
- 1.实现思想
- 2.代码
- 十、客户端数据模块
- 1.实现思想
- 2.代码
- 十一、客户端业务处理模块
- 十二、项目类关系图
- 在这里插入图片描述
一、项目框架定义
1.项目需求
实现本地文件通过客户端上传到网络服务器主机中,客户端也可以实现通过请求服务器查看上传的文件,可以下载云备份的文件,如果下载过程中客户端与服务器终端,还有断点续传功能,就是可以接着中断前的进度下载.
2. 服务端框架搭建思想
按照功能划分:服务端要支持客户端文件上传,备份文件列表查看,文件下载,断点续传,热点文件识别和管理功能,当文件长期不被访问时,对备份文件压缩存储.
按照模块划分:有数据管理模块,负责备份数据的信息管理,网络通信模块,负责与客户端建立通信,业务处理模块,负责文件长传,列表查看,文件下载功能,热点管理模块,负责对
3.客户端框架搭建思想
按照功能划分:客户端要支持指定目录/文件夹的文件检测,如果目录中有文件就将文件获取上传,判断文件是否需要备份,最新一次的上传而且是修改过的文件,并且经过一段时间间隔仍然没有修改的文件就备份.将需要备份的文件上传到服务器.
按照模块儿划分,有数据管理模块,管理备份过的文件信息,文件检测模块,监控指定目录是否有文件,判断文件是否需要备份.文件上传模块.
二、环境搭建
1.gcc编译器升级
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验证安装成功
2.安装第三方库
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库是嵌入式库,就是不用包含头文件,直接将源码拿过来使用的
三、认识第三方库
1.json库使用
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;//转floatbool 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<iostream>
#include<sstream>
#include<string>
#include<memory>
#include<jsoncpp/json/json.h>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<iostream>
#include<sstream>
#include<memory>
#include<jsoncpp/json/json.h>
#include<string>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;}
}
2.bundle库使用
bundle库是嵌入式库,可以直接将源码拿过来使用,使用时可以将源文件拷贝过来复用.
压缩
在读取文件的时候,因为要一次读取整个文件,所以要通过获取文件指针偏移量的方式来获取文件大小,以便设置接收文件内容的缓冲区的大小.
文件的binary打开模式,会自动创建不存在的文件.
fstream的读写功能参数只涉及到缓冲区地址和大小.
pack参数一个事压缩文件的类型,一个是要压缩的内容.
#include<iostream>
#include<fstream>
#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<string>
#include<fstream>
#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是将文件内容和进行哈希映射值.
3.httplib库使用
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 serverstd::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,版本号,请求包头,主体.设置响应字段.
4.简单服务器搭建
#include<iostream>
#include"httplib.h"
#include<string>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;
}
5.简单客户端搭建
#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;
}
四、文件使用工具类设计
1.类的功能结构
成员变量:文件名
成员函数:获取文件大小,获取文件最后修改时间,获取文件最后访问时间,获取文件名,设置文件内容,获取文件内容,获取文件指定位置的指定长度的内容,获取指定目录中所有文件信息,判断文件是否存在,创建目录,文件压缩,文件解压缩;
2.功能实现
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#include <fstream>
#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(){// 填充文件属性结构体statstruct 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(){// 填充文件属性结构体statstruct 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(){// 填充文件属性结构体statstruct 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;};
}
3.常见压缩算法
RLE(run length encoding 运行长度编码)
将连续的重复字符替换为字符+重复次数进行压缩.
HUFFMAN(哈佛曼算法)算法
1.统计字符出现频率
2.将字符用二进制从0,01,11,等开始依次编码,编码顺序由字符出现频率从高到低开始编码.
3.构建哈夫曼二叉树,字符和频率作为节点创建最小堆,取出频率最小的两个堆将他们将的字符合并,频率相加组成新节点插入堆中,依次归并,最终得到所以字符频率之和的新节点结束.
4.从根节点遍历哈夫曼树,路径往左标记0,往右标记1,遍历到叶子节点,记录路径上所有标记形成编码,将叶子节点的字符与编码组成映射关系记录到表中
5.将对应的字符转化编码进行压缩.
DEFLATE算法
1.使用RLE算法去重
2.使用哈夫曼算法堆字符编码
五、json工具类实现
#pragma once
#include<sstream>
#include<iostream>
#include<jsoncpp/json/json.h>
#include<memory>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;}};
}
六、服务端配置信息模块
1.设计思想
程序各个模块需要使用到热点判断时间,文件下载前缀名,文件压缩包后缀名,上传文件存放路径,文件压缩存放路径,备份信息存储文件,IP 地址,端口这个样的固定的关键信息,使用一个类统一管理这些信息,方便管理,便于后续维护.又因为需要不止一个模块,需要访问这些信息,而且使用的时机也不同,所以需要这个类是一个全局唯一对象,单例模式的类管理信息,用文件存储配置信息.在创建对象的时候读取配置文件,将最新的配置信息加载到程序中,使程序每次启动使用的是最新的信息.
2.实现
#pragma once
#include "fileutil.hpp"
#include "json.hpp"
#include <mutex>#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;}
七、服务端数据管理模块
1.设计思想
对客户端上传的文件进行热点管理,将长时间没有被访问的文件进行压缩存储,需要用到文件的服务器本地存储路径,服务器本地压缩路径,文件最后一次访问时间,文件最后一次修改时间,文件压缩标志;客户端下载文件,需要用到文件索引信息URL,浏览备份文件列表,需要用向用户展示文件大小.为了方便维护管理,所以需要用一个单独的模块封装设计.
**功能设计:**文件上传涉及到信息插入,文件列表展示以及热点管理涉及到信息查询,所以类功能设计主要是插入和查询功能.如果一直有用户的上传的文件需要维护,文件的信息就一直需要维护,所以考虑用文件实现信息的持久化存储,每次数据更新就将数据持久化存储.服务器启动时需要读取加载文件信息到程序中,所以还需要一个初始化模块,用户加载文件信息到程序中.以供使用.
管理结构:考虑到热点管理模块对与查询文件信息的操作比较频繁,用哈希表结构管理文件信息,提高查询效率.
数据会被服务器,热点管理多个线程访问,服务器模块本身使用的是一个线程池,处理应用程序发来的请求,请求中有查询和插入操作,热点管理只是查询操作,所以对于数据的访问是读多写少,使用读写锁加锁可以提高并发性能.
2.实现
#pragma once
#include "fileutil.hpp"
#include "config.hpp"
#include "json.hpp"
#include <unistd.h>
#include <unordered_map>
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;// 文件实际存储路径,backdirstd::string real_path;// 文件压缩存储路径 packdirstd::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::ValueJson::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;};
}
八、服务端热点管理模块
1.设计思想
对于客户端上传的文件,实时检测文件是否被经常访问,对于不经常被访问的文件,压缩存储可以减少服务器磁盘压力.
热点对象访问全局数据对象获取所有备份文件信息,遍历所有文件信息,利用数据中的文件访问时间与当前时间作比较,判断该文件是否为热点文件,将非热点文件压缩存储在新的路径下,删除源文件可以避免源文件被重复检测.
2.代码
#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;};
}
九、服务端业务处理模块
1.实现思想
服务端要能响应客户端的文件列表查看请求,文件下载请求,文件上传请求.
文件上传功能的实现:判断响应报文中是否设置了"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(文件内容);
2.代码
#pragma once
#include "httplib.h"
#include "config.hpp"
#include "DataManager.hpp"
#include <sstream>
#include <ctime>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 << "<html><head><title>Download</title></head>";ss << "<body><h1>Download</h1><table>";ss << "<meta charset='UTF-8'>";for (auto a : array){ss << "<tr>";FileUtil fu(a.real_path);ss << "<td><a href='" << a.url << "'>" << fu.FileName() << "</a></td>";ss << "<td align='right'>" << Gettime(a.last_m_time) << "</td>";ss << "<td align='right'>" << a.file_size / 1024 << "k </td>";ss << "</tr>";}ss << "</table></body></html>";// 设置响应展示页面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;};
}
十、客户端数据模块
1.实现思想
客户端要实现的功能是对指定文件夹中的文件自动进行备份上传。但是并不是所有的文件每次都需要上传,需要判断哪些文件需要上传哪些不需要,因此需要将备份的文件信息给管理起来,作为下一次文件是否需要备份的判断。因此需要被管理的信息包含以下:文件路径名称,文件唯一标识:由文件名,最后一次修改时间,文件大小组成的一串信息
2.代码
#pragma once
#include<sstream>
#include<string>
#include<unordered_map>
#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插入arraystd::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<windows.h>
#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;};
}