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开发的核心知识。实际项目中,建议从简单的原型开始,逐步增加功能复杂度,并始终考虑安全性和性能问题。

相关推荐
秦jh_4 小时前
【Linux网络】应用层协议HTTP
linux·运维·服务器·网络·网络协议·tcp/ip·http
qq_260241235 小时前
SSL泄露源IP怎么办?(教学与防护)
网络协议·tcp/ip·ssl
嵌入式在学无敌大神6 小时前
TCP 与 UDP报文
网络协议·tcp/ip·udp
IP管家6 小时前
多级路由器如何避免IP冲突
网络·网络协议·tcp/ip·游戏·智能路由器·ip
10000hours9 小时前
【SGL】Scatter-Gather List内存传输技术
linux·数据结构·网络协议·list·存储·sgl
测试工程喵10 小时前
Bearer Token的神秘面纱:深入解析HTTP认证头的设计哲学
网络·功能测试·网络协议·http·接口测试·模块测试·登录认证
JAVA学习通10 小时前
[JAVAEE]HTTP协议(2.0)
网络·网络协议·http
TE-茶叶蛋12 小时前
HTTP请求与缓存、页面渲染全流程
网络协议·http·缓存
eguid_115 小时前
WebRTC流媒体传输协议RTP点到点传输协议介绍,WebRTC为什么使用RTP协议传输音视频流?
java·网络协议·音视频·webrtc·实时音视频