天天看點

ESP-C3入門12. HTTPS請求、堆記憶體使用及JSON處理

作者:程式設計圈子

一、建立HTTPS請求

1. 基本流程

本文主要内容接上節《建立最基本http請求》的文章。

ESP32 IDF建立http請求的基本流程:

  1. 使用esp_http_client_config_t建立http用戶端;
  2. esp_http_client_init 初始化http用戶端;
  3. esp_http_client_set_method 設定http請求方式;
  4. 設定http請求頭esp_http_client_set_header;
  5. 設定 http 請求體 esp_http_client_set_post_field;
  6. 執行http請求 esp_http_client_perform;
  7. 處理http響應;
  8. 釋放http用戶端esp_http_client_cleanup ;

在https請求中,有一些進行一些額外的步驟,包括 證書的驗證和捆綁。

首先要擷取遠端伺服器的證書;

如果ESP32 IDF無法驗證證書,則需要使用esp_http_client_set_cert_info函數将伺服器證書的SHA-1指紋添加到ESP32 IDF的證書信任清單中。

如果ESP32 IDF無法連接配接到遠端伺服器,則可能需要設定代理伺服器。

2. ESP32 使用https證書的方式

(1) 内置證書

證書已經内置在ESP32的固件中,無需單獨管理證書,可以直接使用。這種方式比較簡單,适用于使用不頻繁的HTTPS請求。

(2) ESP32管理證書

使用esp32管理證書的方式,需要使用者自己管理證書。首先需要在ESP32上安裝證書,然後在代碼中指定證書的路徑和密碼。這種方式需要使用者自己管理證書的更新和安裝,但是可以在需要時更新證書,進而提高了安全性,适用于使用頻繁的HTTPS請求場景。

總結:使用内置證書的方式更加簡單,适用于簡單的HTTPS請求場景;

使用esp32管理證書的方式更加靈活,适用于複雜的HTTPS請求場景。

3. 開發環境配置

(1) 引用 esp-tls 庫

CMakeLists.txt裡添加:

idf_component_register(SRCS "main.c" "network/wifi.c" "network/tcp_server.c" "network/tcp_client.c" "network/http_request.c"
                    INCLUDE_DIRS "network/include"
                    REQUIRES "tcpip_adapter" "nvs_flash" "esp_http_client" "esp-tls"
        )           

(2) 啟用CONFIG_MBEDTLS_CERTIFICATE_BUNDLE

ESP-C3入門12. HTTPS請求、堆記憶體使用及JSON處理

在menuconfig中設定啟用ESP-IDF TLS 庫中的受信任證書捆綁包(一般已經預設選中)。

ESP-C3入門12. HTTPS請求、堆記憶體使用及JSON處理
ESP-C3入門12. HTTPS請求、堆記憶體使用及JSON處理

在使用 TLS 時,需要驗證對等方證書以確定其是可信的。可信任的證書可以是根證書或中間證書,或者可以通過一組根證書建立信任關系。證書的驗證需要證書鍊和 CA 證書的清單。如果 ESP-IDF TLS 庫沒有找到可用的證書鍊或 CA 證書,将不會建立安全連接配接。

受信任的證書捆綁包是一組根證書,其中包括常見 CA 證書,以及其他根證書和中間證書。啟用此選項可以将 ESP-IDF TLS 庫的可信 CA 清單擴充到受信任的證書捆綁包中的證書,進而增加連接配接對等方的成功率。

二、堆記憶體的使用

1. 堆記憶體和棧記憶體

一般情況下聲明的變量是棧記憶體,是一種後進後出的資料結構,用于存儲局部變量、函數參數和傳回位址等。它的記憶體空間由編譯器在程式運作時自動配置設定和釋放,空間大小通常是有限制的。

堆記憶體是由程式員在運作時手動配置設定和釋放的記憶體,它的記憶體空間通常比棧記憶體更大,也更靈活,可以根據需要動态調整記憶體大小。 堆記憶體通常用來存儲動态配置設定的資料結構,如連結清單、樹、圖等。 但使用中要避免記憶體洩露。

在嵌入式系統中,由于記憶體資源有限,使用堆記憶體可以更好地進行記憶體管理。

2. 堆記憶體的使用

(1) 記憶體配置設定

