WebServer -- 日志系统(下)

news2024/10/6 1:40:22

目录

🌼整体思路

🎂基础API

fputs

可变参数宏 __VA_ARGS__

fflush

🚩流程图与日志类定义

流程图

日志类定义

🌼功能实现

生成日志文件 && 判断写入方式

日志分级与分文件


🌼整体思路

日志系统分两部分,

一:单例模式与阻塞队列的定义

二:日志类的定义与使用

这里介绍日志类的定义与使用,具体涉及基础API,流程图与日志类的定义,功能实现

  • 基础API
    描述 fputs,可变参数宏__VA__ARGS__,fflush
  • 流程图与日志类定义
    描述日志系统整体运行流程,介绍日志类的具体定义
  • 功能实现
    结合代码分析同步 / 异步写文件逻辑,分析超行,按天分文件和日志分级的具体实现

🎂基础API

更好的源码阅读体验~

fputs

#include<stdio.h>
int fputs(const char *str, FILE *stream);
  • str,一个数组,包含要写入的   以空字符终止的字符序列
  • stream,指向FILE对象的指针,该FILE对象标识了要被写入字符串的流

可变参数宏 __VA_ARGS__

__VA_ARGS__,可变参数的宏,定义宏时,定义中参数列表的最后一个参数为省略号,实际使用中有时会加##,有时不加

myprintf("CSDN zhanghm1995")
-> printf("CSDN zhanghm1995")
myprintf("CSDN zhanghm1995 is %d years", 2)
-> printf("CSDN zhanghm1995 is %d years", 2)
// 最简单的定义
#define my_print1(...) printf(__VA_ARGS__)

// 搭配va_list的format使用
#define my_print2(format, ...) printf(format, __VA_ARGS__)
#define my_print3(format, ...) printf(format, ##__VA_ARGS__)

__VA_ARGS__ 宏前面加上 ## 的作用:

当可变参数个数为 0,这里的 printf 参数列表中的 ## 回把前面多余的 , 去掉

否则会编译出错

建议用第二种带 ## 的,程序更加健壮

补充解释👇

C++中可变参数宏定义用法实践_c++可变宏-CSDN博客

  1. 处理任意数量的参数:可变参数宏可以接受不定数量的参数,使得宏在调用时可以传入任意数量的参数。

  2. 使用省略号表示:在宏定义中,使用省略号...表示可变数量的参数,这样就可以在宏中处理不确定数量的实参。

  3. 利用递归或辅助函数处理参数:为了处理可变数量的参数,通常会结合使用递归函数或辅助函数来逐个处理每个参数。

  4. 宏展开后形成单一代码块:为避免在宏展开时产生意外的行为,通常在宏定义中使用do...while(0)结构,将宏展开后形成单一的代码块。

  5. 提高代码复用性:通过可变参数宏,可以实现一次宏定义,多处调用,提高代码的复用性和可维护性。

#include <iostream>  // 包含输入输出流库
#include <sstream>   // 包含字符串流库

// 定义一个可变参数宏 PRINT,用于打印任意数量的参数
/*
定义 PRINT 宏,使用反斜杠表示换行继续宏定义
定义宏展开后的代码块开始
创建一个字符串流对象
用辅助函数处理可变参数并将结果存入字符串流
将字符串流中的内容输出到标准输出流
宏展开后的代码块结束,用 do...while(0) 结构避免宏在使用时产生意外的行为
*/
#define PRINT(...) \ 
do { \  
    std::stringstream ss; \  
    print_helper(ss, __VA_ARGS__); \  
    std::cout << ss.str() << std::endl; \  
} while(0)  

// 辅助函数,用于将可变参数格式化为字符串
template <typename T>
void print_arg(std::ostream& os, const T& arg) {  // 辅助函数,将单个参数输出到指定输出流
    os << arg;  // 输出参数到指定输出流
}

// 递归模板函数,将多个参数格式化为字符串
template <typename T, typename... Args>
void print_arg(std::ostream& os, const T& firstArg, const Args&... args) {  // 递归模板函数,处理多个参数的输出
    os << firstArg << " ";  // 输出第一个参数到指定输出流,并添加空格分隔
    print_arg(os, args...);  // 递归调用自身,处理剩余参数
}

// 模板函数,调用递归函数开始处理多个参数
template <typename... Args>
void print_helper(std::ostream& os, const Args&... args) {  // 模板函数,调用递归函数开始处理多个参数
    print_arg(os, args...);  // 调用递归函数开始处理多个参数
}

