目录
一.为什么要配置文件
我们将服务端程序运行中用到的一些关键信息保存到配置文件中,这样可以使程序的运行更加灵活。
这样做的好处是,未来如果我们想要修改一些关键信息,不需要去源文件里修改,避免了文件重新编译等。
配置文件信息
- 热点判断时间 :
决定热点文件隔多长时间会被判定为非热点文件。这个必须是可变的,因为我们
- 文件下载的 URL 前缀路径 :
用于表示客户端的请求是一个下载请求。
一般来说,我们的用户都是输入类似于下面这种URL:http://192.168.122.136:9090/path,通过这样子的方式,默认访问到的是我们根目录下的path文件了吗?那访问我们服务器的用户不都可以通过这种方式获取我服务器上的所有文件了吗?这是很危险的,所以我们会在程序运行时添加一个新的相对的根目录。例如wwwroot。此时用户只能访问/wwwroot/里面的所有文件,这就是一种前缀路径。
示例 :但是当用户发一个请求listshow
来查看备份列表,listshow可能是一种指令,也可能是一个文件的名字,那我们如何判断这个请求不是一个listshow
的文件下载请求呢?
此时我们可以为下载请求添加一个前缀路径,例如/download/listshow
,那么就认为它是一个下载一个名字叫listshow文件的请求
。
- 压缩包后缀名 :
约定一个压缩包命名规则。示例 :在原文件后面加上 .lz
表示该文件的压缩包名称。
- 上传文件存放路径 :
决定上传文件之后,该文件实际存储在服务器的何处。
- 压缩包存放路径 :
决定压缩后的文件存储在何处。
上传的文件也可能是压缩文件,而我们对于上传的文件里面对于非热点文件,我们会进行压缩,如果非热点文件是压缩文件,那我们不能对其再次进行压缩。所以要将压缩包和普通文件分开来。
- 服务端备份信息存放文件 :
服务端记录的备份文件信息的持久化存储路径或文件名。
事实上,我们这个项目并没有使用数据库来存储任何信息,因为这个是一个入门项目,如果加上数据库,就会显得这个项目有点庞大了。所以我们不加上数据库好了。
当程序需要运行在其他主机上,则不需要修改程序,只需要修改一下配置文件就OK了。
- 服务器访问 IP 地址 :
服务器的 IP 地址,用于客户端访问。
当程序需要运行在其他主机上,则不需要修改程序,只需要修改一下配置文件就OK了。
- 服务器访问端口 :
服务器的访问端口,用于客户端与服务端之间的通信。
当程序需要运行在其他主机上,则不需要修改程序,只需要修改一下配置文件就OK了。
二.配置文件的实现
我们这些配置文件都是基于JSON来描述的
cloud.conf
javascript
// cloud.conf
{
"hot_time" : 30,
"server_port" : 9090,
"server_ip" : "117.72.80.239",
"download_prefix" : "/download/",
"packfile_suffix" : ".lz",
"pack_dir" : "./packdir/",
"back_dir" : "./backdir/",
"backup_file" : "./cloud.dat"
}
我们去我们的服务器里,创建一个cloud.conf文件

