搭建 STM32 网关服务器的全流程:集成嵌入式 C++、TCP/IP 通信、Flash 存储及 JWT 认证(含代码示例)

news2024/11/15 19:45:00

引言

随着物联网(IoT)技术的快速发展,基于 STM32 的服务器(类似网关)在数据采集、设备控制等方面的应用越来越广泛。本文将介绍搭建一个基于 STM32 的服务器所需的技术栈,以及详细的搭建步骤和代码示例。

技术栈介绍

在搭建基于 STM32 的服务器时,我们需要用到以下技术栈和组件:

1. 硬件平台

  • STM32 微控制器:选择 STM32F4 或 STM32F7 系列,根据性能需求和外设支持。
  • 网络模块:可以选择 ESP8266(Wi-Fi)、ESP32(Wi-Fi + 蓝牙)或以太网模块(如 W5500)。
  • 电源管理:使用稳压器或电源管理芯片,确保系统供电稳定。

2. 开发环境

  • IDE:使用 STM32CubeIDE 或 Keil MDK 进行开发。
  • 库和驱动
    • STM32 HAL 库:简化硬件访问。
    • LWIP(轻量级IP协议栈):实现 TCP/IP 协议栈。
    • FreeRTOS(可选):支持多任务处理。

3. 网络协议

  • TCP/IP:实现基本的网络通信。
  • HTTP/HTTPS:用于支持 Web 服务。
  • MQTT:适合 IoT 设备间的轻量级通信。

4. 开发语言

  • C/C++:主要用于 STM32 的底层开发。
  • HTML/CSS/JavaScript:用于开发 Web 界面。

5. 数据存储

  • Flash 存储:存储配置和小型数据。
  • SD 卡(可选):用于大容量数据存储。

6. 安全性

  • TLS/SSL:实现数据加密,确保通信安全。
  • 认证机制:如 JWT,确保设备和用户身份验证。

搭建步骤

下面将详细介绍如何搭建基于 STM32 的服务器,涵盖硬件连接、软件开发和测试等步骤。

一、硬件搭建

1.1 准备硬件
  • 开发板:选择 STM32F4 或 STM32F7 开发板。
  • 网络模块:选择 ESP8266 或 W5500 以实现网络连接。
  • 其他配件:稳压电源模块、面包板、杜邦线等。
1.2 硬件连接
  1. 连接 STM32 和网络模块

    • ESP8266/ESP32

      • 将 ESP 模块的 VCC 连接到 STM32 的 3.3V,GND 连接到 GND。
      • 将 ESP 的 TX 接口连接到 STM32 的 RX 引脚,RX 接口连接到 STM32 的 TX 引脚。
    • W5500

      • 将 W5500 的 SPI 接口连接到 STM32 的相应引脚:
        • MOSI -> STM32 MOSI
        • MISO -> STM32 MISO
        • SCK -> STM32 SCK
        • CS -> STM32 GPIO(选择任意 GPIO 作为片选引脚)
      • 连接电源(VCC 和 GND)。
  2. 确保电源管理

    • 使用适当的稳压器,确保 STM32 和其他模块的电压符合要求。
1.3 硬件连接示意图

二、软件开发

2.1 开发环境设置

2.2 引入必要的库文件

  1. 下载 STM32CubeIDE

    • 访问 ST 官网 下载并安装 STM32CubeIDE。
  2. 创建新工程

    • 打开 STM32CubeIDE,选择新建 STM32 项目。
    • 选择所用的 STM32 微控制器型号。
  3. 配置外设

    • 在 STM32CubeMX 中,配置 UART(用于串口通信)和 SPI(如果使用 W5500)。
    • 在“Pinout & Configuration”选项卡中,设置 UART 和 SPI 引脚。
  4. 启用 LWIP 协议栈

    • 在“Middleware”选项卡中,启用 LWIP。配置 LWIP 参数,包括 IP 地址、网络掩码和网关。
    • 配置 LWIP 的使用模式(如 DHCP 或静态 IP)。
  5. 生成代码

    • 点击“Project”菜单,选择“Generate Code”,生成项目代码。
  6. STM32 HAL 库

    • 在项目中默认已经包含 STM32 HAL 库,无需额外引入。
  7. LWIP 协议栈

    • LWIP 协议栈已经在 STM32CubeMX 中配置并生成,可以直接使用。
  8. FreeRTOS(可选)

    • 如果需要多任务处理,选择 FreeRTOS,设置任务优先级及堆大小。