int main() {
    // 使用 PRINT 宏输出多个参数
    PRINT("Hello, ", "World!");  // 使用 PRINT 宏输出多个参数
    PRINT("The answer is", 42, "and", 3.14);  // 使用 PRINT 宏输出多个参数
    return 0;  // 返回执行成功
}
Hello,  World!
The answer is 42 and 3.14

fflush

#include<stdio.h>
int fflush(FILE *stream);

fflush() 会强迫将 缓冲区 的数据,写回参数 stream 指定的文件中,如果参数 stream 为 NULL,fflush() 会将所有打开的文件数据更新

在使用多个输出函数,连续进行多次输出到控制台时,有可能下一个数据的上一数据还没输出完毕,还在输出缓冲区中时,下一个 printf 就把另一个数据加入输出缓冲区,结果冲掉了原来的数据,出现错误

在 printf() 后加上 fflush(stdout); 强制马上输出到控制台,可以避免上述错误

🚩流程图与日志类定义

流程图

  • 日志文件
    • 局部变量的懒汉模式获取实例
    • 生成日志文件,并判断同步和异步写入方式
  • 同步
    • 判断是否分文件
    • 直接格式化输出内容,将信息写入日志文件
  • 异步
    • 判断是否分文件
    • 格式化输出内容,将内容写入阻塞队列,创建一个写线程,从阻塞队列取出内容写入日志文件

下面的图,是我在这个网站上画的👇 比语雀好用一丢丢

Excalidraw | Hand-drawn look & feel • Collaborative • Secure

日志类定义

通过局部变量的懒汉单例模式,创建日志实例,对其进行初始化生成日志文件后,格式化输出内容,并根据不同的写入方式,完成对应逻辑,写入日志文件

日志类包括以下方法👇

  • 公有的实例获取方法
  • 初始化日志文件方法
  • 异步日志写入方法,内部调用私有异步方法
  • 内容格式化方法
  • 刷新缓冲区
  • ....
class Log
{
public:
    // C++11以后,使用局部变量懒汉不用加锁
    static Log *get_instance() // 获取Log类的唯一实例
    {
        static Log instance; // 静态局部变量,保证线程安全的懒汉单例模式
        return &instance; // 返回实例指针
    }

    // 可选的参数有 日志文件,日志缓冲区大小,最大行数,
    // 和 最长日志条队列
    bool init(const char *file_name, int log_buf_size = 8192,
              int split_lines = 5000000, int max_queue_size = 0); // 初始化日志系统
    
    // 异步写日志公有方法,调用私有方法 async_write_log
    static void *flush_log_thread(void *args) // 刷新日志的线程函数
    {
        // Log类的唯一实例指针
        // 类内访问静态成员,也需要声明作用域 ::
        Log::get_instance()->async_write_log(); // 调用日志实例的异步写日志方法
    }

    // 将输出内容按照标准格式整理
    // ... 表示 可变参数列表
    void write_log(int level, const char *format, ...); // 写日志方法

    // 强制刷新缓冲区
    void flush(void); // 强制刷新日志缓冲

private:
    Log(); // 构造函数
    virtual ~Log(); // 虚析构函数

    // 异步写日志方法
    void *async_write_log() // 异步写日志方法
    {
        string single_log; // 单条日志字符串

        // 从阻塞队列中取出一条日志内容,写入文件
        while (m_log_queue->pop(single_log)) // 从阻塞队列中取出日志
        {
            m_mutex.lock(); // 加锁
            fputs(single_log.c_str(), m_fp); // 将日志内容写入文件
            m_mutex.unlock(); // 解锁
        }
    }

private:
    char dir_name[128]; // 路径名
    char log_name[128]; // log文件名
    int m_split_lines; // 日志最大行数
    int m_log_buf_size; // 日志缓冲区大小
    long long m_count; // 日志行数记录
    int m_today; // 按天分文件,记录当前时间哪一天
    FILE *m_fp; // 打开log的文件指针
    char *m_buf; // 要输出的内容
    block_queue<string> *m_log_queue; // 阻塞队列
    bool m_is_async; // 是否同步标志位
    locker m_mutex; // 同步类
};

// 可变参数宏定义
// 以下,4 个宏定义在其他文件使用,主要用于不同类型的日志输出

