【ESP32】打造全网最强esp-idf基础教程——14.VFS与SPIFFS文件系统

VFS与SPIFFS文件系统

       这几天忙着搬砖,差点没时间更新博客了,所谓一日未脱贫,打工不能停,搬砖不狠,明天地位不稳呀。 不多说了,且看以下内容吧~

一、VFS虚拟文件系统
       先来看下文件系统的定义,文件系统是操作系统中用于组织、管理和存储持久性数据的一个关键组件。它是方法和数据结构的集合,使操作系统能够有效地在存储设备(如硬盘驱动器、固态硬盘、USB闪存驱动器等)上存储、检索和管理文件。
文件系统通常由三个主要部分组成:
       1)文件系统的接口:用户和应用程序与文件系统交互的方式。
       2)对对象操纵和管理的软件集合:实现文件创建、删除、读取、写入、重命名等操作的系统软件。
       3)对象及属性:实际存储的数据文件以及与之相关的元数据信息。

       当我们使用标准文件操作时,比如我们使用fwrite(buffer,size,count,file)函数时,我们不用关心到底是写入到磁盘上哪个地址,偏移量是多少等等诸如此类的硬件底层问题,因为这些操作文件系统已经帮我们处理好了,我们只需要关注往哪个文件写入什么内容,从哪个文件读取什么内容即可,文件系统帮我们把这些文件有效的管理组织起来,形成包括文件和目录的层次结构。

       在esp-idf中虚拟文件系统 (VFS) 组件为驱动程序提供一个统一接口,可以操作类文件对象。这类驱动程序可以是 FAT、SPIFFS 等真实文件系统,也可以是提供文件类接口的设备驱动程序。

       VFS 组件支持 C 库函数(如 fopen 和 fprintf 等)与文件系统 (FS) 驱动程序协同工作。在高层级,每个 FS 驱动程序均与某些路径前缀相关联。当一个 C 库函数需要打开文件时,VFS 组件将搜索与该文件所在文件路径相关联的 FS 驱动程序,并将调用传递给该驱动程序。针对该文件的读取、写入等其他操作的调用也将传递给这个驱动程序。

       例如,使用 /fat 前缀注册 FAT 文件系统驱动,之后即可调用 fopen("/fat/file.txt", "w")。之后,VFS 将调用FAT驱动的 open 函数,并将参数 /file.txt 和合适的打开模式传递给 open 函数;后续对返回的 FILE* 数据流调用C库函数也同样会传递给 FAT 驱动。
       如需注册 FS 驱动程序,应用程序首先要定义一个 esp_vfs_t 结构体实例,并用指向 FS API 的函数指针填充它。

        esp_vfs_t myfs = {    
       .flags = ESP_VFS_FLAG_DEFAULT,    
       .write = &myfs_write,    
       .open = &myfs_open,    
       .fstat = &myfs_fstat,    
       .close = &myfs_close,    
       .read = &myfs_read,
       };
       ESP_ERROR_CHECK(esp_vfs_register("/data", &myfs, NULL));

       在上述代码中需要用到 read、 write 或 read_p、 write_p,具体使用哪组函数由 FS 驱动程序 API 的声明方式决定。
       示例 1:声明 API 函数时不带额外的上下文指针参数,即 FS 驱动程序为单例模式,此时使用 write

       ssize_t myfs_write(int fd, const void * data, size_t size);
       // In definition of esp_vfs_t:    
       .flags = ESP_VFS_FLAG_DEFAULT,   
       .write = &myfs_write,// ... other members initialized
       // When registering FS, context pointer (third argument) is NULL:
       ESP_ERROR_CHECK(esp_vfs_register("/data", &myfs, NULL));

        示例 2:声明 API 函数时需要一个额外的上下文指针作为参数,即可支持多个 FS 驱动程序实例,此时使用 write_p

       ssize_t myfs_write(myfs_t* fs, int fd, const void * data, size_t size);
       // In definition of esp_vfs_t:    
       .flags = ESP_VFS_FLAG_CONTEXT_PTR,    
      .write_p = &myfs_write,// ... other members initialized
       // When registering FS, pass the FS context pointer into the third argument
       // (hypothetical myfs_mount function is used for illustrative purposes)
       myfs_t* myfs_inst1 = myfs_mount(partition1->offset, partition1->size);
       ESP_ERROR_CHECK(esp_vfs_register("/data1", &myfs, myfs_inst1));
       // Can register another instance:
       myfs_t* myfs_inst2 = myfs_mount(partition2->offset, partition2->size);
       ESP_ERROR_CHECK(esp_vfs_register("/data2", &myfs, myfs_inst2));

        已注册的 FS 驱动程序均有一个路径前缀与之关联,此路径前缀即为分区的挂载点。
