日志系统第五弹:同步日志器模块

日志系统第五弹:同步日志器模块

  • 一、Logger类的设计
    • 1.功能
    • 2.如何打印日志
    • 3.设计 - - - 成员变量
      • 1.日志输出限制等级
      • 2.资源整合
      • 3.唯一标识
      • 4.互斥锁
    • 4.设计 - - - 成员函数
      • 1.对外的日志打印接口
      • 2.抽象的日志实际落地接口
      • 3.其他接口
    • 5.Logger类的框架
  • 二、Logger类的实现
    • 1.构造和get接口
    • 2.debug等等接口的实现
    • 3.construct的实现
    • 4.完整代码
  • 三、SyncLogger类的实现
    • 1.实现
    • 2.测试
  • 四、LoggerBuilder类的实现
    • 1.设计
    • 2.代码
  • 五、LocalLoggerBuilder的实现
  • 六、日志器管理模块的实现
    • 1.设计
    • 2.实现
  • 七、GlobalLoggerBuilder的实现
  • 八、测试
    • 1.局部日志器的使用
    • 2.全局日志器的使用

一、Logger类的设计

1.功能

日志器模块主要负责对日志消息模块,格式化模块和日志落地模块进行整合
对外提供便捷、好用的接口来完成消息的输出
注意:

  1. 一个日志器支持多个落地方向
  2. 一个日志器只支持一种日志消息格式
  3. 日志器是日志打印的基本单位
  4. 日志器分为同步日志器和异步日志器,分别负责同步日志记录和异步日志记录

想要整合日志的打印,我们首先就需要搞清楚该如何打印日志

2.如何打印日志

首先我们需要构造日志消息:

LogMessage message(LogLevel::value::FATAL,__FILE__,__LINE__,"default_logger","Hello World");

有了日志消息之后,下面就需要构造Formatter并将日志消息转换为带有格式的日志消息

Formatter formatter; // 就用默认日志格式了
std::string log_message = formatter.format(message);

有了带有格式的日志消息之后,下面我只需要将这个log_message 进行日志落地即可

LogSink::ptr log_sink=LogSinkFactory::create<FileSink>("./logfile/test.log");
log_sink->log(log_message.c_str(),log_message.size());

总体代码:

void test_log()
{LogMessage message(LogLevel::value::FATAL, __FILE__, __LINE__, "default_logger", "Hello World");Formatter formatter; // 就用默认日志格式了std::string log_message = formatter.format(message);LogSink::ptr log_sink=LogSinkFactory::create<FileSink>("./logfile/test.log");log_sink->log(log_message.c_str(),log_message.size());
}

在这里插入图片描述
可见,如果不进行整合,那么用户打印日志时就必须这么一步一步来,用户体验太差

而且用户无法像使用printf一样来打印日志,只能打印字符串,灵活性太差

因此才需要我们的日志器模块

3.设计 - - - 成员变量

1.日志输出限制等级

我们的日志器模块需要一个日志输出限制等级,只有当日志等级大于等于这个限制等级的时候,才允许该日志进行打印
这是为了能够更加灵活的来打印日志,而无需在需要限制日志的打印输出时采用注释和解除注释的方式

2.资源整合

一个日志器支持多个落地方向,因此需要一个vector<LogSink::ptr>
一个日志器只支持一种日志消息格式,因此需要一个Formatter::ptr

3.唯一标识

因为日志器需要被管理,因此我们给日志器一个唯一标识,就是日志器名称

4.互斥锁

因为一个日志器可能会被多个线程同时访问,因此存在线程安全问题(主要是日志落地的部分存在线程安全问题)
所以需要互斥锁

4.设计 - - - 成员函数

1.对外的日志打印接口

我们要像printf一样来支持灵活的打印方式,因此 需要用户传入format和后面的不定参,由我们进行解析,构造日志

同时为了方便用户使用,我们为每个日志等级都提供这么一个对外的日志打印接口

void debug(const std::string &file, size_t line, const std::string &body_format, ...);
void info(const std::string &file, size_t line, const std::string &body_format, ...);
void warning(const std::string &file, size_t line, const std::string &body_format, ...);
void error(const std::string &file, size_t line, const std::string &body_format, ...);
void fatal(const std::string &file, size_t line, const std::string &body_format, ...);