// 调试日志输出宏
#define LOG_DEBUG(format, ...) Log::get_instance()->write_log(0, format, __VA_ARGS__)
// 信息日志输出宏
#define LOG_INFO(format, ...) Log::get_instance()->write_log(1, format, __VA_ARGS__)
// 警告日志输出宏
#define LOG_WARN(format, ...) Log::get_instance()->write_log(2, format, __VA_ARGS__)
// 错误日志输出宏
#define LOG_ERROR(format, ...) Log::get_instance()->write_log(3, format, __VA_ARGS__)

#endif

日志类中的方法都不会被其他程序直接调用,末尾的四个可变参数宏,提供了其他程序的调用方法

前述方法对日志等级进行分类,包括DEBUG,INFO,WARN 和 ERROR 四种级别的日志

🌼功能实现

init() 函数实现日志创建,写入方式的判断

write_log() 函数完成写入日志文件中的具体内容,实现  日志分级,分文件,格式化输出内容

生成日志文件 && 判断写入方式

通过单例模式获取唯一的日志类,调用 init() 方法,初始化生成日志文件,服务器启动按当前时刻创建日志,前缀为时间,后缀为自定义 log 文件名,并记录创建日志的时间 day 和 行数 count

写入方式通过初始化时,是否设置队列大小(表示在队列中可以放几条数据)来判断,若队列大小为 0,则为同步,否则为异步

// 异步需要设置阻塞队列的长度,同步不需要设置
bool Log::init(const char *file_name, int log_buf_size,
               int split_lines, int max_queue_size)
{
    // 如果设置了 max_queue_size,则设置为 异步
    if (max_queue_size >= 1) {
        // 设置写入方式 flag
        m_is_async = true;

        // 创建并设置阻塞队列长度
        m_log_queue = new block_queue<string>(max_queue_size);
        pthread_t tid;

        // flush_log_thread 为回调函数,这里表示创建线程异步写日志
        pthread_create(&tid, NULL, flush_log_thread, NULL);
    }

    // 输出内容长度
    m_log_buf_size = log_buf_size;
    m_buf = new char[m_log_buf_size];
    memset(m_buf, '/0', sizeof(m_buf));

    // 日志最大行数
    m_split_lines = split_lines;

    time_t t = time(NULL);  // 获取当前时间
    struct tm *sys_tm = localtime(&t);  // 获取本地时间
    struct tm my_tm = *sys_tm;  // 复制本地时间

    // 从后往前找到第一个 / 的位置
    const char *p = strrchr(file_name, '/');  // 查找最后一个斜杠的位置
    char log_full_name[256] = {0};  // 初始化日志文件名数组

    // 相当于自定义日志名
    // 若输入的文件名没有 /,则直接将时间 + 文件名作为日志名
    if (p == NULL) 
        snprintf(log_full_name, 255, "%d_%02d_%02d_%s",  // 格式化生成日志文件名
                 my_tm.tm_year + 1900, my_tm.tm_mon + 1,
                 my_tm.tm_mday, file_name);
    else {
        // 将 / 的位置向后移动一个位置,然后复制到 logname 中
        // p - file_name + 1 是文件所在路径文件夹的长度
        // dirname 相当于 ./
        strcpy(log_name, p + 1);  // 复制文件名部分
        strncpy(dir_name, file_name, p - file_name + 1);  // 复制路径部分

        // 后面的参数跟 format 有关
        snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s",  // 格式化生成日志文件名
                 dir_name, my_tm.tm_year + 1900, 
                 my_tm.tm_mon + 1, my_tm.tm_mday, log_name);
    }

    m_today = my_tm.tm_mday;  // 记录今天日期

    m_fp = fopen(log_full_name, "a");  // 以追加模式打开日志文件
    if (m_fp == NULL)  // 如果打开文件失败
        return false;

    return true;
}

代码解释👇

这段代码是日志系统的初始化函数,根据传入的参数初始化日志系统

  1. 异步和同步模式选择

    • 通过判断传入的 max_queue_size 参数是否大于等于1 来确定日志系统是以同步模式还是异步模式运行
    • 如果 max_queue_size 大于等于1,则表示使用异步模式,会创建一个阻塞队列用于存储日志消息,并启动一个线程异步写日志
  2. 日志文件相关设置

    • 初始化日志系统的缓冲区大小为 log_buf_size
    • 设置日志分割的最大行数为 split_lines
  3. 生成日志文件名

    • 获取当前时间 t,并将其转换成本地时间 sys_tm
    • 判断传入的 file_name 是否包含路径分隔符 /,若无则直接以当前日期和传入的文件名作为日志文件名;若有,则从路径中提取出文件名和路径部分
    • 根据日期、文件名和路径信息构建完整的日志文件名 log_full_name
  4. 记录今天的日期

    • 将当天的日期存储在变量 m_today 中,用于后续判断是否需要切换日志文件
  5. 打开日志文件

    • 使用追加模式打开生成的日志文件,如果打开失败则返回 false

