?文件使用工具和json序列化反序列化工具
//文件和json工具類的設計實現
#ifndef __UTIL__
#define __UTIL__
#include<iostream>
#include<fstream>
#include<string>
#include <vector>
#include<sys/stat.h>
#include"bundle.h"
#include <experimental/filesystem>
#include <jsoncpp/json/json.h>
namespace cloud
{namespace fs = std::experimental::filesystem;class FileUtil{private:std::string _filename;// ../YUNBEIFEN/cloud.cpp,是一個帶文件名的文件路徑public:FileUtil(const std::string& filename):_filename(filename){}int64_t FileSize()//獲取文件大小{struct stat st;if(stat(_filename.c_str(),&st)<0)//一個系統調用接口,int stat(const char *path, struct stat *buf),參數path是文件路徑(名),輸出型參數struct stat類型的結構體保存文件信息{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 MTime 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 ATime failed!\n";return -1;}return st.st_atime;//返回最后一次訪問時間}std::string FileName()//獲取文件名,../YUNBEIFEN/cloud.cpp->cloud.cpp{size_t pos=_filename.find_last_of("/");if(pos==std::string::npos){return _filename;//類成員_filename本身就是一個不帶路徑的文件名}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 << "file len error\n";return false;}std::ifstream ifst;ifst.open(_filename,std::ios::binary);//以二進制的方式讀取文件,if (ifst.is_open() == false) //如果打開文件失敗{std::cout << "read_open file failed!\n";return false;}ifst.seekg(pos, std::ios::beg);//從文件的起始位置beg偏移pos個單位body->resize(len);ifst.read(&(*body)[0], len);//讀取數據到body中,&(*body)[0]是空間body首地址。if (ifst.good() == false) //如果上一次操作這里是讀取異常{std::cout << "get file content failed\n";ifst.close();return false;}ifst.close();return true;}bool GetContent(std::string *body){size_t fsize=this->FileSize();return GetPosLen(body,0,fsize);}bool SetContent(const std::string &body) //將body里面的內容設置到文件中{std::ofstream ofs;//注意和ifstream區分開了,這里是進行寫入數據ofs.open(_filename, std::ios::binary);//沒有就直接創建,_filename是一個包含文件名的路徑if (ofs.is_open() == false) //文件打開失敗{std::cout << "write_open file failed!\n";return false;}ofs.write(&body[0], body.size());//將body中的數據寫入到ofs當中,&(*body)[0]是空間body首地址。if (ofs.good() == false) {std::cout << "write file content failed!\n";ofs.close();return false;}ofs.close();return true;}bool Compress(const std::string &packname)//對文件進行壓縮,壓縮之后放入到packname中{//第一步是讀取源文件數據std::string body;if (this->GetContent(&body) == false){std::cout << "compress: get file content failed!\n";return false;}//第二步對數據進行壓縮std::string packed = bundle::pack(bundle::LZIP, body);//第三步將壓縮的數據存儲到壓縮包當中FileUtil f(packname);if (f.SetContent(packed) == false){std::cout << "compress: write packed data failed!\n"; return false;}return true;}bool UnCompress(const std::string &filename)//將當前壓縮文件的數據解壓縮到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 f(filename);if(f.SetContent(unpacked)==false){std::cout << "uncompress write packed data failed!\n";return false;}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)//通過arry返回目錄里面的所有文件的完整路徑名稱{for(auto& p: fs::directory_iterator(_filename)) //{if (fs::is_directory(p) == true)//如果是目錄就跳過,因為實際應用中的時候我們傳輸的是普通文件,不可能傳遞文件夾m{continue;}//relative_path 是帶有相對路徑的文件名arry->push_back(fs::path(p).relative_path().string()); }return true;}bool Remove(){if(this->Exists()==false) return true;remove(_filename.c_str());return true;}};class JsonUtil{public:static bool Serialize(const Json::Value &root, std::string *str)//靜態函數,外部可以直接調用,不需要this指針,也就是說不用實例化一個具體的對象來調用。{Json::StreamWriterBuilder swb;std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());//通過StreamWriterBuilder的newStreamWriter()來new一個StreamWriter對象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;cr->parse(str.c_str(), str.c_str() + str.size(), root, &err);return true;}};}
#endif
配置文件信息模塊
?將服務端運行所需要的關鍵信息記錄在配置文件里面,當我們運行程序的時候從我們的配置文件中讀取這些關鍵信息出來,在程序中進行使用,使用配置文件的好處就是我們的配置信息隨時可以進行更改,而更改配置信息之后我們的程序并不需要進行重新生成編譯,只需要重啟一下可執行程序,對于修改后的配置文件進行重新加載即可,用配置文件加載程序更加靈活。
{"hot_time" : 30,//熱點判斷時間"server_port" : 8080,//"server_ip" : "192.159.124.113","download_prefix" : "/download/",//用于表示客戶端請求是一個下載請求。http的url里面包含了協議方案名稱+服務器ip地址和端口+客戶端所請求的資源路徑(如果這個請求是一個文件下載請求的話后面跟的就是文件路徑,文件路徑肯定是存儲在服務器上面的,如果外界可以隨時隨地訪問服務器的任意一個文件就不安全,所以這個路徑是一個相對根目錄路徑,我們會在服務器常見一個文件夾wwwroot做為這個根目錄,把客戶端所請求的資源都放在這個文件夾里面,當我們請求是/abc.txt是會轉換成為./wwwroot/abc.txt,從而避免客戶端可以任意訪問服務端的所有路徑。/download/listshow是下載listshow文件,而/listshow是備份列表查看。"packfile_suffix" : ".lz","pack_dir" : "./packdir/","back_dir" : "./backdir/","backup_file" : "./cloud.dat"//服務端備份信息存放文件,服務端會將我們所有的文件備份信息管理起來,這樣文件就算壓縮之后客戶端查看列表請求因該是源文件信息查看,所以備份文件信息管理可以讓客戶端隨時獲取想要的信息。
}
使用單例模式管理系統配置信息,能夠讓配置信息的管理控制更加統一靈活。
// #配置文件模塊:將服務端運行所需要的關鍵信息記錄在配置文件里面每當我們運行系統的時候,從配置文件里面讀取關鍵信息在程序中使用
// #配置文件的好處:配置信息可以隨時進行更改,更改配置信息程序并不需要進行重新編譯,只需要重啟一下服務端程序,讓服務端重新加載程序就可以了,從而讓程序運行更加的靈活。
// #配置信息
// #1.熱點判斷時間
// #熱點管理:多長時間沒有被訪問的文件算是非熱點文件
// #2.文件下載的url前綴路徑- -用于表示客戶端請求是一個下載請求
// #url: htt://192.168.122.136:9090/path
// #當用戶發來個備份列表查看請求/listshow,我們如何判斷這個不是一個listshow的文件下載請求
// #/download/test.txt, /download/listshow 判斷為下載請求
// #3.壓縮包后綴名:定義了壓縮包命名規則,就是在文件原名稱之后加上后綴。".lz"
// #4上傳文件存放路徑:決定了文件上傳之后實際存儲在服務器的哪里
// #5.壓縮包存放路徑:決定非熱點文件壓縮后存放的路徑
// #6.服務端備份信息存放文件:服務端記錄的備份文件信息的持久化存儲
// #7.服務器的監聽IP地址:當程序要運行在其他主機上,則不需要修改程序
// #8.服務器的監聽端口
// #9.配置文件采用json的格式來進行存放
#ifndef __CONFIG__
#define __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;//服務器ip地址std::string _download_prefix;//下載的url前綴路徑std::string _packfile_suffix;//壓縮包的后綴名稱std::string _pack_dir;//壓縮包存放路徑std::string _back_dir;//備份文件存放路徑std::string _backup_file;//備份數據信息存放文件bool ReadConfigFile()//讀取配置文件{FileUtil f(CONFIG_FILE);std::string body;if(f.GetContent(&body)==false)//讀取出來的body是json格式的字符串,還需要反序列化。{std::cout<<"read 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();_pack_dir = root["pack_dir"].asString();_back_dir = root["back_dir"].asString();_backup_file = root["backup_file"].asString();return true;}public://由于成員變量都是private私有的,所以需要提供對應的public共有接口進行訪問。static Config* GetInstance()//獲取操作句柄,cloud::Config *config = cloud::Config::GetInstance();{if(_instance==NULL){_mutex.lock();if(_instance==NULL){_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 = NULL;//靜態成員變量類外定義std::mutex Config::_mutex;
}#endif
?數據管理模塊
數據持續化存儲的原因是放置服務器每次重啟數據都會丟失。
// 數據管理模塊:需要管理的數據有哪些
// 管理哪些數據,是因為后期要用到哪些數據
// 1.文件的實際存儲路徑:當客戶端要下載文件時,則從這個文件中讀取數據進行響應
// 2.文件壓縮包存放路徑名:如果這個文件是一個非熱點文件會被壓縮,則這個就是壓縮包路徑名稱
// 如果客戶端要下載文件,則需要先解壓縮,然后讀取解壓后的文件數據。
// 3.文件是否壓縮的標志位:判斷文件是否已經被壓縮了
// 4.文件大小
// 5.文件最后一-次修改時間
// 6.文件最后- -次訪問時間
// 7.文件訪問URL中資源路徑path: /download/a.txt
// 如何管理數據:
// 1.用于數據信息訪問:使用hash表在內存中管理數據,以url的path作為key值--查詢速度快
// 2.持久化存儲管理:使用json序列化將所有數據信息保存在文件中#ifndef __DATA__
#define __DATA__
#include <unordered_map>
#include <pthread.h>
#include "util.hpp"
#include "config.hpp"namespace cloud
{struct BackupInfo//數據信息結構體,存儲數據信息,一個文件實例化一個類對象{bool pack_flag;//標志文件是否被壓縮size_t fsize;//文件大小time_t mtime;//文件修改時間time_t atime;//文件訪問時間std::string real_path;//文件的實際存儲路徑std::string pack_path;//文件壓縮包實際存放路徑名std::string url;//外界下載時需要的資源路徑bool NewBackupInfo(const std::string &realpath)//填充 BackupInfo結構體{FileUtil fu(realpath);if(fu.Exists() == false)//如果文件并不存在就直接返回false{std::cout<<"NewBackInfo: file not exists!\n";return false;}Config *config = Config::GetInstance();std::string packdir = config->GetPackDir();//從配置文件中獲取壓縮包存儲路徑:./packdir/std::string packsuffix = config->GetPackFileSuffix();//從配置文件中獲取壓縮文件的后綴名:.lzstd::string download_prefix = config->GetDownloadPrefix();//從配置文件中獲取客戶端下載請求的前綴:/download/,用來構成文件的url成員。this->pack_flag = false;//對于新增文件,該標志位一定是falsethis->fsize = fu.FileSize();this->mtime = fu.LastMTime();this->atime = fu.LastATime();this->real_path = realpath;// ./backdir/a.txt -> ./packdir/a.txt.lzthis->pack_path = packdir + fu.FileName() + packsuffix;//存儲路徑會發生改變,并且多了一個后綴名。// ./backdir/a.txt -> /download/a.txtthis->url = download_prefix + fu.FileName();return true;}};class DataManager//數據管理類{private:std::string _backup_file;//數據信息持久化存儲的一個文件pthread_rwlock_t _rwlock;//數據管理類是會在不同模塊中被使用的,涉及到了多線程訪問,讀寫鎖,讀共享(只是獲取數據),寫互斥,std::unordered_map<std::string, BackupInfo> _table;//在內存中是采用哈希表存儲文件信息public:DataManager()//每次重啟重啟的時候都要加載數據,以及每次數據發生修改或者新增都需要進行重新的持續化存儲。{_backup_file = Config::GetInstance()->GetBackupFile();//持久化文件是從我們的配置文件中獲取,"backup_file" : "./cloud.dat"pthread_rwlock_init(&_rwlock, NULL);//初始化讀寫鎖NewBackupInfoNewBackupInfo鎖InitLoad();//對象在構造的時候需要對其進行加載,也就是從持續化存儲的文件中讀取數據來初始化成員_table}~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)//數據發生更新或需要修改數據,代碼和Insert是一樣的。{pthread_rwlock_wrlock(&_rwlock);//加鎖_table[info.url]=info;//當存在相同key值的時候,會對value進行覆蓋。pthread_rwlock_unlock(&_rwlock); Storage();//當有數據發生修改的時候需要進行持續化存儲return true;}bool GetOneByURL(const std::string &url, BackupInfo *info)//因為前端會給一個url請求要下載某個文件,因為url本身就是table的key值,所以直接可以通過find來進行查找。{pthread_rwlock_wrlock(&_rwlock);//因為要對table進行操作,所以要加鎖auto it = _table.find(url);//通過find查找url,因為url就是key值。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)//根據真實路徑獲取文件信息,因為服務器要不斷檢測一個目錄中的所有文件,判斷文件是否是一個熱點文件。這里遍歷的是目錄,而不是遍歷備份文件信息。//realpath不是table的key值,所以得通過遍歷table來獲取{pthread_rwlock_wrlock(&_rwlock);auto it = _table.begin();for (; it != _table.end(); ++it)//由于realpath不是key值,所以不能通過key值查找。{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> *arry)//獲取所有文件信息,可以用來組織頁面。{pthread_rwlock_wrlock(&_rwlock);auto it = _table.begin();for (; it != _table.end(); ++it){arry->push_back(it->second);}pthread_rwlock_unlock(&_rwlock);return true;}bool Storage()//每次數據新增或者修改都要進行持續化存儲,避免數據丟失。(比如說發生數據新增或者數據修改的時候)則需要持久化存儲一次,并且涉及到了json的序列化,將其轉換成為json的字符串在存儲到文件給當中去//將所有文件的BackupInfo存儲起來,涉及到了json的序列化{//1.獲取所有數據std::vector<BackupInfo> arry;this->GetAll(&arry);//2.將文件信息添加到Json::ValueJson::Value root;for(int i=0;i<arry.size();i++){Json::Value item;item["pack_flag"] = arry[i].pack_flag;item["file_size"] = (Json::Int64)arry[i].fsize;//(Json::Int64)類型強轉,因為json里面并沒有重載size_t這些類型item["atime"] = (Json::Int64)arry[i].atime;item["mtime"] = (Json::Int64)arry[i].mtime;item["real_path"] = arry[i].real_path;item["pack_path"] = arry[i].pack_path;item["url"] = arry[i].url;root.append(item);//添加數組元素,該數組元素是一個json value對象,關鍵寫法。}//3. 對Json::Value序列化std::string body;JsonUtil::Serialize(root, &body);//4. 將序列化的數據寫入到_backup_fileFileUtil 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;//現在不可以采用NewBackupInfo,所有的文件信息都是從son::Value對象中加載的。info.pack_flag = root[i]["pack_flag"].asBool();info.fsize = root[i]["file_size"].asInt64();info.atime = root[i]["atime"].asInt64();info.mtime = root[i]["mtime"].asInt64();info.pack_path = root[i]["pack_path"].asString();info.real_path = root[i]["real_path"].asString();info.url = root[i]["url"].asString();Insert(info);}return true;}};
}
#endif
熱點文件管理模塊
熱點管理模塊:對服務器上備份的文件進行檢測,哪些文件長時間沒有被訪問,則認為是非熱點文件,則壓
縮存儲,節省磁盤空間。
實現思路:
遍歷所有的文件,檢測文件的最后-次訪問時間,與當前時間進行相減得到差值,這個差值如果大
于設定好的非熱點判斷時間則認為是非熱點文件,則進行壓縮存放到壓縮路徑中,刪除源文件
遍歷所有的文件:
1.從數據管理模塊中遍歷所有的備份文件信息
2.遍歷備份文件夾,獲取所有的文件進行屬性獲取,最終判斷
選擇第二種:遍歷文件夾,每次獲取文件的最新數據進行判斷,并且還可以解決數據信息缺漏的問
題,也就是某個文件上傳成功了,但是漏掉了添加信息。
1.遍歷備份目錄,獲取所有文件路徑名稱
2.逐個文件獲取最后一次訪問時間與當前系統時間進行比較判斷
3.對非熱點文件進行壓縮處理,刪除源文件
4.修改數據管理模塊對應的文件信息(壓縮標志修改為true)
// 熱點管理模塊:對服務器上備份的文件進行檢測,哪些文件長時間沒有被訪問,則認為是非熱點文件,則壓 縮存儲,節省磁盤空間。
// 實現思路:
// 遍歷所有的文件,檢測文件的最后- -次訪問時間,與當前時間進行相減得到差值,這個差值如果大于設定好的非熱點判斷時間則認為是非熱點文件,則進行壓縮存放到壓縮路徑中,刪除源文件
// 遍歷所有的文件:
// 1.從數據管理模塊中遍歷所有的備份文件信息
// 2.遍歷備份文件夾,獲取所有的文件進行屬性獲取,最終判斷
// 選擇第二種:遍歷文件夾,每次獲取文件的最新數據進行判斷,并且還可以解決數據信息缺漏的問題
// 1.遍歷備份目錄,獲取所有文件路徑名稱
// 2.逐個文件獲取最后一次訪問時間與當前系統時間進行比較判斷
// 3.對非熱點文件進行壓縮處理,刪除源文件
// 4.修改數據管理模塊對應的文件信息(壓縮標志修改為true)
#ifndef __HOT__
#define __HOT__
#include <unistd.h>
#include "data.hpp"extern cloud::DataManager* _data;
namespace cloud
{class HotManager{//熱點管理流程:// 1.獲取備份目錄下所有文件// 2.逐個判斷文件是否是非熱點文件// 3.非熱點文件壓縮處理// 4.刪除源文件,修改備份信息private:std::string _back_dir;//備份文件路徑std::string _pack_dir;//壓縮文件路徑std::string _pack_suffix;//壓縮包后綴名int _hot_time;//熱點的判斷時間//非熱點文件返回真,熱點文件返回假bool HotJudege(const std::string& filename){FileUtil fu(filename);time_t last_atime=fu.LastATime();time_t cur_time=time(NULL);//獲取當前系統時間if(cur_time-last_atime>_hot_time) return true;else 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 tmp1(_back_dir);FileUtil tmp2(_pack_dir);tmp1.CreateDirectory();//如果目錄不存在就創建tmp2.CreateDirectory();}bool RunModule(){while (1)//這個模塊是一個死循環的過程{//1.遍歷備份的目錄,獲取所有的文件名FileUtil fu(_back_dir);std::vector<std::string> array;fu.ScanDirectory(&array);//2.遍歷判斷文件是否是非熱點文件for(auto& a:array){if(HotJudege(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;}};
}
#endif
cloud::DataManager *_data;//定義一個全局變量,在hot.hpp中會用到:extern cloud::DataManager* _data;
void HotTest()
{ _data=new cloud::DataManager();cloud::HotManager hot;hot.RunModule();
}int main(int argc,char* argv[])
{HotTest();return 0;
}
業務處理模塊
三大業務模塊
服務端業務處理模塊:將網絡通信模塊和業務處理進行了合并(網絡通信通過httplib庫完成)
1.搭建網絡通信服務器:借助httplib完成
2.業務請求處理
1.文件.上傳請求:備份客戶端上傳的文件,響應上傳成功
2.文件列表請求:客戶端瀏覽器請求一個備份文件的展示頁面,響應頁面
3.文件下載請求:通過展示頁面,點擊下載,響應客戶端要下載的文件數據
網絡通信接設計:約定好,客戶端發送什么樣的請求,我們給與什么樣的響應
請求:文件上傳,展示頁面,文件下載
?
?
斷點續傳模塊
功能:當文件下載過程中,因為某種異常而中斷,如果再次進行從頭下載,效率較低,因為需要將
之前已經傳輸過的數據再次傳輸一遍。因此斷點續傳就是從上次下載斷開的位置,重新下載即可,之前已經傳輸過的數據將不需要再重新傳輸。
目的:提高文件重新傳輸效率
實現思想:
客戶端在下載文件的時候,要每次接收到數據寫入文件后記錄自己當前下載的數據量。當異常下載中斷時,下次斷點續傳的時候,將要重新下載的數據區間(下載起始位置,結束位置)發送給服務器,服務器收到后,僅僅回傳客戶端需要的區間數據即可。
需要考慮的問題:如果上次下載文件之后,這個文件在服務器上被修改了,則這時候將不能重新斷
點續傳,而是應該重新進行文件下載操作。
在http協議中斷點續傳的實現:
主要關鍵點:
1.在于能夠告訴服務器下載區間范圍,
2.服務器上要能夠檢測上一次下載之后這個文件是否被修改過
//當我們瀏覽器輸入192.182.142.10:9090的時候,沒有任何的請求的情況下,瀏覽器會默認加上一個\表示這是一個根目錄請求
// 服務端業務處理模塊:將網絡通信模塊和業務處理進行了合并(網絡通信通過httplib庫完成)
// 1.搭建網絡通信服務器:借助httplib完成
// 2.業務請求處理
// 1.文件上傳請求:備份客戶端上傳的文件,響應上傳成功
// 2.文件列表請求:客戶端瀏覽器請求一個備份文件的展示頁面, 響應頁面
// 3.文件下載請求:通過展示頁面,點擊下載,響應客戶端要下載的文件數據// 網絡通信接口設計:約定好,客戶端發送什么樣的請求,我們給與什么樣的響應
// 請求:文件上傳,展示頁面,文件下載
// 接口設計:
// 1.文件上傳
// 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--
// 當服務器收到了一個POST方法的/upload請求,我們則認為這是一個文件上傳請求
// 解析請求,得到文件數據,將數據寫入到文件中
// HTTP/1.1 200 OK
// Content-Length: 0
// 2.展示頁面
// GET /list HTTP/1.1
// Content-Length: 0
//當服務器收到了一個GET方法的/listshow請求,我們則認為這是一個文件頁面展示請求
// HTTP/1.1 200 OK
// Content-Length:
// Content-Type: text/html// <html>..... </html> <!-- 這是展示頁面的數據-->
// 3.文件下載
// GET /download/a.txt http/1.1
// Content-Length: 0
// 當服務器收到了一個GET方法的/download/請求,我們則認為這是一個文件下載請求
// HTTP/1.1 200 OK
// Content-Length: 100000
// ETags: "filename-size-mtime一個能夠唯一標識文件的數據"
// Accept-Ranges: bytes
// 正文就是文件數據#ifndef __SERVICE__
#define __SERVICE__
#include "data.hpp"
#include "httplib.h"extern cloud::DataManager* _data;
namespace cloud
{class Service{private://搭建http服務器,并進行業務處理。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(){//搭建服務器,注冊映射關系,然后listen_server.Post("/upload",Upload);_server.Get("/listshow",ListShow);_server.Get("/",ListShow);//當我們瀏覽器訪問服務器采用172.16.204.184:9090后面什么也不加沒有任何的資源路徑請求,瀏覽器會在末尾默認加一個/是一個相當于一個根目錄請求,我們response一個展示頁面。std::string download_url=_download_prefix+"(.*)";_server.Get(download_url,Download);//(.*)用來捕捉數據,匹配多個字符_server.listen(_server_ip.c_str(),_server_port);return true;}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;}std::cout<<"ok1"<<std::endl;//有文件上傳就獲取數據const auto& file = req.get_file_value("file");//其中兩個成員變量分別指的是: file.filename//文件名稱 file.content//文件數據std::cout<<"ok2"<<std::endl;//找到文件并在備份目錄下創建該文件,并且把數據寫入進去。std::string back_dir = Config::GetInstance()->GetBackDir();std::cout<<"ok3"<<std::endl;std::string realpath = back_dir + FileUtil(file.filename).FileName();//FileUtil(file.filename).FileName()是為了只獲取文件名,路徑并不需要。std::cout<<"ok4"<<std::endl;FileUtil fu(realpath);std::cout<<realpath<<std::endl;std::cout<<"ok5"<<std::endl;fu.SetContent(file.content);//將數據寫入文件中;std::cout<<"ok6"<<std::endl;BackupInfo info;info.NewBackupInfo(realpath);//組織備份的文件信息std::cout<<"ok7"<<std::endl;_data->Insert(info);//向數據管理模塊添加備份的文件信息std::cout<<"ok8"<<std::endl;return;}static std::string TimetoStr(time_t t) //將時間戳轉換成為易讀的字符串,當前函數設置為靜態函數的原因是因為ListShow是靜態成員函數,里面調用的也只能是靜態成員函數,否則this變量從哪里來。{std::string tmp = std::ctime(&t);return tmp;}static void ListShow(const httplib::Request &req, httplib::Response &rsp){//1.獲取所有的備份文件信息std::vector<BackupInfo> arry;//用來放置所有的文件信息,后面用到頁面展示_data->GetAll(&arry);//2.根據所有的備份信息,組織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");//設置正文數據類型,告訴瀏覽器這是一個html數據,你需要對其進行渲染讓后展示在瀏覽器當中。rsp.status = 200;return ;}static std::string GetETag(const BackupInfo &info) //http的ETag頭部字段存儲了一個資源的唯一標識,當客戶端第一次下載一個文件的時候,我們生成一個唯一標識最為響應的一部分給客戶端,//客戶端第二次下載的時候,就會將該信息發送給服務器,讓服務器根據這個唯一標識判斷這個資源自從上次下載過之后是否又被修改過,如果沒有修改的話,直接使用原先緩存的數據,不用再重新下載了。{//etg:filename-fsize-mtime,這個etag是自定義的,也說明了http協議本身對于etag中是什么數據并不關心,只要你服務端可以自己標識就行了。而etag字段不僅僅是緩存用到,還有就是后邊的斷點續傳的實現也會用到,因為斷點續傳也要保證文件沒有被修改過。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){//http協議的Accept-Ranges: bytes字段:用于告訴客戶端我支持斷點續傳 ,并且數據單位以字節作為單位。//1. 獲取客戶端請求的資源路徑path,其中req.path已經組織好了,是一個url資源請求類型的路徑//2. 根據資源路徑,獲取文件備份信息BackupInfo info;_data->GetOneByURL(req.path, &info);//3. 判斷文件是否被壓縮,如果被壓縮,要先解壓縮, if (info.pack_flag == true){FileUtil fu(info.pack_path);//packfu.UnCompress(info.real_path);//將文件解壓到備份目錄下//4. 刪除壓縮包fu.Remove();//5修改備份信息(改為沒有被壓縮)info.pack_flag = false;_data->Update(info);}//斷點續傳的判斷主要在于解析請求中是否If-Range這個字段的信息bool retrans = false;//定義當前是否屬于斷點續傳std::string old_etag;if (req.has_header("If-Range")) {old_etag = req.get_header_value("If-Range");//存在If-Range字段說明需要斷點續傳,這時候獲取該字段的value值,也就是以前該文件的etagif (old_etag == GetETag(info)) //有If-Range字段且,這個字段的值與請求文件的最新etag一致則符合斷點續傳{retrans = true;}}//如果沒有If-Range字段或者有這個字段但是old_etag與當前服務器的etag不匹配的話,則必須重新返回全部的數據,也就是進入正常的下載模式。//6. 讀取文件數據,放入rsp.body中FileUtil fu(info.real_path);if (retrans == false)//響應執行正常的文件下載任務{fu.GetContent(&rsp.body);//7. 設置響應頭部字段: ETag, Accept-Ranges: bytes,Content-Length會自動設置,不需要我們主動設置。rsp.set_header("Accept-Ranges", "bytes");rsp.set_header("ETag", GetETag(info));rsp.set_header("Content-Type", "application/octet-stream");//Content-Type字段:決定了瀏覽器如何處理響應正文,如果不設置瀏覽器無法得知如何處理正文數據,application/octet-stream說明數據是二進制數據流,常用于文件下載。rsp.status = 200;}else//響應執行斷點續傳任務 {//httplib內部實現了對于區間請求也就是斷點續傳請求的處理//只需要我們用戶將文件所有數據讀取到rsp.body中,它內部會自動根據請求//區間,從body中取出指定區間數據進行響應// std::string range = req.get_header_val("Range"); bytes=start-endfu.GetContent(&rsp.body);rsp.set_header("Accept-Ranges", "bytes");rsp.set_header("ETag", GetETag(info));rsp.set_header("Content-Type", "application/octet-stream");//rsp.set_header("Content-Range", "bytes start-end/fsize");rsp.status = 206;//狀態碼206是指服務器成功處理了布馮GET請求。}}};
}
#endif
?