2.3 编写代码

2.3.1 初始化代码

在 main.c 文件中,增加网络初始化和服务器启动代码。以下是基本实现步骤:

#include "lwip/init.h"
#include "lwip/netconn.h"

// 网络配置函数
void init_network() {
    // 初始化硬件
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI_Init(); // 如果使用W5500
    MX_LWIP_Init(); // 初始化LWIP
}

// TCP/IP 服务器实现
void start_server() {
    struct netconn *conn, *newconn;
    err_t err;

    // 创建 TCP 连接
    conn = netconn_new(NETCONN_TCP);
    netconn_bind(conn, NULL, 80); // 绑定到端口 80
    netconn_listen(conn);

    while (1) {
        err = netconn_accept(conn, &newconn); // 接受新的连接
        if (err == ERR_OK) {
            // 处理请求
            // 这里可以添加处理 HTTP 请求的代码
            netconn_delete(newconn); // 处理完毕后关闭连接
        }
    }
}
2.3.2 主函数

在 main() 函数中调用初始化和服务器启动代码:

int main(void) {
    init_network(); // 初始化网络
    start_server(); // 启动TCP服务器

    while (1) {
        sys_check_timeouts(); // 处理 LWIP 超时
    }
}

2.4 处理 HTTP 请求

为了让服务器能够响应 HTTP 请求,可以在 start_server() 函数中添加 HTTP 请求处理逻辑。以下是一个简单的 HTTP 响应示例:

void handle_request(struct netconn *newconn) {
    struct netbuf *inbuf;
    char *buffer;
    u16_t len;

    // 等待接收数据
    netconn_recv(newconn, &inbuf);
    netbuf_data(inbuf, (void**)&buffer, &len);

    // 简单处理 HTTP GET 请求
    if (strncmp(buffer, "GET ", 4) == 0) {
        // 发送 HTTP 响应
        const char* response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"
                               "<html><body><h1>Hello, STM32!</h1></body></html>";
        netconn_write(newconn, response, strlen(response), NETCONN_NOCOPY);
    }

    netbuf_delete(inbuf); // 释放内存
}

// 在 start_server() 中调用
if (err == ERR_OK) {
    handle_request(newconn); // 处理 HTTP 请求
    netconn_delete(newconn);
}

2.5 编译和上传

  1. 编译代码

    • 在 STM32CubeIDE 中,点击“Build”按钮编译项目,确保代码没有错误。
  2. 上传代码

    • 连接开发板,选择正确的调试器(如 ST-Link),点击“Run”按钮将代码上传到 STM32。

2.6 测试服务器

  1. 连接网络

    • 确保 STM32 开发板通过 ESP8266/ESP32 或 W5500 网络模块正确连接到网络。
    • 如果使用 ESP8266/ESP32,请确保模块已连接到 Wi-Fi 网络。如果使用 W5500,请确保以太网线连接到路由器。
  2. 获取 IP 地址

    • 如果使用 DHCP,STM32 会自动获取 IP 地址。
    • 可以在调试输出(例如使用 UART)中打印出分配的 IP 地址。可以在 lwip 初始化后加入如下代码:
      ip_addr_t ipaddr, netmask, gw;
      netif_default->ip_addr.addr = netif_default->ip_addr.addr;
      netif_default->netmask.addr = netif_default->netmask.addr;
      netif_default->gw.addr = netif_default->gw.addr;
      
      printf("IP Address: %s\n", ipaddr_ntoa(&netif_default->ip_addr));
      
  3. 使用浏览器访问服务器

    • 在 PC 或手机的浏览器中输入 STM32 的 IP 地址,例如 http://192.168.1.100(请根据实际分配的 IP 地址修改)。
    • 如果一切正常,您应该能看到服务器返回的 HTML 页面,显示内容为 "Hello, STM32!"。
  4. 调试

    • 如果未能访问网页,请检查以下事项:
      • 确保 STM32 开发板的电源正常。
      • 检查网络连接是否正常。
      • 使用串口监视器查看调试信息,确认 IP 地址是否正确。
      • 使用 Wireshark 等工具监控网络流量,检查请求是否到达 STM32。