日志分级与分文件

日志分级的实现,一般提供 5 种级别👇

  • Debug,调试代码时输出,系统实际运行时一般不使用
  • Warn,这种警告与调试时终端的 warning 类似,同样是调试代码时使用
  • Info,报告系统当前状态,当前执行的流程或接收的信息
  • Error 和 Fatal,输出系统的错误信息

上述方法,会根据开发具体情况改变

TinyWebServer 中,给出了除Fatal外的四种分级,实际使用了 Debug,Info 和 Error

超行,按天分文件 的逻辑具体是👇

  • 日志写入前会判断当前 day 是否为创建日志的时间,行数是否超过最大行限制
    • 若为创建日志时间,写入日志,否则按当前时间创建心 log,更新创建时间和行数
    • 若行数超过最大行限制,在当前日志的末尾加 count / max_lines 为后缀,创建新log

将系统信息格式化后输出,具体为:格式化时间 + 格式化内容

void Log::write_log(int level, const char *format, ...)
{
    struct timeval now = {0, 0};
    gettimeofday(&now, NULL); // 获取当前时间
    time_t t = now.tv_sec; // 获取秒数
    struct tm *sys_tm = localtime(&t); // 转换为本地时间
    struct tm my_tm = *sys_tm; // 复制本地时间
    char s[16] = {0}; // 存储日志级别字符串

    // 日志分级
    switch (level) {
        case 0:
            strcpy(s, "[debug]:"); // 调试级别
            break;
        case 1:
            strcpy(s, "[info]:"); // 信息级别
            break;
        case 2:
            strcpy(s, "[warn]:"); // 警告级别
            break;
        case 3:
            strcpy(s, "[error]:"); // 错误级别
            break;
        case 4:
            strcpy(s, "[fatal]:"); // 致命错误级别
            break;
    } 

    m_mutex.lock(); // 加锁,保证线程安全

    // 更新现有行数
    m_count++;

    // 如果日志不是今天或者写入的日志行数是最大行的倍数
    // m_split_lines 为最大行数
    if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0)
    {
        char new_log[256] = {0}; // 存储新的日志文件名
        fflush(m_fp); // 刷新文件流
        fclose(m_fp); // 关闭文件
        char tail[16] = {0}; // 存储时间部分的字符串

        // 格式化日志名中的时间部分
        snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900,
                 my_tm.tm_mon + 1, my_tm.tm_mday);

        // 如果时间不是今天,就创建今天的日志,更新 m_today 和 m_count
        if (m_today != my_tm.tm_mday) {
            snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);
            m_today = my_tm.tm_mday; // 更新为今天日期
            m_count = 0; // 重置行数为0
        }
        else {
            // 超过最大行,在之前的日志名基础上,加后缀
            // m_count / m_split_lines
            snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail,
                     log_name, m_count / m_split_lines);
        }
        m_fp = fopen(new_log, "a"); // 打开新的日志文件
    }

    m_mutex.unlock(); // 解锁

    va_list valst; // 创建可变参数列表
    va_start(valst, format); // 初始化可变参数列表

    string log_str; // 存储日志内容的字符串
    m_mutex.lock(); // 加锁

    // 写入内容格式:时间 + 内容
    // 时间格式化
    int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ",
                     my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,
                     my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);
    
    // 内容格式化
    int m = vsnprintf(m_buf + n, m_log_buf_size - 1, format, valst);
    m_buf[n + m] = '\n'; // 添加换行符
    m_buf[n + m + 1] = '\0'; // 添加字符串终止符

    log_str = m_buf; // 将格式化后的内容存入字符串

    m_mutex.unlock(); // 解锁

    // 若 m_is_async 为 true 表示异步,默认同步
    // 若异步,就将日志信息加入阻塞队列,同步就加锁向文件中写
    if (m_is_async && !m_log_queue->full()) {
        m_log_queue->push(log_str); // 异步写入日志队列
    } else {
        m_mutex.lock(); // 加锁
        fputs(log_str.c_str(), m_fp); // 同步写入日志文件
        m_mutex.unlock(); // 解锁
    }

    va_end(valst); // 结束可变参数列表
}

