0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

ESP32 HTTP/HTTPS REST API 用戶端完整教學:GET、POST JSON、TLS 握手與 ThingSpeak 上傳

2026 年 6 月 19 日 26點熱度 0人點贊 0條評論

ESP32 HTTP/HTTPS REST API 用戶端

ESP32 作為 IoT 設備的核心任務之一就是將感測器資料上傳到雲端,或從雲端接收控制指令。透過 HTTP/HTTPS 協定與 RESTful API,ESP32 可以與各種後端服務(ThingSpeak、Firebase、AWS IoT、自建伺服器等)進行通訊。

本文將深入探索:

  • HTTP GET/POST 請求與 JSON 資料交換
  • TLS 1.2 安全加密連線(HTTPS)
  • Arduino HTTPClient 與 WiFiClientSecure
  • ESP-IDF esp_http_client 原生 API
  • JSON 解析、記憶體管理與異常處理
  • Cloud API 實戰:ThingSpeak + OpenWeatherMap

軟體架構

ESP32 HTTP/HTTPS 用戶端架構

ESP32 的 HTTP 通訊從上到下分為五層:

  1. 應用層:感測器讀取、資料處理、業務邏輯
  2. HTTP 用戶端:Arduino HTTPClient 或 ESP-IDF esp_http_client
  3. TLS 安全層:mbedTLS 提供加密、證書驗證、金鑰交換
  4. TCP/IP 協定棧:LwIP 開源協定棧(TCP、DNS、DHCP)
  5. Wi-Fi:ESP32 內建 802.11 b/g/n 無線收發器

HTTP GET 請求

HTTP GET 請求流程

Arduino:HTTP GET 擷取資料

// ESP32 HTTP GET 請求(ThingSpeak 為例)
#include <WiFi.h>
#include <HTTPClient.h>

const char* ssid     = "你的WiFi名稱";
const char* password = "你的WiFi密碼";