3.1 增加更多的功能

处理不同的 HTTP 请求

可以根据不同的 URL 路径处理不同的请求,例如通过 GET /data 获取传感器数据。以下是如何实现的步骤:

  1. 修改请求处理函数
    在 handle_request 函数中,根据请求的 URL 处理不同的请求。

    void handle_request(struct netconn *newconn) {
        struct netbuf *inbuf;
        char *buffer;
        u16_t len;
    
        // 等待接收数据
        netconn_recv(newconn, &inbuf);
        netbuf_data(inbuf, (void**)&buffer, &len);
    
        // 简单处理 HTTP GET 请求
        if (strncmp(buffer, "GET /data", 9) == 0) {
            // 假设我们有一个函数获取传感器数据
            char sensor_data[100]; // 假设存储传感器数据的数组
            get_sensor_data(sensor_data); // 实现这个函数以获取传感器数据
    
            // 发送 HTTP 响应
            char response[150];
            snprintf(response, sizeof(response), "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n%s", sensor_data);
            netconn_write(newconn, response, strlen(response), NETCONN_NOCOPY);
        } else {
            // 处理其他请求
            const char* response = "HTTP/1.1 404 Not Found\r\n\r\n";
            netconn_write(newconn, response, strlen(response), NETCONN_NOCOPY);
        }
    
        netbuf_delete(inbuf); // 释放内存
    }
    
    void get_sensor_data(char *data) {
        // 模拟传感器数据
        sprintf(data, "Temperature: 25.5 C\nHumidity: 60%%");
    }
    
实现 MQTT 支持

如果需要实现 IoT 设备间的通信,可以集成 MQTT 协议。可以使用如 Paho MQTT 或 MQTT-C 等轻量级的 MQTT 客户端库。以下是实现步骤:

  1. 下载并集成 MQTT 库

    • 根据选定的库,下载源代码并将其添加到 STM32 项目中。
  2. 初始化 MQTT 客户端

    #include "MQTTClient.h"  // 根据所用的 MQTT 库引入头文件
    
    MQTTClient client;
    char *mqtt_broker = "tcp://broker.hivemq.com:1883"; // MQTT Broker 地址
    
    void mqtt_init() {
        MQTTClient_create(&client, mqtt_broker, "stm32_client_id", MQTTCLIENT_PERSISTENCE_NONE, NULL);
        MQTTClient_setCallbacks(client, NULL, NULL, messageArrived, NULL);
        MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
        conn_opts.keepAliveInterval = 20;
        conn_opts.cleansession = 1;
    
        if (MQTTClient_connect(client, &conn_opts) != MQTTCLIENT_SUCCESS) {
            printf("Failed to connect to MQTT broker\n");
            return;
        }
        printf("Connected to MQTT broker\n");
    }
    
  3. 发布和订阅消息

    void publish_message(const char *topic, const char *payload) {
        MQTTClient_message pubmsg = MQTTClient_message_initializer;
        pubmsg.payload = (void*)payload;
        pubmsg.payloadlen = strlen(payload);
        pubmsg.qos = 1;
        pubmsg.retained = 0;
        MQTTClient_publishMessage(client, topic, &pubmsg, NULL);
    }
    
    void subscribe_to_topic(const char *topic) {
        MQTTClient_subscribe(client, topic, 1);
    }
    

3.2 数据存储

使用 Flash 存储

可以将设备配置(如 Wi-Fi SSID 和密码)存储在 Flash 中,以便在重启时自动加载。

  1. Flash 存储函数
#include "stm32f4xx_hal_flash.h"

