引言

在嵌入式系统开发中,调试是贯穿整个生命周期的关键环节。与传统PC端程序不同,嵌入式设备资源受限(如内存、存储、处理器性能),且运行环境复杂(无显示器、键盘),传统的断点调试或打印到控制台的方式往往难以满足实时性、便捷性需求。此时,​​日志系统(LOG)​​ 成为嵌入式调试的核心工具——它通过将关键运行信息输出到外部设备(如串口),帮助开发者快速定位问题、跟踪程序状态。

本文将以STM32F103系列单片机为例,结合实际工程实践,介绍一款轻量、灵活、易集成的日志系统设计与实现,涵盖日志分级、格式控制、串口输出等核心功能,并通过示例演示其在嵌入式调试中的具体应用。


嵌入式日志系统的核心需求

嵌入式场景下,日志系统需满足以下核心需求:

1. ​​资源友好性​

STM32的内存(如STM32F103C8T6仅有20KB SRAM)和Flash空间有限,日志系统需避免占用过多资源。例如,日志缓冲区需固定大小(如256字节),避免动态内存分配;输出函数需轻量(如直接调用串口发送)。

2. ​​分级控制​

不同调试阶段需要关注不同详细程度的信息。例如:

  • ​开发阶段​​:需要详细的函数调用、变量值(TRACE/DEBUG级别);
  • ​测试阶段​​:关注关键流程状态(INFO/WARN级别);
  • ​发布阶段​​:仅保留错误信息(ERROR/FATAL级别)。
    因此,日志系统需支持​​级别过滤​​,通过配置只输出高于设定级别的日志。

3. ​​格式灵活性​

日志需包含足够的上下文信息以辅助调试,但冗余信息会干扰阅读。常见的日志要素包括:

  • ​级别标识​​(如[TRACE]/[ERROR]):快速区分日志严重程度;
  • ​时间戳​​(如[14:23:45.678]):定位问题发生时刻;
  • ​函数名+行号​​(如[main:45]):追踪代码执行路径;
  • ​原始消息​​(如“文件打开失败”):具体问题描述。
    日志系统需支持​​格式配置​​,允许用户按需组合上述要素。

4. ​​高效输出​

嵌入式系统的串口带宽有限(如常见的115200bps,约11.5KB/s),日志输出需避免阻塞主程序。例如,采用非阻塞发送(或短时间阻塞)、控制单次输出数据量(不超过串口发送缓冲区)。


日志系统设计实现

基于上述需求,我们设计了一款基于STM32 HAL库的日志系统,核心功能包括日志分级、格式控制、串口输出,以下是关键模块的实现细节。

1. 日志级别定义

日志级别采用枚举类型定义,从低到高依次为TRACEDEBUGINFOWARNERRORFATAL,数值越小优先级越高。通过级别过滤,可灵活控制日志输出范围:

typedef enum {LOG_LEVEL_TRACE = 0,  // 最低级别,用于最详细的跟踪信息LOG_LEVEL_DEBUG,      // 调试信息,开发阶段使用LOG_LEVEL_INFO,       // 重要状态信息,测试阶段使用LOG_LEVEL_WARN,       // 警告信息,提示潜在问题LOG_LEVEL_ERROR,      // 错误信息,功能异常但可恢复LOG_LEVEL_FATAL,      // 严重错误,系统可能崩溃LOG_LEVEL_MAX         // 枚举结束标志
} log_level_t;

2. 日志格式控制

日志格式通过宏定义控制,支持按位或组合多种要素:

#define LOG_FMT_RAW           (0u)          // 仅原始消息(无额外信息)
#define LOG_FMT_LEVEL_STR     (1u << 0)      // 级别字符串(如[TRACE])
#define LOG_FMT_TIME_STAMP    (1u << 1)      // 时间戳(如[14:23:45.678])
#define LOG_FMT_FUNC_LINE     (1u << 2)      // 函数名+行号(如[main:45])

用户可通过Ulog_SetFmt()函数动态配置格式(例如LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP表示输出级别和时间戳)。

3. 核心日志函数实现

日志系统的核心是Ulog()函数,负责格式化日志内容并输出。其流程如下:

(1)级别过滤

首先检查当前日志级别是否低于设定的最低输出级别(如设置为LOG_LEVEL_INFO时,TRACEDEBUG日志会被过滤)。

(2)缓冲区初始化