在ESP32開發中,堆空間是動态配置設定的,它的大小是由可用RAM的大小限制的。ESP-IDF中提供了幾個API用于堆記憶體管理,其中之一是heap_caps_malloc函數。heap_caps_malloc是ESP32的堆空間動态記憶體配置設定API,允許使用者從堆空間中配置設定記憶體。

函數原型:

void *heap_caps_malloc(size_t size, uint32_t caps);
           

其中:

  • size參數是要配置設定的記憶體塊的大小
  • caps參數是配置設定記憶體的政策,這是一個32位的标志位,用于設定記憶體的要求和限制。

标志位值清單:

  • MALLOC_CAP_8BIT: 配置設定8位寬的記憶體;
  • MALLOC_CAP_32BIT: 配置設定32位寬的記憶體;
  • MALLOC_CAP_64BIT: 配置設定64位寬的記憶體;
  • MALLOC_CAP_SPIRAM: 配置設定SPI RAM記憶體;
  • MALLOC_CAP_DMA: 配置設定DMA記憶體;
  • MALLOC_CAP_EXEC: 配置設定可執行記憶體;
  • MALLOC_CAP_DEFAULT: 配置設定預設類型的記憶體;
  • MALLOC_CAP_INVALID: 配置設定無效類型的記憶體。

(2) 記憶體釋放

heap_caps_free 進行記憶體釋放。

本文中的響應資料使用變量 char* local_response_buffer ,使用下面的代碼把變量放在了堆上:

local_response_buffer = heap_caps_malloc(MAX_HTTP_OUTPUT_BUFFER, MALLOC_CAP_8BIT);
    if (local_response_buffer == NULL) {
        ESP_LOGE(TAG, "Failed to allocate memory for HTTP output buffer");
        abort();
    }           

上述代碼配置設定了一個大小為MAX_HTTP_OUTPUT_BUFFER的8位寬的記憶體塊。

三、使用cJSON庫

1. 加載cJSON庫

在 CMakeLists.txt中添加 json:

idf_component_register(SRCS "main.c" "network/wifi.c" "network/tcp_server.c" "network/tcp_client.c" "network/http_request.c"
                    INCLUDE_DIRS "network/include"
                    REQUIRES "tcpip_adapter" "nvs_flash" "esp_http_client" "esp-tls" "json"
        )
           

2. 最基本的用法

void parse_json(const char *json_string) {
    // 解析 JSON 字元串
    cJSON *root = cJSON_Parse(json_string);
    if (root == NULL) {
        printf("Failed to parse JSON string\n");
        return;
    }

    // 擷取 "name" 屬性的值
    cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "body");
    if (cJSON_IsString(name) && (name->valuestring != NULL)) {
        printf("body: %s\n", name->valuestring);
    } else {
        printf("Failed to get name property\n");
    }

    cJSON_Delete(root);
}
           

四、https請求示例

1. 項目結構

ESP-C3入門12. HTTPS請求、堆記憶體使用及JSON處理

2. 自定義元件的CMakeLists.txt設定

idf_component_register(SRCS "main.c" "network/wifi.c" "network/tcp_server.c" "network/tcp_client.c" "network/http_request.c"
                    INCLUDE_DIRS "network/include"
                    REQUIRES "tcpip_adapter" "nvs_flash" "esp_http_client" "esp-tls" "json"
        )

# 這将把目前元件的路徑添加到編譯器的頭檔案搜尋路徑中,否則提示找不到string.h等頭檔案
target_include_directories(${COMPONENT_LIB} PRIVATE ${CMAKE_CURRENT_LIST_DIR})

target_include_directories(${COMPONENT_LIB} PUBLIC "network/include"

        )
           

3. http_request.h

#ifndef HTTP_REQUEST_H
#define HTTP_REQUEST_H
#include "esp_http_client.h"

esp_err_t http_event_handler(esp_http_client_event_t *evt);
void request(const char* url);
#endif           

4. http_request.c

#include <esp_err.h>
#include <esp_log.h>
#include "network/include/http_request.h"
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
#include "esp_crt_bundle.h"
#endif
static const char *TAG = "HTTP_REQUEST";
#define MAX_HTTP_OUTPUT_BUFFER 4096
#include "cJSON.h"

char* local_response_buffer ;

