本篇有点长,并且包含源码,希望可以结合文章注释看源码,可以通过目录进行跳转
目录
一、服务端主要功能划分
1、支持文件上传功能
2、支持客户端文件辈分列表查看功能
3、支持文件下载功能(还有文件下载被意外中断时,再次下载可以接着下载的功能—断点续传)
4、热点功能管理(对长时间无访问的文件进行压缩)
二、服务端模块划分
1、文件工具模块(双端都会进行大量的文件操作,通过这个模块可以降低操作难度)
2、数据管理模块(管理备份的文件信息,以便于及时存取)
3、网络通信模块(实现与客户端的网络通信)
4、业务处理模块(上传、查看、下载)
5、热点管理模块(对长时间无访问的文件进行压缩)
三、文件工具模块的实现
不管是客户端还是服务端、文件的传输备份都涉及到文件的读写,包括数据管理信息的持久化也是如此,因此设计文件名操作类,这个类封装完后,在任意模块对文件的操作都将简单化。
文件工具类需要实现的功能:
- 文件名的获取(文件名是会携带自己的地址的,我们有时仅仅需要获取到单纯的文件名)
- 文件大小的获取
- 文件最后一次修改时间以及最后一次访问时间(用于服务热点管理模块)
- 文件的读写
- 目录的创建功能(会用到两个目录——正常存储目录、非热点文件压缩目录)
- 遍历目录的功能(管理文件需要获取上面两个目录里面的文件名)
- 判断文件是否存在
- 文件内容的序列化与反序列化
- 文件的压缩以及解压缩
- 删除文件(压缩和解压缩需要将原文件删除)
以下就是需要实现的函数,在util.hpp实现:
/*util.hpp*/
namespace cloud{
class FileUtil{
private:
std::string _filename;
public:
FileUtil(const std::string &name);
size_t FileSize();
time_t LastATime();//最后一次访问时间
time_t LastMTime();//最后一次修改时间
std::string FileName();
bool GetPosLen(std::string *content, size_t pos, size_t len);//获取文件pos到len的内容
bool GetContent(std::string *content);
bool SetContent(std::strint *content);
bool Compress(const std::string &packname);
bool UnCompress(const std::string &filename);
bool Exists();
bool CreateDirectory();
bool ScanDirectory(std::vector<std::string> *arry);
};
}
1.构造函数、获取文件名的实现
FileUtil(const std::string &filename):_filename(filename){}
std::string FileName()
{
size_t pos;
std::string filename="/"+_filename;
pos=filename.find_last_of("/");//找到最后一个‘/’后面就是我们不带路径的文件名了
if(pos==std::string::npos)
{
std::cout<<"find name failed!(util.hpp)\n";
exit;
}
return filename.substr(pos+1);
}
查找文件名时需要注意的是linux的路径分隔符是‘/’而Windows的分隔符是“\\”,所以这段代码适用于linux却不适用于windows下,不具有良好的跨平台性。
由于我们的客户端是针对的Windows的环境,为了后续代码的可移植性,我们可以写成:
std::string FileName()
{
/*size_t pos;
std::string filename="/"+_filename;
pos=filename.find_last_of("/");
if(pos==std::string::npos)
{
std::cout<<"find name failed!(util.hpp)\n";
exit(1);
}*/
return std::experimental::filesystem::path(_filename).filename().string();
}
这样可以直接获取到不带路径的文件名
2.获取文件大小以及两个时间的功能实现
int64_t FileSize()
{
struct stat st;
if(stat(_filename.c_str(),&st)<0)
{
std::cout<<"get file size failed!\n";
return -1;
}
return st.st_size;
}
time_t LastMTime(){
struct stat st;
if(stat(_filename.c_str(),&st)<0)
{
std::cout<<"get file last mtimg 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 last atimg failed!\n";
return -1;
}
return st.st_atime;
}
介绍一下stat是一个用于获取文件属性的函数,它需要传入一个stat结构体来储存属性,我们利用这一特性获取文件属性。
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
struct stat
{
dev_t st_dev; /* 文件所在设备的 ID */
ino_t st_ino; /* 文件对应 inode 节点编号 */
mode_t st_mode; /* 文件对应的模式 */
nlink_t st_nlink; /* 文件的链接数 */
uid_t st_uid; /* 文件所有者的用户 ID */
gid_t st_gid; /* 文件所有者的组 ID */
dev_t st_rdev; /* 设备号(指针对设备文件) */
off_t st_size; /* 文件大小(以字节为单位) */
blksize_t st_blksize; /* 文件内容存储的块大小 */
blkcnt_t st_blocks; /* 文件内容所占块数 */
struct timespec st_atim; /* 文件最后被访问的时间 */
struct timespec st_mtim; /* 文件内容最后被修改的时间 */
struct timespec st_ctim; /* 文件状态最后被改变的时间 */
};
3.文件的读和写
bool GetPosLen(std::string &body,size_t pos,size_t len)
{
if(this->FileSize()<pos+len)
{
std::cout<<"GetPosLen failed!\n";
return false;
}
std::ifstream ifs;
ifs.open(_filename,std::ios::binary);
if(ifs.good()==false)
{
std::cout<<"open file failed!\n";
return false;
}
ifs.seekg(pos,std::ios::beg);//使ifs的指针指向pos位置
body.resize(len);
ifs.read(&body[0],len);
if(!ifs.good())
{
std::cout<<"read failede!\n";
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 failede!\n";
return false;
}
ofs.write(&body[0],body.size());
if(!ofs.good())
{
std::cout<<"write failede!\n";
return false;
}
ofs.close();
return true;
}
两个流的使用:ofstream是从内存到硬盘——set,ifstream是从硬盘到内存——get,其实所谓的流缓冲就是内存空间
4.目录的两个功能、文件是否存在
#include <experimental/filesystem>
namespace fs=namespace fs=std::experimental::filesystem;
bool Exists()
{
return fs::exists(_filename);
}
bool CreateDirectory()
{
if(this->Exists()==true)
{
return true;
}
return fs::create_directories(_filename);
}
bool ScanDirectory(std::vector<std::string>* arry )
{
for(auto& p:fs::directory_iterator(_filename))
{
if(fs::is_directory(p))
continue;
arry->push_back(fs::path(p).relative_path().string());
}
return true;
}
namespace fs=std::experimental::filesystem这样的写法是C++17引入的一个命名空间别名,用于访问文件系统相关的功能。
std::experimental::filesystem提供了一组函数和类,用于操作文件、目录和路径例如删除、创建和重命名文件或目录,以及获取文件或目录的属性信息等。
包括我们上面获取文件名的方法二就是它提供的。
使用小例子:
#include <iostream>
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
int main() {
// 创建目录
fs::create_directory("my_directory");
// 创建文件
fs::path file_path = "my_directory/my_file.txt";
std::ofstream file(file_path);
file << "Hello, World!";
file.close();
// 判断文件是否存在
if (fs::exists(file_path)) {
std::cout << "File exists." << std::endl;
}
// 获取文件大小
std::cout << "File size: " << fs::file_size(file_path) << " bytes" << std::endl;
// 删除文件
fs::remove(file_path);
// 删除目录
fs::remove("my_directory");
return 0;
}
5.文件的压缩与解压缩、文件删除
bool Compress(const std::string &packname)
{
std::string body;
this->GetContent(&body);
std::string packed=bundle::pack(bundle::LZIP,body);
FileUtil fu(packname);
if(fu.SetContent(packed)==false)
{
std::cout<<"compress failed!\n";
return false;
}
return true;
}
bool UnCompress(const std::string packname)
{
std::string body;
this->GetContent(&body);
std::string upbody2;
upbody2=bundle::unpack(body);
FileUtil fu(packname);
if(fu.SetContent(upbody2)==false)
{
std::cout<<"uncompress failed!\n";
return false;
}
return true;
}
bool Remove(){
if(this->Exists()==false)
return true;
remove(_filename.c_str());
return true;
}
压缩的方法:
- 先获取文件的数据,就是一个字符串
- 将字符串压缩,得到压缩后的数据
- 创建一个压缩包,将压缩后的数据写入
- 最后将原文件删除,不删除也没有节省磁盘空间的作用
创建压缩包的时候,该压缩包创建的地址取决于你传进来的压缩包位置——在该项目中就是在压缩目录packdir中。
解压缩的方法:
- 获取压缩包中的数据
- 将该数据解压
- 将建一个文件,并写入解压数据
- 删除压缩包
6.文件内容的序列化和反序列化的功能
class JsonUtil{
public:
static bool Serialize(Json::Value &root,std::string *str)
{
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
sw->write(root,&ss);
*str=ss.str();
return true;
}
static bool UnSerialize(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<<"error:"<<err<<std::endl;
return false;
}
return true;
}
};
json序列化的使用:
- 通过StreamWriterBuilder实例化出来一个StreamWriter
- 用智能指针进行存储
- 调用write函数,将数据序列化后存入ss字符串
反序列化的使用:
- 通过CharReaderBuilder实例化出来一个CharReader
- 用智能指针存储
- 调用parse函数将反序列化后的数据放入json::Value 变量中
util.hpp源码
#ifndef __MY_UTIL__
#define __MY_UTIL__
#include<iostream>
#include<fstream>
#include<string>
#include<vector>
#include<sys/stat.h>
#include<experimental/filesystem>
#include"bundle.h"
#include<stdio.h>
#include<jsoncpp/json/json.h>
namespace cloud{
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 LastMTime(){
struct stat st;
if(stat(_filename.c_str(),&st)<0)
{
std::cout<<"get file last mtimg 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 last atimg failed!\n";
return -1;
}
return st.st_atime;
}
std::string FileName()
{
size_t pos;
std::string filename="/"+_filename;
pos=filename.find_last_of("/");
if(pos==std::string::npos)
{
std::cout<<"find name failed!(util.hpp)\n";
exit;
}
return filename.substr(pos+1);
}
bool GetPosLen(std::string &body,size_t pos,size_t len)
{
if(this->FileSize()<pos+len)
{
std::cout<<"GetPosLen failed!\n";
return false;
}
std::ifstream ifs;
ifs.open(_filename,std::ios::binary);
if(ifs.good()==false)
{
std::cout<<"open file failed!\n";
return false;
}
ifs.seekg(pos,std::ios::beg);
body.resize(len);
ifs.read(&body[0],len);
if(!ifs.good())
{
std::cout<<"read failede!\n";
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 failede!\n";
return false;
}
ofs.write(&body[0],body.size());
if(!ofs.good())
{
std::cout<<"write failede!\n";
return false;
}
ofs.close();
return true;
}
bool Compress(const std::string &packname)
{
std::string body;
this->GetContent(&body);
std::string packed=bundle::pack(bundle::LZIP,body);
FileUtil fu(packname);
if(fu.SetContent(packed)==false)
{
std::cout<<"compress failed!\n";
return false;
}
return true;
}
bool UnCompress(const std::string packname)
{
std::string body;
this->GetContent(&body);
std::string upbody2;
upbody2=bundle::unpack(body);
FileUtil fu(packname);
if(fu.SetContent(upbody2)==false)
{
std::cout<<"uncompress failed!\n";
return false;
}
return true;
}
bool Remove(){
if(this->Exists()==false)
return true;
remove(_filename.c_str());
return true;
}
bool Exists()
{
return fs::exists(_filename);
}
bool CreateDirectory()
{
if(this->Exists()==true)
{
return true;
}
return fs::create_directories(_filename);
}
bool ScanDirectory(std::vector<std::string>* arry )
{
for(auto& p:fs::directory_iterator(_filename))
{
if(fs::is_directory(p))
continue;
arry->push_back(fs::path(p).relative_path().string());
}
return true;
}
};
class JsonUtil{
public:
static bool Serialize(Json::Value &root,std::string *str)
{
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
sw->write(root,&ss);
*str=ss.str();
return true;
}
static bool UnSerialize(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<<"error:"<<err<<std::endl;
return false;
}
return true;
}
};
}
#endif
四、配置文件及配置文件的管理
配置文件就是用来存放一些不同模块进程需要用到的信息,直接设置一个配置文件利于实现低耦合
配置文件
cloud.conf:
{
"hot_time" :30,
"server_port": 9090,
"server_ip":"0.0.0.0",
"downloap_prefix": "/downloap/",
"packfile_suffix":".lz",
"pack_dir":"./packdir/",
"back_dir":"./backdir/",
"backup_file":"./cloud.dat"
}
配置文件管理模块
重点是实现读取配置文件,源码:
#ifndef __MY_CONFIG__
#define __MY_CONFIG__
#include<mutex>
#include"util.hpp"
namespace cloud
{
#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;
std::string _download_prefix;
std::string _packfile_suffix;
std::string _pack_dir;
std::string _back_dir;
std::string _backup_file;
bool ReadConfigFile()
{
FileUtil fu(CONFIG_FILE);
std::string body;
if(fu.GetContent(&body)==false)
{
std::cout<<"get content false\n";
return false;
}
Json::Value root;
if(JsonUtil::UnSerialize(body,&root)==false)
{
std::cout<<"UnSerialize false! \n";
return false;
}
_hot_time=root["hot_time"].asInt();
_server_port=root["server_port"].asInt();
_server_ip=root["server_ip"].asString();
_download_prefix=root["downloap_prefix"].asString();
_packfile_suffix=root["packfile_suffix"].asString();
_pack_dir=root["pack_dir"].asString();
_back_dir=root["back_dir"].asString();
_backup_file=root["backup_file"].asString();
return true;
}
public:
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 GetPackDir()
{
return _pack_dir;
}
std::string GetBackDir()
{
return _back_dir;
}
std::string GetBackupFile()
{
return _backup_file;
}
};
Config* Config::_instance=nullptr;
std::mutex Config::_mutex;
}
#endif
单例类
这里的设计模式是单例类,这样是为了使这个类只有一个实例,并为其提供全局访问点。
我们这个程序的Config只需要一个对象即可,单纯的获取配置文件信息就可以了。就比如:
有些属性型对象也只需要全局存在一个。比如,假设黑体字属性是一种对象,调用黑体字属性对象的函数来让一个字变成黑体。显然并不需要在每创造一个黑体字时就生成一种属性对象,只需要调用当前存在的唯一对象的函数即可。
单例类的实现:
- 将构造函数放入private影藏起来,这样别人调用new就不会调用构造函数生成一个对象了。
- 只有通过调用GetInstance函数才能获取到Config 指针
- 每次调用GetInstance函数的时候判断一下指针是否为空
- 为了保证只会存在一个Config指针我们需要对new(Config)这一步操作加锁。
这样我们就基本实现了一个单例类了。但这里还是有地方需要改进一下,大部分调用这个函数的线程只是只读操作,而这个程序只有一个获取锁的线程可以使用,低并发环境的话还好,但如果是高并发环境的话,成千上万调用这个函数的线程中只有一个可以正常使用就有点扯了,其他都在阻塞,等待锁,这个锁的使用结束后还会有下一个线程使用锁,这里就会一直保持一个低效率的运行。解决方案——既然你只是只读的请求,那我只返回你数据就行了,除了指针为空时。
皆大欢喜。
这里不只是指针是唯一一个,锁也是唯一一个,两个都是static成员变量,静态成员变量只能在类内声明类外定义
格式:类型+xx::变量名+初始化 xx为作用域,初始化分为自定义变量或内置变量
json::Value数据类型转换
json::Value内封装了类型转换的函数,供我们调用————将数据准确的转换成我们需要的类型。
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
};
五、数据管理模块的实现
操作系统的学习中有一句至理名言:先描述,再组织。
在这个项目中我们应该怎么描述一个文件呢?
我们将数据以哈希表的形式储存起来,并且为了避免每一次启动都要重新获取数据或者丢失数据,我们直接将数据持久化储存到文件中,也就是内存到磁盘的存储。
描述
/*data.hpp*/
typedef struct BackInfo{
//项目需要查看成员变量,可以直接用struct不保护成员变量
bool _pack_flag;
size_t _fsize;
time_t _atime;
time_t _mtime;
std::string _real_path;// 文件实际存储路径 或者说文件在下载前的地址back_path
std::string _pack_path;//压缩路径
std::string _url;
bool NewBackInfo(const std::string realpath){
cloud::Config* config=cloud::Config::GetInstance();
cloud::FileUtil fu(realpath);
std::string packdir=config->GetPackDir();
std::string packsuffix=config->GetPackFileSuffix();
std::string downloapprefix=config->GetDownloadPrefix();
this->_pack_flag=false;
this->_fsize=fu.FileSize();
this->_atime=fu.LastATime();
this->_mtime=fu.LastMTime();
this->_real_path=realpath;
this->_pack_path=packdir+fu.FileName()+packsuffix;
this->_url=downloapprefix+fu.FileName();
return true;
}
}BackInfo;
组织
/*data.hpp*/
typedef struct DataManager{
std::string _backup_file;
pthread_rwlock_t _rwlock;
//以url为key值,backinfo为值的图
std::unordered_map<std::string ,BackInfo> _table;
DataManager()
{
_backup_file=Config::GetInstance()->GetBackupFile();
pthread_rwlock_init(&_rwlock,NULL);
InitLoad();
}
~DataManager()
{
pthread_rwlock_destroy(&_rwlock);
}
bool Insert(BackInfo &info){
pthread_rwlock_wrlock(&_rwlock);
_table[info._url]=info;
/*for(auto a:_table){
std::cout<<a.first<<std::endl;
}*/
pthread_rwlock_unlock(&_rwlock);
Storage();
return true;
}
bool Updata(BackInfo &info){
pthread_rwlock_wrlock(&_rwlock);
_table[info._url]=info;
pthread_rwlock_unlock(&_rwlock);
Storage();
return true;
}
bool GetOneByUrl(const std::string &url,BackInfo *Info){
pthread_rwlock_rdlock(&_rwlock);//读锁
auto it=_table.begin();
it=_table.find(url);
if(it==_table.end()){
pthread_rwlock_unlock(&_rwlock);
return false;
}
pthread_rwlock_unlock(&_rwlock);
*Info=it->second;
return true;
}
bool GetOneByRealPath(std::string &realpath,BackInfo * info){
//直接遍历,通过second的成员来确定
pthread_rwlock_rdlock(&_rwlock);//读锁
auto it=_table.begin();
for(;it!=_table.end();++it){
if(it->second._real_path==realpath)
*info=it->second;
}
if(it==_table.end()){
pthread_rwlock_unlock(&_rwlock);
return false;
}
pthread_rwlock_unlock(&_rwlock);
return true;
}
bool GetAll(std::vector<BackInfo> *arry){
pthread_rwlock_rdlock(&_rwlock);
auto it=_table.begin();
for(;it!=_table.end();++it){
arry->push_back(it->second);
}
pthread_rwlock_unlock(&_rwlock);
return true;
}
bool InitLoad(){
//读取文件
FileUtil fu(_backup_file);
if(fu.Exists()==false)
return true;
std::string body;
fu.GetContent(&body);
Json::Value root;
//先反序列化,获取一个Value
JsonUtil::UnSerialize(body,&root);
//再逐个插入
for(int i=0;i<root.size();i++){
BackInfo 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;
}
bool Storage(){
//先获取数据
std::vector<BackInfo> arry;
this->GetAll(&arry);
Json::Value root;
//遍历存放进value root
for(int i=0;i<arry.size();i++){
Json::Value tem;
tem["pack_flag"]=arry[i]._pack_flag;
tem["fsize"]=(Json::Value::Int64)arry[i]._fsize;
tem["atime"]=(Json::Value::Int64)arry[i]._atime;
tem["mtime"]=(Json::Value::Int64)arry[i]._mtime;
tem["pack_path"]=arry[i]._pack_path;
tem["real_path"]=arry[i]._real_path;
tem["url"]=arry[i]._url;
root.append(tem);
}
//将root序列化
std::string body;
JsonUtil::Serialize(root,&body);
//保存至文件完成持久化储存
FileUtil fu(_backup_file);
fu.SetContent(body);
std::cout<<"storage finish"<<std::endl;
return true;
}
}DataManager;
这里实现了:
- 数据的增删查改
- 数据的持久化存储
- 每次初始化的时候都需要读取持久化的数据
以下是一些需要注意的地方:
a.读写锁
服务器会同时接收多个客户端的请求,每收到一个请求就会创建一个线程执行响应请求(由httplib中的listen实现),因此我们需要注意线程安全问题,在这个项目中线程会访问、读取、修改文件信息,需要用锁解决。但是在这里更合理的处理方式是读写锁——读共享,写互斥。
- 就是读取的时候是允许数据被多个线程读取的但是不允许读取
- 写的时候是不允许其他线程读取数据的。
b.持久化存储原理
- 遍历获取所有的backinfo数据
- 挨个储存至value变量中
- 将value变量组织起来
- 将其序列化
- 序列化后写入一个文件
c.需要持久化储存数据的地方
- 程序退出的时候
- 增加数据的时候
- 修改数据的时候———防止程序意外中断导致数据丢失
源码:
/*data.hpp*/
#ifndef __MY__DATA__
#define __MY__DATA__
#include<unordered_map>
#include<pthread.h>
#include<vector>
#include"util.hpp"
#include"config.hpp"
namespace cloud{
typedef struct BackInfo{
//项目需要查看成员变量,可以直接用struct不保护成员变量
bool _pack_flag;
size_t _fsize;
time_t _atime;
time_t _mtime;
std::string _real_path;// 文件实际存储路径 或者说文件在下载前的地址back_path
std::string _pack_path;//压缩路径
std::string _url;
bool NewBackInfo(const std::string realpath){
cloud::Config* config=cloud::Config::GetInstance();
cloud::FileUtil fu(realpath);
std::string packdir=config->GetPackDir();
std::string packsuffix=config->GetPackFileSuffix();
std::string downloapprefix=config->GetDownloadPrefix();
this->_pack_flag=false;
this->_fsize=fu.FileSize();
this->_atime=fu.LastATime();
this->_mtime=fu.LastMTime();
this->_real_path=realpath;
this->_pack_path=packdir+fu.FileName()+packsuffix;
this->_url=downloapprefix+fu.FileName();
return true;
}
}BackInfo;
typedef struct DataManager{
std::string _backup_file;
pthread_rwlock_t _rwlock;
//以url为key值,backinfo为值的图
std::unordered_map<std::string ,BackInfo> _table;
DataManager()
{
_backup_file=Config::GetInstance()->GetBackupFile();
pthread_rwlock_init(&_rwlock,NULL);
InitLoad();
}
~DataManager()
{
pthread_rwlock_destroy(&_rwlock);
}
bool Insert(BackInfo &info){
pthread_rwlock_wrlock(&_rwlock);
_table[info._url]=info;
/*for(auto a:_table){
std::cout<<a.first<<std::endl;
}*/
pthread_rwlock_unlock(&_rwlock);
Storage();
return true;
}
bool Updata(BackInfo &info){
pthread_rwlock_wrlock(&_rwlock);
_table[info._url]=info;
pthread_rwlock_unlock(&_rwlock);
Storage();
return true;
}
bool GetOneByUrl(const std::string &url,BackInfo *Info){
pthread_rwlock_rdlock(&_rwlock);
auto it=_table.begin();
it=_table.find(url);
if(it==_table.end()){
pthread_rwlock_unlock(&_rwlock);
return false;
}
pthread_rwlock_unlock(&_rwlock);
*Info=it->second;
return true;
}
bool GetOneByRealPath(std::string &realpath,BackInfo * info){
//直接遍历,通过second的成员来确定
pthread_rwlock_rdlock(&_rwlock);
auto it=_table.begin();
for(;it!=_table.end();++it){
if(it->second._real_path==realpath)
*info=it->second;
}
if(it==_table.end()){
pthread_rwlock_unlock(&_rwlock);
return false;
}
pthread_rwlock_unlock(&_rwlock);
return true;
}
bool GetAll(std::vector<BackInfo> *arry){
pthread_rwlock_rdlock(&_rwlock);
auto it=_table.begin();
for(;it!=_table.end();++it){
arry->push_back(it->second);
}
pthread_rwlock_unlock(&_rwlock);
return true;
}
bool InitLoad(){
//读取文件
FileUtil fu(_backup_file);
if(fu.Exists()==false)
return true;
std::string body;
fu.GetContent(&body);
Json::Value root;
//先反序列化,获取一个Value
JsonUtil::UnSerialize(body,&root);
//再逐个插入
for(int i=0;i<root.size();i++){
BackInfo 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;
}
bool Storage(){
//先获取数据
std::vector<BackInfo> arry;
this->GetAll(&arry);
Json::Value root;
//遍历存放进value root
for(int i=0;i<arry.size();i++){
Json::Value tem;
tem["pack_flag"]=arry[i]._pack_flag;
tem["fsize"]=(Json::Value::Int64)arry[i]._fsize;
tem["atime"]=(Json::Value::Int64)arry[i]._atime;
tem["mtime"]=(Json::Value::Int64)arry[i]._mtime;
tem["pack_path"]=arry[i]._pack_path;
tem["real_path"]=arry[i]._real_path;
tem["url"]=arry[i]._url;
root.append(tem);
}
//将root序列化
std::string body;
JsonUtil::Serialize(root,&body);
//保存至文件完成持久化储存
FileUtil fu(_backup_file);
fu.SetContent(body);
std::cout<<"storage finish"<<std::endl;
return true;
}
}DataManager;
}
#endif
六、热点管理模块
热点管理模块的实现逻辑
这个项目我们会接受到文件,然后将这两个文件放入两个文件夹进行管理,这个过程就是热点文件管理模块:
非热点文件就是上次修改时间距离现在超过了我们在配置文件设置的hot_time,这个hot_time将通过单例类的获取hot_time的方式获取
- 接收文件放入backdir文件夹
- 模块遍历这个目录时发现非热点文件
- 压缩,更新信息,删除源文件
这里的更新信息指的是我们维护的数据信息——数据管理模块描述中
是否被压缩的标识,文件下载时会通过这个标识来辨识这个文件是否需要解压缩。
有同学可能会产生疑问:热点文件管理只负责压缩吗?都不需要解压缩吗?
解压缩会在文件下载的时候进行,那时候也会将压缩标识改为false。
hot.hpp源码
#ifndef __MY__HOT__
#define __MY__HOT__
#include"data.hpp"
#include"util.hpp"
#include<unistd.h>
#include<stdio.h>
extern cloud::DataManager * _data;
namespace cloud{
class HotManager{
private:
std::string _back_dir;//文件储存路径
std::string _pack_dir;//文件压缩路径
std::string _packfile_suffix;
int _hot_time;
//遍历文件夹,对比热点时间,压缩文件,删除源文件
public:
HotManager(){
Config *config=Config::GetInstance();
_hot_time=config->GetHotTime();
_packfile_suffix=config->GetPackFileSuffix();
_back_dir=config->GetBackDir();
_pack_dir=config->GetPackDir();
//不创建目录的话会在遍历那里报错
FileUtil tmp1(_pack_dir);
FileUtil tmp2(_back_dir);
tmp1.CreateDirectory();
tmp2.CreateDirectory();
}
bool HotJudge(std::string filename){
time_t cur=time(NULL);
FileUtil fu(filename);
time_t latime=fu.LastATime();
if(cur-latime>_hot_time)
return false;//非热点文件
return true;
}
bool RunModule(){
while(1){
std::vector<std::string> arry;
FileUtil fu(_back_dir);
fu.ScanDirectory(&arry);
for(auto a:arry){
//遍历文件夹
BackInfo bi;
if(HotJudge(a)==true){
//热点文件
continue;
}
//非热点文件
if(_data->GetOneByRealPath(a,&bi)==false){
//没获取到,非热点文件
bi.NewBackInfo(a);
}
FileUtil fu(a);
//压缩文件
fu.Compress(bi._pack_path);
//删除源文件,更新
bi._pack_flag=true;
_data->Updata(bi);
fu.Remove();
}
usleep(1000);//防止循环太快占用资源
}
}
};
}
#endif
结合代码谈谈实现逻辑:
- 使用这个模块前,构造函数会获取需要的配置文件信息(hot_time,文件储存目录、文件压缩目录)
- 使用这个模块时,需要创建一个线程来调用RunModule函数,这是一个死循环,只有这样才能一直监视backdir目录
- 进入循环,我们每次循环都需要获取一次目录下所有文件名,然后通过hotjudge函数判断是否为非热点文件。是热点文件的话直接continue,遍历下一个文件
- 如果是非热点文件的话就对其压缩、更新信息、删除源文件
七、服务端业务处理模块
我们将通信模块哦与业务处理模块进行了合并,网络通信通过httplib实现。
业务请求处理
- 文件上传请求:备份客户端上传的一个文件,响应上传成功
- 文件列表请求:客户端浏览器请求一个备份文件信息的展示页面,响应页面
- 文件下载请求:通过展示页面,点击下载,响应客户端需要下载的文件数据
框架
namespace cloud{
class Service{
//搭建服务器,进行业务处理
//有三个请求upload、listshow、download
private:
int _server_port;
std::string _server_ip;
std::string _download_prefix;
httplib::Server _server;
static void Upload(const httplib::Request &req,httplib::Response &rsp){};
static void ListShow(const httplib::Request &req,httplib::Response &rsp){};
static void Download(const httplib::Request &req,httplib::Response &rsp){};
public:
Service(){
Config *config=Config::GetInstance();
_download_prefix=config->GetDownloadPrefix();
_server_port=config->GetServerPort();
_server_ip=config->GetServerIp();
//编译器会自己调用Server的默认构造函数
}
void RunModule(){
_server.Post("/upload",Upload);
_server.Get("/showlist",ListShow);
_server.Get("/",ListShow);
std::string download_url=_download_prefix+"(.*)";
_server.Get(download_url,Download);
_server.listen(_server_ip.c_str(),_server_port);//110.41.149 9090
}
};
}
这里的listen在接受到不同的请求后就生成对应的线程去处理。
文件上传请求处理
static void Upload(const httplib::Request &req,httplib::Response &rsp){
if(req.has_file("file")==false)
return ;
//获取req中的文件,再创建一个文件放入储存路径
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();//文件储存在back_path
FileUtil fu(realpath);
fu.SetContent(file.content);//将数据写入文件中
BackInfo bi;
bi.NewBackInfo(realpath);
_data->Insert(bi);
return ;
}
模块的业务逻辑
- 从req中获取文件信息(包括文件内容、文件名)
- 在backdir目录创建一个同名的文件
- 给这个文件赋值
- 载入这个文件的信息以供管理
数据的解包和打包
这里双端用的都是struct MultipartFormData对数据进行储存。这个对象是用hash表储存的,get_file_value就是传入一个key值然后再传出second。
我们客户端上传文件就是通给这个对象赋值进行包装的。
bool Upload(std::string& filename) {
//1.获取文件数据
std::string body;
FileUtil fu(filename);
fu.GetContent(&body);
//2.创建本地客户端
httplib::Client client(SERVER_IP, SERVER_PORT);
httplib::MultipartFormData item;
item.content = body;//文件内容
item.filename = fu.FileName();
item.name = "file";//标识符,和服务端是一一对应的
item.content_type = "application/octet-stream";
httplib::MultipartFormDataItems items;
items.push_back(item);
//3.上传
auto res = client.Post("/upload",items);
if (!res || res->status != 200) {
return false;
}
return true;
}
展示请求处理
html页面
我们需要知道展示页面是什么样子的,这样才能让服务器知道要怎么返回一个页面:
<html>
<head>
<title>Download</title>
</head>
<body>
<h1>Download</h1>
<table>
<tr>
<td><a href="/download/test.txt">test.txt</a></td>
<td align="right"> 2021-12-29 10:10:10 </td>
<td align="right"> 28k </td>
</tr>
</table>
</body>
</html>
tr中间就是服务器需要遍历获取并且写入的数据
一旦接收到了申请查看页面的请求,我们就返回上面的页面。
方法很简单,通过字节流将上面的页面格式写入body就可以了
static void ListShow(const httplib::Request &req,httplib::Response &rsp){
//先获取所有文件数据,然后遍历依次返回数据
std::vector<BackInfo > arry;
_data->GetAll(&arry);
//接下来组织html页面信息
std::stringstream ss;
ss<<"<html><head><title>Download</title></head>";
ss<<"<body><h1>Download</h1><table>";
for(auto &a:arry){
ss<<"<tr>";
std::string filename =FileUtil(a._real_path).FileName();
ss<<"<td><a href='"<<a._url<<"'>"<<filename<<"</a></td>";//写入下载地址
ss<<"<td align='right'>"<<TimeToStr(a._mtime)<<"</td>";
ss<<"<td align='right'>"<<a._fsize/1024<<"k </td>";
ss<<"</tr>";
}
ss<<"</table></body></html>";
rsp.body=ss.str();
rsp.set_header("Content-Type","text/html");
rsp.status=200;
return ;
}
返回时我们需要给req设置的一些东西:
- 有内容返回body肯定是不能落下的
- 设置报头,标识body的类型——这决定了浏览器是以什么方式、什么编码读取这个文件
- status就是状态编码,200表示正常
下载请求处理
Etag
http的ETag字段:储存了资源的唯一标识,文件内容改变这个也会改变
客户端第一次下载文件的时候,会收到这个响应信息。第二次下载会将这个响应信息(通过If-Range)发送给服务器,如果服务器对比后发现这个位移标识没变,就直接使用原先缓存的数据下载。
http协议对ETag的内容并不关心,只需要服务端能自己标识就行了,我们采用的ETag格式为:
“文件名-文件大小-最后一次修改时间”
断点续传功能实现原理
有时候文件下载时意外中断了,再次下载就需要重新下载了,这样无疑是低效率的提现,为了提高效率,我们需要实现断点续传功能。这样在恢复链接时,可以接着上次下载的数据继续下载。
原理:
- 客户端每次下载的时候需要记录自己当前下载的数据量。
- 当异常中断时,下次下载将需要下载的数据区间传给服务器
- 服务器接受到后就仅需要传送给客户端需要的数据即可。
需要考虑的问题:如果在上次下载之后,这个文件在服务器被修改了(通过自定义的ETag和If-Range对比可以判断),我们就需要重新下载文件,如果没修改就继续下载即可。
这部分本来是我想要自己进行编写的功能,但是我们调用了httplib库,httplib库已经解决了这个问题。
代码实现逻辑
- 获取req的If-Range,将其和ETag对比
- 如果是一样,就返回文件数据(httplib实现了断点续传,返回客户端发送来的请求区间)
- 不一致,就重新下载。
源码
static std::string GetETag(BackInfo info){
//filename-fsize-lastmtime
FileUtil fu(info._real_path);
std::string etag;
etag+=fu.FileName();
etag+="-";
etag+=info._fsize;
etag+="-";
etag+=info._mtime;
return etag;
}
static void Download(const httplib::Request &req,httplib::Response &rsp){
//获取路径,然后获取文件信息
BackInfo bi;
_data->GetOneByUrl(req.path,&bi);//该路径其实就是网页申请的下载路径
//查看是否压缩,压缩就解压缩
if(bi._pack_flag==true){
//解压缩后要删除压缩包,更改压缩信息
FileUtil fu(bi._pack_path);
fu.UnCompress(bi._real_path);
fu.Remove();
bi._pack_flag=false;
_data->Updata(bi);
}
//获取etag并将其和客户端传上来的进行对比
std::string etag;
int retrans=false;
if(req.has_header("If-Range")){
etag=req.get_header_value("If-Range");
if(etag==GetETag(bi)){
retrans=true;
}
}
//读取文件内容,写入rsp
FileUtil fu2(bi._real_path);
if(retrans==false){
fu2.GetContent(&rsp.body);
//返回header
rsp.set_header("Accept-Ranges","bytes");
rsp.set_header("ETag",GetETag(bi));
rsp.set_header("Content-Type","application/octet-stream");//说明这是一个字节流,表现在网页上就是点击即可下载
rsp.status=200;
}
else{
//httplib内部实现了我们的取件请求也就是断点续传的处理
//只需要用户将文件读取到rsp.body里去,它内部会根据请求区间,从body中取出指定区间数据进行响应。
//std::string range=req.get_header_value("Range");
//bytes= start-end
fu2.GetContent(&rsp.body);
rsp.set_header("Accept-Ranges","bytes");
rsp.set_header("Content-Type","application/octet-stream");//说明这是一个字节流,表现在网页上就是点击即可下载
rsp.set_header("ETag",GetETag(bi));
rsp.status=206;
}
return ;
}
一些小细节
- Accept-Range:bytes报头表示该服务器支持范围请求
- status=206表示已经实现部分get请求
- Content-Type:application/octet-stream表示这是一个字节流,浏览器处理字节流的默认方式就是下载,所以说展示界面点击文件名浏览器就会自动下载文件。
点击文件名
点击完浏览器就会想服务器发送一个Get请求,服务器返回一个字节流,浏览器对字节流默认下载——行云流水。
server.hpp源码
#ifndef __MY__SERVICE__
#define __MY__SERVICE__
#include"httplib.h"
#include"util.hpp"
#include"config.hpp"
#include<time.h>
extern cloud::DataManager *_data;
namespace cloud{
class Service{
//搭建服务器,进行业务处理
//有三个请求upload、listshow、download
private:
int _server_port;
std::string _server_ip;
std::string _download_prefix;
httplib::Server _server;
static void Upload(const httplib::Request &req,httplib::Response &rsp){
if(req.has_file("file")==false)
return ;
//获取req中的文件,再创建一个文件放入储存路径
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();//文件储存在back_path
FileUtil fu(realpath);
fu.SetContent(file.content);//将数据写入文件中
BackInfo bi;
bi.NewBackInfo(realpath);
_data->Insert(bi);
return ;
}
static std::string TimeToStr(time_t time){
return std::ctime(&time);
}
static void ListShow(const httplib::Request &req,httplib::Response &rsp){
//先获取所有文件数据,然后遍历依次返回数据
std::vector<BackInfo > arry;
_data->GetAll(&arry);
//接下来组织html页面信息
std::stringstream ss;
ss<<"<html><head><title>Download</title></head>";
ss<<"<body><h1>Download</h1><table>";
for(auto &a:arry){
ss<<"<tr>";
std::string filename =FileUtil(a._real_path).FileName();
ss<<"<td><a href='"<<a._url<<"'>"<<filename<<"</a></td>";//写入下载地址
ss<<"<td align='right'>"<<TimeToStr(a._mtime)<<"</td>";
ss<<"<td align='right'>"<<a._fsize/1024<<"k </td>";
ss<<"</tr>";
}
ss<<"</table></body></html>";
rsp.body=ss.str();
rsp.set_header("Content-Type","text/html");
rsp.status=200;
return ;
}
static std::string GetETag(BackInfo info){
//filename-fsize-lastmtime
FileUtil fu(info._real_path);
std::string etag;
etag+=fu.FileName();
etag+="-";
etag+=info._fsize;
etag+="-";
etag+=info._mtime;
return etag;
}
static void Download(const httplib::Request &req,httplib::Response &rsp){
//获取路径,然后获取文件信息
BackInfo bi;
_data->GetOneByUrl(req.path,&bi);//该路径其实就是网页申请的下载路径
//查看是否压缩,压缩就解压缩
if(bi._pack_flag==true){
//解压缩后要删除压缩包,更改压缩信息
FileUtil fu(bi._pack_path);
fu.UnCompress(bi._real_path);
fu.Remove();
bi._pack_flag=false;
_data->Updata(bi);
}
//获取etag并将其和客户端传上来的进行对比
std::string etag;
int retrans=false;
if(req.has_header("If-Range")){
etag=req.get_header_value("If-Range");
if(etag==GetETag(bi)){
retrans=true;
}
}
//读取文件内容,写入rsp
FileUtil fu2(bi._real_path);
if(retrans==false){
fu2.GetContent(&rsp.body);
//返回header
rsp.set_header("Accept-Ranges","bytes");
rsp.set_header("ETag",GetETag(bi));
rsp.set_header("Content-Type","application/octet-stream");//说明这是一个二进制数据流,表现在网页上就是点击即可下载
rsp.status=200;
}
else{
//httplib内部实现了我们的取件请求也就是断点续传的处理
//只需要用户将文件读取到rsp.body里去,它内部会根据请求区间,从body中取出指定区间数据进行响应。
fu2.GetContent(&rsp.body);
rsp.set_header("Accept-Ranges","bytes");
rsp.set_header("Content-Type","application/octet-stream");//说明这是一个二进制数据流,表现在网页上就是点击即可下载
rsp.set_header("ETag",GetETag(bi));
rsp.status=206;
}
return ;
}
public:
Service(){
Config *config=Config::GetInstance();
_download_prefix=config->GetDownloadPrefix();
_server_port=config->GetServerPort();
_server_ip=config->GetServerIp();
//编译器会自己调用Server的默认构造函数
}
void RunModule(){
_server.Post("/upload",Upload);
_server.Get("/showlist",ListShow);
_server.Get("/",ListShow);
std::string download_url=_download_prefix+"(.*)";
_server.Get(download_url,Download);
_server.listen(_server_ip.c_str(),_server_port);//110.41.149 9090
}
};
}
#endif