使用固定大小的缓冲区(如256字节)存储日志内容,避免动态内存分配带来的风险。

(3)格式化要素拼接

根据配置的格式,依次拼接级别字符串、时间戳、函数名+行号等信息。例如:

  • 级别字符串通过level_str数组映射(如LOG_LEVEL_TRACE对应"[TRACE]");
  • 时间戳基于HAL_GetTick()获取系统运行时间(毫秒级),格式化为[HH:MM:SS.xxx]
  • 函数名+行号通过__func__(编译器内置宏)和__LINE__(行号宏)获取,并截断过长函数名(避免缓冲区溢出)。
(4)日志内容填充

使用va_list处理可变参数,将用户输入的日志消息格式化到缓冲区中。

(5)输出日志

通过注册的输出函数(默认使用串口)将缓冲区内容发送到外部设备。

void Ulog(uint32_t level, const char *func, uint32_t line, const char *fmt, ...) {// 1. 级别过滤if (level >= LOG_LEVEL_MAX || level < s_ulog_level) return;// 2. 缓冲区初始化char log_buf[CONFIG_ULOG_BUF_SIZE] = {0};va_list args;int idx = 0;// 3. 拼接级别字符串(如[TRACE])if (s_ulog_fmt & LOG_FMT_LEVEL_STR) {static const char *level_str[] = {"TRACE", "DEBUG", "INFO ", "WARN ", "ERROR", "FATAL"};idx += snprintf(log_buf + idx, sizeof(log_buf)-idx, "[%s] ", level_str[level]);}// 4. 拼接时间戳(如[14:23:45.678])if (s_ulog_fmt & LOG_FMT_TIME_STAMP) {char time_buf[32];uint16_t ms = 0;Get_SystemTime(time_buf, sizeof(time_buf), &ms);  // 基于HAL_GetTick获取时间idx += snprintf(log_buf + idx, sizeof(log_buf)-idx, "[%s.%03d] ", time_buf, ms);}// 5. 拼接函数名+行号(如[main:45])if (s_ulog_fmt & LOG_FMT_FUNC_LINE) {char short_func[20] = {0};strncpy(short_func, func, sizeof(short_func)-1);  // 截断过长函数名idx += snprintf(log_buf + idx, sizeof(log_buf)-idx, "[%s:%d] ", short_func, (int)line);}// 6. 填充日志内容(可变参数)va_start(args, fmt);int len = vsnprintf(log_buf + idx, sizeof(log_buf)-idx, fmt, args);  // 格式化消息va_end(args);if (len > 0) idx += len;  // 有效内容则更新索引// 7. 添加换行符(STM32串口常用\r\n)if (idx < CONFIG_ULOG_BUF_SIZE - 2) {snprintf(log_buf + idx, sizeof(log_buf)-idx, "%s", ULOG_NEWLINE_SIGN);idx += strlen(ULOG_NEWLINE_SIGN);}// 8. 输出日志(调用注册的串口发送函数)ulog_output((uint8_t *)log_buf, (uint16_t)idx);
}

4. 串口输出实现

STM32的串口输出通过HAL库实现,核心是Uart_SendData()函数,利用HAL_UART_Transmit()发送数据。为避免阻塞,设置超时时间(如100ms):

