摘要
本笔记详述了 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
- Request Line: method (请求方法 get/post/put/ ...) + sp(空格) + URL + sp + HTTP version + cr(回车) + lf(换行)
- Header: header field name(本机属性):value(属性值,与属性名用分号分隔) + cr + lf (Header 部分成对出现,一个 Key 对应一个 Value,用于告诉 Server 本机浏览器的特性)
- A Blank Line: cr + lf
- Body(optional)
常见 method:
get取得网页内容post提交数据到服务器
访问 baidu.com 的请求日志:

- 请求方法为 GET
- URL 为 / (根目录)
- HTTP 版本 1.1
- 浏览器属性值(键值对)
HTTP Response
- Status Line: HTTP version + sp + status code + sp + phrase(状态描述短语) + cr + lf
- Header: header field name:value + cr + lf (用于描述服务器端 HTTP 属性)
- A Blank Line: cr + lf
- 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
nvs_flash_init()初始化 nvsexample_connect()创建网络连接esp_event_loop_create_default()创建事件处理 taskesp_event_handler_register()创建 IP 获取和连接断开回调 handlerstart_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()发送响应内容给 browserhttpd_req_get_hdr_value_len()检查请求头是否被清楚(在实际项目中不需要)
在 HTTP Server 的文档中明确说明:调用 httpd_resp_send() 或 httpd_resp_send_chunk() 之后,请求头会被清空,如果之后还需要用到请求头,必须事先拷贝出来
httpd_resp_send()一旦被调用,则表示
- 该请求已被响应
- 不能再为该请求发送额外数据
- 一旦调用该 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()创建 socketconnect()连接服务器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