三.单例文件配置类设计
使用单例模式管理系统配置信息,能够让配置信息的管理控制更加统一灵活。该类的设计我们将使用单例模式中的懒汉模式
,即在使用时创建对象。
conf.hpp
javascript
#ifndef _MY_CONFIG_
#define _MY_CONFIG_
#include"util.hpp"
#include<mutex>
namespace cloud {
#define CONFIG_FILE "./cloud.conf" // 配置文件路径
class Config
{
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 GetServerPort()
{
return _server_port;
}
std::string GetSeverIp()
{
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;
}
private:
Config() // 构造函数私有化,因为我们是根据配置文件来对这个类来构造的,所以一个类只能有一个构造函数
//无法在类外进行实例化
{
ReadConfigFile();//一构造就去读取配置文件
}
bool ReadConfigFile()//读取配置文件
{
FileUtil fu(CONFIG_FILE);//这个CONFIG_FILE在上面定义了
std::string body;
if(fu.GetContent(&body) == false)//body存储了配置文件的内容,但配置文件是序列化之后的内容,不能直接使用
{
std::cout << "load config file failed!" << std::endl;
return false;
}
Json::Value root;
if(JsonUtil::Unserialize(body, &root) == false)//root里面存储了配置文件反序列化后的内容
{
std::cout << "parse config file failed!" << std::endl;
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();
}
private:
int _hot_time;//热点判断时间
int _server_port;//服务器监听端口号
std::string _server_ip;//服务器监听的IP
std::string _download_prefix;//下载的url的前缀路径
std::string _packfile_suffix;//压缩文件的后缀名,比如.lz
std::string _pack_dir;// 压缩包存放路径
std::string _back_dir;// 备份文件存放路径
std::string _backup_file;// 备份信息存放文件
static Config* _instance;//句柄,单例模式,只有一个实例,所以加staic
static std::mutex _mutex;//由于是单例模式的懒汉模式,涉及线程安全,所以需要加互斥锁
};
Config* Config::_instance = nullptr;//初始化
std::mutex Config::_mutex;//锁
};
#endif
我们回到我们的服务器上面来,创建一个conf.hpp,把上面那个代码给填进去就OK了。
我们来对它进行检测一下,
javascript
#include"util.hpp"
#include"conf.hpp"
void ConfigTest()
{
cloud::Config*config=cloud::Config::GetInstance();
std::cout<<config->GetHotTime()<<std::endl;
std::cout<<config->GetServerPort()<<std::endl;
std::cout<<config->GetSeverIp()<<std::endl;
std::cout<<config->GetDownloadPrefix()<<std::endl;
std::cout<<config->GetPackFileSuffix()<<std::endl;
std::cout<<config->GetPackDir()<<std::endl;
std::cout<<config->GetBackDir()<<std::endl;
std::cout<<config->GetBackupFile()<<std::endl;
}
int main(int argc,char*argv[])
{
ConfigTest();
return 0;
}
这个时候我们编译,就会发现一个错误

这个是因为util.hpp是被重复包括了,因为cloud.cc和conf.hpp里面都包含了头文件util.hpp,所以我们需要加上防止头文件重复定义的.
我们需要在util.hpp里面加上这个
cpp
#ifndef _MY_UTIL_
#define _MY_UTIL_
...
#endif
现在编译就没有问题了,

我们运行一下

也是一点问题都没有。
到这里,我们还是需要使用git来提交一番

四.源码
util.hpp
cpp
#ifndef _MY_UTIL_
#define _MY_UTIL_
#include<iostream>
#include<fstream>
#include<string>
#include<vector>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include"bundle.h"
#include <experimental/filesystem>
#include <jsoncpp/json/json.h>
namespace cloud {//注意:我们下面这些接口的名称可能会与系统的那些接口的名字重复,所以最好创建名称空间来避免名称污染
namespace fs = std::experimental::filesystem;
class FileUtil
{
public:
FileUtil(const std::string& filename):_filename(filename){}
//主要针对普通文件的接口
int64_t FileSize() // 获取文件大小,这里使用64位的有符号的int,防止文件过大导致文件大小显示异常
{
struct stat st;
int re=stat(_filename.c_str(),&st);
if(re<0)//stat函数获取文件属性失败了
{
std::cout<<"Get FileSize Failed!!"<<std::endl;
return -1;
}
return st.st_size;
}
time_t LastModtime() // 获取文件最后一次修改时间
{
struct stat st;
int re = stat(_filename.c_str(), &st);
if (re < 0)//stat函数获取文件属性失败了
{
std::cout << "Get LastModtime Failed!!" << std::endl;
return -1;
}
return st.st_mtim.tv_sec;
}
time_t LastAcctime() // 获取文件最后一次访问时间
{
struct stat st;
int re = stat(_filename.c_str(), &st);
if (re < 0)//stat函数获取文件属性失败了
{
std::cout << "Get LastAcctime Failed!!" << std::endl;
return -1;
}
return st.st_atim.tv_sec;
}
std::string FileName() // 获取文件路径中的纯文件名称 a/b/c/test.cc --> test.cc
{
size_t pos=_filename.find_last_of("/");//寻找最后一个/
if(pos==std::string::npos)//没找到,说明没有/
{
return _filename;
}
return _filename.substr(pos+1);//从pos截取到末尾
}
bool GetPosLen(std::string* body, size_t pos, size_t len)
{
std::ifstream ifs;
ifs.open(_filename, std::ios::binary);//打开文件,以二进制方式来读取数据
if (ifs.is_open() == false)//打开失败
{
std::cout << "GetPosLen: open file failed!" << std::endl;
return false;
}
size_t fsize = this->FileSize();//获取文件大小
if (pos + len > fsize)//超过文件大小了
{
std::cout << "GetPosLen: get file len error" << std::endl;
return false;
}
ifs.seekg(pos, std::ios::beg); // 将文件指针定位到pos处
body->resize(len);//把存储读取的数据的载体的大小修改到够大的
ifs.read(&(*body)[0], len);//读取数据
if (ifs.good() == false)//上次读取出错了
{
std::cout << "GetPosLen: get file content failed" << std::endl;
ifs.close();
return false;
}
ifs.close();
return true;
}
bool GetContent(std::string* body)
{
size_t fsize = FileSize();
return GetPosLen(body, 0, fsize);
}
bool SetContent(const std::string& body)//写入数据
{
std::ofstream ofs;//也就是输出
ofs.open(_filename, std::ios::binary);//以二进制模式打开
if (ofs.is_open() == false)//打开失败
{
std::cout << "SetContent: write open file failed" << std::endl;
return false;
}
ofs.write(&body[0], body.size());
if (ofs.good() == false)//上次写入文件数据出错了
{
std::cout << "SetContent: write open file failed" << std::endl;
ofs.close();
return false;
}
ofs.close();
return true;
}
bool Compress(const std::string& packname)
{
// 1.获取源文件数据
std::string body;
if (this->GetContent(&body) == false)//源文件数据都存储在body里面
{
//获取源文件数据失败
std::cout << "compress get file content failed" << std::endl;
return false;
}
// 2.对数据进行压缩
std::string packed = bundle::pack(bundle::LZIP, body);
// 3.将压缩的数据存储到压缩包文件中
FileUtil fu(packname);
if (fu.SetContent(packed) == false)
{
//压缩数据写入压缩包文件失败
std::cout << "compress write packed data failed!" << std::endl;
return false;
}
return true;
}
bool UnCompress(const std::string& filename)
{
// 1.将当前压缩包数据读取出来
std::string body;
if (this->GetContent(&body) == false)
{
std::cout << "Uncompress get file content failed!" << std::endl;
return false;
}
// 2.对压缩的数据进行解压缩
std::string unpacked = bundle::unpack(body);
// 3.将解压缩的数据写入到新文件中
FileUtil fu(filename);
if (fu.SetContent(unpacked) == false)
{
std::cout << "Uncompress write packed data failed!" << std::endl;
return false;
}
return true;
}
bool Exists()
{
return fs::exists(_filename);
}
bool Remove()
{
if (Exists() == false)
{
return true;
}
remove(_filename.c_str());
return true;
}
bool CreateDirectory()
{
if (Exists()) return true;
return fs::create_directories(_filename);
}
bool ScanDirectory(std::vector<std::string>* array)
{
for (auto& p : fs::directory_iterator(_filename)) // 迭代器遍历指定目录下的文件,从那个网站上面复制下来的
{
if (fs::is_directory(p) == true)//如果是目录,就不添加进当前目录的文件里
continue;
// relative_path 带有路径的文件名
array->push_back(fs::path(p).relative_path().string());//添加文件
//注意迭代器返回的p不是string,不能直接将p添加进array里面,我们需要使用path将其
}
return true;
}
private:
std::string _filename; // 文件名--包含路径
};
class JsonUtil {
public:
/**
* @brief 将 Json::Value 对象序列化为字符串
* @param root 输入的 JSON 数据结构(待序列化)
* @param str 输出的序列化后的字符串
* @return true 序列化成功,false 序列化失败
*/
static bool Serialize(const Json::Value &root, std::string *str) {
// 1. 创建 JSON 流写入器构建器(可配置格式化选项,如缩进)
Json::StreamWriterBuilder swb;
// 2. 通过构建器生成 StreamWriter 对象(unique_ptr 自动管理内存)
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss; // 用于存储序列化结果
// 3. 将 JSON 数据写入流
// 返回值 0 表示成功,非 0 表示失败(JsonCpp 的约定)
if (sw->write(root, &ss) != 0) {
std::cout << "JSON 序列化失败!" << std::endl;
return false;
}
// 4. 将 stringstream 内容转为字符串
*str = ss.str();
return true;
}
/**
* @brief 将字符串反序列化为 Json::Value 对象
* @param str 输入的 JSON 格式字符串
* @param root 输出的解析后的 JSON 数据结构
* @return true 解析成功,false 解析失败
*/
static bool Unserialize(const std::string &str, Json::Value *root) {
// 1. 创建 JSON 字符读取器构建器
Json::CharReaderBuilder crb;
// 2. 通过构建器生成 CharReader 对象(unique_ptr 自动管理内存)
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err; // 存储解析错误信息
// 3. 解析字符串
// 参数说明:
// - str.c_str():字符串起始地址
// - str.c_str() + str.size():字符串结束地址
// - root:输出解析后的 JSON 对象
// - &err:错误信息输出
bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), root, &err);
if (!ret) {
std::cout << "JSON 解析错误: " << err << std::endl;
return false;
}
return true;
}
};
};
#endif