void write_flash(uint32_t address, uint8_t *data, uint16_t size) {
    HAL_FLASH_Unlock(); // 解锁 Flash 写入

    // 擦除页
    FLASH_Erase_Sector(FLASH_SECTOR_2, VOLTAGE_RANGE_3); // 擦除选择的区域
    // 写入数据
       for (uint16_t i = 0; i < size; i++) {
           if (HAL_FLASH_Program(TYPEPROGRAM_BYTE, address + i, data[i]) != HAL_OK) {
               // 处理写入错误
               return;
           }
       }

       HAL_FLASH_Lock(); // 锁定 Flash 写入
   }

   void read_flash(uint32_t address, uint8_t *data, uint16_t size) {
       for (uint16_t i = 0; i < size; i++) {
           data[i] = *(__IO uint8_t*)(address + i); // 读取数据
       }
   }
使用示例
  1. 写入 Wi-Fi 配置

    void save_wifi_config(const char* ssid, const char* password) {
        uint8_t ssid_len = strlen(ssid);
        uint8_t password_len = strlen(password);
        uint32_t address = 0x080E0000; // 假设选择这个地址存储配置
    
        // 写入 SSID
        write_flash(address, (uint8_t *)ssid, ssid_len);
    
        // 写入密码
        write_flash(address + 0x40, (uint8_t *)password, password_len); // 假设密码紧跟在 SSID 后
    }
    
  2. 读取 Wi-Fi 配置

    void load_wifi_config(char* ssid, char* password) {
        uint32_t address = 0x080E0000; // 读取配置的地址
        uint8_t ssid_len = 32; // 假设 SSID 最大长度为 32
        uint8_t password_len = 32; // 假设密码最大长度为 32
    
        read_flash(address, (uint8_t *)ssid, ssid_len);
        read_flash(address + 0x40, (uint8_t *)password, password_len);
    }
    

使用 SD 卡存储数据

如果需要存储大量数据,可以添加 SD 卡模块,并使用 FATFS 文件系统进行数据读写。

1. 添加 SD 卡模块
  1. 硬件连接
    • 将 SD 卡模块连接到 STM32 的 SPI 接口(MOSI、MISO、SCK 和 CS)。
    • 连接 VCC 和 GND。
2. 配置 FATFS
  1. 在 STM32CubeMX 中启用 FATFS

    • 在中间件部分选择 FATFS,并配置为 SPI 模式。
  2. 生成代码

    • 生成代码后,您将在项目中看到 FATFS 的相关文件。
3. SD 卡读写示例
  1. 初始化 SD 卡

    FATFS FatFs;  // FatFs工作区
    FIL fil;      // 文件对象
    FRESULT fr;  // FATFS 结果
    
    void init_sd_card() {
        fr = f_mount(&FatFs, "", 1); // 挂载文件系统
        if (fr != FR_OK) {
            // 处理错误
        }
    }
    
  2. 写入数据到 SD 卡

    void write_to_sd_card(const char* filename, const char* data) {
        fr = f_open(&fil, filename, FA_WRITE | FA_CREATE_ALWAYS); // 打开文件
        if (fr == FR_OK) {
            f_write(&fil, data, strlen(data), NULL); // 写入数据
            f_close(&fil); // 关闭文件
        } else {
            // 处理打开文件错误
        }
    }
    
  3. 从 SD 卡读取数据

    void read_from_sd_card(const char* filename, char* buffer, uint32_t buffer_size) {
        fr = f_open(&fil, filename, FA_READ); // 打开文件
        if (fr == FR_OK) {
            f_read(&fil, buffer, buffer_size, NULL); // 读取数据
            f_close(&fil); // 关闭文件
        } else {
            // 处理打开文件错误
        }
    }

3.3 增强安全性

启用 HTTPS

使用 TLS/SSL 库(如 mbedTLS)实现 HTTPS,确保数据传输安全。

1. 集成 mbedTLS
  1. 下载 mbedTLS

    • 访问 mbedTLS GitHub 页面 下载最新版本的 mbedTLS。
    • 将相关源代码和头文件添加到 STM32 项目中。
  2. 配置 mbedTLS

    • 在 mbedtls/config.h 中,启用所需功能,例如:
      #define MBEDTLS_SSL_CLI_C
      #define MBEDTLS_SSL_SRV_C
      #define MBEDTLS_TLS_C
      #define MBEDTLS_X509_CRT_PARSE_C
      #define MBEDTLS_SHA256_C
      #define MBEDTLS_AES_C
      