如果挂载点中嵌套了其他挂载点,则在打开文件时使用具有最长匹配路径前缀的挂载点。

        例如,假设以下文件系统已在 VFS 中注册:ssize_t myfs_write(myfs_t* fs, int fd, const void * data, size_t size); 

       // In definition of esp_vfs_t:    
      .flags = ESP_VFS_FLAG_CONTEXT_PTR,    
      .write_p = &myfs_write,// ... other members initialized
      // When registering FS, pass the FS context pointer into the third argument
      // (hypothetical myfs_mount function is used for illustrative purposes)
      myfs_t* myfs_inst1 = myfs_mount(partition1->offset, partition1->size);
      ESP_ERROR_CHECK(esp_vfs_register("/data1", &myfs, myfs_inst1));
      // Can register another instance:
      myfs_t* myfs_inst2 = myfs_mount(partition2->offset, partition2->size);
      ESP_ERROR_CHECK(esp_vfs_register("/data2", &myfs, myfs_inst2));

      在 /data 下注册 FS 驱动程序1
      在 /data/static 下注册 FS 驱动程序2
      那么:
      打开 /data/log.txt 会调用驱动程序FS1;
      打开 /data/static/index.html 需调用驱动程序FS2;

      即便 FS驱动程序2中没有/index.html,也不会在FS驱动程序1中查找 /static/index.html。
      挂载点名称必须以路径分隔符 (/) 开头,且分隔符后至少包含一个字符。但在以下情况中,VFS 同样支持空的挂载点名称:1. 应用程序需要提供一个”最后方案“下使用的文件系统;2. 应用程序需要同时覆盖 VFS 功能。如果没有与路径匹配的前缀,就会使用到这种文件系统。 

      VFS 不会对路径中的点 (.) 进行特殊处理,也不会将 .. 视为对父目录的引用。在上述示例中,使用 /data/static/../log.txt 路径不会调用 FS 驱动程序 1 打开 /log.txt。特定的 FS 驱动程序(如 FATFS)可能以不同的方式处理文件名中的点。
      执行打开文件操作时,FS 驱动程序仅得到文件的相对路径(挂载点前缀已经被去除): 

      以 /data 为路径前缀注册 myfs 驱动;
      应用程序调用 fopen("/data/config.json", ...);
      VFS 调用 myfs_open("/config.json", ...);
      myfs 驱动打开 /config.json 文件。
      VFS 对文件路径长度没有限制,但文件系统路径前缀受 ESP_VFS_PATH_MAX 限制,即路径前缀上限为 ESP_VFS_PATH_MAX。各个文件系统驱动则可能会对自己的文件名长度设置一些限制。
      以上内容是我从官方资料中筛选出来的,大家可能看完了也不是很懂,但没关系,看不懂有两种方法,一是再几遍,二就是直接简单点,用代码演示一下。在代码演示之前,还需要讲解一个文件系统,那就是esp-idf里面的spiffs文件系统。 

       二、SPIFFS文件系统
       SPIFFS 是一个用于 SPI NOR flash 设备的嵌入式文件系统,支持磨损均衡、文件系统一致性检查等功能。在介绍ESP32分区表的时候,我们知道了分区类型Type有两种,一种是app,另一种是data,如果我们想要用到spiffs进行存储时,我们需要新建一个分区,然后指定Type=data,子类型Subtype=spiffs,表示此分区用于spiffs文件系统存储。

       目前spiffs文件系统使用有如下限制: 

  • 目前,SPIFFS 尚不支持目录,但可以生成扁平结构。如果 SPIFFS 挂载在 /spiffs 下,在 /spiffs/tmp/myfile.txt 路径下创建一个文件则会在 SPIFFS 中生成一个名为 /tmp/myfile.txt 的文件,而不是在 /spiffs/tmp 下生成名为 myfile.txt 的文件;
  • SPIFFS 并非实时栈,每次写操作耗时不等;
  • 目前,SPIFFS 尚不支持检测或处理已损坏的块。
  • SPIFFS 只能稳定地使用约 75% 的指定分区容量。
  • 当文件系统空间不足时,垃圾收集器会尝试多次扫描文件系统来寻找可用空间。根据所需空间的不同,写操作会被调用多次,每次函数调用将花费几秒。同一操作可能会花费不同时长的问题缘于 SPIFFS 的设计,且已在官方的 SPIFFS github 仓库 或是 https://github.com/espressif/esp-idf/issues/1737 中被多次报告。这个问题可以通过 SPIFFS 配置 部分缓解。
  • 当垃圾收集器尝试多次(默认为 10 次)扫描整个文件系统以回收空间时,在每次扫描期间,如果有可用的数据块,则垃圾收集器会释放一个数据块。因此,如果为垃圾收集器设置的最大运行次数为 n(可通过 SPIFFS_GC_MAX_RUNS 选项配置,该选项位于 SPIFFS 配置 中),那么 n 倍数据块大小的空间将可用于写入数据。如果尝试写入超过 n 倍数据块大小的数据,写入操作可能会失败并返回错误。
  • 如果 ESP32 在文件系统操作期间断电,可能会导致 SPIFFS 损坏。但是仍可通过 esp_spiffs_check 函数恢复文件系统。详情请参阅官方 SPIFFS FAQ。

