sylar:日志管理

news/2024/12/19 8:25:42/

参照 log4j 先写一个日志系统

以下代码均在同一文件sylar/log.h

开头两行:

#ifndef __SYLAR_LOG_H__
#define __SYLAR_LOG_H__#endif

#ifndef 是 “if not defined” 的缩写,它是一个预处理指令,去检查在当前的编译阶段,SYLAR_LOG_H 这个标识符是否还没有被定义过。如果没有被定义,那么后续在 #ifndef 和对应的 #endif 之间的代码将会被正常编译处理;反之,如果该标识符已经被定义了,那么这中间的代码将会被预处理器跳过,不会参与编译。

在这里插入图片描述

109 - 142 LogLevel

日志级别枚举

enum Level {UNKNOW = 0,   // 未知级别DEBUG = 1,    // 调试级别INFO = 2,     // 信息级别WARN = 3,     // 警告级别ERROR = 4,    // 错误级别FATAL = 5     // 致命错误级别
};

ToString 方法

static const char* ToString(LogLevel::Level level);

具体实现:

const char* LogLevel::ToString(LogLevel::Level level) {switch(level) {
#define XX(name) \case LogLevel::name: \return #name; \break;XX(DEBUG);XX(INFO);XX(WARN);XX(ERROR);XX(FATAL);
#undef XXdefault:return "UNKNOW";}return "UNKNOW";
}

宏定义:
XX(name) 接受一个参数 name,然后生成一个 switch 的 case 语句。

#name 是一个 预处理器字符串化操作,它会将宏参数 name 转换为字符串(例如:name = DEBUG 时,#name 会变为 “DEBUG”)。

如果 level 不匹配任何一个已知的日志级别),switch 语句会进入 default 分支,返回字符串 “UNKNOW”

具体就是通过宏减少了代码量

FromString 方法

static LogLevel::Level FromString(const std::string& str);

具体实现:

LogLevel::Level LogLevel::FromString(const std::string& str) {
#define XX(level, v) \if(str == #v) { \return LogLevel::level; \}XX(DEBUG, debug);XX(INFO, info);XX(WARN, warn);XX(ERROR, error);XX(FATAL, fatal);XX(DEBUG, DEBUG);XX(INFO, INFO);XX(WARN, WARN);XX(ERROR, ERROR);XX(FATAL, FATAL);return LogLevel::UNKNOW;
#undef XX
}

通过使用宏,代码实现变得非常简洁。每一个日志级别的匹配判断都由宏来生成,这样就避免了手动编写多个重复的 if 判断语句。代码的可维护性和可扩展性也得到了提升。

如果需要添加更多的日志级别,只需要在 XX 宏调用处添加新的日志级别,而不需要修改整个函数的结构。

支持了字符串(小写和大写)到日志级别枚举值的转换。

宏和函数定义总结

宏的基本特点

宏是由 预处理器 在编译之前展开的,它通过文本替换来处理代码。宏通常使用 #define 来定义。

