项目编写
工具类设计
我们在实际做项目的使用会用到许多文件操作 我们如果是用到的时候才去写就会导致效率较低并且代码冗余复用率低 因此我们可以在项目编写之前就设计出一个工具类 当我们需要使用该工具类的时候只需要定义一个对象即可
对于该工具类的函数声明如下
/*util.hpp*/ class FileUtil{ private: std::string _name; public: FileUtil(const std::string &name); size_t FileSize(); time_t LastATime(); time_t LastMTime(); std::string FileName(); bool GetPosLen(std::string *content, size_t pos, size_t len); bool GetContent(std::string *content); bool SetContent(std::strint *content); bool Compress(const std::string &packname); bool UnCompress(const std::string &filename); bool Exists(); bool CreateDirectory(); bool ScanDirectory(std::vector<std::string> *arry); };
下面我会带大家完成整个工具类
属性和名称获取
首先我们定义的函数名称可能会和系统库或者第三方库的名称冲突 所以说我们在完成工具类的时候最好是在一个命名域里面操作
我们在查找文件的各种属性的时候使用的一个系统函数叫做stat
它在windows和Linux平台下都存在 所以说我们不用担心跨平台移植性
stat函数
函数原型如下
int stat(const char* path , struct stat *buf)
参数说明:
- const char* path 是我们要寻找的路径(是一个字符串)
- struct stat *buf 这是一个结构体 我们通过该结构体来查看文件的信息
返回值说明:
如果找到该文件返回0 如果没找到返回-1
文件大小获取
int64_t FileSize() { struct stat st; int ret = stat(_filename.c_str(), &st); if (ret < 0) { std::cout << "Get file size failed!" << std::endl; return -1; } return st.st_size; }
文件最后修改时间获取
time_t LastModifyTime() { struct stat st; int ret = stat(_filename.c_str() , &st); if (ret < 0) { std::cout << "Get lastmodifytime failed!" << std::endl; return -1; } return st.st_mtime; }
文件最后访问时间获取
time_t LastAccessTime() { struct stat st; int ret = stat(_filename.c_str() , &st); if (ret < 0) { std::cout << "Get lastaccesstime failed!" << std::endl; return -1; } return st.st_atime; }
文件名获取
为什么要要获取文件名呢
因为用户在上传文件名的时候可能连带着路径一起上传了 可是我们只需要一个文件名
一般来说Linux下路径的格式如下 xxx/yyy/test.c
如果我们要想获取最后这个文件名我们需要以最右边的 /
的后一个字符开始往后截取所有内容
代码表示如下
std::string FileName() { size_t pos = _filename.rfind("/"); if (pos == std::string::npos) { return _filename; } return _filename.substr(pos+1); }
功能测试
这里要跟同学们强调的一点是 我们在自主开发一个项目的时候一定要写一段测试一段
不然到最后可能会出现bug比代码行数都多的现象 这样子调试起来就很头疼了
下面编写一段代码来测试下上面实现的功能
#include "Util.hpp" void TestUtil(const std::string& filename) { shy::FileUtil fu(filename); std::cout << fu.FileName() << std::endl; std::cout << fu.FileSize() << std::endl; std::cout << fu.LastAccessTime() << std::endl; std::cout << fu.LastModifyTime() << std::endl; return; } void Useage() { std::cout << "./main + FileName" << std::endl; return; } int main(int argc , char* argv[]) { if (argc != 2) { Useage(); return -1; } std::string filename = argv[1]; TestUtil(filename); return 0; }
测试效果如下
我们发现各函数使用效果正常
文件读写操作
获取指定位置往后指定长度的数据
bool GetPosLen(std::string& body , size_t pos , size_t len) { size_t fsize = this->FileSize(); if (pos + len < fsize) { std::cout << "Get file len failed!" << std::endl; return false; } std::ifstream ifs; ifs.open(_filename.c_str() , std::ios::binary); if (ifs.is_open() == false) { std::cout << "open file failed!" << std::endl; return false; } ifs.seekg(pos , std::ios::beg); body.resize(len); ifs.read(&body[0] , len); if (ifs.good() == false) { std::cout << "read file failed!" << std::endl; return false; } ifs.close(); return true; }
获取所有文件数据
我们直接复用上面的函数即可
bool GetContent(std::string& body) { size_t fsize = this->FileSize(); return this->GetPosLen(body , 0 , fsize); }
写入文件数据
bool SetContent(std::string& body) { std::ofstream ofs; ofs.open(_filename.c_str() , std::ios::binary); if (ofs.is_open() == false) { std::cout << "open ofs failed" << std::endl; return false; } ofs.write(&body[0] , body.size()); if (ofs.good() == false) { std::cout << "ofs write failed!" << std::endl; return false; } ofs.close(); return true; }
接下来我们测试上面所写的函数
std::string body; fu.GetContent(body); shy::FileUtil nfu("test.txt"); nfu.SetContent(body);
我们在当前文件下创建一个叫做test.txt的文件 如果说我们的读写操作没问题 那么我们的源文件里面的内容应该和body里面的内容一致
测试结果如下
结果符合预期
文件压缩和解压缩
文件压缩和文件解压我们直接使用Bundle库中的函数即可
代码表示如下
压缩
bool Compress(const std::string &packname) { // 1. get source date std::string body; if (this->GetContent(body) == false) { std::cout << "compress get file content failed" << std::endl; return false; } // 2. compress date std::string packed = bundle::pack(bundle::LZIP , body); // 3 write comress date to packname FileUtil fu(packname); if (fu.SetContent(packed) == false) { std::cout << "write compress date failed" << std::endl; return false; } return true; }
解压缩
bool uncompress(std::string& unpackname) { // 1. get compress file name std::string body; if (this->GetContent(body) == false) { std::cout << "get Compress file content failed" << std::endl; return false; } // 2 . uncompress file std::string unpacked = bundle::unpack(body); // 3. write unpacked file FileUtil fu(unpackname); if (fu.SetContent(unpacked) == false) { std::cout << "uncompress write unpacked file failed" << std::endl; return false; } return true; }
演示效果如下
我们发现md5值一样 也就是说压缩和解压缩功能正常
目录操作
在C++17版本中 给我们提供了一个很方便的文件操作库
我们可以使用这个库来进行一些文件目录操作
他的头文件为 <experimental/filesystem>
为了简化我们后面的操作 我们将其命名域重新定义
namespace fs = std::experimental::filesystem;
目录是否存在
检查目录是否存在的函数为
bool exists( const path& p );
返回值说明:
返回值是一个bool类型的数据 如果存在返回true 失败返回false
参数说明:
我们传入一个路径名以判断该路径下的最后一个文件是否存在
使用代码表示如下
bool Exists() { return fs::exists(_filename); }
创建目录
创建目录的函数为
bool create_directories( const path& p );
返回值说明:
如果创建成功 则直接返回true 否则返回false
参数说明:
传入一个路径名即可
代码表示如下
bool CreateDirectory() { if (this -> Exists()) { return true; } return fs::create_directories(_filename); }
查看目录
我们可以使用迭代器来遍历一个目录 之后查看该目录里面的文件
具体代码表示如下
bool ScanDirectory(std::vector<std::string>& arry) { for (auto& p : fs::directory_iterator(_filename)) { if (fs::is_directory(p) == true) { continue; } // return relative path arry.push_back(fs::path(p).relative_path().string()); } return true; }
上面的代码中还有几处小细节
- 目录中还有可能存在目录 如果是目录则我们不查看
- 迭代器本身不是string类型 我们需要通过类型转换之后才能够放进arry数组中
功能测试
在编译之前我们要连接stdc++fs
库
我们以当面目录下的文件作为测试对象
测试代码如下
void TestUtilDirectory(std::string& filename) { shy::FileUtil fu(filename); fu.CreateDirectory(); if (fu.Exists()) { std::vector<std::string> arry; fu.ScanDirectory(arry); for (auto& fn : arry) { std::cout << fn << std::endl; } } }
测试结果如下
确实打印出了除目录以外的所有文件路径
json实用工具类设计
我们之前使用json类的时候又是智能指针 又是定义对象的 十分繁琐
为了简化操作 我们这里设计一个json使用工具类
大体格式如下
/*util.hpp*/ class JsonUtil{ public: static bool Serialize(const Json::Value &root, std::string *str); static bool UnSerialize(const std::string &str, Json::Value *root); };
序列化代码如下
static bool Serialize(const Json::Value& root , std::string& str) { Json::StreamWriterBuilder swb; std::unique_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; // subclass std::unique_ptr<Json::CharReader> cr(crb.newCharReader()); std::string err; bool set = cr->parse(str.c_str() , str.c_str()+str.size() , &root , &err); if (set == false) { return false; } return true; }
测试代码如下
void TestSerialize() { const char* name = "xiaoming"; int age = 18; float score[3] = {90 , 91 , 98}; Json::Value root; root["name"] = name; root["age"] = age; root["score"].append(score[0]); root["score"].append(score[1]); root["score"].append(score[2]); std::string json_str; shy::jsonUtil::Serialize(root , json_str); std::cout << json_str << std::endl; }
配置文件模块
系统配置信息
我们可以将系统需要的一些配置信息加载到文件当中
如果我们这样子做的话 想要修改一些信息我们就只需要修改文件中的信息然后重启服务员即可
这样子就让我们的配置信息变得灵活起来
下面是我们的一些配置信息
热点管理时间
在我们的测试样本中 我们将30s没有访问的文件标志为一个非热点文件 可是在现实中 一个非热点文件可能要在一两天没有被访问之后才会被标记 所以说这是一个多变的数据
文件下载的url前缀路径
url的格式一般是这样子的 http://192.168.1.1:9090/path
而我们这里所说的path实际上是一个相对的根目录 一般是wwwroot
所以说假如我们访问的是 /test.txt的话 我们实际上访问的路径是 .../wwwroot/test.txt
那么当我们要访问一个文件资源是listshow的时候 系统要怎么分别 我是要下载listshow这个文件 还是说我要执行listshow指令来查看服务器上有多少文件呢?
这个时候我们的前缀路径就能很好的解决这个问题 如果说我们访问的前缀加上/download/listshow 就是要下载这个文件 如果没有加则说明我们要执行listshow指令
压缩包后缀名
我们可以根据压缩的规则在文件最后加上后缀 比如说 .zip
.lz
等等
上传文件存放路径
它标志着我们的文件存放在哪里 别人请求之后应该到哪里找
压缩文件存放路径
压缩文件存放路径和上传文件存放路径肯定是不能放在一起的 因为别人的文件有可能就是压缩文件
服务端备份信息存放地址
当我们将文件压缩之后如果客户端像我们发送请求 要求我们列出文件信息 显然 我们列出压缩后的信息是很不合理的
所以说 在文件压缩之前 我们应该将文件的信息保存起来 以便于客户端随时查询
服务器的IP地址和端口
当我们要将该服务器部署到另外一台主机的时候 我们只需要修改下配置文件中的IP地址和端口就可以了
单例配置类的设计
我们采用懒汉模式来设计该单例类 所以说会有一些线程安全的问题
所以 除了我们之前的那些数据之外我们还要额外加锁保证线程安全
关于单例模式还不了解的同学可以参考我的这篇博客
我们的配置文件是使用json格式书写的 代码如下
{ "hot_time" : 30, "server_port" : 8080, "server_ip" : "10.0.8.3", "download_prefix" : "/download/", "packfile_suffix" : ".lz", "pack_dir" : "./packdir/", "back_dir" : "./backdir/", "backup_file" : "./cloud.dat" }
接下来是模块类的设计
#pragma once #include "Util.hpp" #include <mutex> using namespace shy; namespace cloud { #define CONFIG_FILE "./cloud.conf" class Config { private: Config() { ReadConfigFile(); } static Config* _instance; static std::mutex _mutex; private: int _hot_time; int _setver_port; std::string _server_ip; std::string _download_prefix; std::string _packfile_suffix; std::string _pack_dir; std::string _back_dir; std::string _backup_file; bool ReadConfigFile() { FileUtil fu(CONFIG_FILE); std::string body; if (fu.GetContent(body) == false) { std::cout << "load Config file failed" << std::endl; return false; } Json::Value root; if (jsonUtil::UnSerialize(body , root) == false) { return false; } _hot_time = root["hot_time"].asInt(); _setver_port = root["setver_port"].asInt(); _server_ip = root["server_ip"].asString(); _download_prefix = root["download_prefix"].asString(); _pack_dir = root["pack_dir"].asString(); _back_dir = root["back_dir"].asString(); _backup_file = root["backup_file"].asString(); return true; } public: static Config* GetInstance() { if (_instance == nullptr) { _mutex.lock(); if (_instance == nullptr) { _instance = new Config(); } _mutex.unlock(); } return _instance; } int GetHotTime() { return _hot_time; } int GetSetverPort() { return _setver_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 = nullptr; std::mutex Config::_mutex; }
单例配置类的测试
测试的代码也很简单 我们直接使用我们写的cloud类 看看能不能获取各个类型的数据就可以了
void ConfigTest() { cloud::Config* config = cloud::Config::GetInstance(); std::cout << config->GetHotTime() << std::endl; std::cout << config->GetServerIP() << std::endl; std::cout << config->GetSetverPort() << std::endl; std::cout << config->GetPackfileSuffix() << std::endl; }
数据管理模块
数据管理模块信息
我们首先要知道我们为什么要设计数据管理模块
对于一些数据来说 我们接收到之后使用一次就可以丢了 那么我们就可以不需要将它管理起来
如果说一个数据我们以后要经常用 或者说必须要用 我们就必须要将它管理起来了
所以说我们设计数据管理模块的根本原因是我们之后要用到这些数据
我们要存放的数据有哪些呢?
- 文件的实际存储路径:当客户端需要下载文件的时候 服务器从这个路径读取数据并进行响应
- 文件压缩包存放路径名:如果文件是一个非热点文件 那么这个文件就会被压缩 压缩后的文件路径 就是文件压缩包存放路径名 此外如果客户要下载一个压缩文件 我们会先解压缩 并且发送给客户端
- 文件是否被压缩的标志位
- 文件大小
- 文件最后一次修改时间
- 文件最后一次访问时间
- 文件访问的URL路径 (其实就是我们平时见到的下载路径)
如何管理数据
- 用于数据信息访问 :使用hash表在内存中管理数据 以url的path作为key值
- 信息的持久化管理 :使用json序列化将所有的数据保存在文件中
数据管理模块设计
我们这里要介绍到一个新的锁的种类 读写锁
pthread_rwlock_t_rwlock
这个锁的特点是 读共享 写互斥
我们这里之所以不用互斥锁的原因是互斥锁是要串行化的 它的效率特别低
代码表示如下
namespace cloud { using namespace shy; typedef struct BackupInfo_t { bool pack_flag; size_t fsize; time_t mtime; time_t atime; std::string real_path; std::string pack_path; std::string url; public: void NewBackupInfo(const std::string& realpath) { Config* config = Config::GetInstance(); std::string packdir = config->GetPackDir(); std::string packsuffix = config->GetPackfileSuffix(); std::string download_prefix = config->GetDownloadPrefix(); FileUtil fu(realpath); this->pack_flag = false; this->fsize = fu.FileSize(); this->mtime = fu.LastModifyTime(); this->atime = fu.LastAccessTime(); this->real_path = realpath; this->pack_path = packdir + fu.FileName() + packsuffix; this->url = download_prefix + fu.FileName(); } }BackupInfo; }
当然 写完一段代码之后我们最重要的就是测试了 我们在main函数中使用该类并且依次打印其所有信息看看是否有错漏
在此之前 因为每次都要编译bundle.cpp库都需要花费很多的时间 我们可以将其生成一个静态库来使用
具体生成静态库的方法可以参考我的这篇博客 动静态库
测试代码如下
void DataTest(const std::string& filename) { cloud::BackupInfo info; info.NewBackupInfo(filename); std::cout << info.pack_flag << std::endl; std::cout << info.fsize << std::endl; std::cout << info.mtime << std::endl; std::cout << info.atime << std::endl; std::cout << info.real_path << std::endl; std::cout << info.pack_path << std::endl; std::cout << info.url << std::endl; }
测试结果如下
数据管理类的实现
数据管理类中要使用哈希表存储数据 并且要使用读写锁
std::string _backup_file; pthread_rwlock_t _rwlock; std::unordered_map<std::string,BackupInfo> _table;
在数据管理类中 我们要实现 插入 更新 读取操作 实现函数如下
bool Insert(const BackupInfo& info) { pthread_rwlock_wrlock(&_rwlock); _table[info.url] = info; pthread_rwlock_unlock(&_rwlock); return true; } bool Update(const BackupInfo& info) { pthread_rwlock_wrlock(&_rwlock); _table[info.url] = info; pthread_rwlock_unlock(&_rwlock); return true; } bool GetOneByURL(const std::string& url , BackupInfo* info) { pthread_rwlock_rdlock(&_rwlock); auto it = _table.find(url); if (it == _table.end()) { return false; } *info = it->second; pthread_rwlock_unlock(&_rwlock); return true; } bool GetOneByRealPath(const std::string& realpath, BackupInfo* info) { pthread_rwlock_wrlock(&_rwlock); auto it = _table.begin(); while (it != _table.end()) { if (it->second.real_path == realpath) { *info = it->second; pthread_rwlock_unlock(&_rwlock); return true; } it++; } pthread_rwlock_unlock(&_rwlock); return false; } bool GetAll(std::vector<BackupInfo>* array) { pthread_rwlock_wrlock(&_rwlock); auto it = _table.begin(); while (it != _table.end()) { array->push_back(it->second); } pthread_rwlock_unlock(&_rwlock); return true; }
数据管理类的测试
接下来我们在主函数中对于数据管理类进行测试
代码表示如下
void DataTest(const std::string& filename) { cloud::BackupInfo info; info.NewBackupInfo(filename); cloud::DataManager data; data.Insert(info); cloud::BackupInfo tmp; data.GetOneByURL("/download/bundle.h", &tmp); std::cout << "test getonebyrul" << std::endl; std::cout << tmp.pack_flag << std::endl; std::cout << tmp.fsize << std::endl; std::cout << tmp.mtime << std::endl; std::cout << tmp.atime << std::endl; std::cout << tmp.real_path << std::endl; std::cout << tmp.pack_path << std::endl; std::cout << tmp.url << std::endl; info.pack_flag = true; data.Update(info); std::vector<cloud::BackupInfo> arry; data.GetAll(&arry); std::cout << "test getall" << std::endl; for (auto x : arry) { std::cout << x.pack_flag << std::endl; std::cout << x.fsize << std::endl; std::cout << x.mtime << std::endl; std::cout << x.atime << std::endl; std::cout << x.real_path << std::endl; std::cout << x.pack_path << std::endl; std::cout << x.url << std::endl; } data.GetOneByRealPath(tmp.real_path ,&tmp); std::cout << "test getonebyrealpath" << std::endl; std::cout << "--------------------------" << std::endl; std::cout << tmp.pack_flag << std::endl; std::cout << tmp.fsize << std::endl; std::cout << tmp.mtime << std::endl; std::cout << tmp.atime << std::endl; std::cout << tmp.real_path << std::endl; std::cout << tmp.pack_path << std::endl; std::cout << tmp.url << std::endl; }
测试结果如下
可以正常运行
数据的持久化存储
我们的程序有可能关机 但是在下次开机的时候我们可能还是会需要这些数据 所以说我们需要将这些数据进行持久化存储
在前面的第一个项目httpsever中 我们使用mysql存储数据
那么在这个项目中 我们就使用文件来存储数据
持久化存储分为四步
- 获取所有数据
- 使用json格式存储
- 序列化数据
- 写入文件
bool Storage() { // 1. get all date std::vector<BackupInfo> arry; this->GetAll(&arry); // 2. add to json::value Json::Value items; W> for (int i = 0; i < arry.size(); i++) { Json::Value root; root["pack_flag"] = arry[i].pack_flag; root["fszie"] = std::to_string(arry[i].fsize); root["atime"] = std::to_string(arry[i].atime); root["mtime"] = std::to_string(arry[i].mtime); root["real_path"] = arry[i].real_path; root["pack_path"] = arry[i].pack_path; root["url"] = arry[i].url; items.append(root); // append array elements } // 3. serialize the items std::string body; jsonUtil::Serialize(items , body); // 4. write the file FileUtil fu(_backup_file); fu.SetContent(body); return true; }
数据的持久化存储测试
当我们在测试函数内部调用该函数的时候 就会生成一个cloud.dat文件用来作为持久化存储
文件如下
初始化从文件中读取数据
从文件中读取数据分为三步
- 从cloud.dat中读取数据
- 反序列化
- 添加值到table中
bool InitLoad() { // 1 get data from cloud.dat FileUtil fu(_backup_file); if (fu.Exists() == false) { return true; } std::string body; fu.GetContent(body); // 2 unserialize Json::Value root; jsonUtil::UnSerialize(body , root); // 3 add value to table W> for (int i = 0; i < root.size(); i++) { BackupInfo info; info.pack_flag = root[i]["pack_flag"].asBool(); info.fsize = root[i]["fszie"].asInt(); info.mtime = root[i]["mtime"].asInt(); info.atime = root[i]["atime"].asInt(); info.pack_path = root[i]["pack_path"].asString(); info.real_path = root[i]["real_path"].asString(); info.url = root[i]["url"].asString(); Insert(info); } }
这里我们需要注意的是 如果出现类型转化的错误 我们可以使用强制类型转换来解决
测试结果如下
与 cloud.dat
中的数据相同