三、程序例程
       由第一节可知,VFS是虚拟文件系统,它是一个抽象层概念,在注册时需要把它的操作具体关联到某种实际的存储介质,另外,如果我们要想使用SPIFFS文件系统进行存储,也需要配合VFS一起使用。在本例程中VFS与SPIFFS文件系统结合使用,在esp-idf中已经将VFS与SPIFFS的关联操作封装好,我们使用提供的接口就可以轻松的把SPIFF文件系统挂载,请看如下代码。具体代码在esp32-board/spiffs中

#include <stdio.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "esp_err.h"
#include "esp_log.h"
#include "esp_spiffs.h"

static const char *TAG = "spiffs";
void app_main(void)
{
    ESP_LOGI(TAG, "Initializing SPIFFS");

    esp_vfs_spiffs_conf_t conf = {
      .base_path = "/spiffs",   //可以认为挂着点,后续使用C库函数fopen("/spiffs/...")
      .partition_label = NULL,  //指定spiffs分区,如果为NULL,则默认为分区表中第一个spiffs类型的分区
      .max_files = 5,           //最大可同时打开的文件数
      .format_if_mount_failed = true
    };
    //初始化和挂载spiffs分区
    esp_err_t ret = esp_vfs_spiffs_register(&conf);
    //失败处理
    if (ret != ESP_OK) {
        if (ret == ESP_FAIL) {
            ESP_LOGE(TAG, "Failed to mount or format filesystem");
        } else if (ret == ESP_ERR_NOT_FOUND) {
            ESP_LOGE(TAG, "Failed to find SPIFFS partition");
        } else {
            ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
        }
        return;
    }
    //执行SPIFFS文件系统检查
    ESP_LOGI(TAG, "Performing SPIFFS_check().");
    ret = esp_spiffs_check(conf.partition_label);//操作spiffs文件系统器件断电,可能会导致 SPIFFS 损坏,可通过esp_spiffs_check恢复
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "SPIFFS_check() failed (%s)", esp_err_to_name(ret));
        return;
    } else {
        ESP_LOGI(TAG, "SPIFFS_check() successful");
    }
    //获取SPIFFS可用区域大小
    size_t total = 0, used = 0;
    ret = esp_spiffs_info(conf.partition_label, &total, &used);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s). Formatting...", esp_err_to_name(ret));
        esp_spiffs_format(conf.partition_label);
        return;
    } else {
        ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
    }
    //可用空间异常,执行SPIFFS检查
    if (used > total) {
        ESP_LOGW(TAG, "Number of used bytes cannot be larger than total. Performing SPIFFS_check().");
        ret = esp_spiffs_check(conf.partition_label);
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "SPIFFS_check() failed (%s)", esp_err_to_name(ret));
            return;
        } else {
            ESP_LOGI(TAG, "SPIFFS_check() successful");
        }
    }
    //结合VFS,可以使用标准C库函数进行文件读写
    ESP_LOGI(TAG, "Opening file");
    FILE* f = fopen("/spiffs/hello.txt", "w");
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for writing");
        return;
    }
    fprintf(f, "Hello World!\n");
    fclose(f);
    ESP_LOGI(TAG, "File written");
    //检查/spiffs/foo.txt这个文件是否存在,如果存在删除它
    struct stat st;
    if (stat("/spiffs/foo.txt", &st) == 0) {
        // 删除/spiffs/foo.txt文件
        unlink("/spiffs/foo.txt");
    }
    //重命名文件
    ESP_LOGI(TAG, "Renaming file");
    if (rename("/spiffs/hello.txt", "/spiffs/foo.txt") != 0) {
        ESP_LOGE(TAG, "Rename failed");
        return;
    }
    //打开foo文件读取一行
    ESP_LOGI(TAG, "Reading file");
    f = fopen("/spiffs/foo.txt", "r");
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for reading");
        return;
    }
    char line[64];
    fgets(line, sizeof(line), f);
    fclose(f);
    // strip newline
    char* pos = strchr(line, '\n');
    if (pos) {
        *pos = '\0';
    }
    ESP_LOGI(TAG, "Read from file: '%s'", line);
    //测试完成,卸载
    esp_vfs_spiffs_unregister(conf.partition_label);
    ESP_LOGI(TAG, "SPIFFS unmounted");
}

       首先我们需要包含esp_spiffs.h头文件,这个头文件声明了与spiffs和VFS关联的操作
       
