ESP32学习笔记_WiFi(3)——HTTP

摘要

本笔记详述了 HTTP 作为基于 TCP/IP 的无状态应用层协议的技术特性,涵盖请求与响应模型的报文结构(Request Line/Status Line、Header、Body)及 URL 组成规范。 在 ESP32 嵌入式应用层面,文档解析了基于 esp_http_server 的服务端实现流程,包括 URI Handler 注册、GET 参数解析、POST 表单分块接收与 PUT 指令处理;同时展示了基于 POSIX Socket 的客户端实现,涵盖 DNS 解析、Socket 连接建立及原始报文收发,并记录了针对 301 重定向的调试分析。

文章目录

参考资料:
超文本传输协议 - wikipedia
Michael_ee视频教程 - BiliBili
HTTP 服务器 - ESP-IDF 编程指南
ESP-IDF - Gethub espressif
国家气象局提供的天气预报接口及使用说明 - lewo的博客 博客园



超文本传输协议(HTTP)是构建于 TCP/IP 协议栈之上的无状态应用层协议,作为万维网(WWW)的数据通信基石,主要用于分布式超媒体信息系统

该协议严格遵循请求-响应(Request-Response)模型与客户端-服务器(Client-Server)架构,通过统一资源标识符(URI)精确定位网络资源,利用标准方法(如 GET、POST、PUT、DELETE)界定操作语义,并借由状态码(Status Code)与头部字段(Header)控制传输元数据及缓存策略,其核心技术特征在于无状态性(Statelessness),即服务器默认不保留会话上下文

Protocol

Http 协议基于 Client 和 Server 之间的数据通信

Client -- HTTP Request --> Server

Client <-- HTTP Response -- Server

HTTP Request

  1. Request Line: method (请求方法 get/post/put/ ...) + sp(空格) + URL + sp + HTTP version + cr(回车) + lf(换行)
  2. Header: header field name(本机属性):value(属性值,与属性名用分号分隔) + cr + lf (Header 部分成对出现,一个 Key 对应一个 Value,用于告诉 Server 本机浏览器的特性)
  3. A Blank Line: cr + lf
  4. Body(optional)

常见 method:

  • get 取得网页内容
  • post 提交数据到服务器

访问 baidu.com 的请求日志:

  • 请求方法为 GET
  • URL 为 / (根目录)
  • HTTP 版本 1.1
  • 浏览器属性值(键值对)

HTTP Response

  1. Status Line: HTTP version + sp + status code + sp + phrase(状态描述短语) + cr + lf
  2. Header: header field name:value + cr + lf (用于描述服务器端 HTTP 属性)
  3. A Blank Line: cr + lf
  4. Body(optional): 用于返回 HTTP 网页内容

常见 Status Code:

  • 200 处理成功
  • 301 重定向
  • 404 未找到对应网页

访问 baidu_com 的应答日志:

  • HTTP 版本 1.1
  • status code 为 200
  • phrase 为 OK
  • 服务器属性(键值对)

应答主题:html 内容

HTTP/1.1 是互联网工程任务组(IETF)在 RFC 2616(后更新为RFC 7230-7235)中标准化的应用层协议版本,旨在解决 HTTP/1.0 在高延迟与低带宽环境下的连接效率缺陷

URL

example

text 复制代码
https://user:pass@www.google.com:443/search/index.html?q=http+protocol&newwindow=1#main-content

示例 URL,在实际使用过程中部分元素不再使用