优点:

  • 性能:宏在编译前直接替换代码,因此 没有函数调用的开销。这在某些性能关键的代码中非常有用,比如需要大量重复计算的常量表达式。
  • 灵活性:宏可以接受任意复杂的参数,并通过字符串化 (#) 或拼接 (##) 来动态生成代码。

缺点:

  • 调试困难:宏没有类型检查,它们在预处理阶段展开,调试时你无法看到宏展开后的结果。如果宏中存在错误,编译器可能会提示不明确的错误信息。
    可读性差:宏的展开过程是自动进行的,可能导致代码的可读性和可维护性差,尤其是复杂的宏定义。
  • 无法进行类型检查:宏没有类型信息,它们只是简单的文本替换,因此很容易出现错误(例如,传递了错误类型的参数)。
  • 作用域问题:宏是全局的,没有作用域限制,可能会无意中覆盖现有变量或导致名字冲突。

在这里插入图片描述

147 - 252 LogEvent

主要用于表示一次日志事件(日志条目)。每个日志事件包含了丰富的上下文信息,如日志的来源文件、行号、线程信息、日志级别、日志内容等。LogEvent 类是日志系统中非常关键的一个部分,它提供了日志记录所需的所有元数据。

类的构造

LogEvent(std::shared_ptr<Logger> logger, LogLevel::Level level,const char* file, int32_t line, uint32_t elapse,uint32_t thread_id, uint32_t fiber_id, uint64_t time,const std::string& thread_name);
  • std::shared_ptr<Logger> logger:指向日志器对象的智能指针,表示记录该日志事件的日志器。一个日志事件必须通过某个日志器来输出。

  • LogLevel::Level level:日志级别,表示此次日志事件的严重程度。

  • const char* file:触发日志事件的源文件的文件名。

  • int32_t line:触发日志事件的源文件中的行号。

  • uint32_t elapse:程序启动到当前日志事件发生时的时间差(以毫秒为单位)。这可以帮助追踪程序运行的时间。

  • uint32_t thread_id:生成该日志事件的线程 ID。

  • uint32_t fiber_id:生成该日志事件的协程 ID。这个值适用于多协程的程序,有助于区分是哪个协程产生的日志。

  • uint64_t time:日志事件生成的时间戳(通常以秒为单位)。

  • const std::string& thread_name:生成该日志事件的线程名称。

成员函数列表

getFile():返回触发日志事件的源文件的文件名。
getLine():返回触发日志事件的源文件中的行号。
getElapse():返回程序启动后到日志事件发生的毫秒数。
getThreadId():返回生成该日志事件的线程 ID。
getFiberId():返回生成该日志事件的协程 ID。
getTime():返回该日志事件的时间戳(秒)。
getThreadName():返回生成该日志事件的线程名称。
getContent():返回日志内容,即 std::stringstream 中的字符串内容。这是日志的主要信息部分。
getLogger():返回指向日志器对象的智能指针,表示生成该日志事件的日志器。
getLevel():返回日志事件的日志级别。
getSS():返回 std::stringstream 对象的引用,允许将日志内容写入该流中。

11个成员函数,对应10个成员变量,其中m_ss内容流对应两个成员函数:
分别返回m_ss和m_ss.str()

  std::string getContent() const { return m_ss.str();}std::stringstream& getSS() { return m_ss;}

格式化函数

format(const char* fmt, ...)这是一个变参函数,用于将格式化后的字符串内容写入到 m_ss 中。它使用 printf 风格的格式化字符串。

format(const char* fmt, va_list al):这是一个接收 va_list 的版本,用于处理 format 函数中的可变参数列表,支持更灵活的格式化输出。

使用场景

LogEvent 类通常由 Logger 类在日志记录时生成,并提供给 Appender 类用于输出。它通过包含丰富的上下文信息(如文件、行号、线程、协程等),使得开发者可以在分析日志时,快速定位问题。

257 - 285LogEventWrap 日志事件包装器

LogEventWrap 类的目的是对日志事件进行封装,方便在其他地方处理日志事件的相关内容。通过它,可以获取到日志事件对象和日志内容流。

成员变量

LogEvent::ptr m_event;

m_event 是 LogEvent::ptr 类型的成员变量,存储了实际的日志事件。通过这个成员,LogEventWrap 类可以持有并操作一个日志事件对象

类的构造和析构

LogEventWrap(LogEvent::ptr e);
~LogEventWrap();
LogEventWrap::LogEventWrap(LogEvent::ptr e):m_event(e) {
}LogEventWrap::~LogEventWrap() {m_event->getLogger()->log(m_event->getLevel(), m_event);
}

析构里:调用getLogger()返回了一个share_ptr < Logger > 日志器
再使用他的log方法传入logLevel,和logevent
具体的Logger类会说

成员函数

LogEvent::ptr getEvent() const { return m_event; }
std::stringstream& LogEventWrap::getSS() {return m_event->getSS();
}

288 - 366 LogFormatter 日志格式化

成员变量

std::string m_pattern
保存日志格式模板(例如:
%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n)。std::vector<FormatItem::ptr> m_items:
保存解析后的格式化项对象,每个对象对应模板中的一个占位符
(如 %d、%p 等)。bool m_error:
标识日志格式是否出现错误。
如果 m_error 为 true,表示格式解析过程中发生了错误。typedef std::shared_ptr<LogFormatter> ptr;

构造函数

传入字符串参数LogFormatter::LogFormatter(const std::string& pattern):m_pattern(pattern) {init();
}    
init()函数
// 初始化函数,用于解析日志格式模板并生成相应的格式化项
void LogFormatter::init() {// 用于存储格式化项的临时数据(包括字符串部分、格式化部分和标记)std::vector<std::tuple<std::string, std::string, int>> vec;std::string nstr;  // 临时存储普通字符串部分// 遍历日志格式模板for (size_t i = 0; i < m_pattern.size(); ++i) {// 如果当前字符不是 '%',说明它是普通字符,直接加入到 nstr 中if (m_pattern[i] != '%') {nstr.append(1, m_pattern[i]);continue;}// 如果遇到连续的 '%%',将 '%' 添加到 nstr 中,跳过下一个字符if ((i + 1) < m_pattern.size()) {if (m_pattern[i + 1] == '%') {nstr.append(1, '%');continue;}}// 解析格式化项size_t n = i + 1;  // 从 '%' 后的下一个字符开始解析int fmt_status = 0;  // 0: 解析格式标识符, 1: 解析格式内容size_t fmt_begin = 0;  // 格式内容的起始位置std::string str;  // 存储格式标识符std::string fmt;  // 存储格式内容// 遍历模板中的每个字符,直到格式项解析完毕while (n < m_pattern.size()) {// 如果当前字符既不是字母也不是 '{' 或 '}',说明已解析完格式标识符if (!fmt_status && (!isalpha(m_pattern[n]) && m_pattern[n] != '{' && m_pattern[n] != '}')) {str = m_pattern.substr(i + 1, n - i - 1);  // 提取格式标识符break;}if (fmt_status == 0) {// 如果是 '{',说明是带格式内容的格式化项,进入格式化内容解析状态if (m_pattern[n] == '{') {str = m_pattern.substr(i + 1, n - i - 1);  // 提取格式标识符fmt_status = 1;  // 进入格式化内容解析fmt_begin = n;  // 标记格式开始位置++n;  // 跳过 '{'continue;}} else if (fmt_status == 1) {// 如果是 '}',则结束格式内容的解析if (m_pattern[n] == '}') {fmt = m_pattern.substr(fmt_begin + 1, n - fmt_begin - 1);  // 提取格式内容fmt_status = 0;  // 格式内容解析结束++n;  // 跳过 '}'break;}}++n;  // 移动到下一个字符if (n == m_pattern.size()) {// 如果解析到字符串结尾,且没有找到 '}', 说明格式项不完整if (str.empty()) {str = m_pattern.substr(i + 1);  // 提取剩余部分}}}// 如果格式项解析完成,将其存储到 vec 中if (fmt_status == 0) {if (!nstr.empty()) {vec.push_back(std::make_tuple(nstr, std::string(), 0));  // 存储普通字符串部分nstr.clear();}vec.push_back(std::make_tuple(str, fmt, 1));  // 存储格式化项i = n - 1;  // 更新 i 为格式项解析结束的位置} else if (fmt_status == 1) {// 如果格式项解析失败(例如没有找到对应的 '}'),记录错误std::cout << "pattern parse error: " << m_pattern << " - " << m_pattern.substr(i) << std::endl;m_error = true;vec.push_back(std::make_tuple("<<pattern_error>>", fmt, 0));  // 存储错误项}}// 如果 nstr 中还有剩余的普通字符串,添加到 vec 中if (!nstr.empty()) {vec.push_back(std::make_tuple(nstr, "", 0));}// 定义一个静态映射表,将格式标识符(如 'm'、'p' 等)映射到相应的 FormatItem 类型static std::map<std::string, std::function<FormatItem::ptr(const std::string& str)>> s_format_items = {
#define XX(str, C) \{#str, [](const std::string& fmt) { return FormatItem::ptr(new C(fmt));}}XX(m, MessageFormatItem),           // m: 消息XX(p, LevelFormatItem),             // p: 日志级别XX(r, ElapseFormatItem),            // r: 累计毫秒数XX(c, NameFormatItem),              // c: 日志名称XX(t, ThreadIdFormatItem),          // t: 线程 IDXX(n, NewLineFormatItem),           // n: 换行符XX(d, DateTimeFormatItem),          // d: 时间XX(f, FilenameFormatItem),          // f: 文件名XX(l, LineFormatItem),              // l: 行号XX(T, TabFormatItem),               // T: 制表符XX(F, FiberIdFormatItem),           // F: 协程 IDXX(N, ThreadNameFormatItem),        // N: 线程名称
#undef XX};// 遍历 vec,根据格式标识符创建相应的 FormatItem 对象,并加入 m_items 容器for (auto& i : vec) {if (std::get<2>(i) == 0) {// 如果是普通字符串部分,创建 StringFormatItem 对象m_items.push_back(FormatItem::ptr(new StringFormatItem(std::get<0>(i))));} else {// 如果是格式化项,查找对应的 FormatItem 类型auto it = s_format_items.find(std::get<0>(i));if (it == s_format_items.end()) {// 如果没有找到对应的格式化项类型,记录错误m_items.push_back(FormatItem::ptr(new StringFormatItem("<<error_format %" + std::get<0>(i) + ">>")));m_error = true;} else {// 创建对应的 FormatItem 对象并传入格式内容m_items.push_back(it->second(std::get<1>(i)));}}}
}

成员函数format

std::string LogFormatter::format(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {std::stringstream ss;for(auto& i : m_items) {i->format(ss, logger, level, event);}return ss.str();
}std::ostream& LogFormatter::format(std::ostream& ofs, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {for(auto& i : m_items) {i->format(ofs, logger, level, event);}return ofs;
}

这两个 format 函数分别用于将日志事件格式化为字符串或输出流(std::ostream)格式。它们的核心逻辑非常相似,主要依赖于 m_items 容器中的格式化项(FormatItem)来逐步构建最终的日志信息。

返回string类型的是给后面StdoutLogAppender用
返回ostream类型的是给FileLogAppender用

i是m_items里的,是嵌套类FormatItem对象
他有format函数

    virtual void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;

内部类FormatItem

FormatItem内部类用于封装日志内容项格式化的相关逻辑,使得对每一项具体的格式化操作能够独立进行处理。不同的日志内容项(比如消息、日志级别、时间等)都有各自的格式化要求和实现方式,通过将它们抽象成一个个FormatItem对象,

 virtual void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level,LogEvent::ptr event) = 0;

提供用于继承的虚函数

428 - 541 Logger 日志器

成员变量

m_name用于存储日志器的名称,方便对不同日志器进行区分和标识。
m_level记录当前日志器的日志级别,决定了哪些级别的日志可以被该日志器记录。
m_mutex是前面定义的Spinlock类型的锁,用于在多线程环境下保护类中共享资源(如m_appenders、m_formatter等)的并发访问安全。
m_appenders是一个存储LogAppender智能指针的链表,用于管理该日志器关联的所有日志目标对象。
m_formatter是指向日志格式器的智能指针,负责控制日志的输出格式。
m_root是指向主日志器的智能指针,可能在日志系统的层次结构或者一些特殊的管理逻辑中起到关联、继承等相关作用,具体依赖于整个日志系统的设计。

构造函数

Logger::Logger(const std::string& name):m_name(name),m_level(LogLevel::DEBUG) {m_formatter.reset(new LogFormatter("%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"));
}

log方法实现

/*** 记录日志事件* @param level 日志级别* @param event 日志事件*/
void Logger::log(LogLevel::Level level, LogEvent::ptr event) {// 如果日志级别大于等于当前日志器的级别if(level >= m_level) {// 获取当前日志器的共享指针auto self = shared_from_this();// 加锁,保护日志器的互斥量MutexType::Lock lock(m_mutex);// 如果有日志输出目标if(!m_appenders.empty()) {// 遍历所有日志输出目标for(auto& i : m_appenders) {// 调用每个目标的 log 方法,记录日志事件i->log(self, level, event);}// 如果没有日志输出目标,但存在根日志器} else if(m_root) {// 调用根日志器的 log 方法,记录日志事件m_root->log(level, event);}}
}

371 - 423 LogAppender 日志输出目标

定义了一个 LogAppender 类,是一个基类
子类StdoutLogAppender和FileLogAppender继承它
提供给子类继承的方法

	virtual std::string toYamlString() = 0;virtual void log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;

这两个是不同子类之间有区分,所以要虚函数

对于子类没区别的

/*** @brief 更改日志格式器*/void setFormatter(LogFormatter::ptr val);/*** @brief 获取日志格式器*/LogFormatter::ptr getFormatter();/*** @brief 获取日志级别*/LogLevel::Level getLevel() const { return m_level;}/*** @brief 设置日志级别*/void setLevel(LogLevel::Level val) { m_level = val;}

其中,后两个setLevel和getLevel 仅仅是对成员变量 m_level 进行读取或修改,因此它们可以直接在类的定义中实现。

而前面两个需要对m_formatter 和 m_hasFormatter 成员的访问是线程安全的。

546 - 551 StdoutLogAppender 输出到控制台的Appender

class StdoutLogAppender : public LogAppender {
public:typedef std::shared_ptr<StdoutLogAppender> ptr;void log(Logger::ptr logger, LogLevel::Level level, LogEvent::ptr event) override;std::string toYamlString() override;
};

对log和toYamlString进行了重写
默认构造

std::string StdoutLogAppender::toYamlString() {MutexType::Lock lock(m_mutex);  // 1. 锁定互斥量,保证线程安全YAML::Node node;               // 2. 创建一个 YAML 节点对象node["type"] = "StdoutLogAppender";  // 3. 设置输出目标的类型// 4. 如果日志级别不是 UNKNOW,加入 level 字段if (m_level != LogLevel::UNKNOW) {node["level"] = LogLevel::ToString(m_level);  // 将日志级别转换为字符串并设置}// 5. 如果存在格式器并且格式器有效,加入 formatter 字段if (m_hasFormatter && m_formatter) {node["formatter"] = m_formatter->getPattern();  // 获取并设置格式器的模式(模板)}// 6. 将 YAML 节点对象输出到字符串流中std::stringstream ss;ss << node;  // 将 YAML 节点序列化为字符串流内容return ss.str();  // 7. 返回 YAML 字符串
}

得到日志输出的字符串

void StdoutLogAppender::log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {if(level >= m_level) {  // 1. 检查日志级别MutexType::Lock lock(m_mutex);  // 2. 锁定互斥量,确保线程安全m_formatter->format(std::cout, logger, level, event);  // 3. 格式化日志并输出到控制台}
}其中format是这样的:
std::ostream& LogFormatter::format(std::ostream& ofs, 
std::shared_ptr<Logger> logger, LogLevel::Level level, 
LogEvent::ptr event) {for(auto& i : m_items) {i->format(ofs, logger, level, event);}return ofs;
}

556 - 575 FileLogAppender输出到文件的Appender

lass FileLogAppender : public LogAppender {
public:typedef std::shared_ptr<FileLogAppender> ptr;FileLogAppender(const std::string& filename);void log(Logger::ptr logger, LogLevel::Level level, LogEvent::ptr event) override;std::string toYamlString() override;/*** @brief 重新打开日志文件* @return 成功返回true*/bool reopen();
private:/// 文件路径std::string m_filename;/// 文件流std::ofstream m_filestream;/// 上次重新打开时间uint64_t m_lastTime = 0;
};

由于是文件,添加了文件打开时间操作
也有构造函数,因为要传入文件名

580 - 615 LoggerManager 日志器管理类

给logger设置根日志器:

LoggerManager::LoggerManager() {m_root.reset(new Logger);m_root->addAppender(LogAppender::ptr(new StdoutLogAppender));m_loggers[m_root->m_name] = m_root;init();
}

在日志器管理器中寻找文件名是name的日志器

Logger::ptr LoggerManager::getLogger(const std::string& name) {MutexType::Lock lock(m_mutex);auto it = m_loggers.find(name);if(it != m_loggers.end()) {return it->second;}Logger::ptr logger(new Logger(name));logger->m_root = m_root;m_loggers[name] = logger;return logger;
}

返回根目录器

    Logger::ptr getRoot() const { return m_root;}
std::string LoggerManager::toYamlString() {MutexType::Lock lock(m_mutex);YAML::Node node;for(auto& i : m_loggers) {node.push_back(YAML::Load(i.second->toYamlString()));}std::stringstream ss;ss << node;return ss.str();
}

这个函数的目的是将 LoggerManager 管理的所有日志器的配置信息以 YAML 格式输出,以便于查看和管理

25-100 宏定义

#define SYLAR_LOG_LEVEL(logger, level) \if(logger->getLevel() <= level) \sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, \__FILE__, __LINE__, 0, sylar::GetThreadId(),\sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getSS()

定义宏SYLAR_LOG_LEVEL
如果logger->getLevel() <= level就把宏替换成
调用

LogEventWrap::LogEventWrap(LogEvent::ptr e):m_event(e) {
}

的构造函数,

该构造要传入,LogEvent的指针,该指针由

sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, \__FILE__, __LINE__, 0, sylar::GetThreadId(),\sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getSS()

new 出来,赋值给一个std::shared_ptr< LogEvent >
而new需要调用构造函数

LogEvent::LogEvent(std::shared_ptr<Logger> logger, LogLevel::Level level,const char* file, int32_t line, uint32_t elapse,uint32_t thread_id, uint32_t fiber_id, uint64_t time,const std::string& thread_name):m_file(file),m_line(line),m_elapse(elapse),m_threadId(thread_id),m_fiberId(fiber_id),m_time(time),m_threadName(thread_name),m_logger(logger),m_level(level) {
}

其中

pid_t GetThreadId() {return syscall(SYS_gettid);
}uint32_t GetFiberId() {return sylar::Fiber::GetFiberId();
}const std::string& Thread::GetName() {return t_thread_name;
}std::stringstream& LogEventWrap::getSS() {return m_event->getSS();
}// m_event->getSS()std::stringstream& getSS() { return m_ss;}

http://www.ppmy.cn/news/1556341.html

相关文章

windows C#-扩展方式的常见使用模式

集合功能 过去&#xff0c;创建”集合类”通常是为了使给定类型实现 System.Collections.Generic.IEnumerable<T> 接口&#xff0c;并实现对该类型集合的功能。 创建这种类型的集合对象没有任何问题&#xff0c;但也可以通过对 System.Collections.Generic.IEnumerable&…

代码随想录训练营第二十二天| 77. 组合 216.组合总和III 17.电话号码的字母组合

77. 组合 题目链接/文章讲解&#xff1a; 代码随想录 视频讲解&#xff1a;带你学透回溯算法-组合问题&#xff08;对应力扣题目&#xff1a;77.组合&#xff09;| 回溯法精讲&#xff01;_哔哩哔哩_bilibili 经典回溯 一点都看不懂 所以就看题解慢慢来吧 Java代码&#xff1a…

短剧系统开发教程概要

引言 随着移动互联网的快速发展&#xff0c;短剧内容因其简短、精炼、情节紧凑的特点&#xff0c;吸引了大量观众的喜爱。为了满足市场需求&#xff0c;开发一款功能完善、体验优良的短剧平台显得尤为重要。本文将详细介绍短剧源码的开发搭建过程&#xff0c;包括需求分析、技…

USB Type-C接口快充协议芯片的特点与发展趋势

随着智能手机、平板电脑、笔记本电脑及其他便携式设备的普及&#xff0c;USB Type-C接口已经成为主流的连接标准。在这个过程中&#xff0c;USB Type-C接口不仅在数据传输上表现出色&#xff0c;还因其支持高功率传输&#xff0c;成为现代设备快充的核心技术之一。为了满足用户…

网络攻与防

1、两个专网连接 &#xff08;1&#xff09;、两个网卡VMNET2/3---配置IP子网、仅主机模式--除去DHCP设置 路由和两台主机分别ping通 &#xff08;2&#xff09;、路由配置&#xff1a;两个专网之间连接--否拨号连接 两台主机可相互ping通---成功 如果ping不通&#xff0c;…

力扣-图论-12【算法学习day.62】

前言 ###我做这类文章一个重要的目的还是给正在学习的大家提供方向和记录学习过程&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非…

软件集成测试内容和作用简析

在现代软件开发过程中&#xff0c;软件集成测试作为关键的一环&#xff0c;日益受到重视。特别是随着信息技术的快速发展&#xff0c;各类软件系统日益庞大复杂&#xff0c;如何确保系统不同模块的顺畅合作&#xff0c;成为了每个项目成功的重要基础。集成测试是指在软件开发过…

etcd数据迁移

场景1 更换高性能盘 停掉etcd服务高性能盘上创建新的数据目录copy旧数据文件到新数据目录中修改配置文件的数据目录为新目录&#xff0c;然后重启服务 场景2 更换高性能物理机器&#xff0c;不停服切换 新的机器需要先安装好etcd服务先启动安装好的一台etcd&#xff0c;单独…