2. 初始化 mbedTLS
#include "mbedtls/net_sockets.h"
#include "mbedtls/ssl.h"
#include "mbedtls/error.h"

// 全局变量
mbedtls_ssl_context ssl;
mbedtls_ssl_config conf;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;

// 初始化 mbedTLS
void init_mbedtls() {
    mbedtls_ssl_init(&ssl);
    mbedtls_ssl_config_init(&conf);
    mbedtls_entropy_init(&entropy);
    mbedtls_ctr_drbg_init(&ctr_drbg);

    // 设置随机数生成器
    const char *pers = "ssl_client";
    mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *)pers, strlen(pers));

    // 配置 SSL
    mbedtls_ssl_config_defaults(&conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT);
    mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &ctr_drbg);
}

// 连接到 HTTPS 服务器
int connect_https(const char *hostname, const char *port) {
    mbedtls_net_context server_fd;
    mbedtls_net_init(&server_fd);

    // 连接到服务器
    if (mbedtls_net_connect(&server_fd, hostname, port, MBEDTLS_NET_PROTO_TCP) != 0) {
        return -1;
    }

    // 设置 SSL
    mbedtls_ssl_setup(&ssl, &conf);
    mbedtls_ssl_set_hostname(&ssl, hostname);
    mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send, mbedtls_net_recv, NULL);

    // 完成 SSL 握手
    if (mbedtls_ssl_handshake(&ssl) != 0) {
        mbedtls_net_free(&server_fd);
        return -1;
    }

    return 0; // 连接成功
}
3. 发送 HTTPS 请求
void send_https_request(const char *hostname, const char *path) {
    char request[512];
    snprintf(request, sizeof(request),
             "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", path, hostname);

    mbedtls_ssl_write(&ssl, (unsigned char *)request, strlen(request));

    // 读取响应
    unsigned char buf[1024];
    int ret;
    do {
        ret = mbedtls_ssl_read(&ssl, buf, sizeof(buf) - 1);
        if (ret > 0) {
            buf[ret] = '\0'; // 添加字符串结束符
            printf("%s", (char *)buf); // 打印响应
        }
    } while (ret > 0);

    // 清理
    mbedtls_ssl_close_notify(&ssl);
    mbedtls_net_free(&server_fd);
}

用户身份认证

1. JWT 生成和验证
1. 生成 JWT

使用 JWT 库生成 JSON Web Token,以下是如何生成 JWT 的示例代码:

#include "jwt.h"

// 生成 JWT
char* generate_jwt(const char *secret, const char *username) {
    jwt_t *jwt = NULL;
    char *token = NULL;

    // 创建新的 JWT
    if (jwt_new(&jwt) != 0) {
        return NULL; // 处理错误
    }

    // 设置 JWT 的声明
    jwt_add_grant(jwt, "sub", username); // 用户名
    jwt_add_grant_int(jwt, "exp", time(NULL) + 3600); // 设置过期时间为1小时

    // 签名 JWT
    if (jwt_set_alg(jwt, JWT_ALG_HS256, (unsigned char *)secret, strlen(secret)) != 0) {
        jwt_free(jwt);
        return NULL; // 处理错误
    }

    // 获取 JWT 字符串
    token = jwt_encode_str(jwt);
    jwt_free(jwt); // 释放 JWT 结构体

    return token; // 返回生成的 JWT
}
2. 验证 JWT

在服务器端验证 JWT,确保请求的用户身份合法。