URI Component Example Technical Description
Protocol (协议) https 1. http 超文本传输协议 2. https 超文本传输安全协议,采用 TLS/SSL 加密 3. ftp 文件传输协议
Authority (用户信息) user:pass 此格式用于基本身份验证(Basic Auth);因安全性考量,在现代公网环境(比如 Google 主站)中极少使用
Host/Domain (域名) www.google.com 网络主机的全限定域名(FQDN);需通过 DNS 解析映射为具体的服务器 IP 地址
Port (端口) 443 TCP 连接的目标端口。 1. 80 HTTP 默认端口 2. 443 HTTPS 默认端口 3. 21 ftp 命令端口 4. 20 ftp 数据端口
Path (路径) /search/index.html 服务器端的资源层级路径;用于精确定位具体的处理程序或文件资源
Query String (查询参数) ?q=http... ? 起始,包含键值对(Key-Value Pairs);用于向服务器传递动态参数(如搜索关键词)
Anchor/Fragment (片段) #main-content 客户端侧的资源定位符;用于指示浏览器滚动到文档中的特定位置(如特定 ID 的 HTML 元素);该字段不会发送至服务器端

Http Server

使用示例:
https://github.com/espressif/esp-idf/blob/master/examples/protocols/http_server/simple/main/main.c

Initialization

Workflow

  1. nvs_flash_init() 初始化 nvs
  2. example_connect() 创建网络连接
  3. esp_event_loop_create_default() 创建事件处理 task
  4. esp_event_handler_register() 创建 IP 获取和连接断开回调 handler
  5. start_webserver() 启动 webserver
c 复制代码
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());

ESP_ERROR_CHECK(example_connect());

ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &connect_handler, &server)); // When got IP, start web server
// connect_handler can't get the first IP_EVENT_STA_GOT_IP event
// because example_connect() has already got IP before registering the event handler
// So we need to manually start the web server here if Wi-Fi is already connected
// This event is used for restarting the server when the connection is lost and re-established

ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnect_handler, &server)); // When disconnected, stop web server

server = start_webserver(); // Start web server manually

IP_EVENT_STA_GOT_IP 事件的回调函数是在连接建立后创建的,因此并不会捕捉到第一次 IP 获取 event,第一次 webserver 需要手动启动;对于后续连接断开后重新获取 IP 时,webserver 可以通过这个回调函数创建

start_webserver() 函数中通过 httpd_register_uri_handler() 函数注册了对于不同 HTTP method 的处理方法

Get

Get
Browser
Server

text 复制代码
// Input URL in browser
// Get IP address and port from serial monitor
http://192.168.137.138/hello?number=1&id=3

// IP example
I (4410) example_common: - IPv4 address: 192.168.137.138,
I (4430) example: Starting server on port: '80'

GET 示例发送及返回参数

c 复制代码
httpd_register_uri_handler(server, &hello);

/* ---------------- */

// hello definition
static const httpd_uri_t hello = {
    .uri = "/hello",              // URI path
    .method = HTTP_GET,           // HTTP method
    .handler = hello_get_handler, // Handler function
    /* Let's pass response string in user
     * context to demonstrate it's usage */
    .user_ctx = "Hello World!"}; // Response string
  • hello_get_handler() 函数中,通过 httpd_req_get_hdr_value_len() 获取 host 头部长度,httpd_req_get_hdr_value_str() 获取 host 头部内容
  • httpd_req_get_hdr_value_len() 获取 browser 发送的浏览器特征值长度,httpd_req_get_hdr_value_str() 获取浏览器特征值内容,可以通过重复调用这两个 API 获取多个浏览器特征值
  • httpd_req_get_url_query_len() 获取 URL 中查询参数的长度,httpd_query_key_value() 获取查询参数内容,可以通过重复调用这两个 API 获取多个查询参数
    • 在这一步可以通过判断查询参数内容,定义不同的响应逻辑和 req->user_ctx 中的响应内容
  • httpd_resp_set_hdr() 设置响应头部内容,如服务器属性
  • const char *resp_str = (const char *)req->user_ctx 将定义的响应内容传入参数,httpd_resp_send() 发送响应内容给 browser
  • httpd_req_get_hdr_value_len() 检查请求头是否被清楚(在实际项目中不需要)

