一、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.4 HTTP在物联网中的应用
-
设备数据上报:向服务器发送传感器数据
-
远程配置:从服务器获取设备配置
-
固件升级:通过HTTP下载固件包
-
Web控制界面:提供设备管理页面
-
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 代码说明
-
WiFi连接:
-
使用
WiFi.begin()
连接到指定的WiFi网络 -
单独的WiFi监控任务持续检查连接状态并在断开时重新连接
-
-
HTTP功能:
-
使用HTTPClient库实现HTTP协议
-
支持GET和POST请求
-
POST请求发送JSON格式数据
-
自动处理HTTP响应
-
-
FreeRTOS集成:
-
创建了两个任务:一个用于HTTP通信,一个用于WiFi监控
-
任务运行在不同的核心上以提高效率
-
使用
vTaskDelay()
代替delay()
以确保不阻塞其他任务
-
-
多任务处理:
-
HTTP任务负责定期发送HTTP请求
-
WiFi任务持续监控网络连接状态
-
2.2 使用说明
-
修改
ssid
和password
为你自己的WiFi配置 -
修改
serverUrl
为你的服务器地址和API端点 -
根据需要调整POST请求的内容和格式
-
请求频率可以在
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服务器
-
新建一个文件夹作为项目目录
-
在该目录下创建
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 路由处理
-
首页路由:
javascriptapp.get('/', (req, res) => { res.send(htmlPage); });
-
处理根路径请求
-
直接返回HTML页面内容
-
-
GET API接口:
javascriptapp.get('/api/data', (req, res) => { res.json({ message: '数据获取成功', receivedData: receivedData, timestamp: new Date().toISOString() }); });
-
返回所有存储的数据
-
包含状态信息和时间戳
-
-
POST API接口:
javascriptapp.post('/api/data', (req, res) => { // 数据验证和存储 res.status(201).json({ message: '数据接收成功', yourData: req.body }); });
-
接收ESP32发来的JSON数据
-
验证必要字段
-
存储数据并返回确认
-
-
DELETE API接口:
javascriptapp.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 验证步骤
-
确保你的PC和ESP32在同一个局域网
-
修改ESP32代码中的
serverUrl
为你的PC的IP地址和端口(如http://192.168.1.100:3000/api/data
) -
上传ESP32代码并打开串口监视器
-
在浏览器中访问
http://localhost:3000
-
观察串口输出和网页显示的数据
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最佳实践与优化
-
安全考虑:
-
使用HTTPS替代HTTP
-
实现API密钥验证
-
限制请求频率
-
-
性能优化:
-
复用HTTPClient对象
-
减少不必要的头信息
-
使用连接池
-
-
错误处理:
-
实现自动重试机制
-
添加超时设置
-
记录错误日志
-
-
数据格式:
-
使用JSON进行数据交换
-
压缩大数据量
-
分页获取大量数据
-
六、常见HTTP服务器选择
-
本地测试:
-
Node.js + Express
-
Python Flask
-
PHP内置服务器
-
-
生产环境:
-
Nginx
-
Apache
-
IIS
-
-
云服务:
-
AWS API Gateway
-
阿里云API网关
-
腾讯云API网关
-
七、总结与扩展
HTTP作为互联网的基础协议,与ESP32的结合为物联网设备提供了简单可靠的数据通信方案,相对于上一篇MQTT协议,HTTP协议的开发和验证更为简单,若你对MQTT开发感兴趣,可查看ESP32开发入门(六):MQTT开发实践。掌握HTTP开发后,您可以进一步:
-
研究HTTPS安全连接
-
学习WebSocket实现实时通信
-
探索RESTful API设计
-
了解gRPC等高效协议
通过本篇教程,您应该已经掌握了ESP32上HTTP开发的核心知识。实际项目中,建议从简单的原型开始,逐步增加功能复杂度,并始终考虑安全性和性能问题。