补充解释

1)

char *strcpy(char *dest, const char *src);

dest表示目标字符串的地址,src表示源字符串的地址。strcpy会把源字符串(src指向的字符串)的内容复制到目标字符串(dest指向的字符串)中,并以'\0'(空字符)作为结束符

上面的代码中,通过strcpy函数将不同日志级别对应的字符串(比如"[debug]:"、"[info]:")复制到变量s中,以便后续拼接成完整的日志信息

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

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

相关文章

Vue+Flask电商后台管理系统

在这个项目中&#xff0c;我们将结合Vue.js前端框架和python后端框架Flask&#xff0c;打造一个功能强大、易于使用的电商后台管理系统 项目演示视频&#xff1a; VueFlask项目 目录 前端环境&#xff08;Vue.js&#xff09;&#xff1a; 后端环境&#xff08;python-Flask&…

面试数据库篇(mysql)- 07索引创建原则与失效及优化

索引创建原则 1). 针对于数据量较大,且查询比较频繁的表建立索引。 2). 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。 3). 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。 4). 如果是字符…

OpenCV实现目标追踪

目录 准备工作 语言&#xff1a; 软件包&#xff1a; 效果演示 代码解读 &#xff08;1&#xff09;导入OpenCV库 &#xff08;2&#xff09;使用 cv2.VideoCapture 打开指定路径的视频文件 &#xff08;3&#xff09;使用 vid.read() 读取视频的第一帧&#xff0c;ret…

ts的重载

官网示例 TypeScript: Documentation - Template Literal Types 这里大概理解是 T 继承了Number|sting 加上&#xff1f;条件判断就是 T继承Number|sting 部分为true 没有继承部分为false&#xff0c; 就是输入string, 为true, 输入 null 则为false, type Exclude<T, U&…

如何访问内网服务器?

访问内网服务器是在网络架构中常见的需求。内网服务器是指在一个局域网中运行的服务器&#xff0c;可以提供各种服务&#xff0c;如文件共享、网站托管等。由于安全性的考虑&#xff0c;内网服务器一般不直接暴露在公网中&#xff0c;所以需要通过特定的方法来访问。 一种常见的…

【管理咨询宝藏资料29】某大型集团房地产战略报告

本报告首发于公号“管理咨询宝藏”&#xff0c;如需阅读完整版报告内容&#xff0c;请查阅公号“管理咨询宝藏”。 【管理咨询宝藏资料29】某大型集团房地产战略报告 【格式】PPT版本&#xff0c;可编辑 【关键词】战略规划、地产发展、管理咨询 【文件核心观点】 - 以住宅为…

PyTorch基础(19)-- torch.take_along_dim()方法

一、前言 在深挖ML4CO的代码过程中&#xff0c;遇到了torch.take_along_dim()这个方法&#xff0c;影响到我后续的代码阅读&#xff1b;加之在上网搜索资料的过程中&#xff0c;网络上对此函数的介绍文章少之又少&#xff0c;即使有&#xff0c;也是对torch官网文档中的解释进…

医疗行业数据分析,为医疗提质增效提供科学支持

信息化时代的到来&#xff0c;医疗行业数据分析已成为提升医疗服务质量和效率的重要手段。医院拥有大量的医疗数据&#xff0c;医疗数据中包含着很多宝贵的信息与规律&#xff0c;通过深入的数据分析&#xff0c;能够为决策者提供直观、深入的数据洞察&#xff0c;帮助医疗服务…

千兆单口(百兆双口)小体积 24PIN 网络变压器 H82409S 特点

Hqst华轩盛(石门盈盛)电子导读&#xff1a;千兆单口&#xff08;百兆双口&#xff09;小体积 24PIN 网络变压器 H82409S 特点 大家好&#xff0c;石门盈盛电子科技有限公司工程盛先生&#xff0c;今天向大家介绍石门盈盛电子科技有限公司的一款优势产品 - 千兆单口&#xff08;…

一个实时波形图的封装demo(QT)(qcustomplot)

前言&#xff1a; 封装的一个实时波形图的类&#xff0c;可以直接提升使用。 提供了接口&#xff0c;可以更改颜色&#xff0c;样式&#xff0c;等等 参考&#xff1a; Qt Plotting Widget QCustomPlot - Introduction 另外参考了一个大神的作品&#xff0c;链接没找到。 项目…