在 HTTP Server 的文档中明确说明:调用 httpd_resp_send() 或 httpd_resp_send_chunk() 之后,请求头会被清空,如果之后还需要用到请求头,必须事先拷贝出来
httpd_resp_send() 一旦被调用,则表示

  1. 该请求已被响应
  2. 不能再为该请求发送额外数据
  3. 一旦调用该 API,请求头会被清空,若后续还需要使用请求头,必须在调用前将其拷贝出来
Post

post 方法用于提交数据到网页服务器,服务器对数据进行处理后返回相应结果

对 browser 通常使用表单的形式提交数据

先使用 get 方法获取含有表单的页面,填写后提交表单,服务器接收数据并处理,最后返回处理结果页面

(Top) → Component config → HTTP Server 中修改最大报文长度(1024)

填写表单内容,并提交

Post 示例定义

c 复制代码
httpd_register_uri_handler(server, &echo);

/* ---------------- */

// hello definition
static const httpd_uri_t echo = {
    .uri = "/echo",
    .method = HTTP_POST,
    .handler = echo_post_handler,
    .user_ctx = NULL};

定义表单页面

c 复制代码
char formHtml[] = R"(
    <!DOCTYPE html>
    <html>
        <body>
            <form action = "/echo" method = "post">
                name: <input type = "text" name = "name"><br>
                age: <input type = "text" name = "age"><br>
                <input type = "submit" value = "Submit">
            </form>
        </body>
    </html>
)"; // R"()" means raw string literal
// Raw string means special characters like \n are not treated specially

/* ---------------- */
// When the query parameter in Get method is "post", set the response to formHtml
if (httpd_query_key_value(buf, "post", param, sizeof(param)) == ESP_OK) // "query2"
{
    ESP_LOGI(TAG, "Found URL query parameter => query2=%s", param);
    example_uri_decode(dec_param, param, strnlen(param, EXAMPLE_HTTP_QUERY_KEY_MAX_LEN));
    ESP_LOGI(TAG, "Decoded query parameter => %s", dec_param);
    req->user_ctx = formHtml;
}

R"()" 是 C 语言中的原始字符串字面量(Raw String Literal)语法,用于定义包含特殊字符(如换行符 \n、制表符 \t 等)的多行字符串,而无需对这些字符进行转义处理

  • Browser 通过 Get 方法获取表单
  • 填写表单内容后,Browser 通过 Post 方法将数据提交到服务器
  • 服务器通过 httpd_req_recv() 接收表单数据
  • 处理数据后,服务器通过 httpd_resp_send_chunk() 将处理结果返回给 Browser

httpd_resp_send_chunk() 用于分块发送响应数据,适用于大数据量或动态生成的内容;httpd_resp_send() 则用于一次性发送完整响应数据,适用于小数据量的静态内容

HTML form 代码解析

Code Explanation
<!DOCTYPE html> 文档类型声明 (Document Type Declaration),该指令告知用户代理 (User Agent) 当前文档遵循 HTML5 标准,并强制浏览器使用标准模式 (Standards Mode) 进行渲染,避免怪异模式 (Quirks Mode)。
<html> 文档对象模型 (DOM) 的根元素 (Root Element),包含页面的所有子节点。
<body> 文档体元素。封装文档的所有可视化内容 (Visual Content) 和流式内容 (Flow Content)。
<form> 表单元素 (HTMLFormElement)。定义一个包含交互式控件的区域,用于构建并向服务器提交数据载荷。
action="/echo" 表单属性。指定数据提交的目标统一资源标识符 (URI) 端点。在此处,数据将被发送至服务器的 /echo 路径。
method="post" 表单属性。指定 HTTP 传输协议方法为 POST。意味着表单数据将被序列化并封装在 HTTP 请求体 (Request Body) 中传输,而非作为查询字符串 (Query String) 附加在 URL 后。
name: / age: 文本节点 (Text Node)。在 DOM 中作为静态文本呈现,用于辅助用户识别输入框的语义。
<input type="text" ...> 输入控件元素 (HTMLInputElement)。type="text" 属性将其实例化为单行纯文本编辑器。
name="name" / name="age" 控件属性。定义表单数据集中键值对 (Key-Value Pair) 的键 (Key)。在表单提交时,服务器端将通过此标识符获取对应的用户输入值。
<br> 换行元素 (Line Break)。属于空元素 (Void Element),用于在行内强制插入一个换行符。
<input type="submit" ...> 提交按钮控件。type="submit" 属性指示该控件在被激活时触发 submit 事件,执行表单数据的序列化与传输过程。
value="Submit" 控件属性。定义按钮 UI 组件上渲染的文本标签内容。
Put