int verify_jwt(const char *token, const char *secret) {
    jwt_t *jwt = NULL;
    const char *username;

    // 解析 JWT
    if (jwt_decode(&jwt, token, (unsigned char *)secret, strlen(secret)) != 0) {
        return 0; // 验证失败
    }

    // 检查过期时间
    if (jwt_get_grant_int(jwt, "exp") < time(NULL)) {
        jwt_free(jwt);
        return 0; // JWT 已过期
    }

    username = jwt_get_grant(jwt, "sub"); // 获取用户名
    printf("Authenticated user: %s\n", username); // 打印用户信息

    jwt_free(jwt); // 释放 JWT 结构体
    return 1; // 验证成功
}
3. 结合 HTTP 请求与 JWT 验证

在处理 HTTP 请求时,检查 Authorization 头中是否包含有效的 JWT。

void handle_request(struct netconn *newconn) {
    struct netbuf *inbuf;
    char *buffer;
    u16_t len;

    netconn_recv(newconn, &inbuf);
    netbuf_data(inbuf, (void**)&buffer, &len);

    // 示例:处理 GET 请求,检查 JWT
    if (strncmp(buffer, "GET /data", 9) == 0) {
        // 检查 Authorization 头
        char *auth_header = strstr(buffer, "Authorization: ");
        if (auth_header) {
            char *token = strtok(auth_header + 15, "\r\n"); // 提取 JWT (Bearer token)
            if (verify_jwt(token, "your_secret_key")) { // 验证 JWT
                // 返回传感器数据
                char sensor_data[100];
                get_sensor_data(sensor_data); // 获取传感器数据
                char response[150];
                snprintf(response, sizeof(response),
                         "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n%s", sensor_data);
                netconn_write(newconn, response, strlen(response), NETCONN_NOCOPY);
            } else {
                // JWT 验证失败
                const char* response = "HTTP/1.1 401 Unauthorized\r\n\r\n";
                netconn_write(newconn, response, strlen(response), NETCONN_NOCOPY);
            }
        } else {
            // 未提供 JWT
            const char* response = "HTTP/1.1 401 Unauthorized\r\n\r\n";
            netconn_write(newconn, response, strlen(response), NETCONN_NOCOPY);
        }
    } else {
        // 处理其他请求
        const char* response = "HTTP/1.1 404 Not Found\r\n\r\n";
        netconn_write(newconn, response, strlen(response), NETCONN_NOCOPY);
    }

    netbuf_delete(inbuf); // 释放内存
}

总结

通过本文,我们详细介绍了如何搭建一个基于 STM32 的服务器(类似网关),并实现了多种功能,具体包括:

  1. 硬件搭建

    • 选择合适的 STM32 微控制器(如 STM32F4 或 STM32F7)和网络模块(如 ESP8266、ESP32 或 W5500)。
    • 确保电源管理稳定,为系统提供稳定的电压。
  2. 软件开发

    • 设置开发环境,使用 STM32CubeIDE 创建项目。
    • 配置并初始化网络模块,使用 LWIP 协议栈实现 TCP/IP 通信。
    • 编写处理 HTTP 请求的代码,支持根据 URL 路径返回不同的响应。
  3. 增加更多功能

    • 处理不同的 HTTP 请求,获取传感器数据并返回。
    • 集成 MQTT 协议,实现 IoT 设备间的通信。
    • 提供 HTTP 服务和 MQTT 服务,增加系统的灵活性。
  4. 数据存储

    • 使用 Flash 存储设备配置(如 Wi-Fi SSID 和密码)。
    • 使用 SD 卡模块存储大量数据,并通过 FATFS 文件系统读写数据。
  5. 增强安全性

    • 使用 mbedTLS 库启用 HTTPS,确保数据传输的安全性。
    • 实现 JWT(JSON Web Token)身份认证,确保只有授权用户能够访问服务器。

非常感谢您阅读到这里!您的关注和支持是我不断前进的动力。跟随着我探索嵌入式领域,希望因为兴趣而成为嵌入式领域的专家。

在这个快速发展的技术时代,嵌入式系统无处不在,从智能家居到医疗设备,从自动驾驶汽车到工业控制,每一个领域都离不开嵌入式技术的支持。对我来说,嵌入式不仅仅是一门技术,更是一种激情和追求。通过不断学习和实践,我深深爱上了这个充满挑战和机遇的领域。每一次调试成功,每一个创新的实现,都是我继续前行的动力。

——by 极客小张

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

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

