ESP32开发入门(七):HTTP开发实践

一、HTTP协议基础

1.1 什么是HTTP?

HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网上应用最为广泛的一种网络协议,用于从服务器传输超文本到本地浏览器。它是一种无状态的请求/响应协议,工作在客户端-服务器计算模型中。

1.2 HTTP的工作原理

HTTP协议基于请求-响应模型,主要包含以下组件:

  • 客户端(Client):发送HTTP请求(如浏览器、ESP32等设备)

  • 服务器(Server):接收请求并返回响应

  • 请求方法:GET、POST、PUT、DELETE等

  • 状态码:200(成功)、404(未找到)、500(服务器错误)等

bash 复制代码
ESP32设备(客户端) --HTTP请求--> Web服务器 <--HTTP响应-- 浏览器或其他客户端

1.3 HTTP的核心特性

  1. 简单快速:基于文本的简单协议

  2. 无连接:每次连接只处理一个请求

  3. 无状态:协议不保留之前的请求信息

  4. 灵活:可以传输任意类型的数据

  5. 支持多种请求方法:满足不同场景需求

1.4 HTTP在物联网中的应用

  1. 设备数据上报:向服务器发送传感器数据

  2. 远程配置:从服务器获取设备配置

  3. 固件升级:通过HTTP下载固件包

  4. Web控制界面:提供设备管理页面

  5. API交互:与其他系统集成

二、ESP32-S3 HTTP通信程序 (FreeRTOS + Arduino框架)

下面是一个基于ESP32-S3的HTTP通信程序,使用FreeRTOS和Arduino框架实现。这个程序包含HTTP客户端功能,可以向服务器发送GET和POST请求。