Put method 用于向 Server 上传数据或指令

c 复制代码
static const httpd_uri_t ctrl = {
    .uri = "/ctrl",
    .method = HTTP_PUT,
    .handler = ctrl_put_handler,
    .user_ctx = NULL};

可以通过在 Browser 中的 Console 使用 cURL 命令发送 PUT 请求

javascript 复制代码
/**
 * 实例化 XMLHttpRequest 构造函数。
 * 此操作在堆内存中创建一个 XHR 客户端对象实例,用于在客户端(浏览器)与服务器之间建立基于 HTTP 协议的异步通信通道。
 * 此时该对象的 readyState 属性状态为 0 (UNSENT)。
 */
var xhr = new XMLHttpRequest();

/**
 * 初始化请求参数 (Request Initialization)。
 * 调用 open 方法配置 HTTP 事务的元数据,但不建立实际的网络连接:
 * 1. "PUT": 指定 HTTP 请求方法 (Method)。PUT通常用于向服务器上传资源或进行幂等的更新操作。
 * 2. "http://192.168.137.60/ctrl": 指定目标的统一资源标识符 (URI),包含具体的 IPv4 地址及资源路径。
 * 3. true: 设置异步执行标志 (Asynchronous Flag)。指示浏览器在非阻塞 (Non-blocking) 模式下执行该 I/O 操作,防止主线程(UI 线程)挂起。
 * 此操作执行后,readyState 属性状态变更为 1 (OPENED)。
 */
xhr.open("PUT", "http://192.168.137.60/ctrl", true);

/**
 * 发送 HTTP 请求 (Dispatch Request)。
 * 该方法触发实际的网络传输层操作,建立 TCP 连接并发送 HTTP 报文。
 * 参数 "0": 作为请求体 (Request Body) 的有效负载 (Payload) 发送至服务端。
 * 由于前置配置为异步模式,该语句执行后会立即返回,后续的 HTTP 状态变更(如 Headers Received, Loading, Done)将通过事件循环机制处理。
 */
xhr.send("0");

当服务器接收到 PUT 请求的内容为 "0" 时,关闭 /hello/echo 两个 URI 的处理,并注册 err handler (HTTPD_404_NOT_FOUND),此时访问 /hello/echo 会返回 404 错误

text 复制代码
I (861651) example: Unregistering /hello and /echo URIs
I (946021) example: Registering /hello and /echo URIs

这个示例项目中还提供了一个 any_handler,对于任意发送到 /any 的请求,都会返回 "Hello, World!" 内容

Http Client

使用示例:
https://github.com/espressif/esp-idf/tree/master/examples/protocols/http_request

除了作为 Server 端,ESP32 还可以作为 Http Client 向其他 Http Server 发送请求

  • example_connect() 创建网络连接
  • getaddrinfo() 通过域名获取目标服务器 IP 地址
  • socket() 创建 socket
  • connect() 连接服务器
  • write() 发送 Http 请求
  • setsockopt() 设置 socket 选项,如超时
  • bzero() 清空接收缓冲区
  • read() 接收服务器响应
c 复制代码
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());

ESP_ERROR_CHECK(example_connect());

xTaskCreate(&http_get_task, "http_get_task", 4096, NULL, 5, NULL);