相关文章

好的养宠空气净化器是智商税吗?好的养宠空气净化器用户体验

家里养了两只“超级掉毛怪”,家里的猫毛满天飞&#xff0c;衣服床餐具等到处都是&#xff01;感受一下40度高温的养猫人&#xff0c;给掉毛怪疏毛浮毛飘飘&#xff0c;逃不过的饮水机&#xff0c;各个角落&#xff0c;多猫拉臭传来的异味。 一、养猫需要解决的问题 掉毛&#…

Tsan-ThreadSanitizer之As if synchronized via sleep

最近在调试ffmpeg的时候&#xff0c;加入了tsan&#xff0c;结果出现了下面提示&#xff1a; 具体什么意思呢&#xff0c;找了很久找到了官方介绍&#xff1a; https://github.com/google/sanitizers/wiki/ThreadSanitizerReportFormat

大数据-61 Kafka 高级特性 消息消费02-主题与分区 自定义反序列化 拦截器 位移提交 位移管理 重平衡

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

Git的一些简单使用

下列内容适用于git初学者&#xff0c;从创建本地git仓库到提交的一个基本过程1. 1.创建git仓库 在想创建git仓库的路径下打开git bash&#xff0c;输入以下命令行创建仓库&#xff08;一般来说&#xff0c;我觉得直接在code workspace得地方创建git仓库就可以了&#xff0c;这…

acme.sh生成https证书

前言 SSL 价格并不便宜, 本节介绍如何使用 acme.sh 生成免费的 SSL 证书 证书生成原理 CA && Let’s Encrypt 证书颁发机构&#xff08;CA&#xff0c;Certificate Authority&#xff09;是一个负责颁发数字证书的实体。数字证书用于在互联网上验证实体的身份&…

注册或购买的谷歌账号的辅助邮箱是否需要设置?有什么用?设置的要点是什么?

今天早上&#xff0c;有个朋友联系到GG账号服务&#xff0c;问我谷歌账号辅助邮箱怎么用。说实在的这个问题有点抽象&#xff0c;哈哈。 然后我详细了解了一下&#xff0c;原来是这样的&#xff1a; 他的谷歌账号提示异常&#xff08;这个时候账号肯定是被停用了的&#xff09…

【Linux应用编程】Day12线程

线程 与进程类似&#xff0c;线程是允许应用程序并发执行多个任务的一种机制&#xff0c;线程参与系统调度&#xff1b; 事实上&#xff0c;系统调度的最小单元是线程、而并非进程。 ⚫ 线程的基本概念&#xff0c;线程 VS 进程&#xff1b; ⚫ 线程标识&#xff1b; ⚫ 线…

电脑上有什么好用的记笔记软件吗?试试这3款笔记软件,功能丰富又实用

笔记软件千千万&#xff0c;日常使用方便最关键&#xff01;&#xff01; 推荐3个各有亮点的笔记软件&#xff0c;不止是记笔记这么简单&#xff1a; 1、FlowUs 推荐指数&#xff1a;☆☆☆☆☆ 关键词&#xff1a;文档笔记软件 下载链接>>flowus.cn FlowUs是一款在…

ADI - 通过5 V至24 V输入提供双极性、双向DC-DC流入和流出电流

大部分电子系统都依赖于正电压轨或负电压轨&#xff0c;但是有些应用要求单电压轨同时为正负电压轨。在这种情况下&#xff0c;正电源或负电源由同一端子提供&#xff0c;也就是说&#xff0c;电源的输出电压可以在整个电压范围内调节&#xff0c;并且可以平稳转换极性。例如&a…

【mars3d】实现线面内插值计算效果

面插值计算效果展示&#xff1a; &#xff08;离屏渲染方式&#xff09;面插值效果展示&#xff1a; 面内插值计算插点效果展示&#xff1a; 线插值效果展示&#xff1a; &#xff08;离屏渲染方式&#xff09;高密度线内插值计算效果展示&#xff1a; 相关代码&#xff1a; i…

docker二进制包部署(带arm版自动部署包)