esp_vfs_spiffs_conf_t结构体定义了一些内容配置,其中比较关键的是.base_path = "/spiffs",   //这个可以认为是挂着点,也就是后续可以使用C库函数fopen("/spiffs/...")打开文件的前缀。其余的参数大家看注释便知道是什么意思。 

       然后调用esp_vfs_spiffs_register函数把配置设置进去,这个函数会初始化SPIFFS文件系统,然后把底层对SPIFFS文件系统的读写操作注册到VFS,以及将SPIFFS文件系统挂载到指定的挂载点”/spiffs”。

       esp_spiffs_check函数会对SPIFFS分区进行检查,修复损坏的文件,清理未引用的页面等,官方推荐如果SPIFFS_info返回used大于total,或者获取到任何以下错误代码时:SPIFFS_ERR_NOT_FINALIZED、SPIFFS_ERR_NOT_INDEX、SPIFFS_ERR_IS_INDEX、SPIFFS_ERR_IS_FREE、SPIFFS_ERR_INDEX_SPAN_MISMATCH、SPIFFS_ERR_DATA_SPAN_MISMATCH、SPIFFS_ERR_INDEX_REF_FREE、SPIFFS_ERR_INDEX_REF_LU、SPIFFS_ERR_INDEX_REF_INVALID、SPIFFS_ERR_INDEX_FREE、SPIFFS_ERR_INDEX_LU、SPIFFS_ERR_INDEX_INVALID,都应运行检查。

       当挂载成功后,我们就可以使用fopen、fwrite、rename等这些C语言标准文件操作来对spiffs进行操作了,这部分不再叙述。
       在对操作完成后,使用
esp_vfs_spiffs_unregister卸载掉spiffs文件系统,这个函数的参数如果是NULL,则会对分区表中第一个spiffs分区进行操作,会检测这个分区是否已经挂载,如果挂载了就会卸载掉这个分区,如果没有则返回错误。

最后附上相关资料:

ESP32教程资料链接:
https://pan.baidu.com/s/1kCjD8yktZECSGmHomx_veg?pwd=q8er 
提取码:q8er 

配套源码下载地址:
esp32-board: esp32开发板配套的经典例程

鉴于实验需要开发板的支持,我也设计了一款ESP32开发板,包含部分传感器模块,1.69寸LCD高亮屏,Type-C一键下载,方便大家学习和做各种实验。开发板链接如下:

