2024年11月

最新博客文章链接


文字更新时间:2024/11/07

由于学校校园网,如果长时间不重新登陆的话,网速会下降,所以想弄个能定时发送 HTTP 请求的东西。由于不想给路由器刷系统,也麻烦。就开始考虑使用局域网内的服务器,不过由于服务器没有 Wi-Fi 模块,也不想搞 USB 无线 wifi 网卡,就想着干脆用单片机嵌入式。本着怎么便宜怎么来,开始想的是用 ESP8266,不过看其核心板的价格和 ESP32-C3 SuperMini 差不多,干脆就用了比较新的 ESP32(拼多多14块钱包邮)

大体思路

本着学东西,先看官网教程做个大致了解。于是就跟着官网的教程一步一步做了下来。
官方 vscode 插件教程

基本使用方法

  1. 使用 ESP 的 vscode 插件,构建模板工程
  2. 配置页面填写 wifi 名字和密码(需要保证 wifi 信号良好)
  3. 编写 HTTP 与 HTTPS 报文
  4. 设置 SNTP 时间同步
  5. 构建(Build)
  6. 插上板子找串口
  7. 烧录(Flash)
  8. 通过串口查看结果

配置的大部分时间都是鼠标点点点

虽说这里用的是 ESP32-C3,但是感觉所有的 ESP32 系列应该都适用此文章。

用到的东西

  • ESP32-C3 SuperMini


    实物图

    https://www.nologo.tech/assets/img/esp32/esp32c3supermini/esp32c3.png

    产品参数

    • 强大的 CPU:ESP32-C3,32 位 RISC-V 单核处理器,运行频率高达 160 MHz
    • WiFi:802.11b/g/n协议、2.4GhHz、支持Station模式、SoftAP模式、SoftAP+Station模式、混杂模式
    • 蓝牙:Bluetooth 5.0
    • 超低功耗:深度睡眠功耗约43μA
    • 丰富的板载资源:400KB SRAM、384KB ROM 内置 4Mflash
    • 芯片型号 :ESP32C3FN4
    • 超小尺寸:小至拇指 (22.52x18mm) 经典外形,适用于可穿戴设备和小型项目
    • 可靠的安全功能:支持 AES-128/256、哈希、RSA、HMAC、数字签名和安全启动的加密硬件加速器
    • 丰富的接口:1xI2C、1xSPI、2xUART、11xGPIO(PWM)、4xADC
    • 单面元件、表面贴装设计
    • 板载LED蓝灯:GPIO8引脚

    引脚图

    https://www.nologo.tech/assets/img/esp32/esp32c3supermini/esp32c3foot1.png

    原理图

    https://www.nologo.tech/assets/img/esp32/esp32c3supermini/esp32c3schematicdiagram.png

  • HTTP


    超文本传输协议 (HTTP) 是万维网的基础,通过超文本链接加载网页。HTTP 是一种应用程序层协议,旨在在联网设备之间传输信息,并在网络协议栈的其他层之上运行。HTTP 上的一个典型工作流程是客户端计算机向服务器发出请求,然后服务器发送响应消息。


安装官方 ESP-IDF 的 vscode 插件

官方 vscode 插件安装教程

在 vscode 插件市场中搜索
ESP-IDF
并安装,点开插件,选择第一个(express)快速配置项目。

https://cdn.tsanfer.com/image/2024-10-25_22-39-55.png

选择要安装的版本,其他不动(如果 github 连不上的话,可以在第一个选项中选择其他的下载服务器)

https://cdn.tsanfer.com/image/2024-10-25_22-41-53.png

等待安装完成


使用官方 https_request 例子工程

生成官方 https_request 例子工程

可以先看
官方的基础使用教程
(LED 闪烁程序,笔者本人没弄成功),再看后面的部分。或者直接使用官方的
https_request 例子工程

这里,先创建一个文件夹来放所有的 ESP 项目,插件会自动在这个文件夹里再创建一个子文件夹,https_request 模板工程就放在这个插件自动生成的文件夹里

点开插件,在
COMMAND
窗口中点击
Show Examples
,这时会弹出一个框架选择界面,默认即可

在新出现的例子选择页面中,搜索 https,找到
https_request
,然后点击
Create project using example http_request
创建例子工程,工程位置就选择自己创建的放所有的 ESP 项目的文件夹。

选择主控与调试类型

点开插件,在
COMMAND
窗口中点击
Set Espressif Target (IDF_TARGET)
,选择
esp32c3
,Debug 调试方式选择
ESP32-C3 chip (via ESP-PROG)
。debug 的调试方式,笔者是随便选的,本人在调程序的时候没用 debug。

配置项目

点开插件,在
COMMAND
窗口中点击
SDK Configuration Editor (menuconfig)

  • 填写 wifi 信息:

    在配置中搜索 wifi,在
    Example Connection Configuration
    下的
    WiFi SSID

    WiFi Password
    中填写 wifi名 和 wifi密码与断连重试次数(笔者这里设置的是重试 999999 次)

https://cdn.tsanfer.com/image/2024-11-7_13-24-46.png

  • (可选)日志启用
    debug
    输出

    用于输出日志中的 debug 信息,在配置中搜索
    debug
    ,在
    Component config
    下的
    Log output
    中的
    Default log verbosity
    选择
    Debug
    来启用日志 debug 输出。然后点击保存


编辑入口主文件

这里可以先不编辑报文内容,直接构建烧录测试一下,程序会默认测试与
www.howsmyssl.com
之间的 HTTPS 连接。

./main/https_request_example_main.c
文件完整代码见后文,此处仅截取片段讲解,做参考

由于插件已经自动构建好 FreeRTOS、LwIP 以及时间同步的环境,之后就只用编辑入口文件
./main/https_request_example_main.c
中的内容了。

先确定程序调用关系,在此文件中主要是:

app_main()

https_request_task()

https_get_request_using_crt_bundle()

https_get_request()

HTTPS 请求就是在
https_get_request()
函数中执行的。

如果有设置 cookie 的需求的话,ESP32 本身没有直接设置 cookie 的 API,但可以通过
esp_http_client_set_header
向 HTTP 头里添加 cookie 等头数据的方式来设置 cookie。

配置服务器路径与 HTTPS 报文

建议先在电脑上用 postman,或者在线的 http 调试工具弄好后,再用工具转换成 HTTP 报文。笔者是调好后,输出为 cURL 格式,然后再转化成 HTTP 报文格式。

// ./main/https_request_example_main.c 
// 文件中的部分配置内容

/*
*/

// 服务器域名
#define HTTPS_WEB_SERVER "www.baidu.com"
// HTTPS 端口为 443
#define HTTPS_WEB_PORT "443"
// 网页路径
#define HTTPS_WEB_URL "https://www.baidu.com"

// URL 最大长度,一般默认
#define SERVER_URL_MAX_SZ 256

