C/C++图床云存储:从零构建基础组件(日志、配置、错误处理)
1. 项目概述:从零构建一个C/C++图床共享云存储
最近在整理硬盘里那些散落各处的图片时,我又一次被文件管理问题给绊住了。想找个几年前的老照片,得在好几个文件夹里来回翻;想把一张设计图分享给同事,又得先上传到某个在线相册,再复制链接。这种割裂的体验让我萌生了一个想法:为什么不自己动手,用C/C++打造一个私有的、可共享的图床云存储系统呢?这听起来像是个大工程,但拆解开来,核心无非是文件的存储、管理和访问。今天,我们就从这个宏伟蓝图的第一步——“基础组件”开始聊起。这个基础组件,就像是盖房子前要打的地基和准备砖瓦水泥,它不负责炫酷的功能,但决定了整个系统是否稳固、高效,以及未来能盖多高。
所谓“图床共享云存储”,本质上是一个专门用于图片(也可扩展至其他小文件)的网络存储服务。用户可以通过客户端或网页上传图片,系统会为其生成一个唯一的访问链接(URL),其他人拿到这个链接就能直接查看或下载图片,实现便捷的共享。而我们要用C/C++来实现它,一方面是出于性能的极致追求,尤其是在高并发上传、下载的场景下;另一方面,也是对系统底层控制力的体现,从内存管理、网络IO到磁盘操作,每一步都清晰可控。这个系列的第一篇,我们不急着搭框架、写业务逻辑,而是先沉下心来,把那些最基础、却又至关重要的“轮子”造好。这些组件包括但不限于:一个健壮且易用的日志系统、一个高效的配置文件解析器、一个通用的错误码定义与处理机制,以及项目目录结构的规划。磨刀不误砍柴工,把这些基础打牢了,后续添加网络服务、数据库操作、图片处理等功能时,才会事半功倍。
2. 核心基础组件设计与选型思路
在动手写代码之前,我们先得想清楚需要哪些基础组件,以及为什么是它们。一个后台服务,尤其是用C/C++这种偏系统级的语言开发的服务,其基础架构的选型直接关系到后期的开发体验、调试效率和线上稳定性。
2.1 为什么需要独立的日志系统?
很多新手可能会直接用
printf
或
std::cout
来打日志,这在开发初期确实方便。但一旦程序以守护进程(daemon)模式在后台运行,或者需要同时向控制台和文件输出,
printf
就力不从心了。一个专业的日志系统需要解决几个核心问题:
分级输出
(如DEBUG、INFO、WARN、ERROR)、
多目的地输出
(控制台、文件、甚至网络)、
日志滚动
(防止单个日志文件过大)、
线程安全
(多线程环境下日志不乱序)以及
性能
(不能因为打日志而严重拖慢主流程)。在C++中,我们可以选择改造像
spdlog
这样优秀的开源库,或者为了学习目的,自己实现一个轻量级的版本。自己实现的好处是,我们可以完全掌控其行为,并使其完美契合我们项目的特定需求,例如集成特定的监控告警。
2.2 配置文件解析器:告别硬编码
把数据库地址、服务器端口、日志级别这些参数硬编码在代码里,是项目维护的噩梦。每次修改都需要重新编译,并且在不同环境(开发、测试、生产)部署时需要不同的代码版本。因此,一个配置文件解析器是必须的。常见的格式有JSON、YAML、INI和XML。对于C/C++项目,尤其是追求轻量级的场景,INI格式因其简单直观而备受青睐。每一行就是一个
key=value
对,用
[section]
来分组,人类可读,机器也易解析。我们将实现一个能够读取INI格式、支持默认值、支持基本数据类型(字符串、整数、布尔值)转换的配置类。这个类将在程序启动时被初始化,后续所有模块都从这里获取配置,实现“一处配置,处处生效”。
2.3 错误处理:从混乱到有序
C语言中常用的错误处理是返回整数错误码,C++中则更多使用异常。但在一个大型项目中,尤其是底层库和网络服务混合的项目,统一的错误处理机制至关重要。我们的目标是定义一套项目全局的错误码枚举。每一个错误码不仅对应一个数字,还应该对应一段清晰的描述信息。当任何函数遇到问题时,它应该返回这个错误码,而不是直接
printf
错误信息或者
throw
一个字符串。上层调用者根据错误码来决定是重试、降级处理还是向上传递错误。同时,我们的日志系统需要能方便地记录这些错误码及其描述,便于问题定位。这比在代码里散落着各种
“文件打开失败”
这样的魔法字符串要清晰、易维护得多。
2.4 项目目录结构规划
清晰的目录结构是项目可读性和可维护性的基石。我们不能把所有
.h
和
.cpp
文件都扔在一个文件夹里。一个典型的建议结构如下:
cloud_storage/
├── CMakeLists.txt # 项目根CMake构建文件
├── build/ # 构建输出目录(通常.gitignore)
├── src/ # 源代码目录
│ ├── base/ # 基础组件目录
│ │ ├── log.h/.cpp # 日志系统
│ │ ├── config.h/.cpp # 配置解析
│ │ ├── error_code.h # 错误码定义
│ │ └── noncopyable.h # 禁止拷贝的基类(如有)
│ ├── network/ # 网络模块(后续)
│ ├── storage/ # 存储引擎(后续)
│ └── main.cpp # 程序入口
├── conf/ # 配置文件目录
│ └── storage.ini # 示例配置文件
├── tests/ # 单元测试目录
├── third_party/ # 第三方库(如手动集成的)
└── tools/ # 工具脚本目录
这样的结构将不同职责的代码分离,
src/base
就是我们本篇的重点。使用CMake作为构建系统,可以方便地管理编译选项、链接库,并支持跨平台编译。
3. 日志系统的详细实现与核心技巧
接下来,我们深入第一个核心组件:日志系统。我们将实现一个名为
Logger
的类,它支持同步日志(日志调用直接写入)和异步日志(日志先放入队列,由后台线程写入),后者对性能更友好。
3.1 Logger类的接口设计
首先定义日志级别和核心接口。
// src/base/log.h
#ifndef CLOUD_STORAGE_BASE_LOG_H
#define CLOUD_STORAGE_BASE_LOG_H
#include <string>
#include <memory>
namespace cloud_storage {
enum class LogLevel {
DEBUG = 0,
INFO,
WARN,
ERROR,
FATAL
};
class Logger {
public:
using Ptr = std::shared_ptr<Logger>;
// 获取全局单例日志器(简化示例,实际可能需支持多实例)
static Logger::Ptr getInstance();
// 初始化日志器,设置日志级别、输出文件路径等
bool init(LogLevel console_level, LogLevel file_level, const std::string& filepath = "");
// 日志输出接口
void log(LogLevel level, const char* file, int line, const char* format, ...);
// 设置日志级别
void setConsoleLevel(LogLevel level);
void setFileLevel(LogLevel level);
private:
Logger();
// 禁止拷贝
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
// 内部实现方法
void output(LogLevel level, const std::string& message);
std::string formatTime() const;
std::string levelToString(LogLevel level) const;
LogLevel console_level_;
LogLevel file_level_;
FILE* file_fp_; // 文件指针,简单处理。生产环境需考虑线程安全及滚动。
// 实际项目中,这里会有异步队列、后台线程等成员
};
// 方便使用的宏,自动捕获文件名和行号
#define LOG_DEBUG(format, ...) \
cloud_storage::Logger::getInstance()->log(cloud_storage::LogLevel::DEBUG, __FILE__, __LINE__, format, ##__VA_ARGS__)
#define LOG_INFO(format, ...) \
cloud_storage::Logger::getInstance()->log(cloud_storage::LogLevel::INFO, __FILE__, __LINE__, format, ##__VA_ARGS__)
#define LOG_WARN(format, ...) \
cloud_storage::Logger::getInstance()->log(cloud_storage::LogLevel::WARN, __FILE__, __LINE__, format, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) \
cloud_storage::Logger::getInstance()->log(cloud_storage::LogLevel::ERROR, __FILE__, __LINE__, format, ##__VA_ARGS__)
#define LOG_FATAL(format, ...) \
cloud_storage::Logger::getInstance()->log(cloud_storage::LogLevel::FATAL, __FILE__, __LINE__, format, ##__VA_ARGS__)
} // namespace cloud_storage
#endif // CLOUD_STORAGE_BASE_LOG_H
3.2 核心实现与线程安全考量
在
log.cpp
中,我们实现核心的
log
函数。这里的关键是格式化字符串和线程安全的输出。
// src/base/log.cpp
#include “base/log.h“
#include <cstdarg>
#include <cstdio>
#include <ctime>
#include <mutex>
namespace cloud_storage {
// 静态单例实现
std::mutex g_logger_mutex; // 用于保护单例创建
Logger::Ptr Logger::instance_ = nullptr;
Logger::Ptr Logger::getInstance() {
if (instance_ == nullptr) {
std::lock_guard<std::mutex> lock(g_logger_mutex);
if (instance_ == nullptr) {
// 使用std::make_shared会调用私有构造函数,需要额外处理,这里简单new
instance_.reset(new Logger());
}
}
return instance_;
}
bool Logger::init(LogLevel console_level, LogLevel file_level, const std::string& filepath) {
console_level_ = console_level;
file_level_ = file_level;
if (!filepath.empty()) {
file_fp_ = std::fopen(filepath.c_str(), “a“); // 以追加模式打开
if (!file_fp_) {
// 初始化时就打日志可能不行,可以输出到stderr
std::fprintf(stderr, “FATAL: Cannot open log file %s\n“, filepath.c_str());
return false;
}
} else {
file_fp_ = nullptr;
}
return true;
}
void Logger::log(LogLevel level, const char* file, int line, const char* format, ...) {
// 1. 判断级别是否足够输出
if (level < console_level_ && level < file_level_) {
return;
}
// 2. 格式化时间、级别、文件名和行号
std::string prefix = formatTime() + “ [“ + levelToString(level) + “] “ + file + “:“ + std::to_string(line) + “ “;
// 3. 格式化用户消息
char message_buffer[4096]; // 固定缓冲区,简单处理。生产环境需用动态分配或更大的缓冲区。
va_list args;
va_start(args, format);
std::vsnprintf(message_buffer, sizeof(message_buffer), format, args);
va_end(args);
std::string full_message = prefix + message_buffer + “\n“;
// 4. 输出到不同目的地
// 注意:这里对file_fp_的访问不是线程安全的,多个线程同时写一个文件句柄会导致内容交错。
// 解决方案:对每个输出目的地加锁,或使用异步日志。
static std::mutex console_mutex;
static std::mutex file_mutex;
if (level >= console_level_) {
std::lock_guard<std::mutex> lock(console_mutex);
std::fprintf((level >= LogLevel::WARN) ? stderr : stdout, “%s“, full_message.c_str());
std::fflush((level >= LogLevel::WARN) ? stderr : stdout);
}
if (file_fp_ && level >= file_level_) {
std::lock_guard<std::mutex> lock(file_mutex);
std::fprintf(file_fp_, “%s“, full_message.c_str());
std::fflush(file_fp_); // 及时刷新,防止日志丢失,但影响性能。
}
}
std::string Logger::formatTime() const {
std::time_t now = std::time(nullptr);
char buffer[64];
std::strftime(buffer, sizeof(buffer), “%Y-%m-%d %H:%M:%S“, std::localtime(&now));
return std::string(buffer);
}
std::string Logger::levelToString(LogLevel level) const {
switch (level) {
case LogLevel::DEBUG: return “DEBUG“;
case LogLevel::INFO: return “INFO “;
case LogLevel::WARN: return “WARN “;
case LogLevel::ERROR: return “ERROR“;
case LogLevel::FATAL: return “FATAL“;
default: return “UNKNOWN“;
}
}
// ... 其他函数实现
} // namespace cloud_storage
注意:性能与线程安全的权衡 :上述实现为了清晰,使用了简单的互斥锁来保护控制台和文件输出。这在日志量不大时没问题。但在高性能场景下,锁竞争会成为瓶颈。 生产级别的做法是实现一个异步日志器 :
log()函数只负责将日志消息(包含时间、级别、内容等)放入一个无锁队列或阻塞队列,然后立即返回。一个独立的后台线程不断从队列中取出消息,批量写入文件和控制台。这样,业务线程的耗时仅仅是内存拷贝和队列操作,极大地减少了I/O等待时间。muduo网络库的AsyncLogging类就是一个非常经典的实现,值得深入研究。
4. 配置文件解析器的实现与健壮性处理
有了日志,我们再来实现配置解析器。我们选择INI格式,因为它简单。目标是实现一个
Config
类,可以这样使用:
std::string ip = Config::getInstance()->getString(“server“, “ip“, “127.0.0.1“);
int port = Config::getInstance()->getInt(“server“, “port“, 8080);
bool debug = Config::getInstance()->getBool(“log“, “debug“, false);
4.1 Config类的设计与数据结构
配置文件的内容在内存中最好用一个嵌套的数据结构来存储。我们可以用
std::map<std::string, std::map<std::string, std::string>>
,外层key是section,内层map的key是配置项名,value是字符串形式的配置值。
// src/base/config.h
#ifndef CLOUD_STORAGE_BASE_CONFIG_H
#define CLOUD_STORAGE_BASE_CONFIG_H
#include <string>
#include <map>
#include <memory>
namespace cloud_storage {
class Config {
public:
using Ptr = std::shared_ptr<Config>;
static Config::Ptr getInstance();
// 加载配置文件
bool load(const std::string& filepath);
// 获取配置项,提供默认值
std::string getString(const std::string& section, const std::string& key, const std::string& default_val = ““);
int getInt(const std::string& section, const std::string& key, int default_val = 0);
double getDouble(const std::string& section, const std::string& key, double default_val = 0.0);
bool getBool(const std::string& section, const std::string& key, bool default_val = false);
// 设置配置项(可用于运行时修改或测试)
void setString(const std::string& section, const std::string& key, const std::string& value);
private:
Config() = default;
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
// 内部解析行
void parseLine(const std::string& line, std::string& current_section);
// 修剪字符串两端的空白字符
std::string trim(const std::string& str);
// 配置存储: map<section, map<key, value>>
std::map<std::string, std::map<std::string, std::string>> config_map_;
std::mutex mutex_; // 保证线程安全
};
} // namespace cloud_storage
#endif // CLOUD_STORAGE_BASE_CONFIG_H
4.2 文件解析与容错处理
解析INI文件的关键在于按行读取,并正确处理section、key-value对和注释。
// src/base/config.cpp
#include “base/config.h“
#include “base/log.h“ // 使用我们刚实现的日志
#include <fstream>
#include <cctype>
#include <algorithm>
namespace cloud_storage {
Config::Ptr Config::instance_ = nullptr;
std::mutex Config::instance_mutex_;
Config::Ptr Config::getInstance() {
if (instance_ == nullptr) {
std::lock_guard<std::mutex> lock(instance_mutex_);
if (instance_ == nullptr) {
instance_.reset(new Config());
}
}
return instance_;
}
bool Config::load(const std::string& filepath) {
std::ifstream file(filepath);
if (!file.is_open()) {
LOG_ERROR(“Failed to open config file: %s“, filepath.c_str());
return false;
}
std::lock_guard<std::mutex> lock(mutex_);
config_map_.clear(); // 重新加载前清空旧配置
std::string line;
std::string current_section = ““; // 全局section,可以没有[]
int line_num = 0;
while (std::getline(file, line)) {
++line_num;
parseLine(line, current_section);
}
LOG_INFO(“Config loaded from %s successfully.“, filepath.c_str());
return true;
}
void Config::parseLine(const std::string& line, std::string& current_section) {
std::string trimmed = trim(line);
if (trimmed.empty() || trimmed[0] == ‘;‘ || trimmed[0] == ‘#‘) {
// 空行或注释行,跳过
return;
}
if (trimmed[0] == ‘[‘ && trimmed.back() == ‘]‘) {
// Section行
current_section = trimmed.substr(1, trimmed.size() - 2);
current_section = trim(current_section);
// 确保section在map中存在(即使为空map)
config_map_[current_section];
return;
}
// Key=Value 行
size_t pos = trimmed.find(‘=‘);
if (pos == std::string::npos) {
LOG_WARN(“Invalid config line (no ‘=‘): %s“, trimmed.c_str());
return;
}
std::string key = trim(trimmed.substr(0, pos));
std::string value = trim(trimmed.substr(pos + 1));
if (key.empty()) {
LOG_WARN(“Config key is empty in line: %s“, trimmed.c_str());
return;
}
config_map_[current_section][key] = value;
}
std::string Config::trim(const std::string& str) {
auto start = str.begin();
while (start != str.end() && std::isspace(*start)) {
++start;
}
if (start == str.end()) {
return ““;
}
auto end = str.end();
do {
--end;
} while (std::isspace(*end));
return std::string(start, end + 1);
}
// 获取配置项的实现
std::string Config::getString(const std::string& section, const std::string& key, const std::string& default_val) {
std::lock_guard<std::mutex> lock(mutex_);
auto sec_it = config_map_.find(section);
if (sec_it == config_map_.end()) {
return default_val;
}
auto key_it = sec_it->second.find(key);
if (key_it == sec_it->second.end()) {
return default_val;
}
return key_it->second;
}
int Config::getInt(const std::string& section, const std::string& key, int default_val) {
std::string val = getString(section, key, ““);
if (val.empty()) {
return default_val;
}
try {
return std::stoi(val);
} catch (const std::exception& e) {
LOG_ERROR(“Failed to parse config [%s][%s]=%s to int: %s“, section.c_str(), key.c_str(), val.c_str(), e.what());
return default_val;
}
}
// getDouble, getBool 类似实现,getBool可以解析“true“/“false“,“1“/“0“,“on“/“off“等。
bool Config::getBool(const std::string& section, const std::string& key, bool default_val) {
std::string val = getString(section, key, ““);
if (val.empty()) {
return default_val;
}
// 转换为小写比较
std::string lower_val;
std::transform(val.begin(), val.end(), std::back_inserter(lower_val), ::tolower);
if (lower_val == “true“ || lower_val == “1“ || lower_val == “on“ || lower_val == “yes“) {
return true;
} else if (lower_val == “false“ || lower_val == “0“ || lower_val == “off“ || lower_val == “no“) {
return false;
} else {
LOG_ERROR(“Failed to parse config [%s][%s]=%s to bool.“, section.c_str(), key.c_str(), val.c_str());
return default_val;
}
}
} // namespace cloud_storage
注意:配置热重载 :一个更高级的特性是配置热重载。我们的实现是启动时加载一次。在生产环境中,有时需要不重启服务就更新配置(比如调整日志级别)。这可以通过监控配置文件修改时间(
stat系统调用)或使用inotify(Linux)等机制来实现。当检测到文件变化时,在一个线程安全的上下文中重新调用load()方法。但需要小心处理,因为某些配置(如线程池大小)在运行时动态更改可能引发复杂问题。
5. 统一错误码定义与项目整合
基础组件的最后一块拼图是错误码。我们在
error_code.h
中定义一个枚举类,并提供一个将错误码转换为描述字符串的函数。
// src/base/error_code.h
#ifndef CLOUD_STORAGE_BASE_ERROR_CODE_H
#define CLOUD_STORAGE_BASE_ERROR_CODE_H
namespace cloud_storage {
enum class ErrorCode {
OK = 0, // 成功
UNKNOWN_ERROR = 1, // 未知错误
// 配置相关错误 (1000-1999)
CONFIG_FILE_NOT_FOUND = 1001,
CONFIG_PARSE_ERROR = 1002,
CONFIG_VALUE_INVALID = 1003,
// 网络相关错误 (2000-2999)
NETWORK_SOCKET_CREATE_FAILED = 2001,
NETWORK_BIND_FAILED = 2002,
NETWORK_LISTEN_FAILED = 2003,
NETWORK_ACCEPT_FAILED = 2004,
NETWORK_CONNECTION_CLOSED = 2005,
NETWORK_RECV_TIMEOUT = 2006,
// 存储相关错误 (3000-3999)
STORAGE_FILE_OPEN_FAILED = 3001,
STORAGE_FILE_WRITE_FAILED = 3002,
STORAGE_FILE_READ_FAILED = 3003,
STORAGE_NO_SPACE_LEFT = 3004,
STORAGE_FILE_NOT_FOUND = 3005,
// 业务逻辑错误 (4000-4999)
BUSINESS_INVALID_REQUEST = 4001,
BUSINESS_UPLOAD_FILE_TOO_LARGE = 4002,
BUSINESS_INVALID_FILE_TYPE = 4003,
BUSINESS_SHORT_LINK_GEN_FAILED = 4004,
};
// 将错误码转换为可读字符串
const char* errorCodeToString(ErrorCode code);
} // namespace cloud_storage
#endif // CLOUD_STORAGE_BASE_ERROR_CODE_H
对应的
error_code.cpp
实现映射:
#include “base/error_code.h“
namespace cloud_storage {
const char* errorCodeToString(ErrorCode code) {
switch (code) {
case ErrorCode::OK: return “Success“;
case ErrorCode::UNKNOWN_ERROR: return “Unknown error“;
case ErrorCode::CONFIG_FILE_NOT_FOUND: return “Configuration file not found“;
case ErrorCode::CONFIG_PARSE_ERROR: return “Failed to parse configuration file“;
case ErrorCode::CONFIG_VALUE_INVALID: return “Invalid configuration value“;
case ErrorCode::NETWORK_SOCKET_CREATE_FAILED: return “Failed to create socket“;
case ErrorCode::NETWORK_BIND_FAILED: return “Failed to bind socket to address“;
case ErrorCode::NETWORK_LISTEN_FAILED: return “Failed to listen on socket“;
case ErrorCode::NETWORK_ACCEPT_FAILED: return “Failed to accept incoming connection“;
case ErrorCode::NETWORK_CONNECTION_CLOSED: return “Network connection closed by peer“;
case ErrorCode::NETWORK_RECV_TIMEOUT: return “Network receive timeout“;
case ErrorCode::STORAGE_FILE_OPEN_FAILED: return “Failed to open storage file“;
case ErrorCode::STORAGE_FILE_WRITE_FAILED: return “Failed to write to storage file“;
case ErrorCode::STORAGE_FILE_READ_FAILED: return “Failed to read from storage file“;
case ErrorCode::STORAGE_NO_SPACE_LEFT: return “No space left on storage device“;
case ErrorCode::STORAGE_FILE_NOT_FOUND: return “Storage file not found“;
case ErrorCode::BUSINESS_INVALID_REQUEST: return “Invalid client request“;
case ErrorCode::BUSINESS_UPLOAD_FILE_TOO_LARGE: return “Uploaded file exceeds size limit“;
case ErrorCode::BUSINESS_INVALID_FILE_TYPE: return “Uploaded file type is not allowed“;
case ErrorCode::BUSINESS_SHORT_LINK_GEN_FAILED: return “Failed to generate short link“;
default: return “Unrecognized error code“;
}
}
} // namespace cloud_storage
这样,在项目的任何地方,当函数执行失败时,可以返回一个明确的
ErrorCode
。上层调用者可以根据错误码决定下一步动作,并用
errorCodeToString
将其记录到日志中,使得问题定位一目了然。
6. 基础组件的整合测试与常见问题
组件写好了,我们需要验证它们是否能协同工作。让我们写一个简单的
main.cpp
来测试。
6.1 编写测试程序与配置文件
首先,创建一个示例配置文件
conf/storage.ini
:
; 服务器配置
[server]
ip = 0.0.0.0
port = 8080
thread_num = 4
; 日志配置
[log]
console_level = INFO
file_level = DEBUG
file_path = ./logs/cloud_storage.log
; 存储配置
[storage]
root_path = ./data
max_file_size_mb = 10
allowed_extensions = .jpg,.png,.gif,.bmp,.webp
然后,编写测试程序
src/main.cpp
:
#include “base/log.h“
#include “base/config.h“
#include “base/error_code.h“
#include <iostream>
#include <thread>
#include <chrono>
int main(int argc, char* argv[]) {
using namespace cloud_storage;
// 1. 初始化配置
std::string config_file = “../conf/storage.ini“; // 假设从build目录运行
if (!Config::getInstance()->load(config_file)) {
std::cerr << “Failed to load config. Exiting.“ << std::endl;
return static_cast<int>(ErrorCode::CONFIG_FILE_NOT_FOUND);
}
// 2. 初始化日志(依赖配置)
std::string log_file = Config::getInstance()->getString(“log“, “file_path“, ““);
std::string console_level_str = Config::getInstance()->getString(“log“, “console_level“, “INFO“);
std::string file_level_str = Config::getInstance()->getString(“log“, “file_level“, “DEBUG“);
// 将字符串转换为LogLevel枚举(需要实现一个辅助函数,此处简化)
auto strToLevel = [](const std::string& str) -> LogLevel {
if (str == “DEBUG“) return LogLevel::DEBUG;
if (str == “INFO“) return LogLevel::INFO;
if (str == “WARN“) return LogLevel::WARN;
if (str == “ERROR“) return LogLevel::ERROR;
if (str == “FATAL“) return LogLevel::FATAL;
return LogLevel::INFO; // 默认
};
if (!Logger::getInstance()->init(strToLevel(console_level_str), strToLevel(file_level_str), log_file)) {
std::cerr << “Failed to init logger. Exiting.“ << std::endl;
return -1;
}
LOG_INFO(“=== Cloud Storage System Starting ==“);
LOG_INFO(“Config loaded from: %s“, config_file.c_str());
// 3. 测试读取各种配置
std::string ip = Config::getInstance()->getString(“server“, “ip“, “127.0.0.1“);
int port = Config::getInstance()->getInt(“server“, “port“, 8080);
int thread_num = Config::getInstance()->getInt(“server“, “thread_num“, 2);
std::string root_path = Config::getInstance()->getString(“storage“, “root_path“, “./data“);
bool debug_mode = Config::getInstance()->getBool(“log“, “debug“, false); // 测试默认值
LOG_INFO(“Server config - IP: %s, Port: %d, Threads: %d“, ip.c_str(), port, thread_num);
LOG_INFO(“Storage config - Root Path: %s“, root_path.c_str());
LOG_INFO(“Debug mode (from non-existent key): %s“, debug_mode ? “true“ : “false“);
// 4. 测试不同级别的日志
LOG_DEBUG(“This is a DEBUG message, you might not see it if console level is higher.“);
LOG_INFO(“This is an INFO message.“);
LOG_WARN(“This is a WARN message.“);
LOG_ERROR(“This is an ERROR message. Error Code: %d“, static_cast<int>(ErrorCode::NETWORK_BIND_FAILED));
LOG_ERROR(“Error meaning: %s“, errorCodeToString(ErrorCode::NETWORK_BIND_FAILED));
// 5. 模拟多线程日志(测试线程安全,简陋版)
auto log_task = [](int id) {
for (int i = 0; i < 5; ++i) {
LOG_INFO(“Thread %d: Log message %d“, id, i);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
};
std::thread t1(log_task, 1);
std::thread t2(log_task, 2);
t1.join();
t2.join();
LOG_INFO(“=== Cloud Storage System Test Finished ==“);
return 0;
}
6.2 编写CMakeLists.txt构建项目
在项目根目录创建
CMakeLists.txt
:
cmake_minimum_required(VERSION 3.10)
project(cloud_storage VERSION 0.1.0)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 可执行文件输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib)
# 包含目录
include_directories(${PROJECT_SOURCE_DIR}/src)
# 添加可执行文件
add_executable(cloud_storage_test
src/main.cpp
src/base/log.cpp
src/base/config.cpp
src/base/error_code.cpp
)
# 在Linux/macOS下需要链接pthread库
find_package(Threads REQUIRED)
target_link_libraries(cloud_storage_test Threads::Threads)
6.3 编译、运行与问题排查
在项目根目录下执行:
mkdir -p build
cd build
cmake ..
make
编译成功后,会在
bin/
目录下生成
cloud_storage_test
可执行文件。运行前,确保
conf
和计划存放日志的
logs
目录存在。
cd ..
mkdir -p logs
./bin/cloud_storage_test
你应该能在控制台看到INFO及以上级别的日志,同时在
logs/cloud_storage.log
文件中看到DEBUG及以上级别的日志(如果文件路径配置正确的话)。
常见问题与排查技巧:
-
日志文件无法创建或写入 :检查
logs/目录的权限,确保运行程序的用户有写权限。在Logger::init中,如果文件打开失败,我们fallback到了stderr输出错误,这是一个好的实践。 -
配置文件路径错误 :测试程序中使用的是相对路径
“../conf/storage.ini“。这依赖于你从build目录运行程序。如果从其他地方运行,会找不到文件。更健壮的做法是通过命令行参数-c指定配置文件路径,或者使用绝对路径。 -
多线程日志内容交错 :我们用了互斥锁,所以同一行日志内容不会被打断。但来自不同线程的日志行顺序仍然是交错的,这是正常的。异步日志器可以缓解但无法完全避免这一点,因为线程调度顺序不确定。
-
配置项类型转换失败 :我们的
getInt、getBool使用了异常处理,并提供了默认值。在日志中会记录转换失败的错误,程序不会崩溃,而是使用默认值继续运行。这在生产环境中是必要的容错手段。 -
CMake找不到源文件 :检查
CMakeLists.txt中的文件路径是否正确,以及是否在build目录外执行了cmake命令。
至此,我们完成了图床共享云存储系统最底层、也是最关键的基础组件搭建。这些组件虽然不直接处理一张图片,但它们为整个系统提供了可观测性(日志)、灵活性(配置)和可靠性(错误处理)。在后续的文章中,我们将基于这个稳固的基础,一步步构建网络层、协议解析、存储引擎和业务逻辑,最终实现一个功能完整的图床服务。记住,好的基础设施是成功的一半,前期在这些“枯燥”的组件上多花点时间,后期开发效率会成倍提升。
更多推荐
所有评论(0)