https://item.taobao.com/item.htm?ft=t&id=802401650392&spm=a21dvs.23580594.0.0.4fee645eXpkfcp&skuId=5635015963649
 

请大家多多支持。

       

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ThreadPoolExecutor 工作线程Worker自身锁设计

个人博客 ThreadPoolExecutor 工作线程Worker自身锁设计 | iwts’s blog 总集 想要完整了解下ThreadPoolExecutor&#xff1f;可以参考&#xff1a; 基于源码详解ThreadPoolExecutor实现原理 | iwts’s blog Worker-工作线程管理 线程池设计了内部类Worker&#xff0c;主…

【LeetCode】 740. 删除并获得点数

这真是一道好题&#xff01;这道题不仅考察了抽象思维&#xff0c;还考察了分析能力、化繁为简的能力&#xff0c;同时还有对基本功的考察。想顺利地做出这道题还挺不容易&#xff01;我倒在了第一步与第二步&#xff1a;抽象思维和化繁为简。题目的要求稍微复杂一些&#xff0…

大模型压缩-LoRAP

这里写目录标题 1.多头注意力和FFN的权重分布2 多头矩阵的低秩分解FFN无梯度通道剪枝 这篇文章 1期望找到一个“剪枝&#xff0b;低秩分解”的路子&#xff0c;使结构化剪枝达到非结构化剪枝的性能。 1.多头注意力和FFN的权重分布 Fig. 1.1 多头注意力权重矩阵 从Fig.1.1可以看…

《昇思25天学习打卡营第17天 | 昇思MindSporeCycleGAN图像风格迁移互换》

17天 本节学习了CycleGAN图像风格迁移互换。 CycleGAN即循环对抗生成网络&#xff0c;该模型实现了一种在没有配对示例的情况下学习将图像从源域 X 转换到目标域 Y 的方法。该模型一个重要应用领域是域迁移&#xff0c;可以通俗地理解为图像风格迁移。其实在 CycleGAN 之前&a…

Vue下载接口返回流的处理

1.下载接口返回流如下&#xff1a; 2.可以写公共方法处理 excelDownload(obj, name Date.now(), suffix xlsx) {//Date.now()获取当前日期const url window.URL.createObjectURL(//Blob是二进制大对象new Blob([obj], { type: application/vnd.ms-excel }))const aDOM docu…

Java知识点整理 15 — MyBatis框架

一. 什么是 MyBatis MyBatis 是一款优秀的持久层框架&#xff0c;支持自定义 SQL、存储过程以及高级映射。它免除了几乎所有 JDBC代码以及手动设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO&#xff08;普通老式 Jav…

distance delayed sound

distance delayed sound 在本章中&#xff0c;我们将讨论在游戏音频中使用距离延迟的重要性。我们将首先通过一个常见的例子——闪电和雷鸣&#xff0c;来展示这种重要性并解释距离延迟音频的基础知识。我们将讨论计算速度、距离和时间的数学和方程式&#xff0c;以确定距离延迟…

postgre事务id用完后,如何解决这个问题

在PG中事务年龄不能超过2^31 &#xff08;2的31次方2,147,483,648&#xff09;&#xff0c;如果超过了&#xff0c;这条数据就会丢失。 PG中不允许这种情况出现&#xff0c;当事务的年龄离2^31还有1千万的时候&#xff0c;数据库的日志中就会 有如下告警&#xff1a; warning:…

pc端制作一个顶部固定的菜单栏

效果 hsl颜色 hsl颜色在css中比较方便 https://www.w3school.com.cn/css/css_colors_hsl.asp 色相&#xff08;hue&#xff09;是色轮上从 0 到 360 的度数。0 是红色&#xff0c;120 是绿色&#xff0c;240 是蓝色。饱和度&#xff08;saturation&#xff09;是一个百分比值…

模板方法模式在金融业务中的应用及其框架实现

引言 模板方法模式&#xff08;Template Method Pattern&#xff09;是一种行为设计模式&#xff0c;它在一个方法中定义一个算法的框架&#xff0c;而将一些步骤的实现延迟到子类中。模板方法允许子类在不改变算法结构的情况下重新定义算法的某些步骤。在金融业务中&#xff…