// 串口句柄(需在stm32f1xx_hal_conf.h中启用USART1)
extern UART_HandleTypeDef UartHandle;// 串口数据发送函数
static void Uart_SendData(uint8_t *data, uint16_t size) {if (huart1.Instance != NULL) {HAL_UART_Transmit(&UartHandle, data, size, 100);  // 超时100ms}
}

5. 全局配置与接口

通过全局变量管理日志配置(如当前级别、格式、输出函数),并提供接口供用户动态修改:

// 全局配置
static uint32_t s_ulog_fmt = LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP | LOG_FMT_FUNC_LINE;  // 默认格式
static uint32_t s_ulog_level = CONFIG_ULOG_DEF_LEVEL;                                   // 默认级别(TRACE)
static UlogOutputFunc ulog_output = NULL;                                               // 默认输出函数// 注册输出函数(默认使用串口)
void Ulog_RegisterOutput(UlogOutputFunc func) {ulog_output = func ? func : Uart_SendData;  // 未注册时使用串口
}// 设置日志级别
int Ulog_SetLevel(uint32_t level) {if (level >= LOG_LEVEL_MAX) return -1;s_ulog_level = level;return 0;
}// 设置日志格式
void Ulog_SetFmt(uint32_t fmt) {s_ulog_fmt = fmt;
}


日志系统使用示例

以下通过一个完整的测试用例,演示日志系统的实际效果。

1. 工程配置

  • ​硬件连接​​:STM32F103 USART1(PA9-TX,PA10-RX)接USB转串口模块(波特率115200,8-N-1);
  • ​软件配置​​:在main.c中初始化HAL库、系统时钟、USART1,并注册串口输出函数。

2. 测试代码

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "stm32f1xx.h"
#include "./usart/bsp_debug_usart.h"
#include "./log/log.h"  /* 测试函数 */
void Test_LogFunctions(void) 
{LOG_TRACE("开始测试日志功能");LOG_DEBUG("调试信息 - 变量值: %d", 100);LOG_INFO("系统初始化完成");LOG_WARN("内存使用率高达85%%");LOG_ERROR("文件打开失败: %s", "test.log");LOG_FATAL("核心模块初始化失败,系统即将终止");
}void Test_LogRunTimeDebug(void)
{static uint32_t u32Cnt = 0;u32Cnt++;LOG_DEBUG("系统运行中,%04d", u32Cnt);
}int main(void) 
{HAL_Init();        /* 配置系统时钟为72 MHz */ SystemClock_Config();/*初始化USART 配置模式为 115200 8-N-1,中断接收*/DEBUG_USART_Config();/* 注册串口输出函数 */Ulog_RegisterOutput(Uart_SendData);/* 测试完整格式日志 (级别+时间+行号) */Ulog_SetFmt(LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP | LOG_FMT_FUNC_LINE);printf("=== 测试完整格式日志 (级别+时间+行号) ===\r\n");Test_LogFunctions();/* 测试基本格式(级别+时间) */Ulog_SetFmt(LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP);printf("\r\n=== 测试基本格式(级别+时间) ===\r\n");Test_LogFunctions();/* 测试基本格式(级别) */Ulog_SetFmt(LOG_FMT_LEVEL_STR);printf("\r\n=== 测试基本格式(级别) ===\r\n");Test_LogFunctions();/* 测试原始格式(仅消息内容) */Ulog_SetFmt(LOG_FMT_RAW);printf("\r\n=== 测试原始格式(仅消息内容) ===\r\n");Test_LogFunctions();//显示运行时Debug数据Ulog_SetFmt(LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP | LOG_FMT_FUNC_LINE);printf("\r\n=== 显示运行时Debug数据 ===\r\n");while(1) {HAL_Delay(1000);  // 主循环保持运行Test_LogRunTimeDebug();}
}


完整代码

log.h

#ifndef __LOG_H
#define	__LOG_H#include "stm32f1xx.h"
#include "stm32f1xx_hal.h"
#include <stdarg.h>
#include <stdint.h>
#include <string.h>/* 配置宏定义 */
#define CONFIG_ULOG_BUF_SIZE    256u
#define CONFIG_ULOG_DEF_LEVEL   LOG_LEVEL_TRACE
#define ULOG_NEWLINE_SIGN       "\r\n"  // STM32串口常用换行符/* 日志级别枚举 */
typedef enum {LOG_LEVEL_TRACE = 0,LOG_LEVEL_DEBUG,LOG_LEVEL_INFO,LOG_LEVEL_WARN,LOG_LEVEL_ERROR,LOG_LEVEL_FATAL,LOG_LEVEL_MAX
} log_level_t;/* 格式控制宏 */
#define LOG_FMT_RAW           (0u)
#define LOG_FMT_LEVEL_STR     (1u << 0)
#define LOG_FMT_TIME_STAMP    (1u << 1)
#define LOG_FMT_FUNC_LINE     (1u << 2)/* 启用日志级别开关 */
#define LOG_TRACE_EN  1
#define LOG_DEBUG_EN  1
#define LOG_INFO_EN   1
#define LOG_WARN_EN   1
#define LOG_ERROR_EN  1
#define LOG_FATAL_EN  1/* 日志宏定义 */
#if LOG_TRACE_EN
#define LOG_TRACE(fmt, ...)   Ulog(LOG_LEVEL_TRACE, __func__, __LINE__, fmt, ##__VA_ARGS__)
#else
#define LOG_TRACE(fmt, ...)
#endif#if LOG_DEBUG_EN
#define LOG_DEBUG(fmt, ...)   Ulog(LOG_LEVEL_DEBUG, __func__, __LINE__, fmt, ##__VA_ARGS__)
#else
#define LOG_DEBUG(fmt, ...)
#endif#if LOG_INFO_EN
#define LOG_INFO(fmt, ...)    Ulog(LOG_LEVEL_INFO, __func__, __LINE__, fmt, ##__VA_ARGS__)
#else
#define LOG_INFO(fmt, ...)
#endif#if LOG_WARN_EN
#define LOG_WARN(fmt, ...)    Ulog(LOG_LEVEL_WARN, __func__, __LINE__, fmt, ##__VA_ARGS__)
#else
#define LOG_WARN(fmt, ...)
#endif#if LOG_ERROR_EN
#define LOG_ERROR(fmt, ...)   Ulog(LOG_LEVEL_ERROR, __func__, __LINE__, fmt, ##__VA_ARGS__)
#else
#define LOG_ERROR(fmt, ...)
#endif#if LOG_FATAL_EN
#define LOG_FATAL(fmt, ...)   Ulog(LOG_LEVEL_FATAL, __func__, __LINE__, fmt, ##__VA_ARGS__)
#else
#define LOG_FATAL(fmt, ...)
#endif/* 日志输出函数类型 */
typedef void (*UlogOutputFunc)(uint8_t *data, uint16_t size);extern void Ulog_RegisterOutput(UlogOutputFunc func);
extern void Ulog_SetFmt(uint32_t fmt);
extern void Ulog(uint32_t level, const char *func, uint32_t line, const char *fmt, ...);#endif /* __LOG_H */

 log.c

#include "./usart/bsp_debug_usart.h"
#include "./log/log.h"   /* 全局配置 */
static uint32_t s_ulog_fmt = LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP | LOG_FMT_FUNC_LINE;
static uint32_t s_ulog_level = CONFIG_ULOG_DEF_LEVEL;
static UlogOutputFunc ulog_output = NULL;static void Get_SystemTime(char *time_buf, uint16_t buf_size, uint16_t *ms);/* 注册输出函数(默认使用串口) */
void Ulog_RegisterOutput(UlogOutputFunc func) 
{ulog_output = func ? func : Uart_SendData;
}/* 设置日志格式 */
void Ulog_SetFmt(uint32_t fmt) 
{s_ulog_fmt = fmt;
}/* 系统时间获取(基于HAL_GetTick) */
static void Get_SystemTime(char *time_buf, uint16_t buf_size, uint16_t *ms) {uint32_t tick = HAL_GetTick();  // 获取系统运行时间(毫秒)*ms = tick % 1000;uint32_t sec = tick / 1000;uint32_t hour = sec / 3600;uint32_t min = (sec % 3600) / 60;sec = sec % 60;snprintf(time_buf, buf_size, "%02d:%02d:%02d", (int)(hour % 24), (int)min, (int)sec);
}/* 核心日志函数 */
void Ulog(uint32_t level, const char *func, uint32_t line, const char *fmt, ...) 
{/* 级别过滤 */if (level >= LOG_LEVEL_MAX || level < s_ulog_level) return;/* 缓冲区初始化 */char log_buf[CONFIG_ULOG_BUF_SIZE] = {0};va_list args;int idx = 0;/* 级别字符串 */if (s_ulog_fmt & LOG_FMT_LEVEL_STR) {static const char *level_str[] = {"TRACE", "DEBUG", "INFO ", "WARN ", "ERROR", "FATAL"};idx += snprintf(log_buf + idx, sizeof(log_buf)-idx, "[%s] ", level_str[level]);}/* 时间戳 */if (s_ulog_fmt & LOG_FMT_TIME_STAMP) {char time_buf[32];uint16_t ms = 0;Get_SystemTime(time_buf, sizeof(time_buf), &ms);idx += snprintf(log_buf + idx, sizeof(log_buf)-idx, "[%s.%03d] ", time_buf, ms);}/* 函数名+行号 */if (s_ulog_fmt & LOG_FMT_FUNC_LINE) {char short_func[20] = {0};strncpy(short_func, func, sizeof(short_func)-1);idx += snprintf(log_buf + idx, sizeof(log_buf)-idx, "[%s:%d] ", short_func, (int)line);}/* 日志内容 */va_start(args, fmt);int len = vsnprintf(log_buf + idx, sizeof(log_buf)-idx, fmt, args);va_end(args);/* 处理格式化错误 */if (len < 0) {idx += snprintf(log_buf + idx, sizeof(log_buf)-idx, "[LOG FORMAT ERROR]");} else if (len > 0) {idx += len;}/* 添加换行符 */if (idx < CONFIG_ULOG_BUF_SIZE - 2) {snprintf(log_buf + idx, sizeof(log_buf)-idx, "%s", ULOG_NEWLINE_SIGN);idx += strlen(ULOG_NEWLINE_SIGN);}/* 输出日志 */ulog_output((uint8_t *)log_buf, (uint16_t)idx);
}/*********************************************END OF FILE**********************/

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Zephyr的设备驱动模型

默认配置默认配置 boards/arm/nucleo_f401re/ ├── nucleo_f401re.dts ← 板卡设备树主入口 ├── nucleo_f401re_defconfig ← 默认 Kconfig 配置 ├── board.cmake ← CMake 构建入口overlay1.新增加驱动需要修改对应板的设备树文件&#xf…

Mysql字段没有索引,通过where x = 3 for update是使用什么级别的锁

没有索引时&#xff0c;FOR UPDATE 会锁住整个表 现在&#xff0c;你正在一本一本地翻看所有书&#xff0c;寻找“维修中”的书&#xff0c;并且你对管理员说&#xff1a;“在我清点和修改完之前&#xff0c;别人不能动这些书&#xff0c;也不能往这个范围里加新书&#xff01;…

TCP-与-UDP-协议详解:原理、区别与应用场景全解析

TCP 与 UDP 协议详解&#xff1a;原理、区别与应用场景全解析 在日常使用网络的过程中&#xff0c;我们经常听到 TCP 和 UDP 这两个词。你打开网页、发送消息、观看视频&#xff0c;背后都在使用 TCP 或 UDP 进行数据传输。那么这两个协议到底是怎么工作的&#xff1f;它们之间…

GitHub信息收集

目录 简介 一、入门搜索技巧 1. 基本关键词搜索 2. 文件类型限定搜索 3. 用户/组织定向搜索 二、精准定位技巧 1. 组合搜索条件 2. 排除干扰结果 3. 路径限定搜索 三、防御建议 四、法律与道德提醒 简介 GitHub作为全球最大的代码托管平台&#xff0c;存储着数十亿…

由 DB_FILES 参数导致的 dg 服务器无法同步问题

由 DB_FILES 参数导致的 dg 服务器无法同步问题 用户反映&#xff0c;dg 服务器数据从昨晚&#xff08;7月8日&#xff09;开始停止同步。 连接服务器发现没有 mrp 进程&#xff0c;并且 OPEN_MODE 参数也不正确。具体情况如下所示&#xff1a; SQL> select process, status…

Go语言泛型-泛型对代码结构的优化

在Go语言中,Go泛型-泛型对代码结构的优化部分主要探讨了泛型如何帮助我们优化代码结构、减少重复代码,并提高代码的可维护性、可读性和复用性。以下是详细内容: 一、引言 Go 1.18 引入了泛型,极大地提高了语言的灵活性。泛型使得我们可以编写更加通用、可复用且类型安全的…

【1-快速上手】

文章目录前言简介什么是 Konva&#xff1f;安装 Konva概述它是如何工作的&#xff1f;基本形状样式事件拖放滤镜动画选择器序列化与反序列化性能前言 结合项目实际业务需求&#xff0c;在 Fabric、Konva 等图形化框架中&#xff0c;我选择了性能表现好的 Konva。首先去学习官方…

【LeetCode】209. 长度最小的子数组(前缀和 + 二分)

【LeetCode】209. 长度最小的子数组&#xff08;前缀和 二分&#xff09;题目描述前缀和二分优化前缀和总结二分总结题目描述 题目链接&#xff1a;【LeetCode】209. 长度最小的子数组&#xff08;前缀和 二分&#xff09; 给定一个含有 n 个整数的数组和一个整数 target。…

文件系统----底层架构

当我们谈到文件系统的时候&#xff0c;最重要的点在于&#xff1a;文件的内容与属性是如何存储在磁盘中的&#xff1f;以及操作系统是如何精准定位到这些文件内容的&#xff1f;在谈及文件的内核前&#xff0c;我们先来了解一下储存文件的硬件-----硬盘一.理解硬件首先我们来看…

小程序开发平台,自主开发小程序源码系统,多端适配,带完整的部署教程

温馨提示&#xff1a;文末有资源获取方式全开源与自主开发源码完全开放&#xff1a;开发者可自由修改前端界面、后端逻辑及数据库结构&#xff0c;支持深度定制&#xff08;如调整用户端交互流程、商家端管理功能等&#xff09;。技术栈透明&#xff1a;基于主流技术&#xff0…

stp拓扑变化分类

Max Age 20sHellotime 2sForward delay 153、拓扑改变需要多长时间1&#xff09;根桥故障&#xff1a;需要50秒&#xff08;Max age2个forwarding delay&#xff09;2&#xff09;非直连链路&#xff1a;非直连故障在稳定的STP网络&#xff0c;非根桥会定期收到来自根桥的BPDU报…

一、深度学习——神经网络

一、神经网络 1.神经网络定义&#xff1a;人工神经网络&#xff08;Artificial Neural Network&#xff0c;ANN&#xff09;也简称为神经网络&#xff08;NN&#xff09;&#xff0c;是一种模仿生物神经网络结构和功能的计算模型。人脑可以看作是一个生物神经网络&#xff0c;由…

【牛客算法】 小红的奇偶抽取

文章目录 一、题目介绍1.1 题目描述1.2 输入描述1.3 输出描述1.4 示例二、解题思路2.1 核心算法设计2.2 性能优化关键2.3 算法流程图三、解法实现3.1 解法一:字符串分离法3.1.1 初级版本分析3.2 解法二:数学逐位构建法(推荐)3.2.1 优化版本分析四、总结与拓展4.1 关键优化技…

Maven 继承:构建高效项目结构的利器

一、引言 Maven 是一个强大的项目管理工具&#xff0c;它通过标准化的项目结构和依赖管理极大地简化了 Java 项目的开发流程。在 Maven 中&#xff0c;继承是一种非常有用的功能&#xff0c;它允许我们创建一个父项目&#xff0c;其他子项目可以继承这个父项目的配置信息&#…

Mysql组合索引的update在多种情况下的间隙锁的范围(简单来说)

简单来说&#xff0c;当 UPDATE 语句的 WHERE 条件使用了组合索引&#xff0c;并且需要锁定不存在的“间隙”来防止幻读时&#xff0c;就会产生间隙锁。间隙锁的范围取决于 WHERE 条件如何利用组合索引&#xff0c;以及数据库的隔离级别。 我们用图书馆的例子。比如&#xff1a…

什么是Apache Ignite的affinity(亲和性)

在 Apache Ignite 中&#xff0c; affinity&#xff08;亲和性&#xff09; 是一种用于控制数据分布和查询性能的重要机制。它允许开发者指定数据如何在集群中的节点之间分布&#xff0c;从而优化数据访问和查询效率。以下是关于 affinity 的详细解释&#xff1a;数据亲和性&a…

youtube图论

dfs排序lifo & fifo存储方式邻接矩阵dijstra处理过的保存/更新&#xff0c;意味着一个节点避免了重复访问bfs dfs

借助ssh实现web服务的安全验证

背景 公有云服务器 http 服务 80端口&#xff0c;想做到安全访问无须HTTPS 客户端证书方便、快捷、安全 SSH 隧道 本地代理 使用 SSH 隧道将 HTTP 服务“隐藏”在 SSH 之后&#xff1a; # 客户端建立隧道&#xff08;将本地 8080 转发到服务器的 80 端口&#xff09; ssh…

状态机在前端开发中的艺术:从理论到框架级实践

文章目录一 状态机&#xff1a;复杂逻辑的终结者1.1 什么是状态机&#xff1f;1.2 为何前端需要状态机&#xff1f;二 状态机核心概念深度解析2.1 有限状态机&#xff08;FSM&#xff09;与分层状态机&#xff08;HSM&#xff09;2.2 状态机的数学表示三 前端开发中的状态机实战…

把word中表格转成excle文件

把word中表格转成excle文件 from docx import Document from openpyxl import Workbook from pathlib import Path# 打开 Word 文档 document Document(./weather_report.docx) tables document.tables# 输出文件路径 output_file Path(./weather_report.xlsx)# 如果文件已存…