cpp 复制代码
#include <WiFi.h>
#include <HTTPClient.h>
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
​
// WiFi配置
const char* ssid = "你的WiFi名称";
const char* password = "你的WiFi密码";
​
// 服务器配置
const char* serverUrl = "http://你的服务器地址:端口/api/data"; // 示例:"http://192.168.1.100:3000/api/data"
​
// FreeRTOS任务句柄
TaskHandle_t httpTaskHandle = NULL;
TaskHandle_t wifiTaskHandle = NULL;
​
// 连接WiFi函数
void connectToWiFi() {
  Serial.println();
  Serial.print("正在连接WiFi: ");
  Serial.println(ssid);
​
  WiFi.begin(ssid, password);
​
  while (WiFi.status() != WL_CONNECTED) {
    vTaskDelay(500 / portTICK_PERIOD_MS);
    Serial.print(".");
  }
​
  Serial.println("");
  Serial.println("WiFi已连接");
  Serial.print("IP地址: ");
  Serial.println(WiFi.localIP());
}
​
// 发送HTTP GET请求
void sendHttpGetRequest() {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    
    Serial.print("发送GET请求到: ");
    Serial.println(serverUrl);
    
    http.begin(serverUrl);
    int httpCode = http.GET();
    
    if (httpCode > 0) {
      Serial.printf("HTTP响应码: %d\n", httpCode);
      
      if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        Serial.println("服务器响应:");
        Serial.println(payload);
      }
    } else {
      Serial.printf("GET请求失败, 错误: %s\n", http.errorToString(httpCode).c_str());
    }
    
    http.end();
  } else {
    Serial.println("WiFi未连接,无法发送请求");
  }
}
​
// 发送HTTP POST请求
void sendHttpPostRequest() {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    
    Serial.print("发送POST请求到: ");
    Serial.println(serverUrl);
    
    http.begin(serverUrl);
    http.addHeader("Content-Type", "application/json");
    
    // 创建JSON格式的POST数据
    String httpRequestData = "{\"deviceId\":\"ESP32-S3\",\"temperature\":25.5,\"humidity\":60}";
    
    int httpCode = http.POST(httpRequestData);
    
    if (httpCode > 0) {
      Serial.printf("HTTP响应码: %d\n", httpCode);
      
      if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_CREATED) {
        String payload = http.getString();
        Serial.println("服务器响应:");
        Serial.println(payload);
      }
    } else {
      Serial.printf("POST请求失败, 错误: %s\n", http.errorToString(httpCode).c_str());
    }
    
    http.end();
  } else {
    Serial.println("WiFi未连接,无法发送请求");
  }
}
​
// HTTP任务函数
void httpTask(void *pvParameters) {
  while (1) {
    // 每隔10秒发送一次请求
    static uint32_t lastRequestTime = 0;
    uint32_t now = millis();
    
    if (now - lastRequestTime > 10000) {
      lastRequestTime = now;
      
      // 交替发送GET和POST请求
      static bool sendGet = true;
      
      if (sendGet) {
        sendHttpGetRequest();
      } else {
        sendHttpPostRequest();
      }
      
      sendGet = !sendGet;
    }
    
    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
}
​
// WiFi监控任务函数
void wifiMonitorTask(void *pvParameters) {
  while (1) {
    if (WiFi.status() != WL_CONNECTED) {
      Serial.println("WiFi连接丢失,尝试重新连接...");
      connectToWiFi();
    }
    vTaskDelay(10000 / portTICK_PERIOD_MS); // 每10秒检查一次
  }
}
​
void setup() {
  Serial.begin(115200);
  
  // 初始化WiFi连接
  connectToWiFi();
  
  // 创建HTTP任务
  xTaskCreatePinnedToCore(
    httpTask,           // 任务函数
    "HTTP Task",        // 任务名称
    8192,               // 堆栈大小
    NULL,               // 参数
    1,                  // 优先级
    &httpTaskHandle,    // 任务句柄
    1                   // 运行在核心1上
  );
  
  // 创建WiFi监控任务
  xTaskCreatePinnedToCore(
    wifiMonitorTask,    // 任务函数
    "WiFi Task",        // 任务名称
    4096,               // 堆栈大小
    NULL,               // 参数
    1,                  // 优先级
    &wifiTaskHandle,    // 任务句柄
    0                   // 运行在核心0上
  );
}
​
void loop() {
  // 主循环为空,所有功能由FreeRTOS任务处理
  vTaskDelay(1000 / portTICK_PERIOD_MS);
}

2.1 代码说明

  1. WiFi连接

    • 使用WiFi.begin()连接到指定的WiFi网络

    • 单独的WiFi监控任务持续检查连接状态并在断开时重新连接

  2. HTTP功能

    • 使用HTTPClient库实现HTTP协议

    • 支持GET和POST请求

    • POST请求发送JSON格式数据

    • 自动处理HTTP响应

  3. FreeRTOS集成

    • 创建了两个任务:一个用于HTTP通信,一个用于WiFi监控

    • 任务运行在不同的核心上以提高效率

    • 使用vTaskDelay()代替delay()以确保不阻塞其他任务

  4. 多任务处理

    • HTTP任务负责定期发送HTTP请求

    • WiFi任务持续监控网络连接状态

2.2 使用说明

  1. 修改ssidpassword为你自己的WiFi配置

  2. 修改serverUrl为你的服务器地址和API端点

  3. 根据需要调整POST请求的内容和格式

  4. 请求频率可以在httpTask函数中调整

2.3 所需库

  • WiFi.h (Arduino ESP32核心自带)

  • HTTPClient (Arduino ESP32核心自带)

三、HTTP验证步骤 - 搭建Node.js服务器

为了验证ESP32的HTTP功能,我们可以使用Node.js搭建一个简单的服务器,接收ESP32的请求并返回响应。

下面我将详细介绍如何搭建一个完整的Node.js服务器,并将HTML页面数据整合到server.js文件中,以便于ESP32通过HTTP协议与服务器进行通信。

注意:若你电脑没安装node,请自行百度安装,网上教程较多,这里就不赘述了。

3.1 创建Node.js服务器

  1. 新建一个文件夹作为项目目录

  2. 在该目录下创建server.js文件,内容如下:

javascript 复制代码
const express = require('express');
const bodyParser = require('body-parser');
​
const app = express();
const port = 3000;
​
// 中间件
app.use(bodyParser.json());
​
// 存储接收到的数据
let receivedData = [];
​
// HTML页面内容
const htmlPage = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32 数据监控</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        .data-container {
            margin-top: 20px;
            padding: 15px;
            border: 1px solid #ddd;
            border-radius: 5px;
            background-color: #f9f9f9;
        }
        button {
            padding: 10px 15px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background-color: #45a049;
        }
        .data-item {
            margin-bottom: 10px;
            padding: 10px;
            border-bottom: 1px solid #eee;
        }
        .timestamp {
            color: #666;
            font-size: 0.9em;
        }
    </style>
</head>
<body>
    <h1>ESP32 数据监控</h1>
    <button id="refreshBtn">刷新数据</button>
    <div class="data-container">
        <h2>最新上报数据 (共<span id="dataCount">0</span>条)</h2>
        <div id="dataDisplay">
            <p>暂无数据...</p>
        </div>
    </div>
​
    <script>
        const refreshBtn = document.getElementById('refreshBtn');
        const dataDisplay = document.getElementById('dataDisplay');
        const dataCount = document.getElementById('dataCount');
        
        // 格式化数据显示
        function formatData(data) {
            if (data.receivedData && data.receivedData.length > 0) {
                return data.receivedData.map(item => \`
                    <div class="data-item">
                        <div><strong>设备ID:</strong> \${item.data.deviceId || '未知'}</div>
                        <div><strong>温度:</strong> \${item.data.temperature || 'N/A'}°C</div>
                        <div><strong>湿度:</strong> \${item.data.humidity || 'N/A'}%</div>
                        <div class="timestamp">\${new Date(item.timestamp).toLocaleString()}</div>
                    </div>
                \`).join('');
            }
            return '<p>暂无数据...</p>';
        }
        
        // 获取数据函数
        async function fetchData() {
            try {
                const response = await fetch('/api/data');
                const data = await response.json();
                dataCount.textContent = data.receivedData ? data.receivedData.length : 0;
                dataDisplay.innerHTML = formatData(data);
            } catch (error) {
                dataDisplay.innerHTML = \`<p style="color:red;">获取数据失败: \${error.message}</p>\`;
            }
        }
        
        // 初始加载数据
        document.addEventListener('DOMContentLoaded', fetchData);
        
        // 按钮点击事件
        refreshBtn.addEventListener('click', fetchData);
        
        // 每5秒自动刷新
        setInterval(fetchData, 5000);
    </script>
</body>
</html>
`;
​
// 首页路由 - 返回HTML页面
app.get('/', (req, res) => {
  res.send(htmlPage);
});
​
// GET请求处理 - 获取所有数据
app.get('/api/data', (req, res) => {
  console.log('收到GET请求');
  res.status(200).json({
    message: '数据获取成功',
    receivedData: receivedData,
    timestamp: new Date().toISOString()
  });
});
​
// POST请求处理 - 接收ESP32数据
app.post('/api/data', (req, res) => {
  console.log('收到POST请求:', req.body);
  
  // 验证数据
  if (!req.body.deviceId) {
    return res.status(400).json({
      error: '缺少必要字段: deviceId'
    });
  }
  
  // 存储数据
  receivedData.push({
    data: req.body,
    timestamp: new Date().toISOString()
  });
  
  // 限制存储的数据量
  if (receivedData.length > 50) {
    receivedData = receivedData.slice(-50);
  }
  
  res.status(201).json({
    message: '数据接收成功',
    yourData: req.body
  });
});
​
// 清空数据接口
app.delete('/api/data', (req, res) => {
  receivedData = [];
  res.status(200).json({
    message: '所有数据已清空'
  });
});
​
// 启动服务器
app.listen(port, () => {
  console.log(`服务器运行在 http://localhost:${port}`);
  console.log(`API端点:`);
  console.log(`GET /            - 查看数据监控页面`);
  console.log(`GET /api/data    - 获取所有接收到的数据`);
  console.log(`POST /api/data   - 接收ESP32发送的数据`);
  console.log(`DELETE /api/data - 清空所有数据`);
});
3.1.1 代码详细说明
3.1.1.1 初始化设置
javascript 复制代码
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
  • 引入Express框架和body-parser中间件

  • 创建Express应用实例

  • 设置服务器端口为3000

3.1.1.2 数据存储
javascript 复制代码
let receivedData = [];
  • 使用一个数组来存储ESP32发来的所有数据

  • 每个数据项包含原始数据和接收时间戳

3.1.1.3 HTML页面整合
javascript 复制代码
const htmlPage = `...`;
  • 将完整的HTML页面内容作为模板字符串存储在变量中

  • 包含CSS样式和内联JavaScript

  • 使用ES6模板字符串语法方便插入变量

3.1.1.4 路由处理
  1. 首页路由

    javascript 复制代码
    app.get('/', (req, res) => {
      res.send(htmlPage);
    });
    • 处理根路径请求

    • 直接返回HTML页面内容

  2. GET API接口

    javascript 复制代码
    app.get('/api/data', (req, res) => {
      res.json({
        message: '数据获取成功',
        receivedData: receivedData,
        timestamp: new Date().toISOString()
      });
    });
    • 返回所有存储的数据

    • 包含状态信息和时间戳

  3. POST API接口

    javascript 复制代码
    app.post('/api/data', (req, res) => {
      // 数据验证和存储
      res.status(201).json({
        message: '数据接收成功',
        yourData: req.body
      });
    });
    • 接收ESP32发来的JSON数据

    • 验证必要字段

    • 存储数据并返回确认

  4. DELETE API接口

    javascript 复制代码
    app.delete('/api/data', (req, res) => {
      receivedData = [];
      res.json({ message: '所有数据已清空' });
    });
    • 清空存储的数据

    • 用于测试和调试

3.1.1.5 前端JavaScript功能
javascript 复制代码
// 格式化数据显示
function formatData(data) {
  // 将JSON数据转换为HTML显示
}
​
// 获取数据函数
async function fetchData() {
  // 从/api/data获取数据并更新页面
}
​
// 事件监听和自动刷新
document.addEventListener('DOMContentLoaded', fetchData);
refreshBtn.addEventListener('click', fetchData);
setInterval(fetchData, 5000);
  • 使用Fetch API获取数据

  • 动态更新页面内容

  • 自动刷新和手动刷新功能

  • 数据格式化显示

3.2 安装依赖

在项目目录下运行以下命令安装必要的依赖:

bash 复制代码
npm init -y
npm install express body-parser

3.3 启动服务器

bash 复制代码
node server.js

服务器启动后,你将在控制台看到:

bash 复制代码
服务器运行在 http://localhost:3000

现在你可以通过浏览器访问http://localhost:3000来查看ESP32上报的数据。

3.4 验证步骤

  1. 确保你的PC和ESP32在同一个局域网

  2. 修改ESP32代码中的serverUrl为你的PC的IP地址和端口(如http://192.168.1.100:3000/api/data

  3. 上传ESP32代码并打开串口监视器

  4. 在浏览器中访问http://localhost:3000

  5. 观察串口输出和网页显示的数据

3.6 预期结果

  • 串口输出

    bash 复制代码
    发送GET请求到: http://192.168.1.100:3000/api/data
    HTTP响应码: 200
    服务器响应:
    {"message":"Hello from Node.js server!","receivedData":[...],"timestamp":"..."}
    ​
    发送POST请求到: http://192.168.1.100:3000/api/data
    HTTP响应码: 201
    服务器响应:
    {"message":"Data received successfully","yourData":{"deviceId":"ESP32-S3","temperature":25.5,"humidity":60}}
  • 网页显示

    bash 复制代码
    最新上报数据
    {
      "message": "Hello from Node.js server!",
      "receivedData": [
        {
          "data": {
            "deviceId": "ESP32-S3",
            "temperature": 25.5,
            "humidity": 60
          },
          "timestamp": "..."
        }
      ],
      "timestamp": "..."
    }

四、实际项目应用示例

4.1 环境监测系统

功能设计

  • 定期上报温湿度数据

  • 从服务器获取配置参数

  • 实现固件升级检查

cpp 复制代码
void checkForUpdates() {
  HTTPClient http;
  http.begin("http://yourserver.com/api/update");
  
  int httpCode = http.GET();
  if (httpCode == HTTP_CODE_OK) {
    String payload = http.getString();
    DynamicJsonDocument doc(1024);
    deserializeJson(doc, payload);
    
    if (doc["available"] == true) {
      String newVersion = doc["version"];
      String firmwareUrl = doc["url"];
      
      if (newVersion != currentFirmwareVersion) {
        startFirmwareUpdate(firmwareUrl);
      }
    }
  }
  
  http.end();
}

4.2 远程控制面板

功能设计

  • 提供Web控制界面

  • 实现设备状态实时显示

  • 支持多设备管理

cpp 复制代码
void handleRoot() {
  String html = "<html><body>";
  html += "<h1>ESP32 Control Panel</h1>";
  html += "<p>Temperature: " + String(readTemperature()) + "°C</p>";
  html += "<p>Humidity: " + String(readHumidity()) + "%</p>";
  html += "<form method='post' action='/control'>";
  html += "<button name='led' value='on'>Turn LED On</button>";
  html += "<button name='led' value='off'>Turn LED Off</button>";
  html += "</form>";
  html += "</body></html>";
  
  server.send(200, "text/html", html);
}

五、HTTP最佳实践与优化

  1. 安全考虑

    • 使用HTTPS替代HTTP

    • 实现API密钥验证

    • 限制请求频率

  2. 性能优化

    • 复用HTTPClient对象

    • 减少不必要的头信息

    • 使用连接池

  3. 错误处理

    • 实现自动重试机制

    • 添加超时设置

    • 记录错误日志

  4. 数据格式

    • 使用JSON进行数据交换

    • 压缩大数据量

    • 分页获取大量数据

六、常见HTTP服务器选择

  1. 本地测试

    • Node.js + Express

    • Python Flask

    • PHP内置服务器

  2. 生产环境

    • Nginx

    • Apache

    • IIS

  3. 云服务

    • AWS API Gateway

    • 阿里云API网关

    • 腾讯云API网关

七、总结与扩展

HTTP作为互联网的基础协议,与ESP32的结合为物联网设备提供了简单可靠的数据通信方案,相对于上一篇MQTT协议,HTTP协议的开发和验证更为简单,若你对MQTT开发感兴趣,可查看ESP32开发入门(六):MQTT开发实践。掌握HTTP开发后,您可以进一步:

  1. 研究HTTPS安全连接

  2. 学习WebSocket实现实时通信

  3. 探索RESTful API设计

  4. 了解gRPC等高效协议

通过本篇教程,您应该已经掌握了ESP32上HTTP开发的核心知识。实际项目中,建议从简单的原型开始,逐步增加功能复杂度,并始终考虑安全性和性能问题。

相关推荐
小鸟啄米18 分钟前
Elixir通过Onvif协议控制IP摄像机,ExOnvif库给视频流叠加字符
网络协议·elixir·onvif
eqwaak01 小时前
Matplotlib 动态显示详解:技术深度与创新思考
网络·python·网络协议·tcp/ip·语言模型·matplotlib
qq_401700412 小时前
Qt UDP 网络编程详解
网络·网络协议·udp
岁月如歌2292 小时前
07. 运行Linux-5.4+Ubuntu20
嵌入式
油泼辣子多加3 小时前
HTTP 请求体格式详解
网络·网络协议·http
岁月如歌2293 小时前
02. MT7981设备引导程序初探
嵌入式
Hello阿尔法3 小时前
基于 NFS 的文件共享实现
linux·嵌入式
oldking呐呐3 小时前
【CS144】【计网】第一周 check0
网络协议
bantinghy4 小时前
RPC内核细节(转载)
linux·服务器·网络·网络协议·rpc
ZPC82105 小时前
scp 网间拷贝
网络协议·tcp/ip·ssl·信息与通信