15.prometheus.yml的rule_files配置

平凡也就两个字: 懒和惰; 成功也就两个字: 苦和勤; 优秀也就两个字: 你和我。 跟着我从0学习JAVA、spring全家桶和linux运维等知识,带你从懵懂少年走向人生巅峰,迎娶白富美! 关注微信公众号【 IT特靠谱 】,每天都会分享技术心得~ 1.rule_files配置 1.1.rule_files配置解读…

【北京迅为】《iTOP-3588开发板网络环境配置手册》第2章 电脑、开发板直连交换机或路由器

RK3588是一款低功耗、高性能的处理器&#xff0c;适用于基于arm的PC和Edge计算设备、个人移动互联网设备等数字多媒体应用&#xff0c;RK3588支持8K视频编解码&#xff0c;内置GPU可以完全兼容OpenGLES 1.1、2.0和3.2。RK3588引入了新一代完全基于硬件的最大4800万像素ISP&…

BeautifulSoup+xpath+re+css简单复习+新的scrapy的学习

1.BeautifulSoupsoup BeautifulSoup(html,html.parser)all_icosoup.find(class_"DivTable") 2.xpath trs resp.xpath("//tbody[idcpdata]/tr") hong tr.xpath("./td[classchartball01 or classchartball20]/text()").extract() 这个意思是找…

【竞技宝】DOTA2-梦幻联赛S22:AR命悬一线 XG确定晋级淘汰赛

北京时间2024年2月28日&#xff0c;DOTA2梦幻联赛S22的比赛在昨日进入小组赛第三个比赛日&#xff0c;本次梦幻联赛共有AR、XG、IG三支中国区的队伍参赛&#xff0c;那么经过三日激烈的比赛之后&#xff0c;目前三支队伍的积分情况以及晋级形势如何呢&#xff1f; XG XG是小组…

手机使用Python轻松下载闲鱼短视频

目录 一、Python与手机端的结合 二、闲鱼短视频下载原理 三、使用Python实现下载 安装必要的库 捕获视频流 保存视频文件 四、案例分析 五、注意事项 六、总结 在数字化时代&#xff0c;短视频已成为人们获取信息、娱乐休闲的重要方式之一。闲鱼&#xff0c;作为国内知…

(PWM呼吸灯)合泰开发板HT66F2390-----点灯大师

前言 上一篇文章相信大家已经成为了点灯高手了&#xff0c;那么进阶就是成为点灯大师 实现PWM呼吸灯 接下来就是直接的代码讲解了&#xff0c;不再讲PWM原理的 这里部分内容参考了另一个博主的文章 合泰杯——合泰单片机工程7之PWM输出 如果有小伙伴不理解引脚设置和delay函数…

docker (十二)-私有仓库

docker registry 我们可以使用docker push将自己的image推送到docker hub中进行共享&#xff0c;但是在实际工作中&#xff0c;很多公司的代码不能上传到公开的仓库中&#xff0c;因此我们可以创建自己的镜像仓库。 docker 官网提供了一个docker registry的私有仓库项目&#…

【Micropython教程】点亮第一个LED与流水灯

文章目录 前言MicroPython在线仿真GPIO的工作模式一、有哪些工作模式&#xff1f;1.1 GPIO的详细介绍1.2 GPIO的内部框图输入模式输出部分 一、machine.Pin类1.1 machine.Pin 类的构造对象1.2 machine.Pin 类的方法init方法value方法设置高低电平方法 二、延时函数 三、流水灯总…

6U VPX全国产飞腾D2000/8核+复旦微FPGA信息处理主板

产品特性 产品功能 飞腾计算平台&#xff0c;国产化率100% VPX-MPU6503是一款基于飞腾D2000/8核信息处理主板&#xff0c;采用由飞腾D2000处理器飞腾X100桥片的高性能计算机模块&#xff0c;双通道16G贴装内存&#xff0c;板载128G 固态SSD&#xff1b;预留固态盘扩展接口&…

通过XML调用CAPL脚本进行测试(新手向)

目录 0 引言 1 XML简介 2 通过XML调用CAPL脚本 0 引言 纪念一下今天这个特殊日子&#xff0c;四年出现一次的29号。 在CANoe中做自动化测试常用的编程方法有CAPL和XML两种&#xff0c;二者各有各的特色&#xff0c;对于CAPL来说新手肯定是更熟悉一些&#xff0c;因为说到在C…