2.抽象的日志实际落地接口

因为我们既要实现同步日志器,又要实现异步日志器,因此日志的实际打印就有两种不同的方式了

所以我们对外提供的接口一定是需要复用一个抽象的日志实际落地接口

// 抽象接口完成实际的落地输出, 不同的日志器会有不同的实际落地方式
virtual void log(const char *data, size_t len) = 0;

3.其他接口

  1. debug, info… 等等接口,他们打印日志唯一的区别就是日志等级不同,因此我们可以抽离出共同的部分来,而这些接口的核心任务就是:
    1. 解析用户的传参,构造成LogMessage
    2. 格式化LogMessage
    3. 复用log实际落地接口,进行日志落地

所以共同部分我们命名为:construct
因为C的可变参数列表不支持传参,因此我们只能在debug,info…接口当中进行解析

void construct(LogLevel::value level, const std::string &file, size_t line, char *body)
  1. 对外提供一个日志器名称的get接口

5.Logger类的框架

class Logger
{
public:Logger(const std::string &logger_name, LogLevel::value limit_level, const Formatter::ptr &formatter, const std::vector<LogSink::ptr> &sinks);virtual ~Logger() {}const std::string& name() {}void debug(const std::string &file, size_t line, const std::string &body_format, ...);void info(const std::string &file, size_t line, const std::string &body_format, ...);void warning(const std::string &file, size_t line, const std::string &body_format, ...);void error(const std::string &file, size_t line, const std::string &body_format, ...);void fatal(const std::string &file, size_t line, const std::string &body_format, ...);// 因为下面的成员需要能够被子类所访问到, 因此我们不搞成private, 而是搞成protected
protected:void construct(LogLevel::value level, const std::string &file, size_t line, char *body);virtual void log(const char *data, size_t len) = 0;std::string _logger_name;std::mutex _mutex;std::atomic<LogLevel::value> _limit_level;std::vector<LogSink::ptr> _sinks;Formatter::ptr _formatter;
};

这个limit_level为何要搞成原子类?
因为后面的LoggerBuilder类需要设置这个值,因此这个值即会被读,又会被写
但是写操作只需要很简单的一步即可,所以搞成原子类就可以线程安全,就无需再给他加锁了

二、Logger类的实现

1.构造和get接口

Logger(const std::string &logger_name, LogLevel::value limit_level, const Formatter::ptr &formatter, const std::vector<LogSink::ptr> &sinks): _logger_name(logger_name), _limit_level(limit_level), _formatter(formatter), _sinks(sinks){}const std::string &name() {return _logger_name;}

2.debug等等接口的实现

核心任务:根据format来解析...
我们可以使用vasprintf来帮助我们完成这一任务

void debug(const std::string &file, size_t line, const std::string &body_format, ...)
{// 1. 比较日志等级if (_limit_level > LogLevel::value::DEBUG)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "debug: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::DEBUG, file, line, body);
}

同理,info,warning…都是这么写:

void info(const std::string &file, size_t line, const std::string &body_format, ...)
{// 1. 比较日志等级if (_limit_level > LogLevel::value::INFO)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "info: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::INFO, file, line, body);
}void warning(const std::string &file, size_t line, const std::string &body_format, ...)
{// 1. 比较日志等级if (_limit_level > LogLevel::value::WARNING)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "warning: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::WARNING, file, line, body);
}void error(const std::string &file, size_t line, const std::string &body_format, ...)
{// 1. 比较日志等级if (_limit_level > LogLevel::value::ERROR)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "error: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::ERROR, file, line, body);
}void fatal(const std::string &file, size_t line, const std::string &body_format, ...)
{// 1. 比较日志等级if (_limit_level > LogLevel::value::FATAL)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "fatal: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::FATAL, file, line, body);
}

3.construct的实现

