前面的文章中我们介绍了一些需要用到的库文件,以及其对应的简要使用方式,那么在本文中将正式开始项目的构建。
服务端工具类实现 - 文件实用工具类
不管是客户端还是服务端,文件的传输都涉及到文件的读写,包括数据管理信息的持久化,因此首先设计封装文件操作类。
/*util.hpp*/
namespace fs = std::experimental::filesystem;
class FileUtil
{
private:
std::string _filename; // 文件名/文件路径
public:
FileUtil(const std::string &filename) :_filename(filename)
{}
// 获取文件大小
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 LastModTime()
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file LastModTime failed!\n";
return -1;
}
return st.st_mtime;
}
// 获取文件最后一次访问时间
time_t LastAccTime()
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file LastAccTime failed!\n";
return -1;
}
return st.st_atime;
}
// 获取文件路径中的文件名称 /abc/test.txt
std::string FileName()
{
size_t pos = _filename.find_last_of("/\\"); // 从末尾开始寻找文件名
if (pos == std::string::npos)
{
return _filename;
}
return _filename.substr(pos + 1);
}
// 向文件写入数据
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";
}
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 GetContent(std::string *body)
{
size_t fsize = this->FileSize();
return GetPosLen(body, 0, fsize);
}
// 获取文件指定位置 指定长度
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 Exists()
{
return fs::exists(_filename);
}
// 检测目录是否存在,若不存在则创建
bool CreateDirectory()
{
if (this->Exists()) return true;
return fs::create_directories(_filename);
}
// 扫描目录,并将文件名写入vector中
bool ScanDirectory(std::vector<std::string> *array)
{
for (auto &p : fs::directory_iterator(_filename))
{
if (fs::is_directory(p) == true) continue;
// relative_path 带有路径的文件名
array->push_back(fs::path(p).relative_path().string());
}
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 Remove()
{
if (this->Exists() == false)
{
return true;
}
remove(_filename.c_str());
return true;
}
};
服务端工具类实现- Json 实用工具类
/*util.hpp*/
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::stringstream ss;
if (sw->write(root, &ss) != 0)
{
std::cout << "json write failed!\n";
return false;
}
*str = ss.str();
return true;
}
static bool UnSerialize(const std::string &str, Json::Value *root)
{
Json::CharReaderBuilder crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string 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;
}
};
服务端配置信息模块实现-系统配置信息
使用文件配置加载一些程序运行的关键信息,可以让程序更加的灵活
配置信息:
- 热点判断时间 多长时间没有被访问的文件算是非热点文件
- 文件下载URL前缀路径
- 压缩包后缀名称 定义的压缩包命名规则,在文件原名称之后加上后缀
- 上传文件存放路径 决定文件上传之后实际存储在服务器的哪里
- 压缩文件存放路径 决定非热点文件压缩后存放的文件路径
- 服务端备份信息存放文件 服务端记录的备份文件信息的持久化存储
- 服务器访问 IP 地址
- 服务器访问端口
{
"hot_time" : 30,
"server_port" : 9090,
"server_ip" : "192.168.122.136",
"url_prefix" : "/download/",
"arc_suffix" : ".lz",
"pack_dir" : "./packdir/",
"back_dir" : "./backdir/",
"manager_file" : "./back.dat"
}
服务端配置信息模块实现-单例文件配置类
使用单例模式管理配置信息,能够让配置信息的管理控制更加统一灵活。
#define CONFIG_FILE "./cloud.conf"
class Config
{
private:
Config()
{
ReadConfigFile();
}
static Config *_instance;
static std::mutex _mutex;
private:
int _hot_time; // 热点判断时间
int _server_port; // 服务器监听端口
std::string _server_ip; // 服务器IP地址
std::string _download_prefix; // 下载的url前缀路径
std::string _packfile_suffix; // 压缩包后缀名称
std::string _back_dir; // 备份文件存放目录
std::string _pack_dir; // 压缩包存放目录
std::string _backup_file; // 数据信息存放文件
private:
// 读取配置文件
bool ReadConfigFile()
{
FileUtil fu(CONFIG_FILE);
std::string body;
if (fu.GetContent(&body) == false)
{
std::cout << "load config file failed!\n";
return false;
}
Json::Value root;
if (JsonUtil::UnSerialize(body, &root) == false)
{
std::cout << "parse config file failed!\n";
return false;
}
_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();
_back_dir = root["back_dir"].asString();
_pack_dir = root["pack_dir"].asString();
_backup_file = root["backup_file"].asString();
return true;
}
public:
// 使用单例模式来获取配置文件
static Config *GetInstance()
{
if (_instance == nullptr)
{
_mutex.lock();
if (_instance == nullptr)
{
_instance = new Config();
}
_mutex.unlock();
}
return _instance;
}
int GetHotTime()
{
return _hot_time;
}
int GetServerPort()
{
return _server_port;
}
std::string GetServerIP()
{
return _server_ip;
}
std::string GetDownloadPrefix()
{
return _download_prefix;
}
std::string GetPackFileSuffix()
{
return _packfile_suffix;
}
std::string GetBackDir()
{
return _back_dir;
}
std::string GetPackDir()
{
return _pack_dir;
}
std::string GetBackupFile()
{
return _backup_file;
}
};
Config *Config::_instance = NULL;
std::mutex Config::_mutex;
服务端模块实现-管理的数据信息
需要管理的数据信息有文件实际存储路径
- 文件是否压缩标志
- 压缩包存储路径
- 文件访问URL
- 文件最后一次修改时间
- 文件最后一次访问时间
- 文件大小
那么如何管理数据?
- 内存中以文件访问URL为key,数据信息结构为val,使用哈希表进行管理,查询速度快。使用url作为key是因为往后客户端浏览器下载文件的时候总是以 url 作为请求。
- 采用文件形式对数据进行持久化存储(序列化方式采用 json 格式或者自定义方式)
服务端数据管理模块实现-数据管理类
// 定义的数据备份信息
typedef struct BackupInfo
{
bool pack_flag; // 是否压缩标志
size_t fsize; // 文件大小
time_t atime; // 最后一次访问时间
time_t mtime; // 最后一次修改时间
std::string real_path; // 文件实际存储路径
std::string pack_path; // 压缩包存储路径名称
std::string 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.LastModTime();
this->atime = fu.LastAccTime();
this->real_path = realpath;
// ./back_dir/a.txt -> ./pack_dir/a.txt.lz
this->pack_path = packdir + fu.FileName() + packsuffix;
// ./back_dir/a.txt -> ./download/a.txt
this->url = download_prefix + fu.FileName();
return true;
}
} BackupInfo;
// 数据管理类
class DataManager
{
private:
std::string _backup_file; // 持久化存储文件
std::unordered_map<std::string, BackupInfo> _table; // 内存中以hash表存储
pthread_rwlock_t _rwlock; // 读写锁 -- 读共享,写互斥。
public:
DataManager()
{
_backup_file = Config::GetInstance()->GetBackupFile();
pthread_rwlock_init(&_rwlock, nullptr);
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;
}
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;
}
bool GetAll(std::vector<BackupInfo> *arr)
{
pthread_rwlock_wrlock(&_rwlock);
auto it = _table.begin();
for (;it != _table.end(); ++it)
{
arr->push_back(it->second);
}
pthread_rwlock_unlock(&_rwlock);
return true;
}
// 每次数据新增或者修改都需要重新持久化存储,避免数据丢失
bool Storage()
{
// 1. 获取所有数据
std::vector<BackupInfo> array;
this->GetAll(&array);
// 2. 添加到Json::Value
Json::Value root;
for (int i = 0; i < array.size(); i++)
{
Json::Value item;
item["pack_flag"] = array[i].pack_flag;
item["fsize"] = (Json::Int64)array[i].fsize;
item["atime"] = (Json::Int64)array[i].atime;
item["mtime"] = (Json::Int64)array[i].mtime;
item["real_path"] = array[i].real_path;
item["pack_path"] = array[i].pack_path;
item["url"] = array[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.real_path = root[i]["real_path"].asString();
info.pack_path = root[i]["pack_path"].asString();
info.url = root[i]["url"].asString();
Insert(info);
}
return true;
}
};
服务端热点管理模块实现-热点管理实现思路
服务器端的热点文件管理是对上传的非热点文件进行压缩存储,节省磁盘空间。
而热点文件的判断在于上传的文件的最后一次访问时间是否在热点判断时间之内,比如如果一个文件一天都没有被访问过我们就认为这是一个非热点文件,其实就是当前系统时间,与文件最后一次访问时间之间的时间差是否在一天之内的判断。
而我们需要对上传的文件每隔一段时间进行热点检测,相当于遍历上传文件的存储文件夹,找出所有的文件,然后通过对逐个文件进行时间差的判断,来逐个进行热点处理。基于这个思想,我们需要将上传的文件存储位置与压缩后压缩文件的存储位置分开。这样在遍历上传文件夹的时候不至于将压缩过的文件又进行非热点处理了。
关键点:
- 上传文件有自己的上传存储位置,非热点文件的压缩存储有自己的存储位置
- 遍历上传存储位置文件夹,获取所有文件信息。
- 获取每个文件最后一次访问时间,进而完成是否热点文件的判断。
- 对非热点文件进行压缩存储,删除原来的未压缩文件
服务端热点管理模块实现-热点管理类
// 因为业务处理的回调函数没有传入参数的地方,因此无法直接访问外部的数据管理模块数据
// 可以使用lamda表达式解决,但是所有的业务功能都要在一个函数内实现,于功能划分上模块不够清晰
// 因此将数据管理模块的对象定义为全局数据,在这里声明一下,就可以在任意位置访问了
extern cloud::DataManager *_data;
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.LastAccTime();
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 back_dir(_back_dir);
FileUtil pack_dir(_pack_dir);
back_dir.CreateDirectory();
pack_dir.CreateDirectory();
}
bool RunModule()
{
while (1)
{
// 1. 遍历备份目录,获取所有文件路径名称
FileUtil fu(_back_dir);
std::vector<std::string> array;
fu.ScanDirectory(&array);
// 2. 逐个文件获取最后一次访问时间与当前系统时间进行判断比较
for(auto &a : array)
{
if (HotJudge(a) == false)
continue; // 热点文件则不需要处理
// 3. 获取文件的备份信息
BackupInfo bi;
if (_data->GetOneByRealPath(a, &bi) == false)
{
// 现在有一个文件存在,但没有备份信息
bi.NewBackupInfo(a); // 设置一个新的备份信息出来
}
// 4. 对非热点文件进行压缩处理,删除源文件
FileUtil tmp(a);
tmp.Compress(bi.pack_path);
// 5. 修改数据管理模块对应的文件信息
tmp.Remove();
bi.pack_flag = true;
_data->Update(bi);
}
usleep(1000); // 避免空目录循环遍历,消耗CPU资源过高
}
return true;
}
};
服务端业务处理模块实现-业务处理实现思路
云备份项目中 ,业务处理模块是针对客户端的业务请求进行处理,并最终给与响应。而整个过程中包含以下要实现的功能:
- 借助网络通信模块httplib库搭建http服务器与客户端进行网络通信
- 针对收到的请求进行对应的业务处理并进行响应(文件上传,列表查看,文件下载(包含断点续传))
服务端业务处理模块实现-网络通信接口
HTTP文件上传:
POST /upload HTTP/1.1
Content-Length:11
Content-Type:multipart/form-data;boundary= ----WebKitFormBoundary+16字节随机字符
------WebKitFormBoundary
Content-Disposition:form-data;filename=“a.txt”;
hello world
------WebKitFormBoundary–
HTTP/1.1 200 OK
Content-Length: 0
HTTP文件列表获取:
GET /list HTTP/1.1
Content-Length: 0
HTTP/1.1 200 OK
Content-Length:
Content-Type: text/html
形式如下:
Download
a.txt | 1994-07-08 03:00 | 27K |
HTTP断点续传:
GET /download/a.txt http/1.1
Content-Length: 0
If-Range: “文件唯一标识”
Range: bytes=89-999
HTTP/1.1 206 Partial Content
Content-Length:
Content-Range: bytes 89-999/100000
Content-Type: application/octet-stream
ETag: “inode-size-mtime一个能够唯一标识文件的数据”
Accept-Ranges: bytes
对应文件从89到999字节的数据
关于http协议需要注意的一些内容是:
http的ETag头部字段:其中存储了一个资源的唯─标识客户端第一次下载文件的时候,会收到这个响应信息,第二次下载,就会将这个信息发送给服务器,想要让服务器根据这个唯一标识判断这个资源有没有被修改过,如果没有被修改过,直接使用原先缓存的数据,不用重新下载了
http协议本身对于etag中是什么数据并不关心,只要你服务端能够自己标识就行因此我们etag就用“文件名-文件大小-最后一次修改时间"组成,而etag字段不仅仅是缓存用到,还有就是后边的断点续传的实现也会用到因为断点续传也要保证文件没有被修改过。
http协议的Accept-Ranges: bytes 字段:用于高速客户端我支持断点续传,并且数据单位以字节作为单位
Content-Type字段的重要性:决定了浏览器如何处理响应正文
关于断点续传还有一些内容:
断点续传的功能:当文件下载过程中,因为某种异常而中断,如果再次进行从头下载,效率较低,因为需要将之前已经传输过的数据再次传输一遍。因此断点续传就是从上次下载断开的位置,重新下载即可,之前已经传输过的数据将不需要在重新传输。
实现思想:客户端在下载文件的时候,要每次接收到数据写入文件后记录自己当前下载的数据量。当异常下载中断时,下次断点续传的时候,将要重新下载的数据区间(下载起始位置,结束位置)发送给服务器,服务器收到后,仅仅回传客户端需要的区间数据即可。关键点就在于1.在于能够告诉服务器下载区间范围。2.服务器上要能够检测上一次下载之后这个文件是否被修改过实现:
private:
// 上传请求处理函数
static void Upload(const httplib::Request &req, httplib::Response &rsp)
{
// post /upload 文件数据在正文中(正文并不全是文件数据)
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> array;
_data->GetAll(&array);
// 2. 根据所有备份信息,组织HTML文件数据
std::stringstream ss;
ss << "<html><head><title>Download</title></head>";
ss << "<body><h1>Download</h1><table>";
for (auto &a : array)
{
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)
{
// ETag : 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);
}
// 5. 读取文件数据,放入rsp.body中
FileUtil fu(info.real_path);
// -- 进行断点续传
bool retrans = false;
std::string old_etag;
if (req.has_header("If-Range"))
{
old_etag = req.get_header_value("If-Range");
// 有If-Range字段且,这个字段与请求文件的最新etag一致,则符合断点续传
if (old_etag == GetETag(info))
{
retrans = true;
}
}
// 如果没有If-Range字段则是正常下载,或者如果有这个字段,但是它的值与当前文件的etag不一致,则也必须返回全部数据
if (retrans == false)
{
// 6. 设置响应头部字段 ETag Accept-Ranges: bytes
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.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-Range", "bytes start-end/fsize");
rsp.status = 206; // 区间请求响应的是206
}
服务端业务处理模块实现-业务处理类
class Service
{
// 搭建http服务器,并进行业务处理
private:
int _server_port;
std::string _server_ip;
std::string _download_prefix;
httplib::Server _server;
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("0.0.0.0", _server_port);
return true;
}
};
服务端整体运行,以及各部分模块测试函数
void FileUtilTest(const std::string &filename)
{
// 测试获取文件相关属性
// cloud::FileUtil fu(filename);
// std::cout << fu.FileSize() << std::endl;
// std::cout << fu.LastModTime() << std::endl;
// std::cout << fu.LastAccTime() << std::endl;
// std::cout << fu.FileName() << std::endl;
// 测试文件压缩解压缩
// cloud::FileUtil fu(filename);
// std::string body;
// fu.GetContent(&body);
// cloud::FileUtil nfu("./hello.txt");
// nfu.SetContent(body);
// std::string packname = filename + ".lz";
// cloud::FileUtil fu(filename);
// fu.Compress(packname);
// cloud::FileUtil pfu(packname);
// pfu.UnCompress("hello.txt");
// 测试创建目录显示目录中的内容
// cloud::FileUtil fu(filename);
// fu.CreateDirectory();
// std::vector<std::string> array;
// fu.ScanDirectory(&array);
// for(auto &a : array)
// {
// 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;
cloud::JsonUtil::Serialize(root, &json_str);
std::cout << json_str << std::endl;
Json::Value val;
cloud::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()
{
cloud::Config *config = cloud::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)
{
cloud::DataManager data;
std::vector<cloud::BackupInfo> array;
data.GetAll(&array);
for(auto &e : array)
{
std::cout << e.pack_flag << std::endl;
std::cout << e.fsize << std::endl;
std::cout << e.mtime << std::endl;
std::cout << e.atime << std::endl;
std::cout << e.real_path << std::endl;
std::cout << e.pack_path << std::endl;
std::cout << e.url << std::endl;
}
// cloud::BackupInfo info;
// info.NewBackupInfo(filename);
// cloud::DataManager data;
// data.Insert(info);
// cloud::BackupInfo tmp;
// std::cout << "--------------------update and get one----------------------" << std::endl;
// data.GetOneByURL("/download/bundlecpp.tar", &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 get all----------------------" << std::endl;
// info.pack_flag = true;
// data.Update(info);
// std::vector<cloud::BackupInfo> array;
// data.GetAll(&array);
// for(auto &e : array)
// {
// std::cout << e.pack_flag << std::endl;
// std::cout << e.fsize << std::endl;
// std::cout << e.mtime << std::endl;
// std::cout << e.atime << std::endl;
// std::cout << e.real_path << std::endl;
// std::cout << e.pack_path << std::endl;
// std::cout << e.url << std::endl;
// }
// std::cout << "-------------------------realpath---------------------------" << std::endl;
// data.GetOneByRealpath(info.real_path, &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;
// data.Storage();
}
cloud::DataManager *_data;
void HotTest()
{
cloud::HotManager hot;
hot.RunModule();
}
void ServiceTest()
{
cloud::Service srv;
srv.RunModule();
}
int main(int argc, char *argv[])
{
_data = new cloud::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;
}
客户端文件检测模块实现-文件操作实用类
与服务端的相类似,知识功能没有服务端那么多
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;
}
size_t FileSize() {
struct stat st;
if (stat(_filename.c_str(), &st) < 0) {
std::cout << "get file size failed!\n";
return 0;
}
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.find_last_of("\\");
if (pos == std::string::npos) {
return _filename;
}
// return fs::path(_filename).filename().string();
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) {
this->CreateDirectory();
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;
}
};
客户端数据管理模块实现-数据信息
客户端要实现的功能是对指定文件夹中的文件自动进行备份上传。但是并不是所有的文件每次都需要上传,我们需要能够判断,哪些文件需要上传,哪些不需要,因此需要将备份的文件信息给管理起来,作为下一次文件是否需要备份的判断。因此需要被管理的信息包含以下:
- 文件路径名称
- 文件唯一标识:由文件名,最后一次修改时间,文件大小组成的一串信息
客户端数据管理模块实现-数据管理类
1、内存存储:高效率访问 – hashtable
2、持久化:文件中存储,这里使用的是自定义序列化格式
class DataManager {
private:
std::string _backup_file;//备份信息的持久化存储文件
std::unordered_map<std::string, std::string> _table;
public:
DataManager(const std::string& backup_file) :_backup_file(backup_file) {
InitLoad();
}
bool Storage() {
//1. 获取所有的备份信息
std::stringstream ss;
auto it = _table.begin();
for (; it != _table.end(); ++it) {
//2. 将所有信息进行指定持久化格式的组织
ss << it->first << " " << it->second << "\n";
}
//3. 持久化存储
FileUtil fu(_backup_file);
fu.SetContent(ss.str());
return true;
}
int Split(const std::string& str, const std::string& sep, std::vector<std::string>* arry) {
int count = 0;
size_t pos = 0, idx = 0;
while (1) {
pos = str.find(sep, idx);
if (pos == std::string::npos) {
break;
}
if (pos == idx) {
idx = pos + sep.size();
continue;
}
std::string tmp = str.substr(idx, pos - idx);
arry->push_back(tmp);
count++;
idx = pos + sep.size();
}
if (idx < str.size()) {
arry->push_back(str.substr(idx));
count++;
}
return count;
}
bool InitLoad() {
//1. 从文件中读取所有数据
FileUtil fu(_backup_file);
if (fu.Exists() == false)
return true;
std::string body;
fu.GetContent(&body);
//2. 进行数据解析,添加到表中
std::vector<std::string> arry;
Split(body, "\n", &arry);
for (auto& a : arry) {
// b.txt b.txt-34657-345636
std::vector<std::string> tmp;
Split(a, " ", &tmp);
if (tmp.size() != 2) {
continue;
}
_table[tmp[0]] = tmp[1];
}
return true;
}
bool Insert(const std::string& key, const std::string& val) {
_table[key] = val;
Storage();
return true;
}
bool Update(const std::string& key, const std::string& val) {
_table[key] = val;
Storage();
return true;
}
bool GetOneByKey(const std::string& key, std::string* val) {
auto it = _table.find(key);
if (it == _table.end()) {
return false;
}
*val = it->second;
return true;
}
};
客户端文件备份模块实现-文件备份类
#define SERVER_ADDR "120.26.50.214"
#define SERVER_PORT 8989
class Backup {
private:
std::string _back_dir; // 要监控的文件夹
DataManager* _data;
// 计算获取文件的唯一标识
std::string GetFileIdentifier(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 Upload(const std::string& filename)
{
// 1. 获取文件数据
FileUtil fu(filename);
std::string body;
fu.GetContent(&body);
// 2. 搭建http客户端上传文件数据
httplib::Client client(SERVER_ADDR, 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;
}
public:
Backup(const std::string& back_dir, const std::string& back_file) :_back_dir(back_dir)
{
_data = new DataManager(back_file);
}
bool IsNeedUpload(const std::string& filename)
{
// 需要上传的文件判断条件:文件时新增的,不是新增,但是被修改过
// 文件是新增的:看一下有没有历史备份信息,
// 不是新增但是被修改过:有历史信息,但是历史的唯一标志与当前最新的唯一标识不一致
std::string id;
if (_data->GetOneByKey(filename, &id) != false)
{
// 有历史信息
std::string new_id = GetFileIdentifier(filename);
if (new_id == id)
{
// 不需要被上传 -- 上次上传之后没有别修改过
return false;
}
}
// 一个文件比较大,正在被拷贝进目录,拷贝需要过程,如果每次遍历则都会判断标识不一致,需要上传一个几十个G的文件会上传上百次
// 因此应该判断一个文件一段时间都没有被修改过了,则才能够上传(判断文件是否被进程占用,被某个进行打开使用)
FileUtil fu(filename);
if (time(NULL) - fu.LastMTime() < 3) // 3秒钟之内刚修改过 -- 认为文件还在修改中
{
return false;
}
return true;
}
bool RunModule()
{
while (1) {
//1. 遍历获取指定文件夹中所有文件
FileUtil fu(_back_dir);
std::vector<std::string> arry;
fu.ScanDirectory(&arry);
//2. 逐个判断文件是否需要上传
for (auto& a : arry) {
if (IsNeedUpload(a) == false) {
continue;
}
//3. 如果需要上传则上传文件
if (Upload(a) == true) {
_data->Insert(a, GetFileIdentifier(a));//新增文件备份信息
std::cout << a << " upload success!\n";
}
}
Sleep(1);
//std::cout << "-------------------------loop end-------------------------\n";
}
}
};
至此整个项目的所有的模块已经完全的呈现在上方。