文件使用工具和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();//从配置文件中获取压缩文件的后缀名:.lz std::string download_prefix = config->GetDownloadPrefix();//从配置文件中获取客户端下载请求的前缀:/download/,用来构成文件的url成员。 this->pack_flag = false;//对于新增文件,该标志位一定是false this->fsize = fu.FileSize(); this->mtime = fu.LastMTime(); this->atime = fu.LastATime(); this->real_path = realpath; // ./backdir/a.txt -> ./packdir/a.txt.lz this->pack_path = packdir + fu.FileName() + packsuffix;//存储路径会发生改变,并且多了一个后缀名。 // ./backdir/a.txt -> /download/a.txt this->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::Value Json::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_file FileUtil fu(_backup_file); fu.SetContent(body); return true; } bool InitLoad()//初始化加载,每次系统重启都要加载以前的数据。初始化程序运行时从文件读取读取数据,反序列化 { //1. 将数据文件中的数据读取出来 FileUtil fu(_backup_file); if (fu.Exists() == false) { return true;//说明以前都没有保存过数据,那就不用读了。 } std::string body; fu.GetContent(&body); //2. 反序列化 Json::Value root; JsonUtil::UnSerialize(body, &root); //3. 将反序列化得到的Json::Value中的数据添加到table中 for (int i = 0; i < root.size(); i++) { BackupInfo info;//现在不可以采用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);//pack fu.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值,也就是以前该文件的etag if (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-end fu.GetContent(&rsp.body); rsp.set_header("Accept-Ranges", "bytes"); rsp.set_header("ETag", GetETag(info)); rsp.set_header("Content-Type", "application/octet-stream"); //rsp.set_header("Content-Range", "bytes start-end/fsize"); rsp.status = 206;//状态码206是指服务器成功处理了布冯GET请求。 } } }; } #endif