// HTTP 請求的處理函數
esp_err_t http_event_handler(esp_http_client_event_t *evt)
{
    // 緩存http響應的buffer
    static char *output_buffer;
    // 已經讀取的位元組數
    static int output_len;
    local_response_buffer = (char *) evt->user_data;

    switch(evt->event_id) {
        case HTTP_EVENT_ERROR:
            ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
            break;
        case HTTP_EVENT_ON_CONNECTED:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
            break;
        case HTTP_EVENT_HEADER_SENT:
            ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
            break;
        case HTTP_EVENT_ON_HEADER:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
            break;
        case HTTP_EVENT_ON_DATA:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);

            if (!esp_http_client_is_chunked_response(evt->client)) {
                // 如果配置了user_data buffer,則把響應複制到該buffer中
                if (local_response_buffer) {
                    memcpy(local_response_buffer + output_len, evt->data, evt->data_len);
                } else {
                    if (output_buffer == NULL) {
                        output_buffer = (char *) malloc(esp_http_client_get_content_length(evt->client));
                        output_len = 0;
                        if (output_buffer == NULL) {
                            ESP_LOGE(TAG, "Failed to allocate memory for output buffer");
                            return ESP_FAIL;
                        }
                    }
                    memcpy(output_buffer + output_len, evt->data, evt->data_len);
                }
                output_len += evt->data_len;
            }

            break;
        case HTTP_EVENT_ON_FINISH:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
            if (output_buffer != NULL) {
                free(output_buffer);
                output_buffer = NULL;
            }
            output_len = 0;
            break;
        case HTTP_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
            if (output_buffer != NULL) {
                free(output_buffer);
                output_buffer = NULL;
            }
            output_len = 0;
            break;
    }
    return ESP_OK;
}


void parse_json(const char *json_string) {
    // 解析 JSON 字元串
    cJSON *root = cJSON_Parse(json_string);
    if (root == NULL) {
        printf("Failed to parse JSON string\n");
        return;
    }

    // 擷取 "name" 屬性的值
    cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "body");
    if (cJSON_IsString(name) && (name->valuestring != NULL)) {
        printf("body: %s\n", name->valuestring);
    } else {
        printf("Failed to get name property\n");
    }

    cJSON_Delete(root);
}

void request(const char *url) {
    // 響應結果放在這裡
    local_response_buffer = heap_caps_malloc(MAX_HTTP_OUTPUT_BUFFER, MALLOC_CAP_8BIT);
    if (local_response_buffer == NULL) {
        ESP_LOGE(TAG, "Failed to allocate memory for HTTP output buffer");
        abort();
    }

    // 建立一個 HTTP 用戶端配置
    esp_http_client_config_t config = {
            .url = url,
            .event_handler = http_event_handler,
            .user_data = local_response_buffer,
            .crt_bundle_attach = esp_crt_bundle_attach,
    };

    // 建立一個 HTTP 用戶端并執行 GET 請求
    esp_http_client_handle_t client = esp_http_client_init(&config);
    esp_err_t err = esp_http_client_perform(client);

    // 檢查請求是否成功
    if (err == ESP_OK) {
        int len =  esp_http_client_get_content_length(client);
        ESP_LOGI(TAG, "Status = %d, content_length = %d",
                 esp_http_client_get_status_code(client),//狀态碼
                 len);//資料長度
        // 解析json
        parse_json(local_response_buffer);
    } else {
        printf("HTTP GET request failed: %s\n", esp_err_to_name(err));
    }
    printf("Response: %.*s\n", strlen(local_response_buffer), local_response_buffer);

    if (local_response_buffer != NULL) {
        heap_caps_free(local_response_buffer);
        local_response_buffer = NULL;
    }
    //斷開并釋放資源
    esp_http_client_cleanup(client);
}
           

5. main.c

#include <string.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include <nvs_flash.h>
#include "network/include/wifi.h"
#include "network/include/http_request.h"

static const char *TAG = "wifi connection";

#define HTTP_URL "https://jsonplaceholder.typicode.com/posts/1"

void app_main()
{
    ESP_LOGE(TAG, "app_main");
    // 初始化NVS存儲區
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // Wi-Fi初始化
    ESP_LOGI(TAG, "Wi-Fi initialization");
    wifi_initialize();

    // Wi-Fi Station初始化
    wifi_station_initialize();

    vTaskDelay(pdMS_TO_TICKS(1000*10));
    ESP_LOGI(TAG, "request 1");
    request(HTTP_URL);

    vTaskDelay(pdMS_TO_TICKS(2000));
    ESP_LOGI(TAG, "request again");
    request(HTTP_URL);

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}