/* --- http_get_task --- */
int err = getaddrinfo(WEB_SERVER, WEB_PORT, &hints, &res); // Get ip address from domain name
s = socket(res->ai_family, res->ai_socktype, 0); // Create a socket
connect(s, res->ai_addr, res->ai_addrlen); // Connect to server
write(s, REQUEST, strlen(REQUEST)); // Send HTTP request
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &receiving_timeout, sizeof(receiving_timeout)); // Set socket receive timeout
bzero(recv_buf, sizeof(recv_buf));
r = read(s, recv_buf, sizeof(recv_buf) - 1); // Read the response

/* --- REQUEST definition --- */
#define WEB_SERVER "www.weather.com.cn"
#define WEB_PORT "80"
#define WEB_PATH "/data/sk/101010100.html"

static const char *REQUEST = "GET " WEB_PATH " HTTP/1.0\r\n"
                             "Host: " WEB_SERVER ":" WEB_PORT "\r\n"
                             "User-Agent: esp-idf/1.0 esp32\r\n"
                             "\r\n";
// Request body is empty

/**
 * Example URL:
 * https://www.weather.com.cn/data/sk/101010100.html
 * This is a URL from China Meteorological Administration(CMA - 中国气象局)
 * It provides the weather information of Beijing City(北京)
 * Change the city code "101010100" in the URL to get the weather
 * information from https://www.cnblogs.com/jiangxiaobo/p/5849953.html
 * Now, the server will return code 301 means "Moved Permanently"
 * The server needs to be connected via HTTPS
 * So this example may not get the weather information correctly
 * But it still can demonstrate how to use HTTP GET request with POSIX sockets
 */

由于中国气象局现已强制要求通过 HTTPS 协议访问其天气数据接口,直接使用 HTTP GET 请求将会收到 301 永久重定向响应,但是仍旧可以通过该示例了解如何使用 lwIP 套接字进行 HTTP GET 请求的基本流程;通过检查返回值查看 reuqest 是否成功

text 复制代码
I (4465) example: DNS lookup succeeded. IP=58.205.193.77
I (4465) example: ... allocated socket
I (4485) example: ... connected
I (4485) example: ... socket send success
I (4485) example: ... set socket receiving timeout success
HTTP/1.1 301 Moved Permanently
Server: openresty
Date: Tue, 27 Jan 2026 12:19:36 GMT
Content-Type: text/html
Content-Length: 166
Connection: close
Location: https://www.weather.com.cn/data/sk/101010100.html
via: CHN-GDguangzhou-SSPedu2-CACHE2[0]
Permissions-Policy: geolocation=none, fullscreen=none, microphone=none, camera=none, clipboard-read=none
Set-Cookie: sessionId=uniqueSessionIdValue; HttpOnly; Secure; SameSite=Strict

<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>openresty</center>
</body>
</html>
I (31135) wifi:<ba-add>idx:0 (ifx:0, 72:32:17:b7:ad:d1), tid:0, ssn:10, winSize:64
相关推荐
桌面运维家2 小时前
vDisk考场环境网络瓶颈怎么定位?快速排查指南
运维·服务器·网络
70asunflower2 小时前
Zotero论文阅读标记颜色框架
人工智能·学习·考研
兆龙电子单片机设计2 小时前
【STM32项目开源】STM32单片机智能宠物管家
stm32·单片机·物联网·开源·毕业设计·宠物
谢怜822 小时前
计算机网络第四章网络层
网络·计算机网络
测试_AI_一辰2 小时前
Agent & RAG 测试工程 03:第一次为 RAG 写回归测试:防幻觉、保一致、守底线
人工智能·笔记·功能测试·测试用例·ai编程
阿呀呀呀2 小时前
ESP32复位电路分析
单片机·嵌入式硬件
xhbaitxl2 小时前
算法学习day29-贪心算法
学习·算法·贪心算法
xqqxqxxq2 小时前
《智能仿真无人机平台(多线程V3.0)技术笔记》
笔记·无人机·cocos2d
我在人间贩卖青春2 小时前
IP协议及以太网协议
网络·网络协议·tcp/ip