void construct(LogLevel::value level, const std::string &file, size_t line, char *body)
{// 1. 构造LogMessageLogMessage message(level, file, line, _logger_name, body);// 2. free掉body,否则会内存泄漏free(body);body = nullptr;// 3. 将LogMessage进行格式化std::string real_message = _formatter->format(message);// 4. 复用log进行实际的日志落地log(real_message.c_str(), real_message.size());
}

4.完整代码

class Logger
{
public:Logger(const std::string &logger_name, LogLevel::value limit_level, const Formatter::ptr &formatter, const std::vector<LogSink::ptr> &sinks): _logger_name(logger_name), _limit_level(limit_level), _formatter(formatter), _sinks(sinks) {}virtual ~Logger() {}const std::string &name() { return _logger_name; }void debug(const std::string &file, size_t line, const std::string &body_format, ...){// 1. 比较日志等级if (_limit_level > LogLevel::value::DEBUG)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "debug: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::DEBUG, file, line, body);}void info(const std::string &file, size_t line, const std::string &body_format, ...){// 1. 比较日志等级if (_limit_level > LogLevel::value::INFO)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "info: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::INFO, file, line, body);}void warning(const std::string &file, size_t line, const std::string &body_format, ...){// 1. 比较日志等级if (_limit_level > LogLevel::value::WARNING)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "warning: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::WARNING, file, line, body);}void error(const std::string &file, size_t line, const std::string &body_format, ...){// 1. 比较日志等级if (_limit_level > LogLevel::value::ERROR)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "error: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::ERROR, file, line, body);}void fatal(const std::string &file, size_t line, const std::string &body_format, ...){// 1. 比较日志等级if (_limit_level > LogLevel::value::FATAL)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "fatal: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::FATAL, file, line, body);}// 因为下面的成员需要能够被子类所访问到, 因此我们不搞成private, 而是搞成protected
protected:void construct(LogLevel::value level, const std::string &file, size_t line, char *body){// 1. 构造LogMessageLogMessage message(level, file, line, _logger_name, body);// 2. free掉body,否则会内存泄漏free(body);body = nullptr;// 3. 将LogMessage进行格式化std::string real_message = _formatter->format(message);// 4. 复用log进行实际的日志落地log(real_message.c_str(), real_message.size());}virtual void log(const char *data, size_t len) = 0;std::string _logger_name;std::mutex _mutex;std::atomic<LogLevel::value> _limit_level;std::vector<LogSink::ptr> _sinks;Formatter::ptr _formatter;
};

三、SyncLogger类的实现

1.实现

对于同步日志器来说只需要实现其log纯虚函数即可
只需要遍历_sinks,复用其log接口即可
因此:

class SyncLogger : public Logger
{
public:SyncLogger(const std::string &logger_name, LogLevel::value limit_level, const Formatter::ptr &formatter, const std::vector<LogSink::ptr> &sinks): Logger(logger_name, limit_level, formatter, sinks) {}virtual void log(const char *data, size_t len){// 这里必须要加锁,因为存在多线程同时调用同一个日志器对象的log函数的情况std::unique_lock<std::mutex> ulock(_mutex);for (auto &sp : _sinks){sp->log(data, len);}}
};

2.测试

void test_log2()
{std::vector<LogSink::ptr> sinks ={LogSinkFactory::create<StdoutSink>(),LogSinkFactory::create<FileSink>("./logfile/test.log"),LogSinkFactory::create<RollSinkBySize>("./logfile/roll-", 1024 * 1024)};Logger::ptr logger = std::make_shared<SyncLogger>("default_logger", LogLevel::value::WARNING, std::make_shared<Formatter>(), sinks);logger->debug(__FILE__, __LINE__, "Hello %s:%d", "DEBUG", 1);logger->info(__FILE__, __LINE__, "Hello %s:%d", "INFO", 1);logger->warning(__FILE__, __LINE__, "Hello %s:%d", "WARNING", 1);logger->error(__FILE__, __LINE__, "Hello %s:%d", "ERROR", 1);logger->fatal(__FILE__, __LINE__, "Hello %s:%d", "FATAL", 1);
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
测试成功,可是日志器这样用,还是有些不好用,用户体验太差
能不能不给用户暴露这些细节呢?

主要是构造日志器较为麻烦,需要用户先自行构造日志器所需零件
因此我们可以使用建造者模式
而建造者模式需要:

  1. 抽象产品类
  2. 具体产品类
  3. 抽象Builder类
  4. 具体Builder类
  5. 指挥者Director类

我们日志器模块当中每个对象之间的构造没有任何先后的顺序要求,因此无需Director类

四、LoggerBuilder类的实现

1.设计

跟建造者模式一样,用户需要先通过我们提供的接口构造完零件之后,再构造整个日志器对象

因此,我们需要对外提供:

  1. logger_name的build接口
  2. limit_level的build接口
  3. formatter的build接口
  4. Logsink的build接口(添加对应落地方式)
  5. 整体的build接口(纯虚函数)

其中,因为同步日志器和异步日志器仅仅只是实际落地方式不同,因此我们没必要在搞一个同步日志器建造者类和异步日志器建造者类
给一个LoggerType类型进行区分即可

所以还需要给一个logger_type的build接口

如果用户没有构造某个零件,那么我们的默认值是:

logger_name必须要有
limit_level是DEBUG
formatter在build时检查,若为空,则给一个默认构造的Formatter
Logsink在build时检查,若为空,则给一个StdoutSink
logger_type给Sync

2.代码

enum class LoggerType
{SYNC,ASYNC
};class LoggerBuilder
{
public:LoggerBuilder() : _logger_type(LoggerType::SYNC), _limit_level(LogLevel::value::DEBUG) {}virtual ~LoggerBuilder() {}void buildLoggerName(const std::string &logger_name){_logger_name = logger_name;}void buildLoggerType(LoggerType logger_type){_logger_type = logger_type;}void buildLimitLevel(LogLevel::value limit_level){_limit_level = limit_level;}void buildFormatter(const std::string &pattern = ""){if (pattern.empty())_formatter = std::make_shared<Formatter>();else_formatter = std::make_shared<Formatter>(pattern);}template <class SinkType, class... Args>void buildLogSink(Args &&...args){_sinks.push_back(LogSinkFactory::create<SinkType>(std::forward<Args>(args)...));}virtual void build() = 0;protected:// 允许一个建造者在调用build接口构造完对象之后将建造者当中保存的原数据进行重置void reset(){_logger_name.clear();_logger_type = LoggerType::SYNC;_limit_level = LogLevel::value::DEBUG;_sinks.clear();_formatter.reset();}std::string _logger_name;LoggerType _logger_type;LogLevel::value _limit_level;std::vector<LogSink::ptr> _sinks;Formatter::ptr _formatter;
};

五、LocalLoggerBuilder的实现

为了提高日志器使用的灵活性,我们允许用户创建局部日志器和全局日志器这两种日志器

局部日志器就是只在创建该日志器的作用域当中有效
而全局日志器需要在日志器管理模块当中被管理,作用域是全局的

只需要实现LoggerBuilder类的build接口即可

class LocalLoggerBuilder : public LoggerBuilder
{
public:virtual Logger::ptr build(){if (_logger_name.empty()){std::cout << "日志器名称为空!!\n";abort();}if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}if (_sinks.empty()){_sinks.push_back(LogSinkFactory::create<StdoutSink>());}Logger::ptr ret;if (_logger_type == LoggerType::SYNC){ret = std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}else{// 后面实现完异步日志器之后完善}// 重置this->reset();return ret;}
};

六、日志器管理模块的实现

1.设计

因为日志器管理模块的作用域和声明周期需要是全局的
因此我们搞成单例类

如何管理呢?
增删查改,用一个哈希表建立起<logger_name,Logger::ptr>的映射
这个哈希表会被多线程访问,因此需要加锁

我们在单例对象刚被创建的时候,默认先创建一个日志器(用于标准输出的打印)
目的: 让用户在不创建任何日志器的情况下,也能进行标准输出的打印, 方便用户使用

2.实现

const std::string default_logger_name = "_default_logger";class LoggerManager
{
public:static LoggerManager &getInstance(){static LoggerManager logger_manager;return logger_manager;}// 增删查void addLogger(const Logger::ptr &logger){std::unique_lock<std::mutex> ulock(_mutex);_logger_map.insert(std::make_pair(logger->name(), logger));}void delLogger(const std::string &logger_name){std::unique_lock<std::mutex> ulock(_mutex);_logger_map.erase(logger_name);}Logger::ptr getLogger(const std::string &logger_name){std::unique_lock<std::mutex> ulock(_mutex);auto iter = _logger_map.find(logger_name);if (iter == _logger_map.end())return Logger::ptr();elsereturn iter->second;}bool existLogger(const std::string &logger_name){std::unique_lock<std::mutex> ulock(_mutex);return _logger_map.count(logger_name) > 0;}Logger::ptr default_logger(){// 无需加锁,因为没人修改它return _default_logger;}private:// 构造和析构私有LoggerManager(){// 创建默认日志器// 这里必须是局部日志器, 如果是全局日志器的话, 调用其build创建对象之后, 其会将该日志器添加到单例对象当中// 而此时单例对象还没有创建好, 此时就发生相互依赖问题导致的死循环LoggerBuilder::ptr builder = std::make_shared<LocalLoggerBuilder>();builder->buildLoggerName(default_logger_name);_default_logger = builder->build();_logger_map.insert(std::make_pair(default_logger_name, _default_logger));}~LoggerManager() {}// 禁掉拷贝和赋值LoggerManager(const LoggerManager &) = delete;const LoggerManager &operator=(const LoggerManager &) = delete;std::mutex _mutex;Logger::ptr _default_logger;std::unordered_map<std::string, Logger::ptr> _logger_map;
};

七、GlobalLoggerBuilder的实现

全局日志器建造者跟局部日志器建造者唯一的区别就是:
全局日志器建造者需要将建造好的日志器放到日志器管理模块当中

class GlobalLoggerBuilder : public LoggerBuilder
{
public:virtual Logger::ptr build(){if (_logger_name.empty()){std::cout << "日志器名称为空!!\n";abort();}// 查找全局当中是否有该日志器Logger::ptr ret = LoggerManager::getInstance().getLogger(_logger_name);if (ret.get() != nullptr)return ret;if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}if (_sinks.empty()){_sinks.push_back(LogSinkFactory::create<StdoutSink>());}if (_logger_type == LoggerType::SYNC){ret = std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}else{// 后面实现完异步日志器之后完善}// 重置this->reset();LoggerManager::getInstance().addLogger(ret);return ret;}
};

八、测试

1.局部日志器的使用

void test_log3()
{LoggerBuilder::ptr builder = std::make_shared<LocalLoggerBuilder>();builder->buildLoggerName("logger1");builder->buildLogSink<StdoutSink>();builder->buildLogSink<FileSink>("./new_logfile/test.log");builder->buildLogSink<RollSinkBySize>("./new_logfile/roll-", 1024 * 1024);// 其他的都用默认的Logger::ptr logger1 = builder->build();logger1->debug(__FILE__, __LINE__, "%s:%d", "DEBUG", 1);logger1->info(__FILE__, __LINE__, "%s:%d", "INFO", 1);logger1->warning(__FILE__, __LINE__, "%s:%d", "WARNING", 1);logger1->error(__FILE__, __LINE__, "%s:%d", "ERROR", 1);logger1->fatal(__FILE__, __LINE__, "%s:%d", "FATAL", 1);
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.全局日志器的使用

void test_log4()
{LoggerBuilder::ptr builder = std::make_shared<GlobalLoggerBuilder>();builder->buildLoggerName("logger1");builder->buildLogSink<StdoutSink>();builder->buildLogSink<FileSink>("./new_logfile/test2.log");builder->buildLogSink<RollSinkBySize>("./new_logfile/roll2-", 1024 * 1024);// 其他的都用默认的Logger::ptr logger1 = builder->build();logger1->debug(__FILE__, __LINE__, "%s:%d", "DEBUG", 1);logger1->info(__FILE__, __LINE__, "%s:%d", "INFO", 1);logger1->warning(__FILE__, __LINE__, "%s:%d", "WARNING", 1);logger1->error(__FILE__, __LINE__, "%s:%d", "ERROR", 1);logger1->fatal(__FILE__, __LINE__, "%s:%d", "FATAL", 1);
}

只需要把LocalLoggerBuilder改成GlobalLoggerBuilder即可
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
验证成功

以上就是日志系统第五弹:同步日志器模块的全部内容哦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/147784.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

springboot地方特色美食分享系统-计算机毕业设计源码02383

摘要 本论文主要论述了如何使用SpringBoot技术开发一个地方特色美食分享系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述地方特色美食分享系统的当前背景以…

DHCP服务器搭建

1. DHCP工作原理 DHCP动态分配IP地址&#xff0c;客户端广播&#xff0c;服务端单播 2. DHCP服务器安装 2.1 安装DHCP # yum install -y dhcp-server 2.2 修改配置文件 # cd /etc/dhcp/ # ls # vi dhcpd.conf dhcpd.conf 主配置文件 第一行&#xff1a;全局dhcp服务器地…

240922-Ollama使用Embedding实现RAG

A. 最终效果 B. 参考代码 # [嵌入模型 Ollama 博客 - Ollama 中文](https://ollama.org.cn/blog/embedding-models)# 步骤1&#xff1a;生成嵌入import ollama import chromadbdocuments ["Llamas are members of the camelid family meaning theyre pretty closely re…

Golang | Leetcode Golang题解之第423题从英文中重建数字

题目&#xff1a; 题解&#xff1a; func originalDigits(s string) string {c : map[rune]int{}for _, ch : range s {c[ch]}cnt : [10]int{}cnt[0] c[z]cnt[2] c[w]cnt[4] c[u]cnt[6] c[x]cnt[8] c[g]cnt[3] c[h] - cnt[8]cnt[5] c[f] - cnt[4]cnt[7] c[s] - cnt[6]…

手势识别-Yolov5模型-自制数据集训练

1、源码下载&#xff1a; 大家可以直接在浏览器搜索yolov5即可找到官方链接&#xff0c;跳转进github进行下载&#xff1a; 这里对yolov5模型补充说明一下&#xff0c;它是存在较多版本的&#xff0c;具体信息可在master->tags中查看&#xff0c;大家根据需要下载。这些不同…

二叉树(链式存储)

文章目录 一、树的基础概念二、二叉树2.1 概念 性质2.2 二叉树的存储2.2 二叉树的基本操作手动创建一棵二叉树遍历&#xff1a;前、中、后、层序获取树中节点的个数获取叶子节点的个数获取第K层节点的个数获取二叉树的高度检测值为value的元素是否存在判断一棵树是不是完全二叉…

青岛特某电新能源有限公司-充电业务流程及数据交互规范-集控前置-精简版V1.0

1 范围 本流程规定了特某电充电终端所属的集控器与特某电云平台前置之间的充电相关业务流程&#xff0c;明确两端之间的请求和响应。 2 术语 云平台&#xff1a;云平台是提供包括充电设备接入&#xff0c;充电设备信息采集&#xff0c;充电设备管 理&#xff0c;充电设备运维…

Gin框架入门(1)--路由搭建与Json处理

背景知识 为什么要使用Go框架 如果不使用框架&#xff0c;在创建服务器和调用端口时会遇到各种各样“奇怪”的问题&#xff08;就是出错的排查方向可能达到十几种&#xff09;&#xff0c;而且这些问题很难有相似性。同时作为适应于微服务的一门语言&#xff0c;代码的规范化…

构建高可用和高防御力的云服务架构第三部分:ECS集群(3/5)

ECS&#xff08;Elastic Compute Service&#xff09;是一种基础云计算服务&#xff0c;它提供了可伸缩的计算能力&#xff0c;允许用户在不需要预先购买硬件的情况下&#xff0c;根据需求快速扩展或缩减资源。ECS在云计算中的作用主要体现在提供虚拟化的服务器&#xff0c;用户…

食探秘:Spring Boot校园周边美食发现平台

第三章 系统设计 3.1 系统概要设计 本校园周边美食探索及分享平台选择B/S结构(Browser/Server,浏览器/服务器结构)和基于Web服务两种模式。适合在互联网上进行操作&#xff0c;只要用户能连网&#xff0c;任何时间、任何地点都可以进行系统的操作使用。系统工作原理图如图3-1所…

专业学习|动态规划(概念、模型特征、解题步骤及例题)

一、引言 &#xff08;一&#xff09;从斐波那契数列引入自底向上算法 &#xff08;1&#xff09;知识讲解 &#xff08;2&#xff09;matlap实现递归 &#xff08;3&#xff09;带有备忘录的遗传算法 &#xff08;4&#xff09;matlap实现带有备忘录的递归算法 “&#xff1…

linux入门介绍(通俗易懂,快速理解linux)

什么是操作系统&#xff1f; 操作系统&#xff08;Operating System&#xff0c;简称OS&#xff09;是管理和控制计算机硬件与软件资源的计算机程序&#xff0c;是配置在计算机硬件上的第一层软件&#xff0c;任何其它软件都必须在操作系统的支持下才能运行。 简单来说&#…

【LeetCode热题100】位运算

这篇博客先介绍了常见位运算操作&#xff0c;然后记录了关于位运算的几道题&#xff0c;包括判定字符是否唯一、丢失的数字、两整数之和、只出现一次的数字2、消失的两个数字。 在这一部分&#xff0c;我们不妨先来总结一下常见位运算操作&#xff1a; 1.基础位运算 >>…

fastadmin数据库创建说明文档

文章目录 数据库根据字段类型特殊字段以特殊字符结尾的规则注释说明 实例数字下拉列表日期时间文本框权重category_id --单选下拉框category_ids --多选下拉框deletetime --对应回收站status --对应tab常见问题 参考完结 数据库 这里提供的是数据库表字段规则在你创建表时使用…

Linux内核移植实战总结

直接参考【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81 本文仅作为个人笔记使用&#xff0c;方便进一步记录自己的实践总结。 前两章我们简单了解了一下 Linux 内核顶层 Makefile 和 Linux 内核的启动流程&#xff0c;本章我们就来学习一下如何将 NXP官方提供的 Linux 内核移…

17.1ksm关注指标讲解 pod和node状态的统计

本节重点介绍 : 主要的应用 看状态数个数 根据13105大盘模板看ksm指标 节点指标pod和容器指标资源对象按namespace分布指标其他资源指标 主要的应用 看状态&#xff0c;举例图片数个数&#xff0c;举例图片 根据大盘模板 查看指标 https://grafana.com/grafana/dashboard…

Tomcat靶场攻略

一.CVE-2017-12615 1.首页抓包&#xff0c;修改为 PUT 方式提交 ,将jsp木马写到数据包中 2.哥斯拉默认秘钥连接 二.后台弱⼝令部署war包 1.制作WAR包,上传 将JSP⽊⻢压缩为ZIP格式&#xff0c;然后修改后缀为war 2.文件上传成功后&#xff0c;默认会在网站根目录下生成和wa…

Apache 中间件漏洞

CVE-2021-41773 环境搭建 docker pull blueteamsteve/cve-2021-41773:no-cgid 访问172.16.1.4:8080 使⽤curl http://172.16.1.4:8080/cgi-bin/.%2e/.%2e/.%2e/.%2e/etc/passwd

面向对象设计其他原则例题

答案&#xff1a;D 知识点&#xff1a; 面向对象设计其他原则 重用发布等价原则 重用的粒度就是发布的粒度 共同封闭原则 包中的所有类对于同一性质的变化应该是共同封闭的。一个变化若对一个包产生影响&#xff0c;则将对该包里的所有类产生影响&#xff0c;而对于其他的…

Fyne ( go跨平台GUI )中文文档-Fyne总览(二)

本文档注意参考官网(developer.fyne.io/) 编写, 只保留基本用法 go代码展示为Go 1.16 及更高版本, ide为goland2021.2​​​​​​​ 这是一个系列文章&#xff1a; Fyne ( go跨平台GUI )中文文档-入门(一)-CSDN博客 Fyne ( go跨平台GUI )中文文档-Fyne总览(二)-CSDN博客 Fyne…