一 服务端实现
1 util.hpp
这个模块我们提供一些获取文件属性的接口,只需要传入文件名即可,这个参数的传递非常关键,一旦传错了就会找不到文件,filename一般会带路径。例如./test.txt。
namespace cloud
{
class Fileutil
{
public:
Fileutil(const std::string filename)
:filename_(filename)
{
;
}
// 获取文件属性,包括大小,最近访问时间,最近修改时间
int64_t fileSize()
{
struct stat buf;
if (stat(filename_.c_str(), &buf) < 0)
{
std::cout << "stat err" << std::endl;
exit(-1);
}
return buf.st_size;
}
private:
std::string filename_;
};
};
获取最近一次修改时间和访问时间。都在stat结构体中记录了,直接获取即可。
struct tm fileModtime()
{
struct stat buf;
if (stat(filename_.c_str(), &buf) < 0)
{
std::cout << "stat err" << std::endl;
exit(-1);
}
struct tm time;
localtime_r(&buf.st_mtime, &time);
return time;
}
struct tm fileAcctime()
{
struct stat buf;
if (stat(filename_.c_str(), &buf) < 0)
{
std::cout << "stat err" << std::endl;
exit(-1);
}
struct tm time;
localtime_r(&buf.st_mtime, &time);
return time;
}
获取文件名。注意:Linux路径是带"/",windows是带"\",后面实现win环境的客户端也要实现文件获取接口,在获取文件名时,记得注意区分分隔符。
std::string Filename()
{
//./a/b/c
int pos = filename_.find_last_of("/");
//test.txt
return filename_.substr(pos + 1);
}
测试
void test1()//测试util.hpp获取文件属性接口
{
cloud::Fileutil fu("hello.txt");
std::cout<<"size: "<<fu.fileSize()<<std::endl;
struct tm time = fu.fileAcctime();
std::cout<<"Acctime year: "<< time.tm_year+1900<<" month: " <<time.tm_mon+1<<" day: "<<time.tm_mday<<std::endl;
time = fu.fileModtime();
std::cout<<"Modtime year: "<< time.tm_year+1900<<" month: " <<time.tm_mon+1<<" day: "<<time.tm_mday<<std::endl;
}
获取文件数据接口,为什么一定要以二进制读取,在指定位置获取指定长度的数据。
bool GetPosLen(int pos, int len, std::string *body)
{
std::ifstream ifs;
if (pos + len > fileSize()) // 以免读取越界
{
std::cout << "Get err:over size "<< std::endl;
return false;
}
ifs.open(filename_.c_str(), std::ios::binary);
if (ifs.is_open() == false)
{
std::cout << "Get open err: " << errno << std::endl;
ifs.close();
return false;
}
ifs.seekg(pos, std::ios::beg);
// 获取文件数据
body->resize(len);
ifs.read(&(*body)[0], len);
ifs.close();
return true;
}
获取全部内容,复用接口
bool GetContent(std::string *body)
{
return GetPosLen(0, fileSize(), body);
}
bool SetContent(const std::string &body)
{
std::ofstream ofs;
// 打开文件将数据写入
ofs.open(filename_, std::ios::binary);内部会创建文件
if (ofs.is_open() == false)
{
std::cout << "set content open err: " << errno << std::endl;
ofs.close();
return false;
}
ofs.write(&body[0], body.size());
ofs.close();
return true;
}
测试
void test2()//测试
{
cloud::Fileutil fu("hello.txt");
std::string body;
fu.GetContent(&body);
std::cout<<"body "<<body<<std::endl;
body += "test";
fu.SetContent(body);
fu.GetPosLen(0,body.size(),&body);
std::cout<<"body "<<body<<std::endl;
}
实现文件解压压缩,这里要传入一个压缩名,一般是文件加压缩后缀,例如test.txt.lz,这个后缀我们可以外部调用时定死,但是如果要修改,这个就得修改源码了,而且我们还得将所有函数调用位置都改了,为了方便,所以我们将后缀保存到配置文件中。这个文件后面会有个config模块来管理。
// 压缩和解压接口
bool Compress(const std::string packname) // 传入压缩文件名
{
//1 获取文件数据
std::string body;
if (!GetContent(&body))
{
std::cout << "compress get content false" << std::endl;
return false;
}
2 压缩 复用bundle库函数,传使用算法和待压缩的数据
std::string packed = bundle::pack(bundle::LZIP, body);
3 打开文件将压缩数据写入
Fileutil fu(packname);
if(fu.SetContent(packed) == false)
{
std::cout << "compress set content false" << std::endl;
return false;
}
return true;
}
解压
bool UnCompress(const std::string unpackname) // 传入解压文件名
{
//1 获取文件数据
std::string body;
if (!GetContent(&body))
{
std::cout << "UnCompress Get content fail" << std::endl;
return false;
}
//2 解压
std::ofstream ofs;
std::string unpacked = bundle::unpack(body);
//3 打开文件将解压数据写入
Fileutil fu(unpackname);
if(fu.SetContent(unpacked) == false)
{
std::cout << "uncompress set content false" << std::endl;
return false;
}
return true;
}
测试
void test4()//测试压缩和解压缩接口
{
cloud::Fileutil fu("hello.txt");
fu.Compress("hello.zip"); // 传入压缩文件名
cloud::Fileutil nfu("hello.zip");
nfu.UnCompress("hello2.txt"); // 传入解压文件名
}
-lbundle链接压缩库。-L是指定库文件路径,我没有安装到系统路径下,所以要指定。
由于我下载的bundle是拿到了它的.cpp和.h文件,每次都要重新编译,所以我们将其弄成一个静态库。
如何使用一个静态库,-L指定库文件路径,-I指定头文件,-l链接库。
目录操作
复用文件系统库Filesystem library
我们还要实现一个遍历指定目录的接口,可以用库里的这个接口。
遍历指定目录下的文件,将文件路径保存到vector中并返回,我们直接遍历这个vector,即可获得路径名,有意思的是这个路径名是我们一开始传入的目录名+找到的文件名,如果我们刚刚传入./sandbox,返回的文件路径就会变成./sandbox/文件名。
因为返回的是一个库里的类,而我们要的是string字符串,上面是我网上找到的转换方法。
-lstdc++fs链接库才能用。
json实现序列化和反序列化
封装两个接口,让外部只需要传Json::Value对象和字符串。我们内部实现序列化和反序列化。
static bool serialize(std::string *str, const Json::Value &root)
{
Json::StreamWriterBuilder Swb;
std::shared_ptr<Json::StreamWriter> sw(Swb.newStreamWriter());
std::stringstream ss;
sw->write(root, &ss);
*str = ss.str();
return true;
}
static bool Unserialize(const std::string &str, Json::Value *root)
{
Json::CharReaderBuilder crb;
std::shared_ptr<Json::CharReader> cr(crb.newCharReader());
std::stringstream ss;
std::string err;
cr->parse(&str[0], &str[0] + str.size(), root, &err);
return true;
}
Json使用也要带-l链接库。-ljsoncpp
测试使用,js[]就是像我们之前使用map一样是在构建成员,如果用append就是构建一个数组,val[]返回的还是个Json Value类型,是的,其实Value内部即保存了一个hashmap指针,val["key"]的时候就是用"key"在hashmap中查找,然后返回值kv中的v,可是Json::Value不是保存Json格式的字符串吗,怎么还可以只保存一个字符串,这是因为json::value还保存了一个string*指针,此时用字符串给value赋值就会调用构造函数,内部会保存这个字符串。
void test5() // 测试util.hpp中的json接口
{
Json::Value js;
int arr[3] = {80, 90, 100};
js["性别"] = "男";
js["姓名"] = "hqy";
js["成绩"].append(80);
js["成绩"].append(90);
js["成绩"].append(100);
std::string st;
cloud::Jsonutil::serialize(&st, js);
std::cout << "st " << st << std::endl;
Json::Value js2;
cloud::Jsonutil::Unserialize(st, &js2); // 若st = "{"key":value}"->R("{"key":value}")使得内部""为普通字符
std::cout << js2["性别"] << std::endl; // 返回的是Json::value对象,所以可以.访问类内函数,aString(),aInt().
std::cout << js2["姓名"] << std::endl;
std::cout << js2["成绩"] << std::endl;
}
2 config.hpp
服务端配置文件加载类,这个类主要是保存了一些关键数据,例如ip和端口,热点判断时间等一些需要规定好的信息,这些称为程序配置信息,我们将其保存在文件中,这样修改配置消息直接修改文件就好了。所有配置信息如下。
我们学了网络肯定是要知道在浏览器输入网址就可以发起请求,而网址就包含着请求的文件目录,文件下载和页面展示请求都是Get请求,如何区分呢? 可能你会说用文件名,页面展示就让用户请求带上/listshow,文件下载就是/文件名,如果用户上传了一个文件名为listshow的文件呢? 此时用户要Get获取/listshow的时候是页面展示还是下载文件呢? 所以我们要让文件下载请求以加个前缀,/download,后续我们server.hpp模块匹配的时候就知道url是/download/文件名,
这样就能区分页面展示请求和文件下载了。
后面我们要实现对文件的热点管理,你长时间不下载的文件,就要把它压缩了,节省空间,压缩文件放哪里,不能和原先上传文件放一起吗?可以是可以,但是会由于我们热点管理模块会重复地扫描上传目录下的文件,一个已经被压缩了的文件可能会再次被压缩,虽然没影响,但是这是低效的表现,所以还需要两个路径,一个是上传文件的存放路径,还有个是压缩文件路径,也顺便规定了压缩后缀。
还有个字段是backupdir,因为我们后面还要管理文件,那就要保存文件的一些属性信息,为了免得这些属性信息丢失,就有了文件来保存。
配置信息同样用json格式来保存,方便序列化和反序列。诶,奇怪,既然上传文件有路径,那我下载文件的时候/backdir/文件名,然后页面展示就是/listshow,都行,实现是多种多样的,没有唯一答案。既然配置信息保存在文件,还是用Json格式,这里不就用到了Json的序列化和反序列化了吗,而且还要读取文件,先前设计的大多数接口都用上了。
反序列化并且初始化成员。
构造函数内加载配置文件初始化成员。
下面是单例对象获取实现。
我们还对外提供了获取对应文件信息的接口。
int GetHotime()
{
return hot_time_;
}
int GetPort()
{
return serverport_;
}
std::string GetIp()
{
return serverip_;
}
std::string GetDprefix()
{
return download_prefix_;
}
std::string GetPacksuffix()
{
return packfile_suffix_;
}
std::string GetBackfile()
{
return back_file_;
}
std::string GetPackfile()
{
return pack_file_;
}
std::string GetBackUpfile()
{
return backup_file_;
}
测试
js的key不要带空格,不然会匹配失败。
3 data.hpp
数据管理模块实现,这个模块主要是管理上传的文件,你想我们后面要对这些文件做热点管理,那不得先知道有哪些文件,然后获取文件的修改时间才能进行热点管理吗?这些文件属性都由数据管理模块保存起来。
文件信息类成员设计如下。
pack_flag作用后面两个模块会再提及,我们先来看三个路径,我们已经知道文件一开始会先放到上传路径下,然后热点管理模块会一直扫描这个路径下的文件,热点时间到了后会被热点管理模块压缩放到压缩路径下,那url是什么? url其实就是请求报文中的请求路径(/download/文件名)。这个url保存起来在文件管理中用来标识一个文件,下面提,配置信息中的url只保存了前缀,完整路径在信息类中才保存了。
文件信息管理类
我们用hash将文件url和文件信息做映射,那用其它两个路径呢? 就目前的实现来看是可以的,但是文件存储路径是如果发送变更的,此时我们就要重新改变_table中的kv,比较麻烦,所以我们就用了一个虚拟地址,/download/文件名用来做key,/download是我们在配置信息模块中我们保存的,这也算是一个比较重要的配置信息,如果硬编码到代码中,不然如果要把/download变成/down会很麻烦,还是从配置信息中读取吧,。
实现。
struct BackupInfo
{
bool flag_;
size_t fsize_;
int mtime_;
int atime_;
std::string real_path_;
std::string url_; // 下载路径
std::string pack_path_; // 压缩文件路径(包含文件名)"./packdir/文件名.lz
外部传入文件名,我们构建一个文件消息类
bool NewBackupInfo(const std::string &filename)//./hello.txt
{
flag_ = false;
Fileutil fu(filename);
fsize_ = fu.fileSize();
mtime_ = fu.fileModtime();
atime_ = fu.fileAcctime();
Config *cfg = Config::GetInstance();
std::string name = fu.Filename();
url_ = cfg->GetDprefix() + name;// /download/hello.txt
real_path_ = filename;
pack_path_ = cfg->GetPackfile() + name + cfg->GetPacksuffix();
}
};
class DataManager
{
private:
pthread_rwlock_t lock_;
std::unordered_map<std::string, BackupInfo> um;
std::string backupfile_; // 备份文件路径"./cloud.dat",用来保存我们管理的文件信息
DataManager()
{
Config* cfg = Config::GetInstance();
backupfile_ = cfg->GetBackUpfile();
pthread_rwlock_init(&lock_, nullptr);
InitLoad();
}
public:
static DataManager* GetInstance()管理类设为单例类,因为管理信息无需有多份
{
static DataManager data;
return &data;
}
~DataManager()
{
pthread_rwlock_destroy(&lock_);
}
};
所以这个类就帮我们管理了所有的文件,我们后面就可以根据这个模块对文件做增改查实现。
bool Insert(const BackupInfo &info)
写加锁。一定要释放,不然读加锁写加锁会阻塞。
{
pthread_rwlock_wrlock(&lock_);
um[info.url_] = info;
pthread_rwlock_unlock(&lock_);
Storage();
return true;
}
bool Update(const BackupInfo &info)
{
pthread_rwlock_wrlock(&lock_);
um[info.url_] = info;
pthread_rwlock_unlock(&lock_);
Storage();
return true;
}
读加锁
bool GetOnebyurl(const std::string &url, BackupInfo *info)
{
// /download/
pthread_rwlock_rdlock(&lock_);
if (um.find(url) == um.end())
{
pthread_rwlock_unlock(&lock_);
return false;
}
*info = um[url];
pthread_rwlock_unlock(&lock_);
return true;
}
这个遍历整个容器来查找,效率较低。
bool GetOnebyrealpath(const std::string &real_path, BackupInfo *info)
{
//./hello.txt
pthread_rwlock_rdlock(&lock_);
for (auto &e : um)
{
if (e.second.real_path_ == real_path)
{
*info = e.second;
pthread_rwlock_unlock(&lock_);
return true;
}
}
pthread_rwlock_unlock(&lock_);
return false;
}
获取全部文件的管理信息。
bool GetAll(std::vector<BackupInfo> *info)
{
pthread_rwlock_rdlock(&lock_);
for (auto &e : um)
{
info->push_back(e.second);
}
pthread_rwlock_unlock(&lock_);
return true;
}
测试
所有接口都要测试,这里一定要好好测试,不然后面出问题很麻烦,我测试时有个路径传参传错了,查了好几小时。
void test8() // 测试文件信息管理类
{
cloud::BackupInfo info;
info.NewBackupInfo("./hello.txt");
cloud::DataManager* manager = cloud::DataManager::GetInstance();
std::cout<<manager->Insert(info);
std::cout<<manager->GetOnebyrealpath("./hello.txt", &info);
std::cout << "-----------"<< "Insert GetOnebyrealpath" << std::endl;
std::cout << info.flag_ << std::endl;
std::cout << info.fsize_ << std::endl;
std::cout << info.atime_ << std::endl;
std::cout << info.mtime_ << std::endl;
std::cout << info.url_ << std::endl;
std::cout << info.pack_path_ << std::endl;
std::cout << info.real_path_ << std::endl;
std::cout << "-----------"<< " GetOnebyurl" << std::endl;
std::cout<<manager->GetOnebyurl("/download/hello.txt", &info);
info.flag_ = true;
std::cout<<manager->Update(info);
std::cout << info.flag_ << std::endl;
std::cout << info.fsize_ << std::endl;
std::cout << info.atime_ << std::endl;
std::cout << info.mtime_ << std::endl;
std::cout << info.url_ << std::endl;
std::cout << info.pack_path_ << std::endl;
std::cout << info.real_path_ << std::endl;
std::cout << "-----------"<< " GetAll" << std::endl;
std::vector<cloud::BackupInfo> vb;
std::cout<<manager->GetAll(&vb);
for (auto &e : vb)
{
std::cout << e.flag_ << std::endl;
std::cout << e.fsize_ << std::endl;
std::cout << e.atime_ << std::endl;
std::cout << e.mtime_ << std::endl;
std::cout << e.url_ << std::endl;
std::cout << e.pack_path_ << std::endl;
std::cout << e.real_path_ << std::endl;
}
}
持久化存储实现
将哈希表中的info信息存储到文件中,而且要用到json格式。
bool InitLoad()
{
Fileutil fu(backupfile_);
std::string body;
fu.GetContent(&body);读取文件信息
Json::Value root;
Jsonutil::Unserialize(body,&root);反序列化
root中存在多个BackupInfo,root[i]即可访问一个文件消息,
再来个[]就可以查找特定字段。
for (int i = 0; i < root.size();i++)
{
BackupInfo info;
info.flag_ = root[i]["flag"].asBool();
info.atime_ = root[i]["atime"].asInt();
info.mtime_ = root[i]["mtime"].asInt();
info.fsize_ = root[i]["fsize"].asInt();
info.pack_path_ = root[i]["pack_path"].asString();
info.real_path_ = root[i]["real_path"].asString();
info.url_ = root[i]["url"].asString();
um[info.url_] = info;
}
}
保存
bool Storage()
{
std::vector<BackupInfo> vb;
if (GetAll(&vb) == false) 获取所有文件信息
{
return false;
}
Json::Value root;
for (auto &e : vb) 一个个转为Json
{
Json::Value item;
item["flag"] = e.flag_;
item["atime"] = e.atime_;
item["mtime"] = e.mtime_;
item["fsize"] = (Json::Int)(e.fsize_);
要类型强转,因为Json内部不认识size_t,int,内部对这些类型做了重命名
item["pack_path"] = e.pack_path_;
item["real_path"] = e.real_path_;
item["url"] = e.url_;
最后将一个Json类型的变量添加到root中
root.append(item);
}
// 反序列化
std::string body;
Jsonutil::serialize(&body, root);
// 写入文件
Fileutil fu(backupfile_); ./cloud.dat"
fu.SetContent(body);
return true;
}
目前已有模块总结 util.hpp 获取文件内容属性操作 压缩和解压操作 ,json序列化和反序列
config.hpp:保存配置信息。
data.hpp 管理文件信息。
4 hot.hpp
实现思路:遍历所有文件,检测文件的最后一次访问时间,与当前时间相减,若差值大于我们设定好的热点时间,此时文件为非热点文件,要被压缩,原文件要被删除。
如何获取所有文件的最后一次访问时间,1 从数据管理模块中获取所有文件的备份信息。2 遍历上传路径下的文件,直接获取所有文件属性获取,用第二种,因为数据管理模块没有对访问时间进行更新,主要是更新起来也比较麻烦。
热点管理类设计。
class HotManager
{
private:
int hot_time_;
std::string packfile_suffix_; // 压缩文件后缀
std::string back_dir_; // 备份文件目录,所有上传的文件都放在这里
std::string pack_dir_; // 压缩文件路径
public:
HotManager() // 初始化成员
{
Config *cfg = Config::GetInstance();
back_dir_ = cfg->GetBackfile();
pack_dir_ = cfg->GetPackfile();
packfile_suffix_ = cfg->GetPacksuffix();
hot_time_ = cfg->GetHotime();
Fileutil tmp1(back_dir_);
Fileutil tmp2(pack_dir_); // 创建./backdir和./packdir两个目录
if (tmp1.Exits() == false)
tmp1.creat_director();
if (tmp2.Exits() == false)
tmp2.creat_director();
}
};
开始热点判断。
bool HotJudge(const std::string &filename)
{
Fileutil fu(filename);
int atime = fu.fileAcctime();
int cur_time = time(NULL);
if ((cur_time - atime) > hot_time_) 这个hot_time_就是我们配置信息中的热点时间
return false;
return true;
}
bool RunModel()
{
while (1)
{
Fileutil fu(back_dir_);
std::vector<std::string> vs;
fu.ScanDirectory(&vs);
for (auto &e : vs)
{
if (HotJudge(e)) //是热点文件,不作处理
{
continue;
}
判断是否为热点文件后。通过BackupInfo获取文件属性备份信息,
若是失败,此时表示该文件没有被管理起来,我们要自己设置一份备份信息。
// 压缩
Fileutil tmp(e);
// 获取压缩文件名
BackupInfo info;
DataManager *data = DataManager::GetInstance();
if (data->GetOnebyrealpath(e, &info) == false)
{
info.NewBackupInfo(e);//为e文件获取一份备份信息
}
tmp.Compress(info.pack_path_);
// 删除原文件
tmp.Remove();
info.flag_ = true;
data->Update(info);
}
usleep(1000000);
}
return true;
}
我实现到这里发现数据管理模块估计都没必要保存访问时间了。但是已经实现了,就懒得删了,后续我将这个信息用于页面展示了。
这个热点判断是死循环执行的,显然是需要一个线程专门做热点判断。
测试
第一次运行的时候由于压缩目录和备份文件目录没有创建,那scanf浏览目录就会出错,所以我们要判断创建一下。
运行结束创建了两个目录,看看三十秒后是否会被压缩。
5 service.hpp
这个模块是处理请求模块,也就是负责网络通信的模块,ip和端口是必备的,我们学习网络实现了好多版本的服务端了,每个都是要端口的,不过由于我是云服务器,无法绑定ip,ip只能给0.0.0.0,表示任意ip,此时只要服务器收到的数据是给我程序对应的端口,就不校验ip,直接给我这个服务端,不过客户端是要知道服务端的公网ip的,不然如何找到服务端。server_就是我们的服务端实现,我们复用了一下httplib库,就不用自己实现了。
download_prefix是/download/(配置信息保存过,在data.hpp模块中/download/文件名用来做key值),而我们在这个模块同样也需要用到这个虚拟前缀,主要是用来区分不同的Get请求,我曾在config.hpp中提及,如下图。
我们这里结合实现再次强调一下。
因为我们是用的httplib库实现的服务端,httplib库中是将请求路径和处理方法一一对应,/(.*)是正则表达式,可以匹配任意文件名,可是这样就会有个问题,/listshow,我们认为是页面展示请求,这个也会被/(.*)匹配上,然后就变成去调用download函数了,所以我们就让下载路径必须有个前缀,/download/(.*),这样就和/listshow区分开来了。
主要是download_prefix这个虚拟前缀用的地方比较多,所以就当成配置信息保存起来了,/upload和/listshow就直接编码了。
namespace cloud
{
class service
{
private:
int server_port_;
std::string server_ip_;
std::string download_prefix_;
httplib::Server server_;
public:
service()
{
Config *cfg = Config::GetInstance();
ip,端口, download_prefix_都是重要的配置信息,我们专门实现了一个Config模块做管理
server_port_ = cfg->GetPort();
server_ip_ = cfg->GetIp();
download_prefix_ = cfg->GetDprefix();
}
};
};
设置处理函数,我们在Runodel中将请求路径和处理函数一 一对应,httblib库中的服务端会根据访问资源路径(被包含在网址中)和请求方法调用对应的处理函数,即便是我们在浏览器没带请求路径,只有ip和端口,浏览器会默认给个/根目录,表示默认访问展示页面,我们将/listshow也认为是访问资源展示页面。
bool RunModel()
{
server_.Get("/", listshow);
std::string down_load = download_prefix_ + "(.*)";
server_.Get(down_load, download);
server_.Post("/upload", upload);
server_.Get("/listshow", listshow);
server_.listen(server_ip_, server_port_);
return true;
}
接下来就是三个函数的内部实现了。
上传文件请求
由于我们不方便用一个网址发起post请求,必须要借助表单,照着下面打就好了。
然后我们打开就会形成一个可以上传文件的页面,效果如下。
上传请求处理
我们这个先看看请求有没有"file"字段,由于http1.1版本的正文中可以有多个资源,那我们就得分类,file字段是客户端服务端协商的,只要有file字段,表示有普通文件上传,我们直接保存,后面如果还要上传其它特殊文件,要做特殊处理,就用其它字段在请求中找到该资源并处理。
static void upload(const httplib::Request &rq, httplib::Response &rs)
{
if (rq.has_file("file") == false)
{
FATAL("no file 错误码: %d 错误消息: %s",errno,strerror(errno));
return;
}
auto file = rq.get_file_value("file");
std::string name = file.filename;
Config *cfg = Config::GetInstance();
// ./backdir/文件名
std::string realpath = cfg->GetBackfile() + Fileutil(name).Filename();
Fileutil fu(realpath); 创建文件并写入文件内容
if (fu.SetContent(file.content) == false)
{
FATAL("up load Setcontent err 错误码: %d 错误消息: %s",errno,strerror(errno));
return;
}
修改文件管理信息
BackupInfo info;
info.NewBackupInfo(realpath);要用真实路径,内部要获取文件属性
DataManager *data = DataManager::GetInstance();
data->Insert(info);
}
测试
看看文件是否可以上传上来到文件上传路径下,此时我们还没有对文件做热点管理,但是此时数据管理模块和管理信息的文件中应该要有我们上传的文件,直观的就是那个展示页面有新文件。
页面展示请求处理
static void listshow(const httplib::Request &rq, httplib::Response &rs)
{
std::stringstream ss;
ss << "<html><head><title>Download</title></head><body><h1>Download</h1>
<table>";
std::vector<BackupInfo> vb;
DataManager *data = DataManager::GetInstance();
// 获取全部文件信息
data->GetAll(&vb);
for (auto &e : vb)
{
Fileutil fu(e.real_path_);
ss << "<tr>"
<< "<td><a href='" << e.url_;
ss << "'>" << fu.Filename() << "</a></td>";
time_t time = e.mtime_;
ss << "<td align='right'> " << ctime(&time) << "</td>";
ss << "<td align='right'> " << e.fsize_ / 1024 << "k </td>";
不能用fu去获取文件属性,因为文件不一定在/backdir下,可能在压缩路径下了
}
ss << "</tr> </table></body></html>";
rs.body = ss.str();
rs.set_header("Content-Type", "text/html");
rs.status = 200;
}
我是照着这个前端页面写的,我走后端开发,懒得在前端页面花太多时间。
效果:
下载请求处理
先由资源路径从文件数据管理中获取文件信息,判断是否被压缩,然后进行解压,所以为什么要有压缩标记位,就是在下载请求的时候看看文件在不在压缩目录下。
static void download(const httplib::Request &rq, httplib::Response &rs)
{
// 1 获取文件信息
BackupInfo info;
DataManager *data = DataManager::GetInstance();
data->GetOnebyurl(rq.path, &info);
// 2 判断文件是否被压缩
if (info.flag_ == true) // 文件被压缩
{
Fileutil fu(info.pack_path_);
fu.UnCompress(info.real_path_); // 传入解压文件名
这个real_path_是文件上传路径+文件名
fu.Remove();删除压缩文件
更新文件信息
info.flag_ = false;
data->Update(info);
}
// 判断是否是断点续传请求
std::string old_etag;
bool isRange = false;
if (rq.has_header("If-Range"))
{
old_etag = rq.get_header_value("If-Range");
if (old_etag == GetEtag(info)) // 是断点续传且文件未被修改
isRange = true;
}
// 3 读取文件信息给响应
Fileutil fu(info.real_path_);
fu.GetContent(&rs.body);
if (isRange == false)
{
// 4 设置响应报头
rs.set_header("Accept-Ranges", "bytes");
rs.set_header("Content-Type", "application/octet-stream");
rs.set_header("Etag", GetEtag(info)); // Etag:
rs.status == 200;
}
else
{
// 4 设置响应报头
rs.set_header("Accept-Ranges", "bytes");
rs.set_header("Etag", GetEtag(info)); // Etag:
rs.status == 206;
}
}
响应中设置了ETag字段,客户端第一次下载就会收到这个响应信息,因为断点续传就是客户端下载了一半,然后暂停下载,过一会要下载另一半,但是如果文件变了,就要重新下载了,因为可能之前下载的一半在服务端发生了变更,那如何查看文件是否变更呢,就是看ETag字段,此时客户端要下载另一半会重新发起下载请求,请求报文中就带着上次服务端传的ETag字段,我们会再对下载文件制作一个ETag字段,和旧的作对比,相同的话就会接着发送另一半,不然就重新发送。(这个在断点续传实现中再体会。)
static std::string GetEtag(const BackupInfo &info)
{
Fileutil fu(info.real_path_);
std::string ret = fu.Filename();
ret += "-";
ret += std::to_string(fu.fileSize());
ret += "-";
ret += std::to_string(info.mtime_);
return ret;
}
因为每次解压压缩文件修改时间都会变,那Etag就会变,这样外部就每次都要下载了,因为ETag不一样了,所以不能重新获取,所以我们就用info中的修改时间,当前文件大小和文件名做ETag。
测试
看看点击这个页面中的文件是否可以下载文件。
断点续传原理和实现
场景:因为当文件下载时,如果出现异常而中断,此时如果为了节省效率,我们希望可以继续下载,而非重头下载,这就是断点续传的使用场景。
原理: 客户端每次请求带上请求范围,然后接受的时候就记录接收的数据,这样即便中断也知道哪些没收到了。
断点续传第二次下载请求报文大致如下。请求报文会有个if-Range字段,保存先前收到的ETag字段。Range:字段则是保存下载范围。
响应: 也会返回ETag,重要的是状态码206,表示处理了部分Get请求。
那Accept-Ranges字段是干什么的,这个是服务端用来告诉客户端自己支持断点续传的,如果没有这个字段客户端就知道服务端只能发送一整个资源,然后就不会发送Range字段给服务端,表现在上层就是你一暂停下载文件就要重新下载。
实现
断点续传还是属于下载请求,所以这个也在下载函数中处理。
get_header_value获取ETag字段,如果没有if-Range字段,则是正常下载,若是有,若是这个值与当前文件的E-Targ,也要返回整个文件。
当isRange为false时,表示不是断点续传,我们就正常返回就好了。
如果是true,此时我们就进入了else中,这里本来我们是要获取请求返回,读取文件指定范围,结果httplib库已经支持了断点续传,我们就不实现了。它内部是根据我们的响应正文进行截取的,所以你如果自己又实现了,那就会截取两次,下载就不全了。甚至我们可以不实现else部分,httplib库会自己添加。
整体功能联合测试
注意:数据管理模块不用单独启动,而是由热点管理模块和server模块对这个数据管理模块做更新。
注意:压缩了后对展示页面无影响,因为页面上显示的文件信息是从数据管理模块读取的。
压缩文件可以考虑用多线程。
二 客户端实现
客户端扫描指定目录下的文件,自动对指定文件夹文件进行备份,如果有新增和修改就要上传,可是如何判断新增呢?
我举个例子,我们怎么知道一个班级是不是有新增的同学呢,先获取当前所有在教室同学的名字,然后拿着以前的点名册一看谁的名字没登记却在教室那就一定是新增的,所以如果我们有一个模块对指定目录下的文件名做记录,然后定期循环遍历获取这个目录下的文件名,看看是否已经保存过了,没保存过的就是新增文件,此时我们只需要大致了解会有文件管理模块和文件上传模块即可。
1 util.hpp
遍历目录的功能我们之前在服务端实现过了,直接拷贝一份,但是我们这里不需要压缩,懒得搞这么复杂,因为重点是在服务端的实现,至于为什么不要Json序列化我们在数据管理模块再提。
#include <time.h>
#include <string>
#include <iostream>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <vector>
#include <memory>
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
namespace cloud
{
class Fileutil
{
public:
Fileutil(const std::string filename)
: filename_(filename)
{
;
}
void Remove()
{
remove(filename_.c_str());
}
// 获取文件属性,包括大小,最近访问时间,最近修改时间
int fileSize()
{
struct stat buf;
if (stat(filename_.c_str(), &buf) < 0)
{
std::cout << "stat err" << std::endl;
return -1;
}
return buf.st_size;
}
int fileModtime()
{
struct stat buf;
if (stat(filename_.c_str(), &buf) < 0)
{
std::cout << "stat err" << std::endl;
return -1;
}
return buf.st_mtime;
}
int fileAcctime()
{
struct stat buf;
if (stat(filename_.c_str(), &buf) < 0)
{
std::cout << "stat err" << std::endl;
exit(-1);
}
return buf.st_mtime;
}
std::string Filename()
{
//./a/b/c
int pos = filename_.find_last_of("\\");
// test.txt
return filename_.substr(pos + 1);
}
bool GetPosLen(int pos, int len, std::string* body)
{
if (pos + len > fileSize()) // 以免读取越界
{
std::cout << "Get err:over size " << std::endl;
return false;
}
std::ifstream ifs;
ifs.open(filename_.c_str(), std::ios::binary);
if (ifs.is_open() == false)
{
std::cout << "Get open err: " << errno << std::endl;
ifs.close();
return false;
}
ifs.seekg(pos, std::ios::beg);
// 获取文件数据
body->resize(len);
ifs.read(&(*body)[0], len);
ifs.close();
return true;
}
bool GetContent(std::string* body)
{
return GetPosLen(0, fileSize(), body);
}
bool SetContent(const std::string& body)
{
std::ofstream ofs;
// 打开文件将数据写入
ofs.open(filename_, std::ios::binary); // 会创建文件
if (ofs.is_open() == false)
{
std::cout << "set content open err: " << errno << std::endl;
ofs.close();
return false;
}
ofs.write(&body[0], body.size());
ofs.close();
return true;
}
bool Exits()
{
return fs::exists(filename_);
}
bool creat_director()
{
if (Exits())
{
return true;
}
return fs::create_directories(filename_);
}
bool ScanDirectory(std::vector<std::string>* vs)
{
if (Exits() == false)
{
creat_director();
}
for (auto p : fs::directory_iterator(filename_))
{
if (fs::is_directory(p))
continue;
vs->push_back(fs::path(p).relative_path().string());
}
return true;
}
private:
std::string filename_;
};
}
测试
void testutil()
{
cloud::Fileutil fu(".\");
std::vector<std::string> vs;
fu.ScanDirectory(&vs);
for (auto& e : vs)
{
std::cout << e << std::endl;
}
}
结果如下,.\是我们之间实例化Fileutil类传入的,ScanDirectory函数返回的文件名是你传入的目录名+文件名。
2 data.hpp
在服务端我们将管理文件数据序列化到了文件中,这个过程靠的是Json库,但是我们客户端没有那么多要保存的信息,所以我们自己实现序列化即可。格式如下:
文件路径名(非完整路径),空格,然后是一个文件的标识符,标识符组成后提,到这里可能会奇怪,只保存文件名不可以吗?不行,这个标识符对我们判断文件是否被修改有着特别重要的意义,后面文件上传模块才需要判断文件是否被修改,我们这里把文件数据管理就好了。
#include<unordered_map>
#include"util.h"
#include<string>
#include<sstream>
#define BACKUPFILE ".\backup.txt"
namespace cloud
{
class DataManager
{
private:
std::unordered_map<std::string, std::string> um_;
std::string backupfile_; // 备份文件路径"./cloud.dat"
DataManager(const std::string& backupfile)
:backupfile_(backupfile)
{
InitLoad();
}
public:
void Substr(const std::string& body, std::string sep, std::vector<std::string>* vs)
{
int pos, index = 0;
while (1)
{
pos = body.find(sep, index);
if (pos == -1)
break;
vs->push_back(body.substr(index, pos - index));
index = pos + sep.size();
}
vs->push_back(body.substr(index));
}
bool Storage()
{
std::stringstream ss;
for (auto& e : um_)
{
ss << e.first << " " << e.second << "\n";
}
Fileutil fu(backupfile_);
fu.SetContent(ss.str());
return true;
}
bool InitLoad()
{
//1 从文件中读取数据
Fileutil fu(backupfile_);
std::string body;
fu.GetContent(&body);
//2 解析数据放到表中
std::vector<std::string> vs;
Substr(body, "\n", &vs);
for (auto& e : vs)
{
std::vector<std::string> tmp;
Substr(body, " ", &tmp);
if (tmp.size() != 2)
return false;
um_[tmp[0]] = tmp[1];
}
return true;
}
};
}
增改查实现。
static DataManager* GetInstance()
{
static DataManager data(BACKUPFILE);
return &data;
}
bool Insert(const std::string& key, const std::string& value)
{
um_[key] = value;
Storage();
return true;
}
bool GetOneBykey(const std::string& key, std::string* value)//找不到返回false
{
if (um_.find(key) == um_.end())
return false;
*value = um_[key];
return true;
}
bool Update(const std::string& key, const std::string& value)
{
um_[key] = value;
Storage();
return true;
}
3 cloud.hpp
客户端文件备份类,也就是负责网络通信了。既然要上传文件,那总得知道哪些文件要上传吧,所以就要对数据管理模块中的文件信息做分析,还有就是网络通信的实现,类成员如下。
我们先前已经在data.hpp已经对监控文件夹中的文件信息做了记录,但是判断文件是新增,还是被修改了不是由数据管理模块完成的,而是由我们这个类完成,我们从下面实现来理解。
文件唯一标识获取
std::string GetIndetifi(const std::string filename)
{
Fileutil fu(filename);
std::string body;
fu.GetContent(&body);
std::string ret;
ret += fu.Filename() + "-" + std::to_string(fu.fileSize())
+ "-" + std::to_string(fu.fileModtime());
return ret;
}
运行函数实现。每次遍历指定目录下的文件,获取文件名,更新管理信息。
bool RunModel()
{
while (1)
{
// 1 遍历指定文件夹
Fileutil fu(back_dir);
std::vector<std::string> vs;
fu.ScanDirectory(&vs);
// 2 逐个判断是否要上传
for (auto& e : vs)
{
if (IsNeedUpload(e) == true)
{
if (upload(e) == true)//上传成功,添加管理信息
{
std::string id = GetIndetifi(e);
data_->Insert(e, id);
}
}
}
Sleep(1000);
}
}
upload实现和IsNeedUpload实现。
如果文件是新增要上传,如果有修改也要上传,修改就是看文件标识符。
优化,文件不是一修改就要上传,就像是我们在csdn上写一样,是隔一段时间才上传保存,太过频繁,吃力不讨好,所以增加了一个判断,要修改时间超过一定值。
bool upload(const std::string filename)
{
1 创建客户端
httplib::Client client(SERVER_IP,SERVER_PORT);
Fileutil fu(filename);
std::string body;
fu.GetContent(&body);
file是一个标识符,标识是个普通文件,服务端那边会根据file字段找到上传数据做处理
后续我们可能上传视频等其它特殊文件,服务端会在上传请求函数中根据标识符
处理不同文件数据
httplib::MultipartFormData item;
item.name = "file";
item.filename = fu.Filename();
item.content_type = "application/octet-stream";
item.content = body;
httplib::MultipartFormDataItems items;
items.push_back(item);
返回一个响应
auto res = client.Post("/upload", items);
if (!res || res->status != 200)
return false;
return true;
}