代码已上传 gitee
目录
热点管理模块
热点管理模块之前应该提到过,
我们服务端收到文件后,
需要对文件的状态进行检测,
一但发现一个文件长时间未访问,
即从热点文件变成了非热点文件,
那么就可以对它进行压缩,
节省空间。
实现起来非常简单,
大致思路就是:
循环遍历备份文件夹backdir
,
把里面所有的文件信息拿到,
最后根据信息判断是否需要压缩。
CloudBackup/src/hotfilemanager.hpp
#pragma once
#include "datamanager.hpp"
#include <unistd.h>
namespace Cloud
{
const useconds_t sleepTime = 2000000; // 每次两秒
class HotFileManager
{
public:
bool isHotFile(const std::string& filePath);
void run();
void packFile(const std::string& path);
};
} // namespace Cloud
嘿嘿,你没有看错,
就三个函数,
一个成员变量都没有。
把它单独拎到一个模块中,
主要目的是方便日后进行扩展。
run 启动!
void run()
{
while (1)
{
std::vector<std::string> pathes;
FileUtils(Config::getInstance()->getBackDir()).scanDirectory(&pathes); // 这里只扫描 /backdir/
for (const auto &path : pathes)
{
if (isHotFile(path))
continue;
packFile(path);
}
usleep(sleepTime);
}
}
扫描文件夹,用的是 FileUtils
的 scanDirectory
,
需注意,这里只用扫描 backdir
isHotFile 判断热点文件
bool isHotFile(const std::string &filePath)
{
size_t aTime = FileUtils(filePath).getATime();
size_t mTime = FileUtils(filePath).getMTime();
size_t t = aTime > mTime ? aTime : mTime;
size_t hotTime = Config::getInstance()->getHotTime();
size_t curTime = time(nullptr);
std::cout << filePath << ": " << curTime << ' ' << t << ' ' << hotTime << std::endl;
return curTime - t < hotTime;
}
curTime - t < hotTime
则是热点文件,不用压缩。
hotTime
是配置文件里的,随时可改。
至于 t
,我本来想就用 atime
的,
后来上网搜了下 :
“mtime
改变,atime
一定跟着改变吗?”
然后发现不会。
所以我就两者取最大了。
packFile 压缩文件
void packFile(const std::string &path)
{
std::cout << "begin packFile\n";
std::string lhs = Config::getInstance()->getPackDir(), // 添加 /packdir/前缀
name = FileUtils(path).getFileName(), // 去掉 /backdir/前缀
rhs = Config::getInstance()->getPackfileSuffix(), // 添加 .lz 后缀
dst = lhs + name + rhs;
FileUtils(path).compress(dst);
// 先改信息:
FileInfo fi(path);
fi._isPacked = true;
// 再remove:
FileUtils(path).remove();
// 再保存:
DataManager::getInstance()->update(fi);
std::cout << "packed success: " << dst << std::endl;
// 注:等它真的压缩了再更新信息,
// 不能为了表示要更新信息, 就把update放函数开头。
}
这里出过问题,
我把FileInfo
的修改提前了:
因为我想FileInfo
反正都会改,
那为什么不能放在函数开头呢?
结果导致这里还在压缩,
另一边直接去压缩文件夹(packdir
)找文件了。
测试
CloudBackup/src/testhotfilemanager.cpp
#include "hotfilemanager.hpp"
int main()
{
Cloud::HotFileManager().run();
return 0;
}
接下来开始实现业务处理模块,
主要就是使用 cpp-httplib
,
处理服务端传的 Get
和 Post
请求。
那么,有哪些请求呢?
文件查看,文件下载,文件上传。
就这三个。
那么围绕这三点编写代码就行了:
CloudBackup/src/Service.hpp
#pragma once
#include "datamanager.hpp"
#include "httplib.h"
#include <ctime>
#include <utime.h>
namespace Cloud
{
std::string timeToStr(time_t time);
std::string getETag(FileInfo fileInfo);
class Service
{
public:
Service()
: _serverPort(Config::getInstance()->getServerPort()),
_serverIp(Config::getInstance()->getServerIp()),
_downloadPrefix(Config::getInstance()->getDownloadPrefix()),
_server() {}
bool run();
static void upload(const httplib::Request& req, httplib::Response& rsp);
static void show(const httplib::Request& req, httplib::Response& rsp);
static void showOnlineupload(const httplib::Request& req, httplib::Response& rsp);
static void download(const httplib::Request& req, httplib::Response& rsp);
static void showSuccess(const httplib::Request& req, httplib::Response& rsp);
private:
int _serverPort;
std::string _serverIp;
std::string _downloadPrefix;
httplib::Server _server;
};
} // namespace Cloud
成员变量:
_serverPort
、_serverIp
、_downloadPrefix
从配置文件中读。
_server
这个就是 httplib 的服务端。
成员函数:
upload
,处理上传文件的请求。
show
,展示已上传文件信息的界面。
showOnlineupload
,展示在线上传文件的界面。
download
,处理下载文件的请求。
showSuccess
,展示在线上传文件成功的界面。
非成员函数:(辅助用)
timeToStr
,时间戳转字符串。
getETag
,获取文件唯一标识。
业务处理模块
run 启动!
bool run()
{
std::cout << "run" << std::endl;
_server.Get("/", show);
_server.Get("/show", show);
_server.Get("/onlineupload", showOnlineupload);
_server.Get("/success", showSuccess);
_server.Get(_downloadPrefix + "(.*)", download);
_server.Post("/upload", upload);
std::cout << "begin listen" << std::endl;
_server.listen("0.0.0.0", Config::getInstance()->getServerPort());
std::cout << "listen over" << std::endl;
return true;
}
为不同的 URL
路径注册对应的请求处理函数,
然后开始监听。
upload 上传文件
static void upload(const httplib::Request &req, httplib::Response &rsp)
{
std::cout << "get a upload request\n";
if (req.has_file("uploadFile") == false)
{
std::cout << "no file upload\n";
rsp.status = 400;
return;
}
const auto &file = req.get_file_value("uploadFile");
std::string backPath = Config::getInstance()->getBackDir() + file.filename;
std::cout << backPath << std::endl;
rsp.body.clear();
FileUtils(backPath).setContent(file.content);
DataManager::getInstance()->insert(FileInfo(backPath)); // 这个顺序不能反
rsp.set_header("Location", "/success");
rsp.status = 302;
}
这个 "uploadFile"
,和客户端统一就行,可以改成其他名字。
然后获得文件路径,再用路径配合FileUtils
设置文件内容。
此时,文件真正被创建,信息都有了,
再 insert(FileInfo(backPath))
。
注:如果 insert
提前,你会发现又报错了!
rsp.set_header("Location", "/success");
告诉客户端跳到 /success
去。
rsp.status = 302;
设置响应状态码为 302,表示这是一个临时重定向。
showOnlineupload 在线上传文件界面
static void showOnlineupload(const httplib::Request &req, httplib::Response &rsp)
{
std::stringstream ss;
ss << R"(<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>文件上传</title><style>body {font-family: Arial, sans-serif; background-color: #f4f7fb; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh;} .upload-container {background-color: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); text-align: center; width: 100%; max-width: 400px;} .upload-container h2 {font-size: 24px; margin-bottom: 20px;} .upload-container p {font-size: 16px; color: #666; margin-bottom: 20px;} .file-input {margin: 20px 0; padding: 15px; width: 100%; border: 2px solid #007bff; border-radius: 5px; background-color: #f9f9f9; cursor: pointer; font-size: 16px; color: #333; box-sizing: border-box;} .file-input:hover {background-color: #e7f0ff; border-color: #0056b3;} .submit-button {padding: 10px 20px; background-color: #007bff; border: none; border-radius: 5px; color: white; font-size: 16px; cursor: pointer; transition: background-color 0.3s;} .submit-button:hover {background-color: #0056b3;}</style></head><body><div class="upload-container"><h2>上传文件</h2><p>请选择您要上传的文件,支持多种格式。</p><form action="http://113.44.51.126:8899/upload" method="post" enctype="multipart/form-data"><input type="file" name="uploadFile" class="file-input" required><br><input type="submit" value="上传" class="submit-button"></form></div></body></html>)";
rsp.set_content(ss.str(), "text/html");
rsp.status = 200;
}
效果:
showSuccess 在线上传成功界面
static void showSuccess(const httplib::Request &req, httplib::Response &rsp)
{
std::stringstream ss;
ss << R"(<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>上传成功</title><style>body {font-family: Arial, sans-serif; background-color: #f4f7fb; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh;} .success-container {background-color: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); text-align: center; width: 100%; max-width: 400px;} .success-container h2 {font-size: 24px; margin-bottom: 20px;} .success-container p {font-size: 16px; color: #666; margin-bottom: 20px;} .home-button {padding: 10px 20px; background-color: #007bff; border: none; border-radius: 5px; color: white; font-size: 16px; cursor: pointer; transition: background-color 0.3s;} .home-button:hover {background-color: #0056b3;}</style></head><body><div class="success-container"><h2>上传成功!</h2><p>您的文件已经成功上传。</p></div></body></html> )";
rsp.set_content(ss.str(), "text/html");
rsp.status = 200;
}
效果:
show 展示文件
static void show(const httplib::Request &req, httplib::Response &rsp)
{
std::stringstream ss;
ss << R"(<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Download Page</title><style>body{font-family:Arial,sans-serif;background-color:#f4f4f4;margin:0;padding:0}header{background-color:#007BFF;color:#fff;text-align:center;padding:20px 0;box-shadow:0 2px 5px rgba(0,0,0,.1)}h1{margin:0;font-size:36px}.container{width:80%;margin:30px auto;padding:20px;background-color:#fff;box-shadow:0 4px 8px rgba(0,0,0,.1);border-radius:8px}table{width:100%;table-layout:fixed;border-collapse:collapse;margin-top:20px}th,td{padding:12px;text-align:right;border-bottom:1px solid #ddd;word-wrap:break-word}th{background-color:#f2f2f2;color:#333;font-size:18px}td{font-size:16px}td.file-info{text-align:right;color:#888}a{text-decoration:none;color:#fff;background-color:#28a745;padding:10px 15px;border-radius:5px;font-weight:bold;display:inline-block;text-align:center;transition:background-color .3s ease}a:hover{background-color:#218838}th:nth-child(1),td:nth-child(1),th:nth-child(2),td:nth-child(2),th:nth-child(3),td:nth-child(3),th:nth-child(4),td:nth-child(4){width:25%}@media(max-width:768px){.container{width:95%}h1{font-size:28px}td{font-size:14px}a{padding:8px 12px;font-size:14px}}</style></head><body><header><h1>Download Your File</h1></header><div class="container"><table><thead><tr><th>File Name</th><th>Last Modified</th><th>Size</th><th>Action</th></tr></thead><tbody>)";
std::vector<FileInfo> arr;
DataManager::getInstance()->getFileInfo(&arr);
for (const auto &fileInfo : arr)
{
ss << "<tr>";
ss << "<td>" << FileUtils(fileInfo._backPath).getFileName() << "</td>";
ss << "<td>" << timeToStr(fileInfo._atime) << "</td>";
ss << "<td>" << fileInfo._fsize / 1024 << "k</td>";
ss << "<td><a href='" << fileInfo._url << "'>Download</a></td>";
ss << "</tr>";
}
ss << R"(</tbody></table></div></body></html>)";
rsp.set_content(ss.str(), "text/html");
rsp.status = 200;
}
我们处理一下列表部分就行。
其中的 timeToStr
:
std::string timeToStr(time_t time)
{
char str[100]{};
ctime_r(&time, str);
return str;
}
注意别直接用 ctime
,线程不安全。
show
的展示效果:
download 下载
static void download(const httplib::Request &req, httplib::Response &rsp)
{
std::cout << "get a download request\n";
FileInfo fileInfo;
if (DataManager::getInstance()->getInfoByURL(req.path, &fileInfo) == false)
{
std::cout << "request file not exist\n";
return;
}
if (fileInfo._isPacked)
{
std::cout << "file isPacked, decompress to : " << fileInfo._backPath << std::endl;
FileUtils(fileInfo._packPath).deCompress(fileInfo._backPath);
fileInfo._isPacked = false;
DataManager::getInstance()->update(fileInfo);
FileUtils(fileInfo._packPath).remove(); // 这个记得加
}
struct utimbuf new_times = {time(NULL), fileInfo._mtime};
if (utime(fileInfo._backPath.c_str(), &new_times) == -1)
std::cerr << "atime update error!\n";
else
std::cout << "atime update success!\n";
std::string eTag = getETag(fileInfo);
bool isRange = false;
if(req.has_header("If-Range"))
{
std::string clientETag = req.get_header_value("If-Range");
if(eTag == clientETag)
{
isRange = true;
}
}
FileUtils(fileInfo._backPath).getContent(&rsp.body);
rsp.set_header("Accept-Ranges", "bytes"); // 用来支持断点续传的
rsp.set_header("ETag", eTag); // 唯一标识
rsp.set_header("Content-Type", "application/octet-stream");
rsp.status = isRange ? 206 : 200;
}
这部分最麻烦,所以放在了最后讲。
首先,我们得知道客户想要下载哪个文件,
数据管理模块有个哈希表专门干这件事,
所以我们 getInfoByURL(req.path, &fileInfo)
就行。
而,刚刚才写的热点管理模块,
会把非热点文件直接压缩、放进 packdir
,
所以得先判断 if (fileInfo._isPacked)
,
如果已压缩,
需要先解压到 backdir
,
再进行后面的步骤。
注:这里一定要记得删掉压缩文件,和改状态。
那个热点管理模块。。。有点呆,
时间一到,好!压缩!
又因为测试时,热点时间不能设长了,
所以得想想办法和热点管理模块抢时间。
(毕竟你可能还在下载,它就又给你压缩了)
目前,我想到的是,
每次 download
时,改改文件的 atime
。
本来是这样写的:
if(!std::ifstream(fileInfo._backPath))
std::cerr << "open file to update atime error\n";
结果发现没用,改不了,
查了查,似乎又是什么优化之类的。
所以我选择直接设置:
struct utimbuf new_times = {time(NULL), fileInfo._mtime};
if (utime(fileInfo._backPath.c_str(), &new_times) == -1)
std::cerr << "atime update error!\n";
else
std::cout << "atime update success!\n";
顺带一提,如果没有解压缩,
单纯的下载请求是不会改 atime
的。
至少我的不会。
最后是支持断点续传,
其实这部分 httplib 已经处理好了,
我们用就行。(具体怎么处理的,等我有空再写)
传输文件的过程,
可能因为种种情况中断,
之后,客户端可以再次发起请求,
而服务端会从刚刚断掉的地方开始继续传。
这就是断点续传。
如果断掉的过程中,文件被修改了呢?
所以我们需要一个唯一标识ETag
,
通过对比当前的eTag
和客户传来的之前的clientETag
,
判断是 传部分数据 还是 整个重新传:
std::string eTag = getETag(fileInfo);
bool isRange = false;
if(req.has_header("If-Range"))
{
std::string clientETag = req.get_header_value("If-Range");
if(eTag == clientETag)
{
isRange = true;
}
}
getETag
比较简陋,不过能跑:
std::string getETag(FileInfo fileInfo)
{
return fileInfo._url + std::to_string(fileInfo._mtime);
}
用到的是文件的URL和最后修改时间。
最后需要设置两个字段:
rsp.set_header("Accept-Ranges", "bytes"); // 用来表示支持断点续传
rsp.set_header("ETag", eTag); // 唯一标识
测试
CloudBackup/src/testservice.cpp
#include "service.hpp"
int main()
{
Cloud::Service().run();
return 0;
}
我们联合!
热点文件管理模块有个死循环,
业务处理模块也会一直卡在监听。
为了让程序正常跑起来,
很明显,main函数需要两个线程。
CloudBackup/src/Server.cpp
#include "hotfilemanager.hpp"
#include "service.hpp"
#include <thread>
void h()
{
Cloud::HotFileManager().run();
}
void s()
{
Cloud::Service().run();
}
int main()
{
std::thread thread_h(h);
std::thread thread_s(s);
thread_h.join();
thread_s.join();
return 0;
}
而写到这里,服务端就已经实现完了,
下一篇将速速过掉客户端。
希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!