void setup() {
    Serial.begin(115200);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("
WiFi 已連線");
}

void loop() {
    if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;

        // 發送 GET 請求
        http.begin("http://api.thingspeak.com/channels/123456/feeds/last.json");
        int httpCode = http.GET();

        if (httpCode > 0) {
            Serial.printf("HTTP 狀態碼: %d
", httpCode);

            if (httpCode == HTTP_CODE_OK) {
                String payload = http.getString();
                Serial.println("回應內容:");
                Serial.println(payload);
            }
        } else {
            Serial.printf("HTTP 請求失敗: %s
", http.errorToString(httpCode).c_str());
        }

        http.end();  // 釋放資源
    }
    delay(30000);  // 30 秒查詢一次
}

使用 WiFiClient (底層 API)

// ESP32 原始 TCP Socket HTTP GET
#include <WiFi.h>

void httpGet(const char* host, const char* path) {
    WiFiClient client;
    if (!client.connect(host, 80)) {
        Serial.println("連線失敗!");
        return;
    }

    // 手動建立 HTTP 請求
    client.printf("GET %s HTTP/1.1
", path);
    client.printf("Host: %s
", host);
    client.print("Connection: close

");

    // 等待並讀取回應
    unsigned long timeout = millis();
    while (client.connected() && millis() - timeout < 5000) {
        while (client.available()) {
            String line = client.readStringUntil('
');
            Serial.println(line);
            timeout = millis();  // 重設超時
        }
    }
    client.stop();
}

void setup() {
    Serial.begin(115200);
    WiFi.begin("SSID", "PASSWORD");
    while (WiFi.status() != WL_CONNECTED) delay(500);
    httpGet("jsonplaceholder.typicode.com", "/todos/1");
}

HTTPS(TLS 安全連線)

TLS 1.2 完整握手流程

HTTPS 在 HTTP 之上添加了 TLS 加密層,流程如下:

  1. TCP 三次握手:建立 TCP 連線(SYN, SYN+ACK, ACK)
  2. ClientHello:ESP32 發送支援的 TLS 版本、密碼套件清單
  3. ServerHello + Certificate:伺服器選擇密碼套件,發送憑證(含公鑰)
  4. ServerDone:伺服器通知握手階段結束
  5. ClientKeyExchange:ESP32 產生 Pre-Master Secret,用伺服器公鑰加密後發送
  6. CCS + Finished:雙方確認啟用加密,握手完成
  7. Application Data:加密傳輸 HTTP 請求/回應

完整 TLS 握手需 2 個 RTT(Round Trip Time),總共約 200~600ms。

Arduino:HTTPS 請求(憑證驗證)

// ESP32 HTTPS GET — 使用 WiFiClientSecure + 憑證指紋驗證
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>

// 伺服器憑證 SHA-1 指紋(可用 openssl s_client 取得)
const char* fingerprint = "CF 05 98 6A 6D 44 9E 38 1C C2 88 5F 9A 3D 8B 5C 0C 6E E9 B7";

void httpsGet() {
    WiFiClientSecure client;
    client.setFingerprint(fingerprint);  // 憑證指紋驗證

    HTTPClient http;
    http.begin(client, "https://api.openweathermap.org/data/2.5/weather?q=Taipei&appid=YOUR_KEY");

    int httpCode = http.GET();
    if (httpCode == HTTP_CODE_OK) {
        Serial.println(http.getString());
    }
    http.end();
}

ESP-IDF:esp_http_client(原生高效能)

// ESP-IDF HTTP 用戶端範例
#include "esp_http_client.h"

esp_err_t _http_event_handler(esp_http_client_event_t *evt) {
    switch (evt->event_id) {
        case HTTP_EVENT_ON_DATA:
            // 接收資料回呼
            printf("Received %d bytes
", evt->data_len);
            break;
        case HTTP_EVENT_ON_FINISH:
            printf("HTTP 請求完成
");
            break;
        default:
            break;
    }
    return ESP_OK;
}

void esp_http_get_example(void) {
    esp_http_client_config_t config = {
        .url = "http://worldtimeapi.org/api/timezone/Asia/Taipei",
        .event_handler = _http_event_handler,
        .timeout_ms = 10000,
    };

    esp_http_client_handle_t client = esp_http_client_init(&config);
    esp_err_t err = esp_http_client_perform(client);

    if (err == ESP_OK) {
        printf("HTTP 狀態: %d
", esp_http_client_get_status_code(client));
        printf("Content-Length: %lld
", esp_http_client_get_content_length(client));
    }
    esp_http_client_cleanup(client);
}

HTTP POST:上傳資料到雲端

HTTP POST JSON 資料上傳

Arduino:POST JSON 到 Firebase

// ESP32 HTTP POST JSON — 上傳到 Firebase Realtime Database
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

const char* firebaseHost = "your-project.firebaseio.com";
const char* firebaseSecret = "YOUR_DATABASE_SECRET";

void postSensorData(float temp, float hum, float press) {
    if (WiFi.status() != WL_CONNECTED) return;

    HTTPClient http;

    // 建立 JSON
    StaticJsonDocument<256> doc;
    doc["temperature"] = temp;
    doc["humidity"]    = hum;
    doc["pressure"]    = press;
    doc["timestamp"]   = millis() / 1000;

    String jsonStr;
    serializeJson(doc, jsonStr);

    // 發送 POST
    String url = String("https://") + firebaseHost + "/sensors.json?auth=" + firebaseSecret;
    http.begin(url);
    http.addHeader("Content-Type", "application/json");

    int httpCode = http.POST(jsonStr);
    if (httpCode > 0) {
        Serial.printf("POST 成功,狀態碼: %d
", httpCode);
        Serial.println(http.getString());
    } else {
        Serial.printf("POST 失敗: %s
", http.errorToString(httpCode).c_str());
    }
    http.end();
}

POST multipart/form-data (檔案上傳)

// ESP32 HTTP POST multipart — 上傳圖片或二進位資料
#include <WiFi.h>
#include <HTTPClient.h>

void uploadPhoto(uint8_t* imageData, size_t imageLen) {
    HTTPClient http;
    http.begin("http://your-server.com/upload");

    // 設定 boundary
    String boundary = "----ESP32Boundary";
    http.addHeader("Content-Type", "multipart/form-data; boundary=" + boundary);

    // 建立 multipart body
    String bodyStart = "--" + boundary + "
";
    bodyStart += "Content-Disposition: form-data; name=\"file\"; filename=\"photo.jpg\"
";
    bodyStart += "Content-Type: image/jpeg

";

    String bodyEnd = "
--" + boundary + "--
";

    String totalBody = bodyStart + bodyEnd;
    int contentLen = bodyStart.length() + imageLen + bodyEnd.length();

    http.addHeader("Content-Length", String(contentLen));

    // 手動發送
    int httpCode = http.sendRequest("POST", totalBody.c_str());
    // 注意:對於大檔案,需使用 WiFiClient 分塊發送
    http.end();
}

JSON 解析(ArduinoJson)

// ESP32 使用 ArduinoJson 解析 API 回應
#include <ArduinoJson.h>

void parseWeatherResponse(String jsonResponse) {
    // 靜態配置(較小檔案)
    StaticJsonDocument<1024> doc;

    DeserializationError error = deserializeJson(doc, jsonResponse);
    if (error) {
        Serial.printf("JSON 解析失敗: %s
", error.c_str());
        return;
    }

    // 讀取值
    const char* cityName  = doc["name"];                      // 城市名稱
    float temp            = doc["main"]["temp"];             // 溫度
    float humidity        = doc["main"]["humidity"];         // 濕度
    int pressure          = doc["main"]["pressure"];         // 氣壓
    const char* weather   = doc["weather"][0]["description"];// 天氣描述
    float windSpeed       = doc["wind"]["speed"];            // 風速

    Serial.printf("城市: %s
", cityName);
    Serial.printf("溫度: %.1f &deg;C
", temp - 273.15);
    Serial.printf("濕度: %.0f%%
", humidity);
    Serial.printf("天氣: %s
", weather);

    // 動態分配(較大 JSON 時使用)
    // DynamicJsonDocument doc(4096);
    // deserializeJson(doc, jsonResponse);
}

建構巢狀 JSON

// 建構複雜 JSON 結構
void buildComplexJSON() {
    StaticJsonDocument<512> doc;

    // 巢狀物件
    JsonObject device = doc.createNestedObject("device");
    device["id"]   = "ESP32-001";
    device["type"] = "environmental";
    device["ver"]  = "2.1.0";

    // 陣列
    JsonArray readings = doc.createNestedArray("readings");
    readings.add(millis() / 1000);

    // 巢狀陣列物件
    JsonObject r1 = readings.createNestedObject();
    r1["sensor"] = "bme280";
    r1["value"]  = 25.3;

    JsonObject r2 = readings.createNestedObject();
    r2["sensor"] = "mpu6050";
    r2["value"]  = 0.98;

    // 輸出
    String output;
    serializeJsonPretty(doc, output);
    Serial.println(output);
}

完整範例:定時上傳到 ThingSpeak

// ESP32 定時上傳感測器資料到 ThingSpeak
#include <WiFi.h>
#include <HTTPClient.h>

#define THINGSPEAK_API_KEY "YOUR_WRITE_API_KEY"
#define SEND_INTERVAL 60000  // 60 秒

unsigned long lastSend = 0;

void setup() {
    Serial.begin(115200);
    WiFi.begin("SSID", "PASSWORD");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
    }
    Serial.println("WiFi 已連線");
}

void loop() {
    if (millis() - lastSend > SEND_INTERVAL) {
        // 讀取感測器(此處使用模擬值)
        float temp = 25.0 + random(-20, 20) / 10.0;
        float hum  = 60.0 + random(-50, 50) / 10.0;
        int light  = analogRead(34);

        if (WiFi.status() == WL_CONNECTED) {
            HTTPClient http;

            // ThingSpeak HTTP GET API(使用 GET 上傳)
            String url = "http://api.thingspeak.com/update?api_key=";
            url += THINGSPEAK_API_KEY;
            url += "&field1=" + String(temp, 1);
            url += "&field2=" + String(hum, 1);
            url += "&field3=" + String(light);

            http.begin(url);
            int httpCode = http.GET();

            if (httpCode == HTTP_CODE_OK) {
                String response = http.getString();
                Serial.printf("資料已上傳 (entry: %s)
", response.c_str());
            } else {
                Serial.printf("上傳失敗: %d
", httpCode);
            }
            http.end();
        }
        lastSend = millis();
    }
    delay(100);
}

延遲分析與最佳化

HTTPS API 請求延遲分解

從延遲分解圖可以看出:

  • TLS 握手 (200~600ms) 是最大的瓶頸,佔總時間 40~50%
  • TCP 連線 (30~200ms) 受 Wi-Fi RSSI 影響顯著
  • HTTP 請求/回應本身 (<50ms) 不是主要延遲來源

最佳化技巧

技術 做法 效果
Keep-Alive 重複使用 TCP 連線,避免每次重新握手 省 TCP + TLS 延遲 (200~800ms)
gzip 壓縮 Accept-Encoding: gzip 減少傳輸量 50~70%
批次上傳 收集多筆資料一次 POST 減少請求次數 10~60x
非同步 HTTP 使用 esp_http_client 的非同步模式 不阻塞主迴圈
降低 JSON 大小 縮短 key 名稱,刪除冗餘欄位 減少記憶體與傳輸量
TLS Session 重用 儲存 Session ID/Ticket 跳過完整握手 省 1 RTT (50~300ms)

常見問題與排查

問題 原因 解法
WiFiClientSecure 連線失敗 根憑證未更新或指紋錯誤 更新 SHA-1 指紋,或使用 setInsecure() 測試
ESP32 重啟 記憶體不足 (OOM) 降低 JSON buffer,使用 DynamicJsonDocument
HTTP POST 無回應 Content-Length 未正確設定 手動計算 body 長度並設定
TLS 握手 timeout Wi-Fi RSSI 太弱 改善天線位置,或改用 HTTP
HTTPS 憑證驗證失敗 ESP32 時鐘未同步 使用 NTP 同步時間再進行 TLS
請求間距過短 API rate limit 增加 delay 或實作指數退避
JSON 解析亂碼 編碼不符 UTF-8 確保 API 回應為 UTF-8 編碼

NTP 時鐘同步(HTTPS 必要)

// HTTPS 需要正確的系統時間驗證憑證
#include "esp_sntp.h"
#include "esp_log.h"

void syncTime() {
    sntp_setoperatingmode(SNTP_OPMODE_POLL);
    sntp_setservername(0, "pool.ntp.org");
    sntp_init();

    // 等待時間同步
    time_t now = 0;
    int retry = 0;
    while (now < 100000 && retry < 20) {
        delay(1000);
        time(&now);
        retry++;
    }

    if (retry < 20) {
        struct tm timeinfo;
        localtime_r(&now, &timeinfo);
        printf("時間已同步: %s
", asctime(&timeinfo));
    } else {
        printf("NTP 同步失敗
");
    }
}

TLS Session 重用(效能最佳化)

// 啟用 TLS Session Cache 加速後續 HTTPS 請求
#include <WiFiClientSecure.h>

WiFiClientSecure client;
static const char* caCert = "-----BEGIN CERTIFICATE-----
...";

void setup_https_client() {
    client.setCACert(caCert);

    // 啟用 Session Cache
    // 儲存在 mbedTLS 的 Session Cache 中
    // 第二次連線將跳過 Certificate + ServerKeyExchange
    // 節省約 1 RTT (100~300ms)
}

void reuse_https_get() {
    // 第一次呼叫:完整握手(~500ms)
    // 第二次呼叫:Session 重用(~200ms)
    if (!client.connected()) {
        client.connect("api.example.com", 443);
    }

    client.println("GET /data HTTP/1.1");
    client.println("Host: api.example.com");
    client.println("Connection: keep-alive");
    client.println();
}

總結

ESP32 的 HTTP/HTTPS 用戶端能力使其能無縫接入現代雲端生態系。從簡單的 GET 請求到 TLS 加密的 POST JSON 上傳,ESP32 提供了 Arduino HTTPClient 到 ESP-IDF esp_http_client 的多層 API 選擇。

選型參考:

  • 簡單資料上傳:Arduino HTTPClient + ThingSpeak GET API(最簡)
  • 安全傳輸:WiFiClientSecure + HTTPS + 憑證指紋驗證
  • 高效能量產:ESP-IDF esp_http_client + event callback
  • 大量資料上傳:批次收集 + 定時 POST + gzip 壓縮
  • 即時雙向通訊:WebSocket 或 MQTT over TLS
標籤: 教學
最後更新:2026 年 6 月 19 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang