热点管理模块
热点管理模块的设计
首先我们要理解热点管理模块的作用是什么
为了节省磁盘空间
对于服务器上备份的文件 如果长时间没有被访问 则认为是非热点文件 我们就将其压缩 以节省磁盘空间
实现思路:
- 我们遍历所有文件 使用文件的最后一次访问时间减去当前时间 我们就能得到一个差值 如果这个差值大于等于我们设定好的一个阈值 则对其进行压缩 存放到压缩路径中并且删除源文件
其中 我们遍历所有文件有两种方式
- 从数据管理模块中遍历所有的文件备份信息
- 遍历备份文件夹 获取所有的文件信息
我们选择第二种方式来遍历 这是因为如果使用第一种方式有可能有文件上传成功但是并没有加载到数据管理模块中 有可能会导致数据的缺失
并且 我们文件的最后一次访问时间不能以数据管理模块中的最后一次访问时间进行判断 因为这个时间如果我们没有更新 它就是长时间保持不变的
热点管理流程
- 获取备份目录下的所有文件
- 逐个判断文件是否为非热点文件
- 非热点文件压缩处理
- 删除源文件 修改备份信息
设计热点管理类
私有成员有
- 备份文件路径
- 压缩文件路径
- 压缩包后缀名
- 热点时间
成员函数有
- 构造函数
- 运行模块 (实现热点管理流程)
热点管理模块的实现
extern函数的作用
当我们使用extern关键字修饰一个变量的时候 它的意思是告诉编译器 虽然你现在没有找到关于这个变量的定义 但是这个变量在其他文件中是一个全局变量 你现在直接放行即可(不报错)
remove函数的作用
在Linux中 我们可以使用remove函数来删除一个文件
例如 remove(filename)
如果成功会返回0 如果失败会返回-1 同时错误码将被设置
整体代码如下
extern cloud::DataManager* _data; namespace cloud { 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) { // hot return false; // non hot return true; FileUtil fu(filename); time_t last_atime = fu.LastAccessTime(); time_t cur_time = time(nullptr); if (cur_time - last_atime > _hot_time) { return true; } return false; } public: HotManager() { Config* config = Config::GetInstance(); _pack_dir = config->GetBackDir(); _back_dir = config->GetPackDir(); _pack_suffix = config->GetPackfileSuffix(); _hot_time = config->GetHotTime(); } bool RunModule() { while (1) { // 1. traverse the backup directory to get all file names FileUtil fu(_back_dir); std::vector<std::string> arry; fu.ScanDirectory(arry); // 2. judge if the file is a non hotspot file for (auto a : arry) { if (HotJudge(a) == false) { continue; } BackupInfo bi; if(_data -> GetOneByRealPath(a , &bi) == false) { // we have a file but not recorded bi.NewBackupInfo(a); // set a new BackupInfo } // 3. compress the non hotspot file FileUtil tmp(a); tmp.Compress(bi.pack_path); // 4. delete source file and update the infomation tmp.Remove(); bi.pack_flag = true; _data->Update(bi); } usleep(1000); } } }; }
热点管理模块的测试
我们可以试验将一个文件放到backdir中 当一段时间(hot_time)后
backdir中的文件就会自动压缩并且存放到packdir中
服务端处理模块
服务端处理模块思路
我们想要处理服务端发送过来的请求 首先就要和服务端建立连接 (这里我们通过httplib库完成)
步骤如下
- 搭建网络通信服务器 通过httplib完成
- 业务处理请求
其中 业务处理请求有如下三种
- 文件上传请求: 备份客户端上传的文件 响应上传成功
- 文件列表请求: 客户端浏览器请求一个备份文件的展示页面 响应页面
- 文件下载请求: 通过展示页面 点击下载 响应客户端要下载的文件数据
网络通信接口设计
其实设计网络通信接口就是约定好 客户端发送什么样的请求 我们就给予什么样的响应
上传请求
当客户端使用post方法的/update请求的时候 我们认为这是一个文件上传请求
服务器于是对于该请求进行解析 获取文件名和文件内容 并且在服务器上对于该内容进行保存
而响应的话我们只需要返回一个成功就可以 比如说
HTTP/1.1 200 OK
展示页面
如果客户端使用GET方法请求 /listshow
则我们此时返回给客户端一个html页面
html页面的内容就是我们所有的文件信息
文件下载
如果客户端使用GET方法请求 /download/xxx
则我们此时通过正文给客户端响应该文件的所有信息
业务处理类设计
业务处理类的设计实际上就是服务类的设计 因为服务类需要和客户端保持通信 所以说其私有成员如下
- 端口号
- IP地址
- 下载前缀
- httplib库中的sever对象
成员函数如下
- 构造函数
- 运行模块
- 三个业务处理函数
代码表示如下
Service() { Config* config = Config::GetInstance(); _server_port = config->GetSetverPort(); _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(_server_ip , _server_port); return true; }
上传/展示/下载函数编写
上传函数编写
static void Upload(const httplib::Request &req, httplib::Response &rsp) { // post /upload auto ret = req.has_file("file"); // judge it if there is a file update area if (ret == false) { rsp.status = 400; return; } const auto& file = req.get_file_value("file"); std::string back_dir = Config::GetInstance()->GetBackDir(); std::string realpath = back_dir + FileUtil(file.filename).FileName(); FileUtil fu(realpath); std::string content = file.content; fu.SetContent(content); // write data to file BackupInfo info; info.NewBackupInfo(realpath); _data->Insert(info); return ; }
展示函数编写
对于展示函数来说 我们首先要获取所有备份文件信息 其次组织下html文件数据即可
时间转化函数
string ctime(time_t* t)
它要求我们传入一个时间戳 我们会返回一个字符串的时间数据
代码表示如下
static void ListShow(const httplib::Request &req , httplib::Response &rsp) { // 1. get all file backup info std::vector<BackupInfo> arry; _data->GetAll(&arry); // 2. create html pages based on file data 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; }
运行结果如下
下载函数编写
HTTP的ETag字段
这个字段中存储了一个资源的唯一标识
客户端第一次下载文件的时候会收到这个响应信息
第二次下载的时候就会将这个信息发送给服务器 想要让服务器根据这个唯一标识判断这个资源有没有被修改过 如果没有被修改过直接使用原先缓存的数据 不用重新下载
而HTTP协议本身对于etag中是什么数据并不关心 只要你服务端能够自己标识就行
因此我们的etag就使用 “文件名 - 文件大小 - 最后一次修改时间” 组成
HTTP协议的Accept-Ranges字段
用于告诉客户端服务器支持断点续传 并且以字节作为单位
代码表示如下
static void Download(const httplib::Request &req, httplib::Response &rsp) { // 1. get client Request path // 2. get backup info BackupInfo info; _data->GetOneByURL(req.path , &info); // 3. uncompress if compressed if (info.pack_flag == true) { FileUtil fu(info.pack_path); fu.uncompress(info.real_path); // 4. delete packup info fu.Remove(); info.pack_flag = false; _data->Update(info); } // 5. read file info FileUtil fu(info.real_path); fu.GetContent(rsp.body); // 6. set ETag accept-ranges rsp.set_header("Accept-Ranges" , "bytes"); rsp.set_header("ETag" , GetETag(info)); rsp.status = 200; }
如果我们在刚刚的 html 页面中点击超链接 我们就会发现什么都不会发生
这是因为我们没有设置响应报头中 Content-Type 字段是什么
实际上 Content-Type 字段是一个非常重要的属性 它告诉了接收/响应方 正文是什么格式的 是html呢?还是一个文本呢?还是一个下载连接呢?
在我们加上 Content-Type
字段 application/octet-stream
之后 我们就可以告诉浏览器 这是一个二进制流文件(通常标识为一个可下载文件)
之后点击我们的文件名就可以下载了
断点续传
为什么要实现断点续传
当我们在网络传输的过程中 如果因为某种异常而下载中断而需要从头开始传输的话效率较低 因为这相当于把已经传输的文件再次传输一遍
所以说 我们要实现断点续传功能来避免这种情况
如果出现异常中断 我们只需要从中断的位置继续传输即可
实现思想和需要考虑的问题
客户端在下载文件的时候 每次接收到数据写入文件的时候记录下自己当前下载的数据量
当异常下载中断的时候 下次断点续传的时候 只需要将自己需要数据的区间告诉服务器
之后服务器回传客户端需要的数据即可
需要注意的问题:
如果上次下载文件之后 文件在服务器上被修改了 那么此时我们不应该进行断点续传 而应该重新进行文件的下载操作
http协议中 断点续传的实现
实现断点续传最重要的两个点
- 告诉服务器需要下载的范围
- 检测上一次下载后该文件有没有被修改
HTTP协议实现
http协议服务器中有两个字段 分别是
- etag
- accept-ranges
第一个字段是唯一标识一份资源 第二个字段是告诉客户端自己是否支持断点续传
http协议客户端中有两个字段 分别是
- ifrange
- range 10~1000
ifrange表明了 服务器是否支持断点续传
而range字段表明了 需要从服务器下载的 10到1000个字节
206状态码
206状态码表示请求成功 返回一部分内容
断点续传实现
其实我们的httplib库已经实现了断点续传功能 就算我们不改上面的代码 服务器也是支持这个功能的
但是我们还是要理解断点续传是怎么实现的
结构代码大体如下
old_etag = req.get_header_value("If-Range"); if (old_etag == GetETag(info)) { retrans = true; } } if (retrans == false) { // 6. set ETag accept-ranges rsp.set_header("Accept-Ranges" , "bytes"); rsp.set_header("Content-Type" , "application/octet-stream"); rsp.set_header("ETag" , GetETag(info)); rsp.status = 200; } else { // httplib has achieced resume fu.GetContent(rsp.body); rsp.set_header("Accept-Ranges" , "bytes"); rsp.set_header("ETag" , GetETag(info)); rsp.status = 206; // range resquest responce status code 206 }
我们会拿出客户端的ETag 和 文件当前的ETag进行判断
如果不同 则不进行断点续传 进行正常的下载操作
如果不同 则我们进行断点续传
我们要用客户端的request请求中获取range字段 获取起始位置 终止位置
并且从文件中读取好这些位置的信息 返回给客户端即可
客户端编写
客户端要实现的功能: 自动对指定文件夹中的文件进行备份
也就是说要实现下面三个模块
- 数据管理模块:管理备份的文件信息
- 目录遍历模块:获取指定文件夹中的所有文件路径名
- 文件备份模块:将需要备份的文件上传备份到服务器
由于客户端和服务器的文件操作基本相同 所以我们将我们服务器文件操作的代码复制一份即可
数据管理类设计
我们的客户端只需要有一个文件的唯一标识 因为我们客户端只需要判断该文件是否要被上传即可
一个唯一标识就能很好的做到这一点
此外 该类还应该具有以下函数
- 初始化(从文件中读取数据)
- 持久化存储
- 插入数据
- 更新
- 查找到一个数据
代码标识如下
namespace cloud { 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) {} bool Storage() { std::stringstream ss; // 1. get all backup info auto it = _table.begin(); while (it != _table.end()) { // 2. key val\n key val\n key val\n ss << it->first << " " << it->second << std::endl; } // 3. persistence storage 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; size_t 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. get all data from file FileUtil fu(_backup_file); std::string body; fu.GetContent(body); // 2. analysis the string and add info to table std::vector<std::string> arry; for (auto a : arry) { 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; return true; } bool Update(const std::string& key, const std::string& val) { _table[key] = val; 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; } }; }
数据管理类测试
我们在写完数据管理类之后在main函数中写出下面的代码
cloud::FileUtil fu("./"); std::vector<std::string> arry; fu.ScanDirectory(arry); cloud::DataManager data(BACKUP_FILE); for (auto x : arry) { data.Insert(x, "adadd"); }
这段代码的含义是 在 ./
目录中 创建一个BACKUP_FILE
文件 并且将arry中的所有数据保存到这个文件中
我们打开目录即可看到 backup.dat
文件 文件信息如下
文件备份类设计
我们需要自动将指定文件夹中的文件备份到服务器中
所以我们要做以下流程
- 遍历指定文件夹 获取文件信息
- 逐一判断文件是否需要备份
- 需要备份的文件进行上传备份
所以说需要的私有成员如下
- 要监控的文件夹
- 数据管理类
需要的成员函数如下
- 构造函数
- 运行模块
- 得到文件唯一标识
- 上传文件
文件的唯一标识
我们使用文件名+大小+最后一次修改时间作为文件的唯一标识
整体函数如下
namespace cloud { #define SERVER_ADDR "43.143.132.22" #define SERVER_PORT 8080 class Backup { private: std::string _back_dir; DataManager* _data; public: Backup(const std::string& backup_dir, const std::string& back_file) { _back_dir = backup_dir; _data = new DataManager(back_file); } std::string GetFileIdentifier(std::string filename) { // a.txt-fsize-mtime; FileUtil fu(filename); std::stringstream ss; ss << fu.FileName() << "-" << fu.FileSize() << "-" << fu.LastModifyTime(); return ss.str(); } bool Upload(const std::string& filename) { // 1. get all data FileUtil fu(filename); std::string body; fu.GetContent(body); // 2. send data to client httplib::Client client(SERVER_ADDR, SERVER_PORT); httplib::MultipartFormData item; item.content = body; item.filename = fu.FileName(); item.name = "file"; // mark 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; } bool IsNeedUpload(const std::string& filename) { // we have two condition to judge if the file is new // 1. we don have the info in the table // 2. the unicode change std::string id; if (_data->GetOneByKey(filename, id) == false) { return true; } std::string new_id = GetFileIdentifier(filename); if (new_id == id) { return false; } // copy need time // so if the file has big content we will continue upload it FileUtil fu(filename); if (time(NULL) - fu.LastModifyTime() < 3) { return false; } return true; } bool Runmodule() { while (1) { FileUtil fu(_back_dir); std::vector<std::string> arry; fu.ScanDirectory(arry); for (auto a : arry) { if (IsNeedUpload(a) == false) { continue; } if (Upload(a) == true) { _data->Insert(a, GetFileIdentifier(a)); } } // judge if the file need upload Sleep(100); } } }; }
接下来我们的客户端就完成了
运行客户端和服务器之后 客户端就会实时检测backup目录下的文件 检测到未备份的文件便会自动上传客户端
此外在最后运行代码的时候我遇到了个小问题
Linux中的目录分隔符和Windows中的目录分隔符是不一样的
所以说我们可以使用C++17文件管理的获取文件名来完成
std::string FileName() { std::experimental::filesystem::path p(_filename); return p.filename().string(); }
项目总结
项目名称:云备份系统
项目功能:搭建云备份服务器与客户端,客户端程序运行在客户机上自动将指定目录下的文件备份到服务器,并且能够支持浏览器查看与下载,其中下载支持断点续传功能,并且服务器端对备份的文件进行热点管理,将长时间无访问文件进行压缩存储。
开发环境: centos7.6/vim、g++、gdb、makefile 以及 windows10/vs2017
技术特点: http 客户端/服务器搭建, json 序列化,文件压缩,热点管理,断点续传,线程池,读写锁,单例模式
项目模块:
服务端:
- 数据管理模块:内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
- 业务处理模块:搭建 http 服务器与客户端进行通信处理客户端的上传,下载,查看请求,并支持断点续传
- 热点管理模块:对备份的文件进行热点管理,将长时间无访问文件进行压缩存储,节省磁盘空间。 客户端
- 数据管理模块:内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
- 文件检索模块:基于 c++17 文件系统库,遍历获取指定文件夹下所有文件。
- 文件备份模块:搭建 http 客户端上传备份文件。
项目扩展
- 给客户端开发一个好看的界面,让监控目录可以选择
- 内存中的管理的数据也可以采用热点管理
- 压缩模块也可以使用线程池实现
- 实现用户管理,不同的用户分文件夹存储以及查看
- 实现断点上传
- 客户端限速,收费则放开
项目常见问题
- 介绍下你的项目
项目介绍参考 上面的总结即可
- 项目中的某个技术点是怎么实现的
本文中对于使用到的技术都详细介绍了
- 服务器为什么不自己搭建
可以参考博主的另外一篇博客 自主实现http服务器
- 多个客户端同时上传文件如何处理
本项目实现的单个目录存储 所以说多个客户端同时上传文件也会放到同个目录下
- 断点续传怎么实现
本文中有详细的介绍
- 云备份的传输速度有多少
因为博主自己使用的服务器带宽是2mb 所以传输速度也就是200多kb
大家可以参考自己的云服务器参考下
- 服务器支持多少个客户端
博主使用多线程测试了下 大概能支持二十多个客户端 并且此时的速度明显变慢了
如果面试官问到listen的第二个参数相关 可以参考我的这篇博客 详解TCP