只看HTTP 层可以先记住一句话HTTP 层没有像 UART 那种固定“帧头/帧尾/校验”的二进制帧格式。HTTP/1.1 主要是文本协议格式是起始行 头部字段 空行 可选正文 Body。天气案例里ESP32 发的是HTTP Request 请求报文服务器回的是HTTP Response 响应报文。1. HTTP 请求报文格式HTTP 请求格式请求行\r\n 请求头1\r\n 请求头2\r\n 请求头3\r\n ...\r\n \r\n 请求体 Body也就是┌──────────────────────────────┐ │ Request Line 请求行 │ ├──────────────────────────────┤ │ Headers 请求头 │ ├──────────────────────────────┤ │ 空行 \r\n │ ├──────────────────────────────┤ │ Body 请求体可选 │ └──────────────────────────────┘天气 GET 请求通常没有 Body。2. 天气案例 HTTP GET 请求比如 ESP32 请求大阪天气GET /data/2.5/weather?qOsakaappidYOUR_API_KEYunitsmetriclangzh_cn HTTP/1.1 Host: api.openweathermap.org User-Agent: esp32 Connection: close注意最后必须有一个空行。在 C 字符串里要写成constchar*http_requestGET /data/2.5/weather?qOsakaappidYOUR_API_KEYunitsmetriclangzh_cn HTTP/1.1\r\nHost: api.openweathermap.org\r\nUser-Agent: esp32\r\nConnection: close\r\n\r\n;3. 请求行格式请求行格式Method SP Request-URI SP HTTP-Version CRLF对应天气请求GET /data/2.5/weather?qOsakaappidYOUR_API_KEYunitsmetriclangzh_cn HTTP/1.1拆开看GET /data/2.5/weather?qOsakaappidYOUR_API_KEYunitsmetriclangzh_cn HTTP/1.1含义字段含义GET请求方法表示获取数据/data/2.5/weather?...请求资源路径和参数HTTP/1.1HTTP 协议版本其中SP就是空格CRLF就是\r\n。4. 请求头格式每个请求头格式都是Header-Name: Header-Value\r\n比如Host: api.openweathermap.org User-Agent: esp32 Connection: close这些头部字段的作用请求头作用Host告诉服务器你访问的是哪个域名User-Agent告诉服务器客户端是谁Connection: close请求完成后关闭连接HTTP/1.1 里Host基本是必须的。5. 空行很重要请求头结束后要有一个空行\r\n完整请求末尾实际是Connection: close\r\n \r\n这个空行的意思是请求头结束了后面如果还有内容就是 Body。天气 GET 请求没有 Body所以空行后面就没东西了。6. HTTP 响应报文格式服务器返回格式状态行\r\n 响应头1\r\n 响应头2\r\n 响应头3\r\n ...\r\n \r\n 响应体 Body结构是┌──────────────────────────────┐ │ Status Line 状态行 │ ├──────────────────────────────┤ │ Headers 响应头 │ ├──────────────────────────────┤ │ 空行 \r\n │ ├──────────────────────────────┤ │ Body 响应体 │ └──────────────────────────────┘7. 天气案例 HTTP 响应服务器可能返回HTTP/1.1 200 OK Content-Type: application/json; charsetutf-8 Content-Length: 168 Connection: close { weather: [ { main: Clouds, description: 多云 } ], main: { temp: 23.5, humidity: 60 }, name: Osaka }8. 状态行格式状态行格式HTTP-Version SP Status-Code SP Reason-Phrase CRLF比如HTTP/1.1 200 OK拆开字段含义HTTP/1.1HTTP 协议版本200状态码OK状态描述常见状态码状态码含义200 OK请求成功301/302重定向400 Bad Request请求格式或参数错误401 UnauthorizedAPI Key 错误或无权限403 Forbidden禁止访问404 Not FoundAPI 路径错误500 Internal Server Error服务器内部错误9. 响应头格式响应头也是Header-Name: Header-Value\r\n比如Content-Type: application/json; charsetutf-8 Content-Length: 168 Connection: close含义响应头含义Content-TypeBody 的数据类型这里是 JSONContent-LengthBody 的长度单位字节Connection连接控制10. 响应体 Body空行后面的内容就是 Body{weather:[{main:Clouds,description:多云}],main:{temp:23.5,humidity:60},name:Osaka}ESP32 真正要解析的是这一部分。也就是HTTP 响应头告诉你返回了什么 HTTP 响应体真正的天气数据11. HTTP GET 请求完整字节格式天气 GET 请求在 HTTP 层实际就是一串 ASCII 字符GET /data/2.5/weather?qOsakaappidYOUR_API_KEYunitsmetriclangzh_cn HTTP/1.1\r\n Host: api.openweathermap.org\r\n User-Agent: esp32\r\n Connection: close\r\n \r\n如果把换行展开看GET /data/2.5/weather?qOsakaappidYOUR_API_KEYunitsmetriclangzh_cn HTTP/1.1[CRLF] Host: api.openweathermap.org[CRLF] User-Agent: esp32[CRLF] Connection: close[CRLF] [CRLF][CRLF]就是\r\n对应十六进制0D 0A所以 HTTP 头部每行结尾都是0D 0A头部结束是两个连续的 CRLF0D 0A 0D 0A12. HTTP 响应完整格式服务器响应在 HTTP 层也是文本加 BodyHTTP/1.1 200 OK\r\n Content-Type: application/json; charsetutf-8\r\n Content-Length: 168\r\n Connection: close\r\n \r\n {weather:[{main:Clouds,description:多云}],main:{temp:23.5,humidity:60},name:Osaka}可以理解为响应头部分 HTTP/1.1 200 OK\r\n Content-Type: application/json; charsetutf-8\r\n Content-Length: 168\r\n Connection: close\r\n \r\n 响应体部分 {weather:[...],main:{...},name:Osaka}13. GET 请求为什么没有 Body天气查询是获取数据一般参数都放在 URL 后面/data/2.5/weather?qOsakaappidYOUR_API_KEYunitsmetric所以 GET 请求通常没有 Body。GET 请求 参数在 URL 里 Body 一般为空14. 如果是 POSTHTTP 层格式会不一样比如 ESP32 上传传感器数据POST /api/weather/upload HTTP/1.1 Host: example.com Content-Type: application/json Content-Length: 31 Connection: close {temp:23.5,humidity:60}POST 报文结构请求行 请求头 空行 请求体其中请求体是{temp:23.5,humidity:60}POST 必须告诉服务器 Body 多长所以经常有Content-Length: 3115. Content-Length 是干啥的比如响应Content-Length: 168意思是空行后面的 Body 有 168 字节。ESP32 接收时可以根据这个判断 Body 有没有收完整。如果没有Content-Length也可能用Transfer-Encoding: chunked16. Chunked 格式是什么有些服务器不知道一次返回多大就会分块返回HTTP/1.1 200 OK Content-Type: application/json Transfer-Encoding: chunked A 0123456789 5 abcde 0格式是块大小十六进制 块内容 块大小十六进制 块内容 0 结束对新手来说你先知道Content-Length提前告诉你 Body 总长度 chunked一块一块发最后用 0 表示结束如果你用esp_http_client很多细节它会帮你处理。如果你自己 socket 手写 HTTP就要注意这个坑。17. 只看 HTTP 层请求和响应总结HTTP 请求┌────────────────────────────────────────────┐ │ 请求行 │ │ GET /data/2.5/weather?... HTTP/1.1 │ ├────────────────────────────────────────────┤ │ 请求头 │ │ Host: api.openweathermap.org │ │ User-Agent: esp32 │ │ Connection: close │ ├────────────────────────────────────────────┤ │ 空行 │ │ \r\n │ ├────────────────────────────────────────────┤ │ 请求体 │ │ GET 一般为空 │ └────────────────────────────────────────────┘HTTP 响应┌────────────────────────────────────────────┐ │ 状态行 │ │ HTTP/1.1 200 OK │ ├────────────────────────────────────────────┤ │ 响应头 │ │ Content-Type: application/json │ │ Content-Length: xxx │ │ Connection: close │ ├────────────────────────────────────────────┤ │ 空行 │ │ \r\n │ ├────────────────────────────────────────────┤ │ 响应体 │ │ {main:{temp:23.5},weather:[...]} │ └────────────────────────────────────────────┘18. 你作为嵌入式开发要重点记住HTTP 层最重要的是这几个1. 请求行 / 状态行 2. Header 头部 3. 空行 \r\n 4. Body 5. GET 通常没有 Body 6. POST 通常有 Body 7. Body 长度由 Content-Length 或 chunked 决定 8. 天气 JSON 在响应 Body 里面一句话总结HTTP 请求 你问服务器要什么 HTTP 响应 服务器告诉你结果并把数据放在 Body 里天气案例中ESP32 HTTP Request GET /data/2.5/weather?... HTTP/1.1 服务器 HTTP Response HTTP/1.1 200 OK ... 天气 JSON 数据更详细的底层原理天气案例里HTTP 请求和应答不是直接裸奔在 Wi-Fi 上的它是一层一层封装的。你可以这样理解天气 JSON ↑ HTTP 协议 ↑ TCP 协议 ↑ IP 协议 ↑ Wi-Fi / 以太网 ↑ 无线电波 / 网线对 ESP32 来说获取天气时真正发出去的数据大概是[Wi-Fi帧头] [IP头] [TCP头] [HTTP请求内容]服务器返回时也是[Wi-Fi帧头] [IP头] [TCP头] [HTTP响应内容]1. 天气 HTTP 请求完整流程假设 ESP32 要访问http://api.openweathermap.org/data/2.5/weather?qOsakaappidAPI_KEYunitsmetriclangzh_cn完整过程是ESP32 连接 Wi-Fi ↓ DNS 解析 api.openweathermap.org 得到服务器 IP ↓ TCP 三次握手连接服务器 80 端口 ↓ 发送 HTTP GET 请求 ↓ 服务器返回 HTTP 响应 ↓ ESP32 解析 JSON ↓ TCP 连接关闭2. 协议栈分层格式从上到下看应用层HTTP 传输层TCP 网络层IP 链路层Wi-Fi / Ethernet 物理层无线信号天气请求封装后大概是这样┌────────────────────────────────────┐ │ Wi-Fi / Ethernet 帧头 │ ← 局域网传输用 ├────────────────────────────────────┤ │ IP Header │ ← 源 IP、目标 IP ├────────────────────────────────────┤ │ TCP Header │ ← 源端口、目标端口、序号 ├────────────────────────────────────┤ │ HTTP Request │ ← GET 天气请求 └────────────────────────────────────┘HTTP 响应也是类似┌────────────────────────────────────┐ │ Wi-Fi / Ethernet 帧头 │ ├────────────────────────────────────┤ │ IP Header │ ├────────────────────────────────────┤ │ TCP Header │ ├────────────────────────────────────┤ │ HTTP Response │ ← 天气 JSON 数据 └────────────────────────────────────┘3. HTTP 请求报文格式HTTP 请求本身长这样GET /data/2.5/weather?qOsakaappidAPI_KEYunitsmetriclangzh_cn HTTP/1.1 Host: api.openweathermap.org User-Agent: esp32 Connection: close注意最后有一个空行。标准格式是请求行 请求头1 请求头2 请求头3 空行 请求体对于 GET 请求一般没有请求体。所以天气 HTTP GET 请求可以拆成请求行 GET /data/2.5/weather?qOsakaappidAPI_KEYunitsmetriclangzh_cn HTTP/1.1 请求头 Host: api.openweathermap.org User-Agent: esp32 Connection: close 空行 \r\n 请求体 GET 请求通常为空4. HTTP 请求行格式这一行GET /data/2.5/weather?qOsakaappidAPI_KEYunitsmetriclangzh_cn HTTP/1.1拆开是GET 请求方法 /data/2.5/weather?... 请求路径和参数 HTTP/1.1 HTTP 协议版本也就是方法 空格 URL路径 空格 HTTP版本 \r\n真正格式GET /xxx/xxx?参数1值1参数2值2 HTTP/1.1\r\n5. HTTP 请求头格式请求头每一行格式是字段名: 字段值\r\n比如Host: api.openweathermap.org User-Agent: esp32 Connection: close对应含义请求头含义Host要访问哪个网站/服务器User-Agent客户端身份这里是 ESP32Connection: close服务器返回后关闭连接HTTP 请求头结束后必须有一个空行\r\n所以完整 HTTP 请求在 C 语言里一般写成这样constchar*requestGET /data/2.5/weather?qOsakaappidAPI_KEYunitsmetriclangzh_cn HTTP/1.1\r\nHost: api.openweathermap.org\r\nUser-Agent: esp32\r\nConnection: close\r\n\r\n;\r\n是 HTTP 协议要求的换行符。6. 这个 HTTP 请求在 TCP 里长什么样HTTP 本身只是 TCP 负载。TCP 包大概这样┌────────────────────────────────────┐ │ TCP Header │ │ - 源端口随机端口例如 54321 │ │ - 目标端口80 │ │ - 序号 seq │ │ - 确认号 ack │ │ - 标志位 PSH ACK │ ├────────────────────────────────────┤ │ TCP Payload │ │ GET /data/2.5/weather?... HTTP/1.1 │ │ Host: api.openweathermap.org │ │ User-Agent: esp32 │ │ Connection: close │ │ │ └────────────────────────────────────┘也就是说TCP 不知道你这是天气请求。 TCP 只知道我要把这一串字节可靠地送到服务器 80 端口。HTTP 的内容在 TCP 的数据区里。7. 这个 TCP 包在 IP 里长什么样IP 包大概这样┌────────────────────────────────────┐ │ IP Header │ │ - 源 IPESP32 的 IP比如 192.168.1.20 │ - 目标 IP天气服务器 IP │ - 协议号6表示 TCP │ - TTL │ - 总长度 ├────────────────────────────────────┤ │ TCP Segment │ │ ┌────────────────────────────────┐ │ │ │ TCP Header │ │ │ ├────────────────────────────────┤ │ │ │ HTTP GET 请求 │ │ │ └────────────────────────────────┘ │ └────────────────────────────────────┘IP 层关心的是从哪个 IP 来 要发到哪个 IP 去 上层协议是什么对 HTTP 来说上层协议是 TCP所以 IP Header 里Protocol 68. 这个 IP 包在 Wi-Fi 里长什么样ESP32 用 Wi-Fi 发出去时还会再加 Wi-Fi 帧头。简化后┌────────────────────────────────────┐ │ Wi-Fi MAC Header │ │ - 源 MACESP32 的 MAC │ │ - 目标 MAC路由器/AP 的 MAC │ ├────────────────────────────────────┤ │ LLC/SNAP │ ├────────────────────────────────────┤ │ IP Packet │ │ ┌────────────────────────────────┐ │ │ │ IP Header │ │ │ ├────────────────────────────────┤ │ │ │ TCP Header │ │ │ ├────────────────────────────────┤ │ │ │ HTTP GET 请求 │ │ │ └────────────────────────────────┘ │ ├────────────────────────────────────┤ │ FCS 校验 │ └────────────────────────────────────┘你可以先不用纠结 Wi-Fi 帧头细节。嵌入式软件开发时重点先掌握HTTP 在 TCP 里面 TCP 在 IP 里面 IP 在 Wi-Fi / Ethernet 里面9. 服务器返回的 HTTP 响应格式服务器返回大概是HTTP/1.1 200 OK Content-Type: application/json; charsetutf-8 Content-Length: 180 Connection: close { weather: [ { main: Clouds, description: 多云 } ], main: { temp: 23.5, humidity: 60 }, name: Osaka }HTTP 响应格式是状态行 响应头1 响应头2 响应头3 空行 响应体10. HTTP 响应状态行这一行HTTP/1.1 200 OK拆开是HTTP/1.1 协议版本 200 状态码 OK 状态说明常见状态码状态码含义200 OK请求成功301/302重定向400请求参数错误401API Key 错误或无权限404路径不存在500服务器内部错误11. HTTP 响应头比如Content-Type: application/json; charsetutf-8 Content-Length: 180 Connection: close含义是响应头含义Content-Type返回的数据类型这里是 JSONContent-Length响应体长度Connection连接是否关闭12. HTTP 响应体空行后面的才是真正的天气数据{weather:[{main:Clouds,description:多云}],main:{temp:23.5,humidity:60},name:Osaka}ESP32 最后用cJSON解析的是这部分。你关心的是main.temp 温度 main.humidity 湿度 weather[0].description 天气描述 name 城市名13. 响应在协议栈里怎么封装服务器返回时方向反过来。服务器应用层生成 HTTP 响应 ↓ TCP 封装 ↓ IP 封装 ↓ 链路层封装 ↓ 互联网传输 ↓ 路由器 ↓ Wi-Fi 发给 ESP32ESP32 收到后逐层拆包Wi-Fi 驱动收到无线帧 ↓ 取出 IP 包 ↓ IP 层发现这是发给自己的 ↓ 取出 TCP 段 ↓ TCP 层重组数据流 ↓ 交给 HTTP client ↓ HTTP client 得到响应头和响应体 ↓ 你的代码拿到 JSON14. 请求和应答的完整协议栈格式对比ESP32 发请求Wi-Fi Frame └── IP Packet └── TCP Segment └── HTTP Request ├── Request Line │ └── GET /data/2.5/weather?... HTTP/1.1 ├── Headers │ ├── Host: api.openweathermap.org │ ├── User-Agent: esp32 │ └── Connection: close ├── Empty Line └── Body: 空服务器回响应Wi-Fi Frame └── IP Packet └── TCP Segment └── HTTP Response ├── Status Line │ └── HTTP/1.1 200 OK ├── Headers │ ├── Content-Type: application/json │ ├── Content-Length: xxx │ └── Connection: close ├── Empty Line └── Body └── {main:{temp:23.5},weather:[...]}15. 加上 TCP 三次握手完整通信长这样HTTP 请求不是上来就发的先要建立 TCP 连接。ESP32 天气服务器 │ │ │ SYN │ │────────────────────────────────────────────│ │ │ │ SYN ACK │ │────────────────────────────────────────────│ │ │ │ ACK │ │────────────────────────────────────────────│ │ │ │ HTTP GET /data/2.5/weather?... │ │────────────────────────────────────────────│ │ │ │ HTTP/1.1 200 OK JSON天气数据 │ │────────────────────────────────────────────│ │ │ │ FIN / ACK 关闭连接 │ │───────────────────────────────────────────│所以真正完整顺序是DNS 解析 TCP 三次握手 HTTP 请求 HTTP 响应 TCP 四次挥手/关闭连接16. DNS 解析也是协议栈的一部分你写的是域名api.openweathermap.org但 TCP/IP 真正连接的是 IP 地址。所以 ESP32 要先问 DNS 服务器api.openweathermap.org 的 IP 是多少DNS 查询通常是应用层DNS 传输层UDP 网络层IP 链路层Wi-Fi格式大概是Wi-Fi Frame └── IP Packet └── UDP Datagram └── DNS Query └── 查询 api.openweathermap.orgDNS 返回 IP 后ESP32 才能建立 TCP 连接。17. 如果是 HTTPS协议栈格式变成什么HTTPWi-Fi └── IP └── TCP └── HTTPHTTPSWi-Fi └── IP └── TCP └── TLS └── HTTP也就是中间多了一层 TLS。HTTPS 请求真实格式不是TCP 里面直接放 HTTP GET而是TCP 里面放 TLS 加密数据 TLS 解密后才是 HTTP GET18. HTTPS 天气请求封装格式Wi-Fi Frame └── IP Packet └── TCP Segment └── TLS Record └── Encrypted HTTP Request └── GET /data/2.5/weather?... HTTP/1.1服务器响应Wi-Fi Frame └── IP Packet └── TCP Segment └── TLS Record └── Encrypted HTTP Response └── HTTP/1.1 200 OK JSON抓包时HTTP 可以看到明文GET /data/2.5/weather?qOsakaappidAPI_KEY HTTP/1.1 Host: api.openweathermap.orgHTTPS 抓包时看到的是TLS Application Data 加密数据 加密数据 加密数据看不到具体天气请求和 API Key。19. HTTP 和 HTTPS 协议栈对比项目HTTPHTTPS默认端口80443应用层内容明文 HTTPHTTP 被 TLS 加密协议栈Wi-Fi/IP/TCP/HTTPWi-Fi/IP/TCP/TLS/HTTP是否能抓包看到 API Key能正常看不到ESP32 资源占用低更高正式产品推荐不推荐推荐20. 最核心的一张图天气 HTTP 请求ESP32 应用代码 ↓ HTTP GET 请求字符串 ↓ TCP 头 HTTP 数据 ↓ IP 头 TCP 数据 ↓ Wi-Fi 头 IP 数据 ↓ 无线发送出去天气 HTTP 响应无线收到数据 ↓ 去掉 Wi-Fi 头 ↓ 去掉 IP 头 ↓ TCP 重组数据 ↓ 得到 HTTP 响应 ↓ 去掉 HTTP 响应头 ↓ 得到 JSON 天气数据 ↓ cJSON 解析21. 你在 ESP32 代码里看到的是哪一层如果你用esp_http_clientesp_http_client_perform(client);你主要操作的是HTTP 层底层的DNS TCP IP Wi-Fi大部分 ESP-IDF 已经帮你处理了。如果你用 socket 手写socket()connect()send()recv()你操作的是TCP 层HTTP 请求字符串要你自己拼。如果你用 Wi-Fi 驱动esp_wifi_start()你操作的是Wi-Fi 链路层/网络接口22. 一句话总结天气案例中的 HTTP 协议栈格式就是Wi-Fi 帧 里面装 IP 包 IP 包里面装 TCP 段 TCP 段里面装 HTTP 请求/响应 HTTP 响应体里面装 JSON 天气数据最关键记住这句HTTP 不是直接在 Wi-Fi 上跑的。 HTTP 是 TCP 的数据内容。 TCP 又被 IP 封装。 IP 最后才通过 Wi-Fi 发出去。如果是 HTTPS就是Wi-Fi ↓ IP ↓ TCP ↓ TLS 加密层 ↓ HTTP ↓ JSON 天气数据