Python的numpy简单使用

1.可以调用引入numpy里面的函数&#xff0c;如add可以把俩数相加&#xff0c;也可以创建一个数组arr&#xff0c;arr.shape是数组arr的属性&#xff0c;如果后有跟&#xff08;&#xff09;就是里面的一个函数 type()函数可以知道里面是什么类型 变量.shape可以知道这个变量是…

[数据集][目标检测]猪只状态吃喝睡站检测数据集VOC+YOLO格式530张4类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;530 标注数量(xml文件个数)&#xff1a;530 标注数量(txt文件个数)&#xff1a;530 标注类别…

适配手机《植物大战僵尸杂交版》最新整合包,附Android、iOS、Windows保姆级教程和工具合集!

最近&#xff0c;新版的《植物大战僵尸杂交版》火爆全网啊&#xff01;许多小伙伴不知道手机和电脑怎样安装设置才能畅玩《杂交版》&#xff0c;所以今天阿星特意为大家准备了一份安装工具集。 里面有安卓、iOS及电脑端的安装包&#xff0c;包含安装视频教程、修改器、防闪退、…

探索ChatGPT是如何改变癌症护理

了解生成式人工智能&#xff08;尤其是 ChatGPT&#xff09;如何通过高级数据集成和个性化患者管理来增强诊断和治疗&#xff0c;从而改变癌症治疗。了解 Color Health 的创新副驾驶模型及其对早期检测和患者结果的影响。 近年来&#xff0c;人工智能与医疗保健的融合为癌症治疗…

使用 fvm 管理 Flutter 版本

文章目录 Github官网fvm 安装Mac/Linux 环境Windows 环境 fvm 环境变量fvm 基本命令 Github https://github.com/leoafarias/fvmhttps://github.com/flutter/flutter 官网 https://fvm.app/ fvm 安装 Mac/Linux 环境 Install.sh curl -fsSL https://fvm.app/install.sh …

基于模糊神经网络的时间序列预测(以hopkinsirandeath数据集为例,MATLAB)

模糊神经网络从提出发展到今天,主要有三种形式&#xff1a;算术神经网络、逻辑模糊神经网络和混合模糊神经网络。算术神经网络是最基本的&#xff0c;它主要是对输入量进行模糊化&#xff0c;且网络结构中的权重也是模糊权重&#xff1b;逻辑模糊神经网络的主要特点是模糊权值可…

shark云原生-日志管理体系-filebeat

文章目录 1. deploy 文件1.1 RBAC1.2. DaemonSet1.2.1. Elasticsearch 连接信息1.2.2. Volume 1.3. ConfigMap1.3.1. 日志收集路径1.3.2. 日志事件输出目标 2. 在控制平面节点上运行Filebeat3. 查看输出3.1. 关于处理器 processors 4. 日志收集配置4.1. 手动指定日志收集路径4.…

索引:通往高效查询的桥梁(五)

引言 上一章&#xff0c;我们探索了SQL的基础知识&#xff0c;从DDL、DML到DQL&#xff0c;掌握了构建和操作数据库的基本技能。现在&#xff0c;我们将目光转向数据库性能的核心——索引。索引&#xff0c;犹如图书馆中的目录系统&#xff0c;极大地加速了数据检索过程&#…

Unity实现简单的MVC架构

文章目录 前言MVC基本概念示例流程图效果预览后话 前言 在Unity中&#xff0c;MVC&#xff08;Model-View-Controller&#xff09;框架是一种架构模式&#xff0c;用于分离游戏的逻辑、数据和用户界面。MVC模式可以帮助开发者更好地管理代码结构&#xff0c;提高代码的可维护性…

CloudFlare Tunnel实现内网穿透

CloudFlare Tunnel 背景&#xff1a; 家中设备处于内网NAT环境&#xff0c;希望使用CF tunnel构建内网穿透的环境。 有了CF tunnel后&#xff0c;可实现&#xff1a; 家中的NAS可以直接SSH AWS的云服务可迁到到NAS NAT主机借助CF tunnel部署服务 步骤&#xff1a; clou…