文章目录
- 前言
- 一、整体项目简介
- 二、服务端环境及功能简介
- 三、 客户端环境及功能简介
- 四、服务端文件管理类的实现
- 1. 获取文件大小,最后一次修改时间,最后一次访问时间,文件名称,以及文件内容的读写等功能
- 2. 判断文件是否存在,创建目录,浏览目录等功能
- 3. 对文件进行压缩和解压缩处理
- 五、服务端Json序列化工具的实现
- 六、 服务端读取配置文件类的实现
- 七、服务端构建备份信息的类的实现
- 八、服务端数据管理类的实现
- 九、服务端热点管理的实现
- 十、服务端网络通信及业务处理实现
- 十一、客户端数据管理
- 十二、客户端网络通信及备份请求
- 总结
前言
项目主要是是将客户端的文件备份到服务端,并且在服务端合理进行压缩处理,节省空间。并且服务端还支持通过浏览器请求页面展示, 来展示服务端备份的文件,同时还支持下载请求,将服务端备份的文件下载到客户端, 若中途服务器停止运行,还支持断点续传
一、整体项目简介
二、服务端环境及功能简介
- 服务端环境 Linux Ubuntu 18.04
- 具体功能包括 文件管理,Json序列化和反序列化, 读取配置文件, 备份信息, 数据管理, 热点管理,网络通信等
- 项目再编写服务端代码是,使用vscode远程链接Linux服务器,方便编码。
三、 客户端环境及功能简介
- 客户端环境 Windows vs2022
- 具体功能包括 文件管理, 数据管理, 客户端网络通信及备份信息等
四、服务端文件管理类的实现
1. 获取文件大小,最后一次修改时间,最后一次访问时间,文件名称,以及文件内容的读写等功能
- 目的, 文件管理类主要是便于后续(数据管理,热点管理等)对文件的操作
-
- 使用C语言的系统调用函数获取文件的大小,最后一次修改时间,最后一次访问时间
-
- 通过string的查找成员函数,找到路径分隔符,截取文件名称
-
- 通过ofstream将字符串设置为文件内容
-
- 通过ifstream获取文件指定位置,指定长度的内容,并复用获取整个文件内容
namespace hhbcloud
{
// 简化命名空间
namespace fs = std::experimental::filesystem;
class FileUtil
{
public:
FileUtil(const std::string& filename)
:_filename(filename)
{
}
bool Remove()
{
if (remove(_filename.c_str()) == 0)
{
return true;
}
return false;
}
// 获取文件大小
size_t FileSize()
{
struct stat st; // 使用stat来获取文件的各种信息
if (stat(_filename.c_str(), &st) < 0) // stat函数成功返回0,失败返回-1
{
std::cout << "get file size error" << std::endl;
return -1;
}
return st.st_size;
}
// 获取文件最后一次修改时间
time_t LastMTime()
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get lastmtime error" << std::endl;
return -1;
}
return st.st_mtime;
}
// 获取文件最后一次访问时间
time_t LastATime()
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get lastatime error" << std::endl;
return -1;
}
return st.st_atime;
}
// 获取文件路径中的文件名称
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 error" << std::endl;
return false;
}
// 打开文件成功后,将body的内容写到文件中
ofs.write(&body[0], body.size());
if (ofs.good() == false) // 判断文件写入过程中是否出错
{
std::cout << "write file error" << std::endl;
ofs.close();
return false;
}
ofs.close();
return true;
}
// 读取文件指定位置 指定长度的内容, body用来接收获取到的数据
bool GetPosLen(std::string* body, size_t pos, size_t len)
{
// 判断 pos + len 是否超出文件大小
size_t size = FileSize();
if (pos + len > size)
{
std::cout << "pos + len more than file size" << std::endl;
return false;
}
// 读的形式打开文件
std::ifstream ifs;
ifs.open(_filename.c_str(), std::ios::binary);//二进制形式打开
if (ifs.is_open() == false) // 打开失败报错
{
std::cout << "read open file error" << std::endl;
return false;
}
// 打开成功后,先为body开辟足够的大小
body->resize(len); // 避免多次安装
// 从文件起始位置定位到pos位置
ifs.seekg(pos, std::ios::beg);
// 读取内容
ifs.read(&(*body)[0], len);
if (ifs.good() == false) // 判断读取文件过程是否出错
{
std::cout << "read file error" << std::endl;
ifs.close();
return false;
}
ifs.close();
return true;
}
// 读取整个文件的内容
bool GetContent(std::string* body)
{
size_t size = FileSize();
return GetPosLen(body, 0, size); // 复用获取文件指定位置指定长度函数
}
private:
std::string _filename;
};
}
测试上述功能
void TestFileUtil(const std::string& filename)
{
hhbcloud::FileUtil fu(filename);
std::cout << fu.FileSize() << std::endl;
std::cout << fu.LastMTime() << std::endl;
std::cout << fu.LastATime() << std::endl;
std::cout << fu.FileName() << std::endl;
std::string str = "hello world!!!--";
fu.SetContent(str);
std::string body;
fu.GetContent(&body);
std::cout << body << std::endl;
hhbcloud::FileUtil nfu("./hello.txt");
nfu.SetContent(body);
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cout << "argv[2]" << std::endl;
return -1;
}
TestFileUtil(argv[1]);
return 0;
}
2. 判断文件是否存在,创建目录,浏览目录等功能
- 此处功能需要C++17的文件系统, C++14也支持
- 需要高版本的gcc才能支持
- 简化命名空间 可以便于使用文件系统
需要包含的头文件, #include<experimental/filesystem>
简化命名空间可以使用: namespace fs = std::experimental::filesystem;
// 判断文件是否存在
bool Exists()
{
return fs::exists(_filename); // c++17文件系统的函数判断文件是否存在
}
// 创建目录
bool CreateDirectory()
{
if (Exists() == true) return true; // 若文件存在则不必创建
return fs::create_directories(_filename); // 创建目录时,传入路径,则创建多级目录
}
// 获取指定目录下的所有文件名,并存放到array数组中
bool ScanDirectory(std::vector<std::string>* array)
{
// 使用 fs::directory_iterator 函数获取指定目录下的所有文件
for (auto& file : fs::directory_iterator(_filename))
{
// 应该判断获取的文件是否是目录,因为目录下可能还有目录
if (fs::is_directory(file) == true) // is_derectory 判断是否是目录
{
continue; // 如果是目录文件则跳过
}
// file并不是string类对象,可以使用path函数实例化,relative_path获取相对路径
// string带路径的string的文件名
array->push_back(fs::path(file).relative_path().string());
}
}
测试上述功能
void TestFileUtil(const std::string& filename)
{
hhbcloud::FileUtil fu(filename);
fu.CreateDirectory();
std::vector<std::string> array;
fu.ScanDirectory(&array);
for (auto& e : array)
{
std::cout << e << std::endl;
}
std::cout << std::endl;
}
3. 对文件进行压缩和解压缩处理
- 下载第三方bundle库, 并将bundle.cpp和bundle.h 移动到当前文件夹下
- 在编译时,bundle.cpp编译过长,所以将bundle.cpp生成静态库
- 将bundle
- 生成.o文件
g++ -c bundle.cpp -o bundle.o
- 依赖bundle.o生成静态库
ar -cr libbundle.a bundle.o
- 创建lib目录,将libbundle.a放置lib目录下,完成后可以删除bundle.cpp和bundle.o,编译时需要如下
g++ -o test Test.cpp -Llib -lstdc++fs -lbundle -pthread
- bundle库内部使用了多线程,所以编译需要注意
// 压缩文件
bool Compress(const std::string& packname)
{
// 先将文件读出来
std::string body;
if (GetContent(&body) == false)
{
std::cout << "compress get file content error" << std::endl;
return false;
}
// 使用pack函数对读出来的内容进行压缩
std::string packed = bundle::pack(bundle::LZIP, body); // packed中存储的即为压缩后的数据
// 将压缩后的文件写入packname文件中
FileUtil fu(packname);
if (fu.SetContent(packed) == false)
{
std::cout << "compress set packname content error" << std::endl;
return false;
}
return true;
}
// 文件解压缩
bool Uncompress(const std::string& unpackname)
{
// 先读取文件内容
std::string body;
if (GetContent(&body) == false)
{
std::cout << "uncompress get file content error" << std::endl;
return false;
}
// 使用unpack解压缩文件
std::string unpacked = bundle::unpack(body); // unpacked存储的是解压缩后的数据
// 将解压后的文件写到解压文件中
FileUtil fu(unpackname);
if (fu.SetContent(unpacked) == false)
{
std::cout << "uncompress set file content error" << std::endl;
return false;
}
return true;
}
测试压缩和解压缩功能
void TestFileUtil(const std::string& filename)
{
hhbcloud::FileUtil fu(filename);
std::string packname = filename + ".lz";
fu.Compress(packname);
std::string unpackname = "hello.txt";
hhbcloud::FileUtil nfu(packname);
nfu.Uncompress(unpackname);
}
五、服务端Json序列化工具的实现
- json工具本质是对序列化和反序列化进行封装,方便后续操作
- json只有两个成员函数(序列化和反序列化),使用时不要实例化类对象
- 所以json的两个成员函数都是静态的。
- 使用jsoncpp之前先安装jsoncpp, 并且编译时,需要加上 -ljsoncpp
class JsonUtil // json工具目的是封装序列化和反序列化的函数
{
public:
// JsonUtil的序列化和反序列化是一种封装比较单一,不需要创建对象调用成员函数,所以将成员函数设置成静态
static bool Serialization(Json::Value& root, std::string* str) // 讲一个结构体序列化为字符串
{
Json::StreamWriterBuilder swb; // 创建一个 json::StreamWriterBuilder 对象
// 使用智能指针管理一个由swb创建的Json::StreamWriter 对象
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss; // 创建stringstream,将root序列化后的字符串写入stringstream
// 使用sw的write函数将root写入stringstream
if (sw->write(root, &ss) != 0)
{
std::cout << "Serialization write error" << std::endl;
return false;
}
*str = ss.str();
return true;
}
// 反序列化就是将一个字符串,反序列化成一个json结构体
static bool Deserialization(const std::string& str, Json::Value* root)
{
Json::CharReaderBuilder crb; // 创建一个Json::CharReaderBuilder
// 使用智能指针管理crb创建的son::CharReader对象
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err; // 用来记录错误信息,cr->parse函数要求必须err参数
if (cr->parse(str.c_str(), str.c_str() + str.size(), root, &err) == false)
{
std::cout << "Deserialization parse error" << std::endl;
return false;
}
return true;
}
};
测试json的功能
void TestJsonUtil()
{
// 序列化
// 构建一个JSON结构体
Json::Value root;
root["姓名"] = "小帅";
root["年龄"] = 18;
float score[3] = { 88.5, 66.7, 99.8 };
for (int i = 0;i < 3; i++)
{
root["分数"][i] = score[i];
}
// 序列化到str中
std::string str;
hhbcloud::JsonUtil::Serialization(root, &str);
std::cout << str << std::endl;
// 返序列化
Json::Value root1;
hhbcloud::JsonUtil::Deserialization(str, &root1);
std::cout << root1["姓名"].asString() << std::endl;
std::cout << root1["年龄"].asInt() << std::endl;
for (int i = 0;i < root1["分数"].size(); i++)
{
std::cout << root1["分数"][i].asFloat() << std::endl;
}
}
int main(int argc, char* argv[])
{
TestJsonUtil();
}
六、 服务端读取配置文件类的实现
- 定义配置文件如下:
- 借助文件管理类,实现读取配置配置文件的类
- 整个项目只有一个配置文件,配置文件类只需要在使用的时候实例化一份对象
- 所以将读取配置文件类设计成单例模式的懒汉模式
namespace hhbcloud
{
class Config // 类的作用是获取配置文件中的数据,成员变量即为配置文件中所有的内容
{
public:
// 因为整个系统同一时刻只需要一个读取配置文件的类,因此把他设计成单例模式,并使用懒汉模式提高局部性能
static Config* GetInstance() // 不需要创建类对象,所以静态成员函数
{
if (_instance == nullptr)
{
_mutex.lock(); // 由于此处加锁会极大的影响效率,所以在外部加一层判断,提高效率
if (_instance == nullptr) // 此处需要加锁,防止多线程出错
{
_instance = new Config();
}
_mutex.unlock();
}
return _instance;
}
// 获取热点管理时间
int GetHotTime()
{
return _hot_time;
}
// 获取服务器监听ip
std::string GetServerIp()
{
return _server_ip;
}
// 获取服务器监听port
int GetServerPort()
{
return _server_port;
}
// 获取下载文件的url前缀
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;
}
private:
Config()
{
ReadConfigFile(); // 在创建对象时,调用读取文件信息的函数
}
bool ReadConfigFile() // 利用文件管理工具和json序列化反序列化工具,将配置文件的内容读到config对象中
{
FileUtil fu(CONFIG_FILE);
std::string body;
fu.GetContent(&body); // 获取整个文件的内容
// 将字符串反序列化为JSON结构体
Json::Value root;
JsonUtil::Deserialization(body, &root);
_hot_time = root["hot_time"].asInt();
_server_ip = root["server_ip"].asString();
_server_port = root["server_port"].asInt();
_download_prefix = root["download_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();
}
private:
int _hot_time; // 热点判断时间
std::string _server_ip; // 服务器监听ip
int _server_port; // 服务器监听端口
std::string _download_prefix; // 下载文件的url前缀
std::string _packfile_suffix; // 压缩文件的后缀
std::string _pack_dir; // 压缩文件的存储路径
std::string _back_dir; // 备份文件的存储路径
std::string _backup_file; // 备份文件信息的存储文件
// 外部使用config时,使用GetInstance函数,因为其是静态,无法访问非静态成员变量,所以将_instance设置为静态
static Config* _instance; // 懒汉模式,默认先创建一个指针,需要的时候在实例化对象
static std::mutex _mutex; // 懒汉模式在实例化对象时,需要加锁
};
Config* Config::_instance = nullptr; // 静态成员变量默认在类外定义
std::mutex Config::_mutex;
}
测试读取配置文件的类
void TestConfig()
{
hhbcloud::Config* config = hhbcloud::Config::GetInstance();
std::cout << config->GetHotTime() << std::endl;
std::cout << config->GetServerIp() << std::endl;
std::cout << config->GetServerPort() << 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;
}
int main(int argc, char* argv[])
{
TestConfig();
return 0;
}
七、服务端构建备份信息的类的实现
struct Backinfo // 定义并获取文件存在在服务器上的备份信息
{
bool NewBackinfo(const std::string& realpath)
{
FileUtil fu(realpath);
if (fu.Exists() == false) // 文件不存在则报错返回
{
std::cout << "newBackinfo file is not exists" << std::endl;
return false;
}
_pack_flag = false;
_fsize = fu.FileSize();
_atime = fu.LastATime();
_mtime = fu.LastMTime();
_real_path = realpath;
// config获取配置文件中定义的单例类
Config* config = Config::GetInstance();
// 压缩路径应该是 配置文件中的压缩目录 + 文件名称 + 文件压缩后缀
_pack_path = config->GetPackDir() + fu.FileName() + config->GetPackfileSuffix();
// url下载路径应该是 配置文件中下载前缀 + 文件名称
std::string downloadPrefix = config->GetDownloadPrefix();
std::string urlpath = downloadPrefix + fu.FileName();
_url_path = urlpath;
return true;
}
bool _pack_flag; // 文件是否被压缩标志
size_t _fsize; // 文件的大小
time_t _atime; // 文件最后一次访问时间
time_t _mtime; // 文件最后一次修改时间
std::string _real_path; // 文件实际存储路径名称
std::string _pack_path; // 亚搜包存储路径名称
std::string _url_path; // url下载文件的路径
};
测试备份信息功能
void TestDataManager(const std::string& realpath)
{
hhbcloud::Backinfo info;
if (info.NewBackinfo(realpath) == false)
{
return;
}
std::cout << info._pack_flag << std::endl;
std::cout << info._atime << std::endl;
std::cout << info._mtime << std::endl;
std::cout << info._fsize << std::endl;
std::cout << info._pack_path << std::endl;
std::cout << info._real_path << std::endl;
std::cout << info._url_path << std::endl;
}
八、服务端数据管理类的实现
- 数据管理类是将组织好的文件的备份信息统一存放在备份信息文件中,并且可以从文件中读取备份信息,进行增、改、 查等功能, 并可以重新写回文件的类。
class DataManager
{
public:
DataManager()
{
// _backup_file 持久化存储文件,就是配置文件中的文件备份信息的文件
// 对数据的管理本质就是对服务器备份的文件信息的管理,在进行操作时,需要将当前所有文件信息加载到内存中
// 这里用哈希表来存储,提高效率
_backup_file = Config::GetInstance()->GetBackupFile(); // 通过config单例类获取
// 初始化读写锁
pthread_rwlock_init(&_rwlock, nullptr);
// 创建对象的同时,初始化加载所有备份信息数据
InitLoad();
}
// 初始化加载 每次创建类都需要先初始化加载所有的数据
bool InitLoad()
{
FileUtil fu(_backup_file);
if (fu.Exists() == false)
{
std::cout << "DataManager InitLoad file is not exists" << std::endl;
return false;
}
// 获取备份信息文件中的所有备份信息
std::string body;
fu.GetContent(&body);
// 反序列化
Json::Value root;
JsonUtil::Deserialization(body, &root);
// 通过反序列化得到的json结构体,构建备份信息,插入到哈希表中
for (int i = 0;i < root.size(); i++)
{
Backinfo backinfo;
backinfo._pack_flag = root[i]["pack_flag"].asBool();
backinfo._fsize = root[i]["fsize"].asInt();
backinfo._atime = root[i]["atime"].asInt();
backinfo._mtime = root[i]["mtime"].asInt();
backinfo._real_path = root[i]["real_path"].asString();
backinfo._pack_path = root[i]["pack_path"].asString();
backinfo._url_path = root[i]["url_path"].asString();
// 获取的一个文件备份信息插入到哈希表中
Insert(backinfo);
}
return true;
}
// 新增数据
bool Insert(const Backinfo& backinfo)
{
// 读写锁,在写入时,将写进行加锁,读不加锁,一次只能有一个线程在执行写操作
pthread_rwlock_wrlock(&_rwlock);
// 所有数据都已经加载到哈希表中了,所以此时只需要操作哈希表即可
// 在哈希表中我们以备份文件信息中的url路径和文件备份信息构成键值对
_table[backinfo._url_path] = backinfo;
pthread_rwlock_unlock(&_rwlock);
// 每插入一个数据都要持久化存储
Storage();
return true;
}
// 更新数据
bool Update(const Backinfo& backinfo)
{
pthread_rwlock_wrlock(&_rwlock);
_table[backinfo._url_path] = backinfo; // key值相同,会默认覆盖之前的值
pthread_rwlock_unlock(&_rwlock);
// 每更新一个数据都要持久化存储
Storage();
return true;
}
// 查询数据 通过url获取单个文件的备份信息
// 当是下载请求时,我们需要获取单个文件的信息,但是如果是页面展示请求,则需要获取多个文件的信息
bool GetOneByUrl(const std::string& url, Backinfo* backinfo)
{
pthread_rwlock_wrlock(&_rwlock);
auto it = _table.find(url);
if (it == _table.end())
{
std::cout << "GetOneByUrl is not find" << std::endl;
pthread_rwlock_unlock(&_rwlock);
return false;
}
pthread_rwlock_unlock(&_rwlock);
*backinfo = it->second;
return true;
}
// 通过文件真是路径来获取文件备份信息 通过真实路径查询在备份信息文件中是否有当前文件,判断文件是否被备份
bool GetOneByRealpath(const std::string& realpath, Backinfo* backinfo)
{
pthread_rwlock_wrlock(&_rwlock);
auto it = _table.begin();
for (; it != _table.end(); ++it)
{
if (it->second._real_path == realpath)
{
*backinfo = it->second;
pthread_rwlock_unlock(&_rwlock);
return true;
}
}
pthread_rwlock_unlock(&_rwlock);
return false;
}
// 获取所有备份文件的信息 用于展示请求
bool GetAll(std::vector<Backinfo>* array)
{
pthread_rwlock_wrlock(&_rwlock);
auto it = _table.begin();
for (;it != _table.end(); ++it)
{
array->push_back(it->second);
}
pthread_rwlock_unlock(&_rwlock);
return true;
}
// 持久化存储,也就是将已经操作的(插入或修改)加载到内存中的哈希表中的数据写入到备份信息的文件中
bool Storage()
{
// 创建一个备份文件信息的数据
std::vector<Backinfo> array;
// 获取所有的备份信息到数组中
GetAll(&array);
// 先将所有的备份信息,存储到json数组中,然后通过序列化, 最后统一写入文件
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_path"] = array[i]._url_path;
root.append(item); // 构建好备份文件信息后,写入json数组中
}
// 序列化
std::string body;
JsonUtil::Serialization(root, &body);
// 此时root被序列化为一个字符串,放入body中
FileUtil fu(_backup_file);
if (fu.SetContent(body) == false) // 若文件不存在fu.setContent中的ofstream流在打开文件时,会自动创建
{
std::cout << "DataManager storage set file content error" << std::endl;
return false;
}
return true;
}
~DataManager()
{
// 析构时,销毁读写锁
pthread_rwlock_destroy(&_rwlock);
}
private:
std::string _backup_file; // 持久化存储的文件,也就是配置文件中 备份文件信息的文件
pthread_rwlock_t _rwlock; // 读写锁, -- 读共享,写互斥
std::unordered_map<std::string, Backinfo> _table; // 一个哈希表,用来高效管理数据
};
测试数据管理功能
void TestDataManager(const std::string& realpath)
{
hhbcloud::Backinfo info;
if (info.NewBackinfo(realpath) == false)
{
return;
}
std::cout << info._pack_flag << std::endl;
std::cout << info._atime << std::endl;
std::cout << info._mtime << std::endl;
std::cout << info._fsize << std::endl;
std::cout << info._pack_path << std::endl;
std::cout << info._real_path << std::endl;
std::cout << info._url_path << std::endl;
hhbcloud::DataManager DM;
DM.Insert(info);
hhbcloud::Backinfo backinfo;
DM.GetOneByUrl(info._url_path, &backinfo);
std::cout << "================Insert && GetOneByUrl=====================" << std::endl;
std::cout << backinfo._pack_flag << std::endl;
std::cout << backinfo._atime << std::endl;
std::cout << backinfo._mtime << std::endl;
std::cout << backinfo._fsize << std::endl;
std::cout << backinfo._pack_path << std::endl;
std::cout << backinfo._real_path << std::endl;
std::cout << backinfo._url_path << std::endl;
info._pack_flag = true;
DM.Update(info);
std::vector<hhbcloud::Backinfo> array;
DM.GetAll(&array);
std::cout << "===================Update && GetAll=====================" << std::endl;
for (auto& e : array)
{
std::cout << e._pack_flag << std::endl;
std::cout << e._atime << std::endl;
std::cout << e._mtime << std::endl;
std::cout << e._fsize << std::endl;
std::cout << e._pack_path << std::endl;
std::cout << e._real_path << std::endl;
std::cout << e._url_path << std::endl;
}
hhbcloud::Backinfo tmp;
DM.GetOneByRealpath(realpath, &tmp);
std::cout << "=================GetOneByRealpath=======================" << std::endl;
std::cout << tmp._pack_flag << std::endl;
std::cout << tmp._atime << std::endl;
std::cout << tmp._mtime << std::endl;
std::cout << tmp._fsize << std::endl;
std::cout << tmp._pack_path << std::endl;
std::cout << tmp._real_path << std::endl;
std::cout << tmp._url_path << std::endl;
}
九、服务端热点管理的实现
- 持续检测备份目录下的文件,若满足条件则进行压缩处理。压缩后的文件放入压缩目录下,删除备份目录中的文件
extern hhbcloud::DataManager* data; // 创建一个全局的数据管理类,便于对数据的操作
namespace hhbcloud
{
class HotManager // 热点管理类
{
public:
HotManager()
{
// 备份文件,压缩文件,最后一次访问时间,压缩文件前缀均可从配置文件中读取
Config* config = Config::GetInstance();
_back_dir = config->GetBackDir();
_pack_dir = config->GetPackDir();
_hot_time = config->GetHotTime();
_packfile_suffix = config->GetPackfileSuffix();
// 如果压缩文件目录和备份文件目录不存在,则创建目录
FileUtil fu1(_back_dir);
fu1.CreateDirectory(); // 函数内部对文件存在做了判断,若文件已经存在,则直接返回
FileUtil fu2(_pack_dir);
fu2.CreateDirectory();
}
// 运行模块
bool RunMoudle()
{
// 热点管理需要不断循环检测备份文件目录下的文件因此是一个死循环
while (1)
{
// 首先浏览指定目录下的所有文件
FileUtil fu(_back_dir);
std::vector<std::string> array;
fu.ScanDirectory(&array);
for (auto& e : array) // 循环每个文件,判断是否是热点文件,是否需要压缩
{
if (HotJundge(e) == false)
{
// 热点文件 直接跳过,不用管
continue;
}
Backinfo info;
// 此时为非热点文件
//从备份信文件中无法找到,文件存在,但是没有备份
if (data->GetOneByRealpath(e, &info) == false)
{
info.NewBackinfo(e);// 给文件创建一个备份信息
}
// 此时info中的已经有了备份信息(在备份信息文件中找到的, 或者对遗漏的文件创建的备份信息)
FileUtil tmp(e);
tmp.Compress(info._pack_path); // 对文件进行压缩
// 删除原来的备份文件
tmp.Remove();
info._pack_flag = true;
data->Update(info);
}
usleep(1000); // 为了避免空目录持续循环,造成cpu资源消耗过高,间隔1000ms也就是0.1s
}
return true;
}
private:
bool HotJundge(const std::string& filename)
{
FileUtil fu(filename);
time_t last_atime = fu.LastATime();
time_t cur_time = time(nullptr);
if ((cur_time - last_atime) > _hot_time)
{
// 最后一次访问时间 - 当前时间 如果大于 热点判断时间,说明为非热点文件,需要压缩
return true;
}
return false;
}
private:
std::string _back_dir; // 备份文件,热点管理类需要从备份文件中获取所有文件判断热点,所以需要备份文件
std::string _pack_dir; // 压缩文件,热点管理类对非热点文件进行压缩处理,并存放在压缩目录下
std::string _packfile_suffix; // 压缩文件的后缀
int _hot_time; // 热点判断时间
};
}
测试热点管理功能
hhbcloud::DataManager* data;
void TestHotManager()
{
data = new hhbcloud::DataManager;
hhbcloud::HotManager ht;
ht.RunMoudle();
}
十、服务端网络通信及业务处理实现
- 下载httplib第三方库,并将httplib.h复制到当前目录下
- 使用httplib第三方库搭建服务器,并处理上传,页面展示,和下载业务
extern hhbcloud::DataManager* data;
namespace hhbcloud
{
class Service
{
public:
Service()
{
Config* config = Config::GetInstance();
// 从配置文件中读取服务器端口,服务器ip,下载前缀
_server_port = config->GetServerPort();
_server_ip = config->GetServerIp();
_download_prefix = config->GetDownloadPrefix();
// 若文件此时没有指定备份目录,则需要创建文件目录
std::string back_dir = config->GetBackDir();
FileUtil fu(back_dir);
fu.CreateDirectory(); // createDirectory函数内部对文件存在做了判断,如存在则不创建
}
bool RunMoudle() // 运行模块
{
// post请求上传文件, 并且业务处理函数Upload的参数必须是Request 和 Response,所以必须要静态,否则会有隐含的this指针参数
_server.Post("/upload", Upload);
_server.Get("/listshow", ListShow);
_server.Get("/", ListShow);
std::string download = _download_prefix + "(.*)"; // .*正则表达式表示匹配任意字符,任意次
_server.Get(download, DownLoad);
// listen启动服务器,并指定监听的主机和端口
_server.listen(_server_ip.c_str(), _server_port);
return true;
}
private:
static void Upload(const httplib::Request& req, httplib::Response& resp)
{
auto ret = req.has_file("file"); // 判断客户端上传有没有这个字段
if (ret == false)
{
// 没有这个字段,服务器无法处理,设置状态码为400,并返回
resp.status = 400;
return;
}
std::cout << "has_file && upload" << std::endl;
// 有字段,获取文件内容
auto file = req.get_file_value("file");
// 在备份文件目录下创建文件,并写入数据
std::string back_dir = Config::GetInstance()->GetBackDir();
// 获取文件的实际存储路径 -- /backdir/filename
std::string realpath = back_dir + FileUtil(file.filename).FileName();
FileUtil fu(realpath);
// 向文件中写入数据 写入数据函数中ofstream在打开文件时,若文件不存在,会自动创建文件, 若没有指定目录,则需要自己创建
fu.SetContent(file.content);
// 组织备份信息
Backinfo backinfo;
backinfo.NewBackinfo(realpath);
data->Insert(backinfo); // 将数据写入备份信息文件中
return;
}
static std::string TimetoStr(time_t time)
{
return ctime(&time);
}
static void ListShow(const httplib::Request& req, httplib::Response& resp)
{
// 获取备份信息文件中的所有数据
std::vector<Backinfo> array;
data->GetAll(&array);
// 使用备份信息组织html页面
std::stringstream ss;
ss << "<html><meta charset = 'utf-8'><head><title>cloudBackup</title></head>";
ss << "<body><h1>Download</h1><table>";
for (auto& e : array)
{
// 构建每一行的数据 文件名称 文件最近访问时间 文件大小(k)
ss << "<tr>";
std::string filename = FileUtil(e._real_path).FileName();
ss << "<td>" << "<a href = '" << e._url_path << "'>" << filename << "</a></td>";
ss << "<td>" << TimetoStr(e._atime) << "</td>";
ss << "<td>" << e._fsize / 1024 << " K" << "</td>";
ss << "</tr>";
}
ss << "</table></body></html>";
resp.body = ss.str();
resp.status = 200;
resp.set_header("Content-Type", "text/html");
return;
}
// 由文件名,文件大小, 文件最后一次修改构成的ETag信息
static std::string GetETag(const Backinfo& backinfo)
{
FileUtil fu(backinfo._real_path);
std::string ETag = fu.FileName();
ETag += "-";
ETag += fu.FileSize();
ETag += "-";
ETag += fu.LastMTime();
return ETag;
}
static void DownLoad(const httplib::Request& req, httplib::Response& resp)
{
// 通过url路径获取文件的备份信息
// req.path就是文件的url路径
Backinfo backinfo;
data->GetOneByUrl(req.path, &backinfo);
// 如果文件压缩标志是true,则需要先解压缩
if (backinfo._pack_flag == true)
{
FileUtil fu(backinfo._pack_path);
fu.Uncompress(backinfo._real_path); // 解压到真实路径
fu.Remove(); // 删除压缩包
// 更改文件压缩标志,并更改文件备份信息
backinfo._pack_flag = false;
data->Update(backinfo);
}
// 将解压后的文件的内容写到resp.body中
FileUtil fu(backinfo._real_path);
bool retrans = false; // 标记是否是断电续传
std::string old_etag; // 下载请求时,已有的Etag值
// 判断是否有if-Range, 若有则是断点续传,若没有则不是
if (req.has_header("if-Range"))
{
old_etag = req.get_header_value("if-Range");
if (old_etag == GetETag(backinfo))// 有if-Range,但是与当前的Etag不一致,则也需要正常下载
{
retrans = true;
}
}
if (retrans == false)
{
// 如果retrans == false ,需要断点续传,否则不需要
fu.GetContent(&resp.body); // 读取文件内容,写入resp.body
// 设置响应报头字段
resp.set_header("Accept-Ranges", "bytes"); // 允许断点续传
resp.set_header("ETag", GetETag(backinfo));
// Content-Type 决定了浏览器如何处理数据,设置文件二进制流,常用于文件下载
resp.set_header("Content-Type", "application/octet-stream");
resp.status = 200;
return;
}
else
{
// 此时已经是要断点续传了,按照Range字段中指定的区间,取出指定区间的数据进行下载
// httplib库已经内置了,这里只需要取出数据即可
fu.GetContent(&resp.body); // 读取文件内容,写入resp.body
// 设置响应报头字段
resp.set_header("Accept-Ranges", "bytes"); // 允许断点续传
resp.set_header("ETag", GetETag(backinfo));
// Content-Type 决定了浏览器如何处理数据,设置文件二进制流,常用于文件下载
resp.set_header("Content-Type", "application/octet-stream");
resp.status = 206;
return;
}
}
private:
int _server_port; // 服务器开放端口号
std::string _server_ip; // 服务器ip
std::string _download_prefix; // 下载前缀
httplib::Server _server; // 实例化一个服务器Server对象 ,自动调用构造函数初始化
};
}
测试网络通信及业务处理功能
html页面代码:
<html>
<body>
<form action = "http://1.14.97.154:8080/upload" method = "post" enctype = "multipart/form-data">
<div>
<input type = "file" name = "file">
</div>
<div><input type = "submit" value = "上传"> </div>
</form>
</body>
</html>
void TestServer()
{
data = new hhbcloud::DataManager;
hhbcloud::Service svr;
svr.RunMoudle();
}
服务端进行多线程同时处理热点管理模块和网络通信及业务处理模块
十一、客户端数据管理
#ifndef __MY_DATAMANAGER__
#define __MY_DATAMANAGER__
#include "Util.hpp"
#include <unordered_map>
namespace hhbcloud
{
class DataManager
{
public:
DataManager(const std::string& backup_file)
:_backup_file(backup_file)
{
InitLoad();
}
int Split(const std::string& body, const std::string& sep, std::vector<std::string>* array)
{
int count = 0;
int start = 0;
while (1)
{
size_t pos = body.find(sep, start);
if (pos == std::string::npos)
{
break;
}
if (start == pos)
{
start = start + sep.size();
continue;
}
array->push_back(body.substr(start, (pos - start) + 1));
count++;
start = pos + sep.size();
}
if (start < body.size())
{
array->push_back(body.substr(start));
count++;
}
return count;
}
bool InitLoad()
{
FileUtil fu(_backup_file);
if (fu.Exists() == false)
{
std::cout << "client Data manager file is not exists" << std::endl;
return false;
}
std::string body;
fu.GetContent(&body);
std::vector<std::string> array;
Split(body, "\n", &array);
for (auto& e : array)
{
std::vector<std::string> tmp;
Split(e, " ", &tmp);
if (tmp.size() != 2)
{
continue;
}
_table[tmp[0]] = tmp[1];
}
return true;
}
bool Storage()
{
std::stringstream ss;
auto it = _table.begin();
for (; it != _table.end(); ++it)
{
ss << it->first << " " << it->second << "\n";
}
FileUtil fu(_backup_file);
fu.SetContent(ss.str());
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;
}
private:
std::string _backup_file;
std::unordered_map<std::string, std::string> _table;
};
}
#endif
十二、客户端网络通信及备份请求
#define _CRT_SECURE_NO_WARNINGSf
#ifndef __MY_BACKUP__
#define __MY_BACKUP__
#include "Util.hpp"
#include "DataManager.hpp"
#include "httplib.h"
#include <Windows.h>
#define SERVER_ADDR "1.14.97.154"
#define SERVER_PORT 8080
namespace hhbcloud
{
class Backup
{
public:
Backup(const std::string& back_dir, const std::string& backup_file)
:_back_dir(back_dir)
{
FileUtil fu(_back_dir);
if (fu.Exists() == false)
{
fu.CreateDirectorys();
}
_data = new DataManager(backup_file); // 初始化数据管理类对象
}
// 运行模块 持续的,并进行上传
bool RunMoudle()
{
while (1)
{
// 浏览备份目录下的所有文件,并将数据存放在数组中
std::vector<std::string> array;
FileUtil fu(_back_dir);
fu.ScanDirectory(&array);
// 获取的文件,
for (auto& e : array)
{
// 判断文件是否需要上传
if (IsNeedUpload(e) == false)
{
// 不需要上传,则跳过
continue;
}
// 判断文件是否上传成功
if (Upload(e) == true)
{
// 文件若上传成功,在备份信息文件中插入数据
// 创建文件唯一标识(文件名 - 文件大小 - 文件最后一次修改时间),
std::string id = GetFileIdentify(e);
_data->Insert(e, id); //并将数据插入文件备份信息文件中
}
}
Sleep(1); // 间隔1ms检测一次,防止cpu资源消耗过多
}
}
private:
// 创建文件唯一标识
std::string GetFileIdentify(const std::string& filename)
{
std::stringstream ss;
FileUtil fu(filename);
ss << fu.FileName() << "-" << fu.FileSize() << "-" << fu.LastMTime();
return ss.str();
}
bool IsNeedUpload(const std::string filename)
{
std::string id;
if (_data->GetOneByKey(filename, &id) != false)
{
std::string new_id = GetFileIdentify(filename);
if (id == new_id)
{
return false;
}
}
FileUtil fu(filename);
if (time(nullptr) - fu.LastMTime() < 3)
{
return false;
}
return true;
}
bool Upload(const std::string& filename)
{
FileUtil fu(filename);
std::string body;
fu.GetContent(&body);
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 ret = client.Post("/upload", items);
if (ret->status != 200 || ret == nullptr)
{
return false;
}
return true;
}
private:
std::string _back_dir;
DataManager* _data;
};
}
#endif
- 项目改进
服务端对文件压缩处理时,可以使用多线程,提高效率。
总结
云备份实战项目主要是基于httplib, bundle, jsoncpp等第三方库实现客户端对文件的备份等功能