// 输出提示信息的开头
static const char *HTTPS_TAG = "HTTPS";

static const char HTTPS_REQUEST[] = "GET " HTTPS_WEB_URL " HTTP/1.1\r\n"
                                    "Host: "HTTPS_WEB_SERVER":"HTTPS_WEB_PORT"\r\n"
                                    "User-Agent: esp-idf/1.0 esp32\r\n"
                                    "Accept: */*\r\n"
                                    "\r\n";
/*
*/

设置定时时间

使用
tm
结构体格式,设置定时时间,可实现类似 crontab 的定时任务

// ./main/https_request_example_main.c 

/*
*/

static void https_request_task(void *pvparameters)
{
    ESP_LOGI(HTTPS_TAG, "Start https_request example");

#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE && CONFIG_EXAMPLE_USING_ESP_TLS_MBEDTLS

    time_t now;
    struct tm *timeinfo;

    while (1) {
        // 获取当前时间的时间戳
        now = time(NULL);
        // 将时间戳转换为当地时间结构
        timeinfo = localtime(&now);

        // 此处设置为每天 2:30:00 定时执行
        // 更多时间格式参考 tm 结构体定义
        // 打印当前时间
        ESP_LOGI(HTTPS_TAG, "Current time: %02d:%02d:%02d\n", timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
        if (timeinfo->tm_hour == 2 && timeinfo->tm_min == 30 && timeinfo->tm_sec == 0)
        {
            https_get_request_using_crt_bundle();
            // 打印当前时间
            ESP_LOGI(HTTPS_TAG, "Current time: %02d:%02d:%02d\n", timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
            // 打印堆使用情况
            ESP_LOGI(HTTPS_TAG, "Minimum free heap size: %" PRIu32 " bytes", esp_get_minimum_free_heap_size());
        }

        // 每秒检查一次时间
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
#endif
    // 后面的代码执行不到
    //
    ESP_LOGI(HTTPS_TAG, "Minimum free heap size: %" PRIu32 " bytes", esp_get_minimum_free_heap_size());
    ESP_LOGI(HTTPS_TAG, "Finish https_request example");
    vTaskDelete(NULL);
}

/*
*/

时间同步 SNTP

时间同步代码,修改自官方模板,系统启动连上互联网后,先同步系统时间

// ./main/https_request_example_main.c 

/*
*/

#include "esp_netif_sntp.h"

/*
*/

static void obtain_time(void)
{
    ESP_LOGI(NTP_TAG, "Initializing and starting SNTP");

    esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("ntp.aliyun.com");

    esp_netif_sntp_init(&config);

    // wait for time to be set
    time_t now = 0;
    struct tm timeinfo = { 0 };
    int retry = 0;
    const int retry_count = 15;
    // while (esp_netif_sntp_sync_wait(2000 / portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT && ++retry < retry_count) {
    //     ESP_LOGI(NTP_TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
    // }
    while (esp_netif_sntp_sync_wait(2000 / portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT) {
        ESP_LOGI(NTP_TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
        // NTP 同步多次失败,重启系统
        if(++retry > retry_count){
            esp_restart();
        }
    }
    time(&now);
    localtime_r(&now, &timeinfo);

    // 将时区设置为中国标准时间
    setenv("TZ", "UTC-8", 1);
    tzset();

    esp_netif_sntp_deinit();
}

/*
*/

HTTP 请求

HTTP 请求官方也有
http_request
的例子工程,剪切其中的 http 请求代码到入口文件
./main/https_request_example_main.c
中,并添加相应宏定义。然后在入口函数
app_main()
末尾处,添加 HTTP 任务即可

https://cdn.tsanfer.com/image/2024-10-26_01-03-58.png


入口文件完整代码示例

可同时分别执行 HTTP 与 HTTPS 请求,文件位置:
./main/https_request_example_main.c

// ./main/https_request_example_main.c 

/*
 * HTTPS GET Example using plain Mbed TLS sockets
 *
 * Contacts the howsmyssl.com API via TLS v1.2 and reads a JSON
 * response.
 *
 * Adapted from the ssl_client1 example in Mbed TLS.
 *
 * SPDX-FileCopyrightText: The Mbed TLS Contributors
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * SPDX-FileContributor: 2015-2023 Espressif Systems (Shanghai) CO LTD
 */

#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include <time.h>
#include <sys/time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "protocol_examples_common.h"
#include "esp_netif_sntp.h"
#include "esp_sntp.h"
#include "esp_netif.h"

#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/netdb.h"
#include "lwip/dns.h"

#include "esp_tls.h"
#include "sdkconfig.h"
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE && CONFIG_EXAMPLE_USING_ESP_TLS_MBEDTLS
#include "esp_crt_bundle.h"
#endif
#include "time_sync.h"

/* Constants that aren't configurable in menuconfig */
#define HTTP_WEB_SERVER "www.catb.org"
#define HTTP_WEB_PORT "80"
#define HTTP_WEB_URL "http://www.catb.org/esr/writings/taoup/html/"

#define HTTPS_WEB_SERVER "www.baidu.com"
#define HTTPS_WEB_PORT "443"
#define HTTPS_WEB_URL "https://www.baidu.com"

#define SERVER_URL_MAX_SZ 256

static const char *HTTP_TAG = "HTTP";
static const char *HTTPS_TAG = "HTTPS";
static const char *NTP_TAG = "NTP";

/* Timer interval once every day (24 Hours) */
#define TIME_PERIOD (86400000000ULL)

static const char HTTP_REQUEST[] = "GET " HTTP_WEB_URL " HTTP/1.0\r\n"
                                    "Host: "HTTP_WEB_SERVER":"HTTP_WEB_PORT"\r\n"
                                    "User-Agent: esp-idf/1.0 esp32\r\n"
                                    "Accept: */*\r\n"
                                    "\r\n";

static const char HTTPS_REQUEST[] = "GET " HTTPS_WEB_URL " HTTP/1.1\r\n"
                                    "Host: "HTTPS_WEB_SERVER":"HTTPS_WEB_PORT"\r\n"
                                    "User-Agent: esp-idf/1.0 esp32\r\n"
                                    "Accept: */*\r\n"
                                    "\r\n";

/* Root cert for howsmyssl.com, taken from server_root_cert.pem

   The PEM file was extracted from the output of this command:
   openssl s_client -showcerts -connect www.howsmyssl.com:443 </dev/null

   The CA root cert is the last cert given in the chain of certs.

   To embed it in the app binary, the PEM file is named
   in the component.mk COMPONENT_EMBED_TXTFILES variable.
*/
extern const uint8_t server_root_cert_pem_start[] asm("_binary_server_root_cert_pem_start");
extern const uint8_t server_root_cert_pem_end[]   asm("_binary_server_root_cert_pem_end");

extern const uint8_t local_server_cert_pem_start[] asm("_binary_local_server_cert_pem_start");
extern const uint8_t local_server_cert_pem_end[]   asm("_binary_local_server_cert_pem_end");

static void https_get_request(esp_tls_cfg_t cfg, const char *WEB_SERVER_URL, const char *REQUEST);
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE && CONFIG_EXAMPLE_USING_ESP_TLS_MBEDTLS
static void https_get_request_using_crt_bundle(void);
#endif
static void obtain_time(void);

static void https_request_task(void *pvparameters)
{
    ESP_LOGI(HTTPS_TAG, "Start https_request example");

#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE && CONFIG_EXAMPLE_USING_ESP_TLS_MBEDTLS

    time_t now;
    struct tm *timeinfo;

    while (1) {
        // 获取当前时间的时间戳
        now = time(NULL);
        // 将时间戳转换为当地时间结构
        timeinfo = localtime(&now);

        // 此处设置为每天 2:30:00 定时执行
        // 更多时间格式参考 tm 结构体定义
        // 打印当前时间
        ESP_LOGI(HTTPS_TAG, "Current time: %02d:%02d:%02d\n", timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
        if (timeinfo->tm_hour == 2 && timeinfo->tm_min == 30 && timeinfo->tm_sec == 0)
        {
            https_get_request_using_crt_bundle();
            // 打印当前时间
            ESP_LOGI(HTTPS_TAG, "Current time: %02d:%02d:%02d\n", timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
            // 打印堆使用情况
            ESP_LOGI(HTTPS_TAG, "Minimum free heap size: %" PRIu32 " bytes", esp_get_minimum_free_heap_size());
        }

        // 每秒检查一次时间
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
#endif
    // 后面的代码执行不到
    //
    ESP_LOGI(HTTPS_TAG, "Minimum free heap size: %" PRIu32 " bytes", esp_get_minimum_free_heap_size());
    ESP_LOGI(HTTPS_TAG, "Finish https_request example");
    vTaskDelete(NULL);
}

static void http_request_task(void *pvParameters)
{
    const struct addrinfo hints = {
        .ai_family = AF_INET,
        .ai_socktype = SOCK_STREAM,
    };
    struct addrinfo *res;
    struct in_addr *addr;
    int s, r;
    char recv_buf[64];

    time_t now;
    struct tm *timeinfo;

    while(1) {
        // 获取当前时间的时间戳
        now = time(NULL);
        // 将时间戳转换为当地时间结构
        timeinfo = localtime(&now);

        // 此处设置为每天 2:30:00 定时执行
        // 更多时间格式参考 tm 结构体定义
        // 打印当前时间
        ESP_LOGI(HTTP_TAG, "Current time: %02d:%02d:%02d\n", timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
        if (timeinfo->tm_hour == 2 && timeinfo->tm_min == 30 && timeinfo->tm_sec == 0)
        {
            // 打印当前时间
            ESP_LOGI(HTTP_TAG, "Current time: %02d:%02d:%02d\n", timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);

            int err = getaddrinfo(HTTP_WEB_SERVER, HTTP_WEB_PORT, &hints, &res);

            if(err != 0 || res == NULL) {
                ESP_LOGE(HTTP_TAG, "DNS lookup failed err=%d res=%p", err, res);
                vTaskDelay(1000 / portTICK_PERIOD_MS);
                continue;
            }

            /* Code to print the resolved IP.

            Note: inet_ntoa is non-reentrant, look at ipaddr_ntoa_r for "real" code */
            addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
            ESP_LOGI(HTTP_TAG, "DNS lookup succeeded. IP=%s", inet_ntoa(*addr));

            s = socket(res->ai_family, res->ai_socktype, 0);
            if(s < 0) {
                ESP_LOGE(HTTP_TAG, "... Failed to allocate socket.");
                freeaddrinfo(res);
                vTaskDelay(1000 / portTICK_PERIOD_MS);
                continue;
            }
            ESP_LOGI(HTTP_TAG, "... allocated socket");

            if(connect(s, res->ai_addr, res->ai_addrlen) != 0) {
                ESP_LOGE(HTTP_TAG, "... socket connect failed errno=%d", errno);
                close(s);
                freeaddrinfo(res);
                vTaskDelay(4000 / portTICK_PERIOD_MS);
                continue;
            }

            ESP_LOGI(HTTP_TAG, "... connected");
            freeaddrinfo(res);

            if (write(s, HTTP_REQUEST, strlen(HTTP_REQUEST)) < 0) {
                ESP_LOGE(HTTP_TAG, "... socket send failed");
                close(s);
                vTaskDelay(4000 / portTICK_PERIOD_MS);
                continue;
            }
            ESP_LOGI(HTTP_TAG, "... socket send success");

            struct timeval receiving_timeout;
            receiving_timeout.tv_sec = 5;
            receiving_timeout.tv_usec = 0;
            if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &receiving_timeout,
                    sizeof(receiving_timeout)) < 0) {
                ESP_LOGE(HTTP_TAG, "... failed to set socket receiving timeout");
                close(s);
                vTaskDelay(4000 / portTICK_PERIOD_MS);
                continue;
            }
            ESP_LOGI(HTTP_TAG, "... set socket receiving timeout success");

            /* Read HTTP response */
            do {
                bzero(recv_buf, sizeof(recv_buf));
                r = read(s, recv_buf, sizeof(recv_buf)-1);
                for(int i = 0; i < r; i++) {
                    putchar(recv_buf[i]);
                }
            } while(r > 0);

            // ESP_LOGI(HTTP_TAG, "\r\n\r\n校园网登陆已刷新\r\n");

            ESP_LOGI(HTTP_TAG, "Minimum free heap size: %" PRIu32 " bytes", esp_get_minimum_free_heap_size());
            
            ESP_LOGI(HTTP_TAG, "... done reading from socket. Last read return=%d errno=%d.", r, errno);
            close(s);

            ESP_LOGI(HTTP_TAG, "Starting again!");

            // 打印堆使用情况
            ESP_LOGI(HTTP_TAG, "Minimum free heap size: %" PRIu32 " bytes", esp_get_minimum_free_heap_size());
        }

        // 每秒检查一次时间
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

void app_main(void)
{
    ESP_ERROR_CHECK( nvs_flash_init() );
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK( esp_event_loop_create_default() );

    ESP_ERROR_CHECK(example_connect());

    obtain_time();

    /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
     * Read "Establishing Wi-Fi or Ethernet Connection" section in
     * examples/protocols/README.md for more information about this function.
     */

    // if (esp_reset_reason() == ESP_RST_POWERON) {
    //     ESP_LOGI(HTTPS_TAG, "Updating time from NVS");
    //     ESP_ERROR_CHECK(update_time_from_nvs());
    // }

    const esp_timer_create_args_t nvs_update_timer_args = {
        .callback = (void *)&fetch_and_store_time_in_nvs,
    };

    esp_timer_handle_t nvs_update_timer;
    ESP_ERROR_CHECK(esp_timer_create(&nvs_update_timer_args, &nvs_update_timer));
    ESP_ERROR_CHECK(esp_timer_start_periodic(nvs_update_timer, TIME_PERIOD));

    xTaskCreate(&https_request_task, "https_get_task", 8192, NULL, 5, NULL);
    xTaskCreate(&http_request_task, "http_get_task", 8192, NULL, 5, NULL);
}

static void obtain_time(void)
{
    ESP_LOGI(NTP_TAG, "Initializing and starting SNTP");

    esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("ntp.aliyun.com");

    esp_netif_sntp_init(&config);

    // wait for time to be set
    time_t now = 0;
    struct tm timeinfo = { 0 };
    int retry = 0;
    const int retry_count = 15;
    // while (esp_netif_sntp_sync_wait(2000 / portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT && ++retry < retry_count) {
    //     ESP_LOGI(NTP_TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
    // }
    while (esp_netif_sntp_sync_wait(2000 / portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT) {
        ESP_LOGI(NTP_TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
        // NTP 同步多次失败,重启系统
        if(++retry > retry_count){
            esp_restart();
        }
    }
    time(&now);
    localtime_r(&now, &timeinfo);

    // 将时区设置为中国标准时间
    setenv("TZ", "UTC-8", 1);
    tzset();

    esp_netif_sntp_deinit();
}

#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE && CONFIG_EXAMPLE_USING_ESP_TLS_MBEDTLS
static void https_get_request_using_crt_bundle(void)
{
    ESP_LOGI(HTTPS_TAG, "https_request using crt bundle");
    esp_tls_cfg_t cfg = {
        .crt_bundle_attach = esp_crt_bundle_attach,
    };
    https_get_request(cfg, HTTPS_WEB_URL, HTTPS_REQUEST);
}
#endif // CONFIG_MBEDTLS_CERTIFICATE_BUNDLE && CONFIG_EXAMPLE_USING_ESP_TLS_MBEDTLS

static void https_get_request(esp_tls_cfg_t cfg, const char *WEB_SERVER_URL, const char *REQUEST)
{
    char buf[512];
    int ret, len;

    esp_tls_t *tls = esp_tls_init();
    if (!tls) {
        ESP_LOGE(HTTPS_TAG, "Failed to allocate esp_tls handle!");
        goto exit;
    }

    if (esp_tls_conn_http_new_sync(WEB_SERVER_URL, &cfg, tls) == 1) {
        ESP_LOGI(HTTPS_TAG, "Connection established...");
    } else {
        ESP_LOGE(HTTPS_TAG, "Connection failed...");
        int esp_tls_code = 0, esp_tls_flags = 0;
        esp_tls_error_handle_t tls_e = NULL;
        esp_tls_get_error_handle(tls, &tls_e);
        /* Try to get TLS stack level error and certificate failure flags, if any */
        ret = esp_tls_get_and_clear_last_error(tls_e, &esp_tls_code, &esp_tls_flags);
        if (ret == ESP_OK) {
            ESP_LOGE(HTTPS_TAG, "TLS error = -0x%x, TLS flags = -0x%x", esp_tls_code, esp_tls_flags);
        }
       goto cleanup;
    }

    size_t written_bytes = 0;
    do {
        ret = esp_tls_conn_write(tls,
                                 REQUEST + written_bytes,
                                 strlen(REQUEST) - written_bytes);
        if (ret >= 0) {
            ESP_LOGI(HTTPS_TAG, "%d bytes written", ret);
            written_bytes += ret;
        } else if (ret != ESP_TLS_ERR_SSL_WANT_READ  && ret != ESP_TLS_ERR_SSL_WANT_WRITE) {
            ESP_LOGE(HTTPS_TAG, "esp_tls_conn_write  returned: [0x%02X](%s)", ret, esp_err_to_name(ret));
            goto cleanup;
        }
    } while (written_bytes < strlen(REQUEST));

    ESP_LOGI(HTTPS_TAG, "Reading HTTP response...");
    do {
        len = sizeof(buf) - 1;
        memset(buf, 0x00, sizeof(buf));
        ret = esp_tls_conn_read(tls, (char *)buf, len);

        if (ret == ESP_TLS_ERR_SSL_WANT_WRITE  || ret == ESP_TLS_ERR_SSL_WANT_READ) {
            continue;
        } else if (ret < 0) {
            ESP_LOGE(HTTPS_TAG, "esp_tls_conn_read  returned [-0x%02X](%s)", -ret, esp_err_to_name(ret));
            break;
        } else if (ret == 0) {
            ESP_LOGI(HTTPS_TAG, "connection closed");
            break;
        }

        len = ret;
        ESP_LOGD(HTTPS_TAG, "%d bytes read", len);
        /* Print response directly to stdout as it is read */
        for (int i = 0; i < len; i++) {
            putchar(buf[i]);
        }
        putchar('\n'); // JSON output doesn't have a newline at end
    } while (1);

cleanup:
    esp_tls_conn_destroy(tls);
exit:
}


构建、烧录、查看效果

代码写完了,剩下的就是点点点了。

构建

点开插件,在
COMMAND
窗口中点击
Build
构建,第一次可能会久一点,后面就是增量更新了,时间会短一点。

查看芯片空间使用

可选步骤,点开插件,在
COMMAND
窗口中点击
IDF Size
查看空间使用率,笔者这里看 RAM 用了100KB左右(这个界面的右上角,点击 Flash 可以烧录程序)。

https://cdn.tsanfer.com/image/2024-10-26_00-22-28.png

开发板跑起来之后,通过
esp_get_minimum_free_heap_size()
函数查看到的剩余堆空间,还有 160KB 左右。相当于只用了大约 50% 的内存。Flash 用了大约 1MB,只使用了 25% 左右。

https://cdn.tsanfer.com/image/2024-10-26_21-03-43.png

主要是没啥需求,只是个 HTTPS 客户端,发发报文。如果是 HTTPS 服务器的话,估计占用要高些。这 14 块钱的 esp32,性能对笔者来说都过剩了(一天发一个包,没有压力测试需求)。

选择串口

点开插件,在
COMMAND
窗口中点击
Select Serial Port
选择串口。如果这里不确定是哪个口,可以一个一个试,看哪个口能烧录进去

烧录

点开插件,在
COMMAND
窗口中点击
Flash
,选择烧录类型为串口
UART

查看效果

点开插件,在
COMMAND
窗口中点击
Monitor

这里会通过串口显示系统输出的信息,ESP32 会在这里输出服务器返回的报文。没问题的话,应该和在电脑上调试的结果一致。

https://cdn.tsanfer.com/image/2024-10-26_19-42-45.png


结尾

现在的单片机嵌入式开发都这么方便了吗,环境配置安个插件就行,官方给的例子也多。不同主控,点击就能配置,构建和烧录也是点一下就行,没有出现问题(也有可能是因为笔者没用外设的原因)。终于不用忍受在 keil 和 stc-isp 上开发 stc 芯片的繁琐配置过程了。而且还能一键生成 FreeRTOS、LwIP、SNTP 环境

不过能在这之上再作进一步的抽象就好了,隐藏底层的细节,让开发者只关注业务。比如像笔者自己,就只想发个 HTTP 包,其他无所谓,要是连上板子,填入 wifi 信息后,能直接写 HTTP 报文就好了(要啥自行车)。

如果只是发个 HTTP 报文,不太要求硬件成本和设备使用环境的话,在通用性方面,感觉还是通用 PC 好一点。直接装个 linux 发行版,再装个服务器面板,用面板建个 systemd 定时任务,直接调用 shell 脚本执行 cURL 命令就完了,配置起来也更灵活。

本文由
Tsanfer's Blog
发布!


Reviewbot
是七牛云开源的一个项目,旨在提供一个自托管的代码审查服务, 方便做 code review/静态检查, 以及自定义工程规范的落地。


静态检查不是个新鲜事。

我记得早在几年前,我们就调研并使用过 sonarqube 做静态检查,但当时并没有大范围的推广。主要原因在于,一是发现的问题多数是风格问题,较少能发现缺陷; 二是 sonarqube 社区版的 worker 数有限制,满足不了我们大规模代码扫描的需求。当然,也是因为前一个问题,感觉付费并不是很划算。

而由于七牛主要使用 golang 语言,所以在静态检查方面,我们基本使用 go vet/fmt/lint 等,再就是后来的 golangci-lint,好像也够用了。

但随着代码仓库的增多,以及对工程规范的不断强化,我们越来越发现当前的落地方式,已经开始无法满足我们的需求。

Linter 工具的引入与更新问题

以 golangci-lint 为例,它是 go 语言 linters 聚合器,内置了 100+ linters,使用也很简单,
golangci-lint run
一条命令即可。但你知道吗?如果没有特殊配置,你这条命令其实仅仅执行其中的 6 个 linter,绝大部分 linters 都没有执行!

另外,工具本身需要更新,且很多时候我们也会自己定制 linter 工具,这种时候该怎么做呢?如果仅有少量仓库,可能还好,但如果仓库很多,那维护成本就上去了。

还有就是新业务,新仓库,如何保证相关的检查能够及时配置,相关的规范能够正确落地?

靠自觉一定是不行的。

Linter 问题的发现与修复情况

如何确保发现的问题能够被及时修复?如何让问题能更及时、更容易的被修复?

埋藏在大量 raw log 中的问题,一定是不容易被发现的,查找起来很麻烦,体验很差。

历史代码仓库的存量问题,谁来改?改动就需要时间,但实际上很多业务研发可能并没有动力来跟进。同样,变动总是有风险的,有些 lint 问题修复起来也并不简单,如果因修复 lint 问题而引入风险,那就得不偿失了。

如果想了解当前组织内 lint 问题的分布及修复情况,又该怎么办呢?

如何解决,方向在哪里?

不可否认,linter 问题也是问题,如果每行代码都能进行充分的 lint 检查,那一定比不检查要强。

另一方面,组织内制定好的工程规范,落地在日常的开发流程中,那一定是希望被遵守的,这类就是强需。

所以这个事情值得做,但做的方式是值得思考的,尤其是当我们有更高追求时。

参考
CodeCov
的服务方式,以及
golangci-lint
reviewdog
等工具的设计理念,我们认为:

  • 如果能对于新增仓库、历史仓库,不需要专人配置 job,就能自动生效,那一定是优雅的
  • 如果能只针对 PR/MR 中的变动做分析和反馈,类似我们做 Code Review 那样,那对于提 PR 的同学来讲一定是优雅的,可接受的,随手修复的可能性极大
    • 而进一步,针对 PR/MR 中涉及的文件中的历史代码进行反馈,在合理推动下,支持夹带修改,持续改进的可能性也会大大增强
  • Lint 工具多种多样,或者我们自己开发出新工具时,能够较为轻松的让所有仓库都自动生效,那也一定是非常赞的,不然就可能陷入工具越多负担越重的风险

基于上面的思考,我认为我们需要的是:
一个中心化的 Code Review/静态检查服务,它能自动接受整个组织内 PR/MR 事件,然后执行各种预定义的检查,并给与精确到变动文 �� 级的有效反馈。它要能作为代码门禁,持续的保障入库代码质量。


Reviewbot
就是这样一个项目。

Reviewbot 在设计和实现上有哪些特点?

面向改进的反馈方式

这将是 Reviewbot 反馈问题的核心方式,它会尽可能充分利用各 Git 平台的自身能力,精确到变动的代码行,提供最佳的反馈体验。

  • Github Check Run (Annotations)

  • Github Pull Request Review (Comments)

支持多种 Runner

Reviewbot 是自托管的服务,推荐大家在企业内自行部署,这样对私有代码更友好。

Reviewbot 自身更像个管理服务,不限制部署方式。而对于任务的执行,它支持多种 Runner,以满足不同的需求。比如:

  • 不同的仓库和 linter 工具,可能需要不同的基础环境,这时候你就可以将相关的环境做成 docker 镜像,直接通过 docker 来执行
  • 而当任务较多时,为了执行效率,也可以选择通过 kubernetes 集群来执行任务。

使用也很简单,在配置文件中的目标仓库指定即可。类似:

dockerAsRunner:
  image: "aslan-spock-register.qiniu.io/reviewbot/base:go1.22.3-gocilint.1.59.1"
kubernetesAsRunner:
  image: "aslan-spock-register.qiniu.io/reviewbot/base:go1.23.2-gocilint.1.61.0"
  namespace: "reviewbot"

零配置+定制化

本质上,Reviewbot 也是个 webhook 服务,所以我们只需要在 git provider 平台配置好 Reviewbot 的回调地址即可(github 也可以是 Github App)。

绝大部分的 linter 的默认最佳执行姿势都已经固定到代码中,如无特殊,不需要额外配置就能对所有仓库生效。

而如果仓库需要特殊对待,那就可以通过配置来调整。

类似:

org/repo:
  linters:
    golangci-lint:
      enable: true
      dockerAsRunner:
        image: "aslan-spock-register.qiniu.io/reviewbot/base:go1.22.3-gocilint.1.59.1"
      command:
        - "/bin/sh"
        - "-c"
        - "--"
      args:
        - |
          source env.sh
          export GO111MODULE=auto
          go mod tidy
          golangci-lint run --timeout=10m0s --allow-parallel-runners=true --print-issued-lines=false --out-format=line-number >> $ARTIFACT/lint.log 2>&1

可观察

Reviewbot 是在对工程规范强管理的背景下产生的,那作为工程规范的推动方,我们自然有需求想了解组织内当前规范的执行情况。比如, 都有哪些问题被检出?哪些仓库的问题最多?哪些仓库需要特殊配置?

目前 Reviewbot 支持通过企业微信来接收通知,比如:

  • 检出有效问题

  • 遇到错误

当然,未来可能也会支持更多方式。

其他更多的功能和姿势,请参考仓库:
https://github.com/qiniu/reviewbot

Reviewbot 的未来规划

作为开源项目,Reviewbot 还需要解决很多可用性和易用性问题,以提升用户体验,比如最典型的,接入更多的 git provider(gitlab/gitee 等),支持 CLI 模式运行。

但我个人认为,作为 code review 服务,提供更多的检测能力,才是重中之重。因为这不光是行业需求,也是我们自身需要。

所以后面我们除了会引入七牛内部推荐的规范,也会调研和探索更多的行业工具,同时会考虑引入 AI,探索 AI 在 code review 中的应用等等。

Anyway,Reviewbot 还很年轻,我们在持续的改进中,非常欢迎大家试用并提出宝贵意见。当然,更欢迎大家一起参与到项目建设中来。

感谢大家。

简介

image

泛型参考资料烂大街,基本资料不再赘述,比如泛型接口/委托/方法的使用,逆变与协变。

泛型好处有如下几点

  1. 代码重用
    算法重用,只需要预先定义好算法,排序,搜索,交换,比较等。任何类型都可以用同一套逻辑
  2. 类型安全
    编译器保证不会将int传给string
  3. 简单清晰
    减少了类型转换代码
  4. 性能更强
    减少装箱/拆箱,泛型算法更优异。

为什么说泛型性能更强?

主要在于装箱带来的托管堆分配问题以及性能损失

  1. 值类型装箱会额外占用内存
            var a = new List<int>()
            {
                1,2, 3, 4
            };
            var b = new ArrayList()
            {
                1,2,3,4
            };

变量a:72kb
image
变量b:184kb
image

  1. 装箱/拆箱会消耗额外的CPU
	public void ArrayTest()
	{
		Stopwatch stopwatch = Stopwatch.StartNew();
		stopwatch.Start();
		ArrayList arrayList = new ArrayList();
		for (int i = 0; i < 10000000; i++)
		{
			arrayList.Add(i);
			_ = (int)arrayList[i];
		}
		stopwatch.Stop();
		Console.WriteLine($"array time is {stopwatch.ElapsedMilliseconds}");
	}

	public void ListTest()
	{
		Stopwatch stopwatch = Stopwatch.StartNew();
		stopwatch.Start();
		List<int> list = new List<int>();
		for (int i = 0; i < 10000000; i++)
		{
			list.Add(i);
			_ = list[i];
		}
		stopwatch.Stop();
		Console.WriteLine($"list time is {stopwatch.ElapsedMilliseconds}");
	}

image

如此巨大的差异,无疑会造成GC的管理成本增加以及额外的CPU消耗。

思考一个问题,如果是引用类型的实参。差距还会如此之大吗?
如果差距不大,那我们使用泛型的理由又是什么呢?

开放/封闭类型

CLR中有多种
类型对象
,比如引用类型,值类型,接口类型和委托类型,以及泛型类型。

根据创建行为,他们又被分为
开放类型/封闭类型

为什么要说到这个? 泛型的一个有优点就是代码复用,只要定义好算法。剩下的只要往里填就好了。比如List<>开放给任意实参,大家都可以复用同一套算法。

举个例子

  1. 开放类型是指类型参数尚未被指定,他们不能被实例化 List<>,Dictionary<,>,interface 。它们只是搭建好了基础框架,开放不同的实参
            Type it = typeof(ITest);
            Activator.CreateInstance(it);//创建失败

            Type di = typeof(Dictionary<,>);
            Activator.CreateInstance(di);//创建失败
  1. 封闭类型是指类型已经被指定,是可以被实例化 List<string
    >,String 就是封闭类型。它们只接受特定含义的实参
            Type li = typeof(List<string>);
            Activator.CreateInstance(li);//创建成功

代码爆炸

所以当我们使用
开放类型时,都会面临一个问题
。在JIT编译阶段,CLR会获取泛型的IL,再寻找对应的实参替换,生成合适的本机代码。
但这么做有一个缺点,要为每一种不同的泛型类型/方法组合生成,各种各种的本机代码。这将明显增加程序的Assembly,从而损害性能
CLR为了缓解该现象,做了一个特殊的优化:
共享方法体

  1. 相同类型实参,共用一套方法
    如果一个Assembly中使用了List<Struct>另外一个Assembly也使用了List<Struct>
    那么CLR只会生成一套本机代码。

  2. 引用类型实参,共用一套方法
    List<String>与List<Stream> 实参都是引用类型,它们的值都是托管堆上的指针引用。因此CLR对指针都可以用同一套方式来操作
    值类型就不行了,比如int与long. 一个占用4字节,一个占用8字节。占用的内存不长不一样,导致无法用同一套逻辑来复用

眼见为实1

示例代码
    internal class Program
    {
        static void Main(string[] args)
        {
            var a = new Test<string>();
            var b = new Test<Stream>();
            
            Debugger.Break();
        }
    }

    public class Test<T>
    {
        public void Add(T value)
        {
		
        }
        public void Remove(T value)
        {

        }
    }

变量a:
image

变量b
image

仔细观察发现,它们的EEClass完全一致,它们的Add/Remove方法的MethodDesc也完全一直。这印证了上面的说法,引用类型实参引用同一套方法。

眼见为实2

点击查看代码
    internal class Program
    {
        static void Main(string[] args)
        {
            var a = new Test<int>();
            var b = new Test<long>();
            var c = new Test<MyStruct>();
            
            Debugger.Break();
        }
    }

    public class Test<T>
    {
        public void Add(T value)
        {

        }
        public void Remove(T value)
        {

        }
    }

    public struct MyStruct
    {
        public int Age;
    }

我们再把引用类型换为值类型,再看看它们的方法表。
变量a:
image
变量b:
image
变量c:
image

一眼就能看出,它们的MethodDesc完全不一样。这说明在Assembly中。CLR为泛型生成了3套方法。

细心的朋友可能会发现,引用类型的实参变成了一个叫System.__Canon的类型。CLR 内部使用 System.__Canon 来给所有的引用类型做“占位符”使用
有兴趣的小伙伴可以参考它的源码:coreclr\System.Private.CoreLib\src\System__Canon.cs

为什么值类型无法共用同一套方法?

其实很好理解,引用类型的指针长度是固定的(32位4byte,64位8byte),而值类型的长度不一样。导致值类型生成的底层汇编无法统一处理。因此值类型无法复用同一套方法。

眼见为实

点击查看代码
    internal class Program
    {
        static void Main(string[] args)
        {
            var a = new Test<int>();
            a.Add(1);
            var b = new Test<long>();
            b.Add(1);

            var c = new Test<string>();
            c.Add("");
            var d = new Test<Stream>();
            d.Add(null);
            
            Debugger.Break();
        }
    }

    public class Test<T>
    {
        public void Add(T value)
        {
            var s = value;
        }
        public void Remove(T value)
        {

        }
    }
//变量a
00007FFBAF7B7435  mov         eax,dword ptr [rbp+58h]  
00007FFBAF7B7438  mov         dword ptr [rbp+2Ch],eax    //int 类型步长4 2ch

//变量b
00007FFBAF7B7FD7  mov         rax,qword ptr [rbp+58h]  
00007FFBAF7B7FDB  mov         qword ptr [rbp+28h],rax  //long 类型步长8 28h 汇编不一致

//变量c
00007FFBAF7B8087  mov         rax,qword ptr [rbp+58h]  
00007FFBAF7B808B  mov         qword ptr [rbp+28h],rax  // 28h

//变量d
00007FFBAF7B8087  mov         rax,qword ptr [rbp+58h]  
00007FFBAF7B808B  mov         qword ptr [rbp+28h],rax  // 28h 引用类型地址步长一致,汇编也一致。

泛型的数学计算

在.NET 7之前,如果我们要利用泛型进行数学运算。是无法实现的。只能通过dynamic来曲线救国

image

.NET 7中,引入了新的数学相关泛型接口,并提供了接口的默认实现。
image

https://learn.microsoft.com/zh-cn/dotnet/standard/generics/math

数学计算接口的底层实现

C#层:
相加的操作主要靠IAdditionOperators接口。
image

IL层:
+操作符被JIT编译成了op_Addition抽象方法
image

对于int来说,会调用int的实现
System.Int32.System.Numerics.IAdditionOperators
image

对于long来说,会调用long的实现
System.Int64.System.Numerics.IAdditionOperators
image

从原理上来说很简单,BCL实现了基本值类型的所有+-*/操作,只要在泛型中做好约束,JIT会自动调用相应的实现。

结论

一路无话,无非打打杀杀。
泛型,用就完事了。就是要
稍微注意
(硬盘比程序员便宜多了)值类型泛型造成的代码爆炸。

依赖管理

依赖管理解决的问题

我们学完一个语言的语法之后,我们应该都能知道函数的引用或者类的创建等了。为了方便管理,我们就会把一些特定的功能实现写在一个代码文件中,我们只需要使用的时候导入就行了,这样无论是修改还是阅读都更加的方便简洁,但这样会出来一个问题,一旦我们多个文件你引用我,我引用你,就会出现蜈蚣一般的嵌套(比如A引用B,B引用C),这样我们迁移或者打包的整个项目的时候就会很复杂,整个引用宛如迷宫一般,人工来管理就会很麻烦。这个时候Maven就能解决这个问题

Maven的作用

当我们声明了A的依赖的时候,Maven会自己判断这个模块和其他被引用的模块是否还有其他的依赖,如果有,就会自动导入其他的依赖,不用我们去判断是否有其他的依赖了。那我们怎么声明呢,就是在
pom.xml
​这文件里面

pom.xml


pom.xml
​这个文件很有用,是项目的maven的配置文件,我们来仔细看看这个文件

<project ...>


	<modelVersion>4.0.0</modelVersion>
	<groupId>com.itranswarp.learnjava</groupId>
	<artifactId>hello</artifactId>
	<version>1.0</version>
	<packaging>jar</packaging>


	<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.release>17</maven.compiler.release>
	</properties>


	<dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.16</version>
        </dependency>
	</dependencies>


</project>

我们来梳理一下整文件的结构:

我们可以分为几个大块

<project ...>


  • pom.xml
    ​ 的根元素,包含了所有项目的配置信息。
  • 定义了命名空间和模式,确保文件遵循 Maven POM 规范。
<project ...>

	所有项目的配置信息。

</project>

整个项目自己的信息

<modelVersion>4.0.0</modelVersion>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>hello</artifactId>
<version>1.0</version>
<packaging>jar</packaging>

这些都是你管理项目的信息

一个Maven工程就是由
groupId
​,
artifactId
​和
version
​作为唯一标识。


<modelVersion>

  • 定义 POM 文件的模型版本,目前通常为
    4.0.0
    ​。


<groupId>

  • 表示项目的组 ID,通常是项目所属组织的反向域名(如
    com.example
    ​)。
  • 必须唯一,通常用来标识公司或组织。


<artifactId>

  • 项目的唯一标识符,表示模块或项目的名称(如
    my-app
    ​)。

  • artifactId
    ​ 应该在同一
    groupId
    ​ 下是唯一的。


<version>

  • 定义项目的版本号(如
    1.0.0
    ​)。
  • 如果是开发版本,可以使用
    -SNAPSHOT
    ​ 标识(如
    1.0.0-SNAPSHOT
    ​),表示不稳定版本。


<packaging>

  • 指定项目的打包方式,默认为
    jar
    ​。
  • 其他常见值有
    war
    ​(Web 应用)、
    pom
    ​(父项目)等。

<properties..>

这个标签里面是统一管理版本号、编码等配置。


  • project.build.sourceEncoding
    ​:表示项目源码的字符编码,通常应设定为
    UTF-8
    ​;

  • maven.compiler.release
    ​:表示使用的JDK版本,例如
    21
    ​;

  • maven.compiler.source
    ​:表示Java编译器读取的源码版本;

  • maven.compiler.target
    ​:表示Java编译器编译的Class版本。
	<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.release>17</maven.compiler.release>
	</properties>
值得注意的是

从Java 9开始,推荐使用
maven.compiler.release
​属性,保证编译时输入的源码和编译输出版本一致。如果源码和输出版本不同,则应该分别设置
maven.compiler.source
​和
maven.compiler.target
​。

通过
<properties>
​定义的属性,就可以固定JDK版本,防止同一个项目的不同的开发者各自使用不同版本的JDK。

<dependencies..>

这里面就是整个项目所需的依赖

  • 定义项目的依赖项,每个依赖项用
    <dependency>
    ​ 标签表示。

    	<dependencies>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-simple</artifactId>
                <version>2.0.16</version>
            </dependency>
    	</dependencies>
    
  • 关键字元素:



    • <groupId>
      ​:依赖的组 ID。

    • <artifactId>
      ​:依赖的 artifact ID。

    • <version>
      ​:依赖的版本号。

我们在引用其他第三方库的时候,只需要通过这3个变量确定。例如,依赖
org.slfj4:slf4j-simple:2.0.16
​:这个依赖是我们可以直接在线下载下来直接调用的,解析下来就是

groupId
​为
org.slfj4


artifactId
​ 为
slf4j-simple


version
​ 为
2.0.16

一般我们在表示Maven依赖时,使用简写形式
groupId:artifactId:version

这个时候回去看
pom.xml
​我们就发现整个结构已经非常清晰了,就是各种配置的结合体

xml

既然提到了,那我们就顺便介绍一遍xml这个格式吧,通过上面的例子我们能看出xml是非常像html的语法的,但是xml有着很高的自由度,因为xml的本意就是靠着这样的层级信息去表达主题各式各样的信息

这样的表示优点就是非常的结构化,结构化对于整个计算机的信息表示无疑是非常契合和高效的

xml的表示既非常地公式化,使用
<>
​来将这个盒子命名就行,里面随便输入信息就行,因为xml的功能就像文章一样,只是用来看的。也非常的灵活,每个
<>
​都可以进行嵌套

一、日常问题

1)临时小需求

在日常研发过程中,难免会临时加些小需求,例如增加个标识、字体换个颜色、间距增加等。

这类需求虽然不复杂,但是很多时候都会打乱自己的开发节奏。

最近我收到个修改需求,来来回回改了四次。因为只是和我口述了下需求,我按照口述修改。

后面测试就发现了些场景需要过滤,再马上修复。上线后,由于没有设计稿,我所设计的界面效果,与产品所想的不一致。

再做了两次修改,虽然花的时间不多,但是着实费劲。归根到底,还是因为需求不明确导致的。

下次遇到此类问题,需要和产品将需求描述清楚,有必要的话,还可以叫上测试,从场景到呈现,都要一一询问,以免遗漏。

2)服务调用错误

周二晚上有人上报某个排行榜数据不更新,排查后发现是 Node 调用服务端的服务没成功(服务调用错误 getaddrinfo ENOTFOUND xxx)。

从而让 Node 报错引起 Pod 重启,接口就访问不到数据了。

其实调用服务端接口都已经做错误捕获(try-catch),但是在 catch 分支中没有返回对象。

最直接的办法就是先给一个默认的返回值,不出现 undefined 的错误,也能让 pod 不再重启。

改完代码上线后也到了晚上 23 点,pod 是不再重启了,服务端接口大部分能成功调用,但也有比较少的失败。

第二天来公司,运维和我说,后端的 Pod,当 CPU 过高时就会自动重启,而这种情况在访问量比较大的时间段会比较频繁。

这个骚操作也是无奈之举,他们现在也没资源去做代码优化,只能通过重启的办法来缓解线上过慢的请求。

那么运维给我们部署了一套单独的服务,专门就由我们来调用,不会重启,调用的域名更新后,果然不再有请求失败的错误了。

其实还有一种叫做熔断的模式,就是如果发现上游服务调用慢,或者有大量超时的时候,直接中止对于该服务的调用,直接返回信息,快速释放资源。

这里就需要再做代码优化了,后续可以优化优化。

3)数据库CPU异常

从 10 月 8 号开始,每天凌晨 3 点数据库都会推送异常告警,CPU 的使用率超过了 60%。

一开始以为是偶发现象,因为之前也有这种突然增长的情况,但每天都会告警就有问题了。

找运维排查,说了一张表,将表名推给相关组排查,发现并不是他们的服务引起了。

这说明运维的推断有误,因为每天都是定时的,所以感觉是在跑一个定时任务。

运维再次锁定到一条 delete 语句,用于删除七天前的监控日志,执行时间长达 10 分钟,在这段时间,CPU 飙升。

DELETE FROM `web_monitor` WHERE `ctime` <= '2024-10-08 00:00'

很有可能与最近的日志量上涨有关,之前每日的数据在 70W 条左右,而现在达到了 100W 条左右。

运维说他那边也可以配置数据库的定时操作,然后在语句中会加 limit 限制,这样就不会占用太长时间。

不过,我最终还是没有让他配置,主要是因为如果定时操作出现异常,还得找运维修复,并且没有告警,异常了也不会知道。

这个服务对于我比较重要,所以还是决定自己优化,方式也简单,同样是加 limit 限制,只不过多几次循环。

最近,服务端的接口也老报 500 错误,有几天报的比较厉害,都影响了我监控的性能指标,也反馈了两次。

二、工作优化

1)协作依赖

最近在做组内 1V1 时,发现了协作依赖的问题。

就是在多组协作时,会存在依赖关系,但这是个单向依赖,并且被依赖对象并不知道有人在依赖他。那么当修改或遗漏逻辑时,也不会去通知依赖人,就有可能出现问题。

就是你的代码逻辑有个前置条件存在于其他组,当其他组更新代码时,并不知道会影响你,那你的这段代码就会无法执行,导致用户上报。

这个双月遇到了两次这个问题,一次是我们依赖别人,另一次是别人依赖我们。

有个审核的功能,服务端会将一条记录插入一张表中,我们会从这张表中去查是否有这条记录。

但这次服务端换了个人做更新业务,他没有将记录插入,从而导致我们组的逻辑异常。

这个问题我更倾向于觉得他们组对常规功能没有保留详尽的技术文档,出现了逻辑遗漏。

另一次是数据组在做数据统计时,会依赖操作记录的一个字段,我们会写入这个字段,这次产品修改了这个字段的格式,从而导致统计异常。

这个问题我更倾向于若有数据相关的需求,尽量提前告知数据组,避免无法统计结果。

其实最简单直接的解决方案是提前通知依赖人,但是难点就是不知道有这么一个人存在,所以在实际项目中就会出现遗漏。

而且我感觉这种协作问题应该还蛮多的。

2)告警不是一串数字

国庆假期前,偶尔收到了几个 500 的错误,没有当回事儿,以为就是偶发现象。

没想到国庆假期期间突然出现了大量的 500 警告,一查原来是网关转发的时候报 502、503、504 错误。

这就导致收到了非标准的 JSON 格式,调用 response.data.xxx 就会报 undefined 的错误。

知道原因后,马上修改,将网关转发改成内部的接口调用,并且给代码加了些 undefined 的判断。

3 号 23 点多的时候发布代码,4 号的指标就正常了。

期间还发现了大量的慢响应,是之前正常的 20 多倍,查看接口日志,最后锁定是依赖的服务端接口出现了异常。

联系了运维和服务端的人,后者没有回应,前者去查了下,说是其他接口影响了整个服务,而这些接口并不是我们调用的。

最后给我们单独配了 POD,只有我们访问的接口才会请求这个 POD,5 号的慢响应占比马上就恢复了。

对数据的不敏感,以及无视告警,让自己在国庆期间还要连夜改代码,都是自己作的,怨不得别人。

虽然是上游影响了下游,但是造成影响后,还是得下游来背锅,所以未来的话,数据还是要盯紧些,不要只是当成一串数字。