文章目录 1.概述2.Docker二进制包下载3.安装脚本制作4.安装5.卸载6.注意事项7.分享一个arm版自动部署安装包8.懒人 X86 版安装包 1.概述 最近需要在Linux上部署docker&#xff0c;于是自己做了一个自动部署包。脚本的写法不区分X86或arm&#xff0c;通用的。 2.Docker二进制包…

网络安全和数据安全到底有什么区别?(非常详细)零基础入门到精通,收藏这一篇就够了

随着信息技术的迅猛发展&#xff0c;网络安全和数据安全已经成为当今社会不可忽视的重要议题。两者在保障信息系统安全、防范数据泄露和保障用户权益方面起着至关重要的作用。然而&#xff0c;尽管网络安全与数据安全在某些方面有着密切的联系&#xff0c;但它们在定义、目标和…

“八股文”:程序员的福音还是梦魇?

——一场关于面试题的“代码战争” 在程序员的世界里&#xff0c;“八股文”这个词儿可谓是“如雷贯耳”。不&#xff0c;咱们可不是说古代科举考试中的那种八股文&#xff0c;而是指程序员面试中的那些固定套路的题目。如今&#xff0c;各大中小企业在招聘程序员时&#xff0…

11.2.0.4 ADG故障 LGWR (ospid: 30945):terminating the instance due to error 4021

11.2.0.4 ADG无法连接&#xff0c;查看数据库为关闭状态&#xff0c;重新启动实例&#xff0c;应用日志后即可正常同步数据并打开到只读模式。 查看alert日志发现有以下报错&#xff1a; 0RA-04021:timeout occurred while waiting to lock obiectLGWR (ospid: 30945):termi…

矩阵、向量、张量 一文彻底理清!

矩阵&#xff1a;可理解为二维数组、二维张量 向量Vector&#xff1a;是只有一列的矩阵 张量&#xff1a;是矩阵向任意维度的推广。 机器学习经常会用到张量做变换&#xff0c;所以下文重点介绍张量。 可以通过.ndim查看numpy数据的张量维度。张量的维度&#xff08;dimens…

【熊猫派对】

游戏简介 熊猫派对是一款滑稽打闹游戏&#xff0c;玩法容易上手简单&#xff0c;游戏中玩家将操控自己的熊猫人&#xff0c;与其他对手对战&#xff0c;重拳、飞脚甚至还有各种各样的武器都可用来击败你的对手。 游戏特色 1、滑稽角色 网络超火的滑稽角色&#xff0c;从表情包…

JNI原理是什么?JNI在DDS binding JAVA中/DDS移植android平台中有什么作用?

1 JNI是什么2 如何在JAVA中调用C/C方法&#xff08;通过JNI调用的demo&#xff09;java中声明一个本地native方法生成JNI头文件Java native方法转换成C的规则与语法说明C实现的native方法本地实现以及.o .dll库的生成查看hello.dll库中的函数运行一下HelloJNI JNI在DDS移植andr…

微信小程序 - 自定义计数器

微信小程序通过自定义组件&#xff0c;实现计数器值的增加、减少、清零、最大最小值限定、禁用等操作。通过按钮事件触发方式&#xff0c;更新计数器的值&#xff0c;并修改相关联的其它变量。通过提升用户体验&#xff0c;对计数器进行优化设计&#xff0c;使用户操作更加便捷…

PHP教育培训小程序系统源码

&#x1f680;【学习新纪元】解锁教育培训小程序的无限可能✨ &#x1f4da; 引言&#xff1a;教育培训新风尚&#xff0c;小程序来引领&#xff01; Hey小伙伴们&#xff0c;是不是还在为找不到合适的学习资源而烦恼&#xff1f;或是厌倦了传统教育模式的单调&#xff1f;今…

Monaco 使用 SignatureHelpProvider

Monaco 中 SignatureHelpProvider 是方法提示说明&#xff0c;当敲入方法名时&#xff0c;系统会提示方法名称和对应的参数信息。效果如下&#xff1a; 通过 registerSignatureHelpProvider 实现 SignatureHelpProvider 处理函数。 实现 signatureHelpTriggerCharacters 和 pro…