项目日记 -云备份 -服务端热点管理模块、业务处理模块

发布于:2025-04-02 ⋅ 阅读:(17) ⋅ 点赞:(0)

博客主页:【夜泉_ly
本文专栏:【项目日记-云备份
欢迎点赞👍收藏⭐关注❤️

在这里插入图片描述
代码已上传 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);
    }
}

扫描文件夹,用的是 FileUtilsscanDirectory
需注意,这里只用扫描 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
处理服务端传的 GetPost 请求。
那么,有哪些请求呢?
文件查看,文件下载,文件上传。
就这三个。
那么围绕这三点编写代码就行了:
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语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!


网站公告

今日签到

点亮在社区的每一天
去签到