教程 37:从 httpc_get_file_dns() 到 Body Callback——HTTP/HTTPS Client、DNS、altcp 与下载数据通路
摘要:从 lwIP HTTP client 公共入口追踪 DNS、altcp/TCP、GET、Header/Body callback,并说明 TLS allocator 如何把同一客户端切换成 HTTPS。
[TOC]
HTTP(Hypertext Transfer Protocol)是应用层 request/response 协议。**DNS(Domain Name System,域名系统)**负责把 Server hostname 解析成 IP address,altcp 是 lwIP 的 TCP-like transport abstraction,用于让同一应用逻辑选择 plain TCP 或 TLS-over-TCP。Stage 32 已经从 Server 方向建立 HTTP message 与 TCP byte-stream boundary,Stage 33 建立 TLS wrapper;Stage 37 从相反方向研究设备作为 Client 主动访问 Server 的路径:DNS、connect、GET、response Header/Body 与可选 HTTPS transport。S1S2S3S6S7
本文源码继续绑定用户提供的 lwip.zip 与 upstream commit d08f4773edd0182b7910fc8f046eed82ffcd67c9。主叙事是 Source-driven,真实公共入口选择 httpc_get_file_dns()。
0. 本篇所需的最小 HTTP/HTTPS Client 模型
Stage 32~33 是协议主讲位置,本篇不再复制整套 HTTP/TLS 教程;但即使不回看前文,也需要先恢复以下最小模型。
进入时序图前先锁定五个术语边界:
- GET 是 HTTP Method,表示获取 Request Target 指向的资源;当前
httpc生成 HTTP/1.1 GET request。S2S6 - HTTP status(例如
200)是 Server 对 request 的应用层结果;它与 DNS success、TCP connect success、TLS handshake success、最终 transfer completion 都不是同一个状态。S6 - Header/Body boundary 由 HTTP message framing 决定,不等于单个 TCP segment 或
pbuf的边界。当前 parser 会在连续 RX 数据中寻找 Header 结束,并把 Body 单独交给recv_fn。S2S7 HostHeader 指明 HTTP request 目标主机/authority;当前域名入口会把server_name同时用于 DNS/address 处理与 HTTPHostHeader 构造。S2S6Connection: Close是当前 request 明确发送的 HTTP Header,表示本次 response 完成后不复用这条 connection。源码也直接注明当前httpc尚不支持 persistent connection。S2S7- HTTPS 只改变 transport:HTTP request/response 仍保持同样的语义和 parser;
altcp是 lwIP 的 TCP-like transport abstraction,altcp_allocator决定创建 plain TCP 还是 TLS-over-TCP connection。S3S4
1 | sequenceDiagram |
“使用 TLS allocator”仍不等于已经完成完整 Web PKI identity verification。Client 需要合适的 CA/trust configuration、hostname verification,并在启用证书有效期检查时提供正确系统时间;这些安全边界会在后文回到当前 Mbed TLS port 的实际配置。S5S10
推荐进一步阅读 MDN HTTP messages、RFC 9110 与 RFC 9112,用于核对 HTTP 语义与 wire-format;正文后续仍直接解释当前源码真正依赖的字段和状态。S9S6S7
1. 为什么从 httpc_get_file_dns() 开始,而不是从某个 example 开始
当前 upstream 没有像 MQTT/SNTP 那样提供一条独立的 contrib/examples/http_client/... application 入口;HTTP client 的真实对外入口就在 public API 中。因此本篇 Source-driven 主线从:
1 | httpc_get_file_dns() |
开始,而不是人为制造一个不存在的 upstream demo。S1S2
public header 给出两条主要 GET 入口:
1 | err_t httpc_get_file(const ip_addr_t* server_addr, u16_t port, const char* uri, const httpc_connection_t *settings, |
区别很直接:
1 | httpc_get_file() |
因此本篇选 httpc_get_file_dns() 作为域名访问主入口。
2. 应用给 httpc 的四类信息分别控制什么
进入源码前先只解释 httpc_get_file_dns() 此刻真正用到的参数:S1
1 | server_name |
其中 httpc_connection_t 最关键:
1 | typedef struct _httpc_connection { |
altcp_allocator 就是 Stage 33 已经见过的 transport 注入点:
1 | NULL |
所以 HTTP 与 HTTPS 并不是两套 httpc parser。
3. 进入 httpc_get_file_dns():先创建 request state,再启动 DNS/Connect
下面是完整 public entry。S2
1 | err_t |
主流程因此先分成两步:
1 | flowchart LR |
这一点很重要:ERR_OK 表示“异步 HTTP request 已经成功启动”,不是“文件已经下载完成”。最终结果必须等待 result_fn。
4. 进入 httpc_init_connection_common():HTTP request 在 connect 前就已经生成
httpc_init_connection() 很薄,只转入 httpc_init_connection_common()。这个函数同时创建 state、构造 GET request、创建 altcp connection,并注册全部 callback。S2
先看对象关系:
1 | flowchart TD |
当前 httpc_state_t 保存:
pcb:当前 altcp connection;remote_addr/remote_port:连接目标;request:还没有发出的 HTTP GET request;rx_hdrs:Header 尚未收完整时的 pbuf chain;rx_status:HTTP status code;hdr_content_len/rx_content_len:Header 声明长度与实际接收 Body 长度;parse_state:当前正在等 status line、Header 还是 Body。
settings 本身不是深拷贝,而是以 pointer 保存到 req->conn_settings。因此应用不能用一个已经离开生命周期的临时 httpc_connection_t 给长期异步 request 使用;至少要保证它存活到 transfer 完成 callback。S2
5. httpc_create_request_string():当前 client 是一个刻意最小化的 HTTP/1.1 GET client
state 分配前,httpc_init_connection_common() 会两次调用 httpc_create_request_string():第一次用 buffer=NULL 计算长度,分配完成后第二次把 request 真正写入 PBUF_RAM。S2 先进入 request 构造函数:
1 | static int |
它根据 proxy 与 use_host 选择不同 format;普通 httpc_get_file_dns() 路径会使用带 Host header 的 HTTP/1.1 format。对应 request template 是:S2
1 | GET <uri> HTTP/1.1\r\n |
从源码开头的 TODO 与 request template 可以直接确定当前能力边界;GET/HTTP request-response 语义由 HTTP Semantics 定义,而这里具体生成的是 HTTP/1.1 message syntax。S2S6S7
- request method 固定为 GET;
- HTTP version 固定生成 HTTP/1.1;
- 明确发送
Connection: Close,当前不做 persistent connection; - 源码 TODO 明确还没有自动 follow redirect;
- 支持简单 HTTP proxy;
- Header parser 是轻量实现,不是完整通用 HTTP framework。
所以它非常适合:
1 | 下载一个明确资源 |
但不应该因为名字叫 HTTP Client 就推断它具备完整 Browser/cURL 功能。
6. 同一个初始化点决定 HTTP 还是 HTTPS:altcp_new() 看 allocator
httpc_init_connection_common() 中最关键的一行是:S2
1 | req->pcb = altcp_new(settings ? settings->altcp_allocator : NULL); |
altcp_new_ip_type() 的实现规则非常简单:S3
1 | allocator == NULL |
因此 HTTP/HTTPS 的分界点不是 request parser,而是 connection allocator:
1 | flowchart TD |
这就是 Stage 32~35 一直在建立的 altcp 价值:应用协议不需要重新实现一套 TLS 版本。
7. callback 在真正 connect 之前就全部注册完成
继续阅读 httpc_init_connection_common()。创建 transport 以后,callback、request buffer 和 application context 在同一个初始化函数里一起固定下来:S2
1 | req->pcb = altcp_new(settings ? settings->altcp_allocator : NULL); |
这里的顺序很关键:altcp_recv/err/poll/sent 在 DNS 和 connect 之前已经绑定,因此后续无论 plain TCP 还是 TLS outer connection,都通过同一个 httpc_state_t 回到这组 callback。
因此后面出现:
1 | connected |
都能通过同一个 httpc_state_t 找回当前 request state。
这条初始化链要在第一次使用 callback 前说明清楚,否则后面的 httpc_tcp_recv() 会显得像凭空出现。
8. 回到 httpc_get_file_dns():进入 httpc_get_internal_dns()
state 与 callback 都准备好后,httpc_get_file_dns() 调用 httpc_get_internal_dns()。S2
继续进入 httpc_get_internal_dns();LWIP_DNS=1 时,resolver 的 callback 和 req context 就在这里绑定:S2
1 | static err_t |
ERR_OK 表示 cache/IP string 已立即得到地址;ERR_INPROGRESS 表示函数先返回,等待 resolver 日后调用 httpc_dns_found()。继续进入这个 callback:S2
1 | static void |
异步 DNS 成功后 callback 直接调用 httpc_get_internal_addr();失败则通过 httpc_close() 把 HTTPC_RESULT_ERR_HOSTNAME 送到最终 result callback。于是三种结果是:
1 | flowchart TD |
当 LWIP_DNS=0,这个函数只能尝试把 server_name 当成 IP address string 解析;普通域名不会神奇地被解析。S2
Stage 14 已经完整讲过 DNS cache / UDP / async callback,本篇只保留这条 bridge。
9. DNS 返回以后进入 httpc_get_internal_addr():开始真正的 transport connect
httpc_dns_found() 成功时直接调用 httpc_get_internal_addr(req, ipaddr)。下面进入这个函数。S2
1 | static err_t |
如果是 plain HTTP:
1 | altcp_connect() |
如果是 TLS allocator:
1 | altcp_connect(TLS outer) |
也就是说,HTTPS 模式下 httpc_tcp_connected() 和 Stage 35 的 mqtt_tcp_connect_cb() 一样,看到的是 altcp transport 已 ready,而不只是裸 TCP 三次握手刚结束。S4S5
10. 进入 httpc_tcp_connected():GET request 只发送一次
transport connect 成功后进入 httpc_tcp_connected()。S2
1 | static err_t |
这里有两个明确的实现选择:
- 整个 request 在 connect 前已经准备好;
altcp_write()如果不能一次接受这个“小 request”,当前代码直接失败,不建立像 HTTPD/MQTT 那样复杂的 request TX state machine。
成功写入后 request pbuf 就释放,因为 TCP_WRITE_FLAG_COPY 已要求下层复制数据。
plain HTTP 与 HTTPS 到这里仍使用同样代码:
1 | HTTP request bytes |
11. Server response 到达:httpc_tcp_recv() 先积累 Header,再把 Body 交给应用
httpc_init_connection_common() 早已注册:
1 | altcp_recv(req->pcb, httpc_tcp_recv); |
所以 transport 收到 application plaintext 后会进入 httpc_tcp_recv()。HTTPS 时 TLS 已经先解密,httpc parser 不处理 TLS record。S2S5
继续阅读 httpc_tcp_recv()。当当前状态还不是 Body,函数先把新收到的 pbuf 接到 rx_hdrs,然后依次推进 status-line 与 Header 两个 parser;Header 完整后通过 pbuf_free_header() 把同一个 pbuf chain 的数据视图推进到 Body,并立即允许本次 callback 继续消费剩余 Body:S2
1 | if (req->parse_state != HTTPC_PARSE_RX_DATA) { |
因此 parser 的三个状态不是抽象说明,而是这个 callback 中真实推进的状态:
1 | HTTPC_PARSE_WAIT_FIRST_LINE |
首次数据到达时,如果 Header 跨多个 TCP segment/pbuf,httpc_tcp_recv() 会把这些 pbuf 用 pbuf_cat() 暂存到 req->rx_hdrs,直到能找到完整 \r\n\r\n。HTTP message boundary 与 TCP segment/pbuf boundary 的协议背景已经在前置资料中说明;这里的新增信息是:当前 httpc 通过累积 req->rx_hdrs 恢复这条 application-level boundary。 S2S7
12. status line 与 Header:当前 parser 只提取它真正需要的最小信息
httpc_tcp_recv() 在 HTTPC_PARSE_WAIT_FIRST_LINE 状态调用 http_parse_response_status()。下面进入该 parser;它必须先找到第一行 \r\n、HTTP/ 前缀和空格,再把 version/status 写回 caller 提供的字段:S2
1 | static err_t |
成功返回后 httpc_tcp_recv() 把状态切到 HTTPC_PARSE_WAIT_HEADERS。随后进入 http_wait_headers();它搜索完整 Header 结束符,并尝试读取 Content-Length:S2
1 | static err_t |
它识别的 Header 结束边界是 \r\n\r\n,并尝试读取:
1 | Content-Length: <number> |
当前实现对 Content-Length: 使用直接字符串搜索,源码里还留有“case insensitive?” TODO。S2
Header 完整后:
1 | altcp_recved(header bytes) |
如果同一个 TCP pbuf 中同时包含:
1 | HTTP Header + Body 前几个字节 |
pbuf_free_header() 只移动数据视图,Body 不会被丢掉,而是继续进入后面的 Body callback。
13. headers_done_fn 与 recv_fn 是两种不同 callback
httpc_connection_t::headers_done_fn 只在完整 Header 被确认后调用一次,可以读取:
1 | HTTP status/header pbuf |
并允许通过返回非 ERR_OK 主动 abort。
而 recv_fn 接收的是 Body pbuf。当前 httpc_tcp_recv() 在进入 Body 状态后会:
1 | req->rx_content_len += p->tot_len |
这里它直接把 pbuf ownership 交给 callback,没有在返回前自动 pbuf_free(p)。S2
源码自带的 httpc_fs_tcp_recv() 给出了正确消费模式:S2
1 | static err_t |
因此自定义 OTA/配置下载 callback 也必须认真处理:
1 | 消费 pbuf chain |
不能只保存 p->payload pointer 后立即返回,再假设它长期有效。
14. p == NULL 才进入 transfer completion 判断
当前 request template 明确发送 Connection: Close,因此 remote close 被纳入这份 client 的 transfer lifecycle。S2S7 RFC 9112 对 close-delimited framing 的一般规则与限制直接参考前置资料;本文只追当前 httpc 收到 p == NULL 后怎样结合 parser state 与可选 Content-Length 判断结果。
httpc_tcp_recv() 收到 p == NULL 时会判断:S2
1 | 还没进入 Body |
然后进入:
1 | httpc_close() |
注意 HTTPC_RESULT_OK 表示 HTTP transfer 在当前 client 的 transport/body 完整性语义下成功完成。HTTP status code 仍通过 srv_res 单独返回。
当前实现不会因为收到 404/500 就自动把它转换成一个完整的高级“REST exception”模型;应用应该结合 httpc_result 与 srv_res 判断业务结果。S1S2
15. Poll timeout:没有进展的连接最终会被 httpc_tcp_poll() 关闭
初始化时 httpc 注册:
1 | altcp_poll(req->pcb, httpc_tcp_poll, HTTPC_POLL_INTERVAL) |
httpc_state_t::timeout_ticks 初始为 HTTPC_POLL_TIMEOUT。当前源码:
1 | HTTPC_POLL_INTERVAL = 1 |
收到有效 Body 数据时会把 tick counter 重置。若 poll callback 连续把它减到 0:
1 | httpc_close(..., HTTPC_RESULT_ERR_TIMEOUT, ...) |
所以 timeout 同样属于异步 callback lifecycle,而不是 application thread 阻塞等待一个固定秒数。S2
16. HTTPS 只替换 transport:HTTP parser 不变
TLS handshake 与 BIO 的完整机制归属 Stage 33;对当前 httpc 主线而言,HTTPS 集成只需要给 httpc_connection_t::altcp_allocator 提供 TLS allocator;httpc_init_connection_common() 仍走同一套 request builder、callback 注册和 parser。S3S4
下面只保留最小集成关系,代码是基于 upstream API 的集成示意,不是 upstream example:
1 | tls_config = altcp_tls_create_config_client(ca_cert, ca_cert_len); |
此时对象关系是 httpc -> altcp TLS outer -> mbedTLS -> altcp TCP inner;HTTP request/response parser 不需要第二份实现。
17. TLS allocator 不等于完整 HTTPS 身份认证
当前 lwIP mbedTLS port 允许 client config 传入 CA,但默认 ALTCP_MBEDTLS_AUTHMODE 是 MBEDTLS_SSL_VERIFY_OPTIONAL。S5 更关键的是,目标 http_client.c / altcp_tls_mbedtls.c 中,server_name 用于 DNS 和 HTTP Host,却没有自动进入 mbedtls_ssl_set_hostname();Mbed TLS 官方 guidance 明确要求 certificate-authenticated client 把预期 server name 交给 TLS layer 才能完成 hostname authentication。S2S5S10
因此产品必须单独审核 CA trust、verification mode、certificate validity time、peer hostname verification 与 SNI;port=443 + TLS allocator 不能直接等价为“完整 Web PKI 身份验证已建立”。Stage 33 负责 TLS transport 机制,本节只保留 httpc 集成边界。
18. Stage 36 的系统时间怎样影响 HTTPS
启用基于系统时间的 X.509 validity checking 时,Mbed TLS 使用当前 wall clock 判断 certificate 是否 expired / not-yet-valid。S10 因而产品常把 Stage 36 的时间同步放在 certificate-authenticated HTTPS/MQTT-TLS 之前;但若系统已有 RTC、可信启动时间或关闭了 date-based checking,则不能把“必须先 SNTP”写成协议硬要求。
1 | flowchart LR |
19. OTA 下载里 httpc 的职责边界
httpc 适合承担 DNS -> TCP/TLS -> GET -> Body pbuf stream 这条网络传输链;Flash erase/program、slot 选择、hash/签名验证、版本策略、rollback、bootloader 状态和掉电恢复仍属于 OTA manager。也就是说,httpc 交付的是 byte stream,不是完整 OTA 状态机。
20. LWIP_HTTPC_HAVE_FILE_IO 只是 Host/example helper
打开 LWIP_HTTPC_HAVE_FILE_IO 后,源码会编译 httpc_get_file_to_disk() / httpc_get_file_dns_to_disk(),内部用 fopen/fwrite/fclose 消费 Body;public header 明确把它定位为 interface example implementation。S1S2 MCU OTA 完全可以把 recv_fn 直接接到 Flash writer,不需要先引入 POSIX filesystem。
21. HTTP client 的运行上下文仍然属于 callback-style lwIP core API
httpc 最终直接创建 altcp PCB、注册 callback 并调用 altcp_connect()。因此和 MQTT/SNTP 一样,在 NO_SYS=0 的 RTOS 模式下要遵守 lwIP core execution-context contract。S8
也就是说,未来 RT-Thread application thread 想发起:
1 | httpc_get_file_dns(...) |
不能因为它“看起来是应用层 API”就忽略线程约束。应通过 TCPIP thread 调度或已配置的 core locking 进入,具体 RT-Thread 集成会留到 Stage 39,而不是在本篇展开 RT-Thread 内部实现。
22. Stage 37 的完整主调用链
把本篇已经出现的对象和 callback 按实际顺序串起来:
1 | flowchart TD |
Stage 37 到这里建立的是:
1 | HTTP Client |
把 altcp_allocator 换成 TLS allocator 后,这条链就成为 HTTPS;httpc 本身不需要第二套 parser。
Stage 38 将转向 lwipopts.h、源码选择、OS Port 与 Network Port 的裁剪和移植。
资料来源
[S1] lwIP HTTP client public API
- 类型:用户提供源码快照 + upstream 对照
- 版本:用户提供
lwip.zip;相关文件 blob 与 upstream commitd08f4773edd0182b7910fc8f046eed82ffcd67c9一致 - 定位:
src/include/lwip/apps/http_client.h:httpc_connection_t、httpc_result_t、httpc_get_file()、httpc_get_file_dns()、Header/Body callback contract - URL/文档:lwIP http_client.h
- 使用位置:“公共入口”“settings/allocator/callback contract”“File I/O helper 边界”
- 支撑内容:限定应用可配置的 HTTP client interface
[S2] lwIP HTTP client implementation
- 类型:用户提供源码快照 + upstream 对照
- 版本:同上
- 定位:
src/apps/http/http_client.c:httpc_get_file_dns()、httpc_init_connection_common()、httpc_get_internal_dns()、httpc_dns_found()、httpc_get_internal_addr()、httpc_tcp_connected()、httpc_tcp_recv()、httpc_tcp_poll()、httpc_close()、httpc_fs_tcp_recv() - URL/文档:lwIP http_client.c
- 使用位置:Stage 37 Source-driven 主调用链
- 支撑内容:GET request 构造、DNS bridge、Header/Body parsing、pbuf ownership、timeout、completion 与当前实现能力边界
[S3] lwIP altcp allocator abstraction
- 类型:目标版本上游源码
- 版本:同上
- 定位:
src/core/altcp.c:allocator 说明、altcp_new_ip_type();src/include/lwip/altcp.h:altcp_allocator_t - URL/文档:lwIP altcp.c
- 使用位置:“HTTP 与 HTTPS 如何共享 httpc”“NULL allocator 与 TLS allocator 的分界”
- 支撑内容:证明 transport layer 由 runtime allocator 决定
[S4] lwIP altcp TLS allocator API
- 类型:目标版本上游源码
- 版本:同上
- 定位:
src/include/lwip/altcp_tls.h:client config、altcp_tls_alloc();src/core/altcp_alloc.c:altcp_tls_new()、altcp_tls_alloc() - URL/文档:lwIP altcp_tls.h、lwIP altcp_alloc.c
- 使用位置:“HTTPS allocator 集成”“TLS outer + TCP inner”
- 支撑内容:说明 HTTP client 如何在不改变 parser 的情况下创建 TLS-over-TCP connection
[S5] lwIP altcp mbedTLS client implementation
- 类型:目标版本上游源码
- 版本:同上
- 定位:
src/apps/altcp_tls/altcp_tls_mbedtls.c:altcp_tls_create_config_client_common()、altcp_tls_create_config_client()、TLS handshake/BIO;src/include/lwip/apps/altcp_tls_mbedtls_opts.h:ALTCP_MBEDTLS_AUTHMODE - URL/文档:lwIP altcp_tls_mbedtls.c、lwIP altcp TLS options
- 使用位置:“HTTPS handshake”“CA/authmode/hostname verification 安全边界”“系统时间与证书有效期”
- 支撑内容:证明当前 mbedTLS port 的 client verification 默认与 config 行为,避免把“用了 TLS”误写成“完整 Web PKI 验证已自动建立”
[S6] RFC 9110 — HTTP Semantics
- 类型:IETF Internet Standard
- 版本:RFC 9110 / STD 97,2022
- URL/文档:RFC 9110
- 使用位置:“HTTP request/response 与 GET 语义”“status code 与 transfer result 的区别”
- 支撑内容:提供 HTTP 通用语义;当前 lwIP parser 能力仍以目标源码为准
[S7] RFC 9112 — HTTP/1.1
- 类型:IETF Internet Standard
- 版本:RFC 9112 / STD 99,2022
- URL/文档:RFC 9112
- 使用位置:“HTTP/1.1 message syntax”“Connection: Close”“Header/body framing 背景”
- 支撑内容:提供 HTTP/1.1 wire-format 与 connection-management 规范背景
[S8] lwIP Multithreading / Common pitfalls
- 类型:目标版本上游 Doxygen 文档
- 版本:commit
d08f4773edd0182b7910fc8f046eed82ffcd67c9 - 定位:
doc/doxygen/main_page.h:Multithreading、Common pitfalls - URL/文档:lwIP multithreading guidance
- 使用位置:“HTTP client callback-style API 的 RTOS execution context”
- 支撑内容:说明未来 RT-Thread task 调用 httpc 时仍必须满足 TCPIP thread/core locking contract
[S9] MDN — HTTP messages
- 类型:高质量协议学习资料
- 版本:MDN Web Docs,访问于 2026-10-03
- URL/文档:MDN HTTP messages
- 使用位置:HTTP Client 最小协议模型、request/response message 结构
- 支撑内容:提供 HTTP/1.1 request/response、start-line/Header/Body 的直观学习入口;正文仍自行恢复当前 httpc 主线需要的最小模型,规范结论以 RFC 9110/9112 为准
[S10] Mbed TLS — TLS client hostname 与 X.509 time verification
- 类型:TLS library 官方文档
- 版本:Mbed TLS 2.28.x / 官方安全 guidance
- URL/文档:Mbed TLS hostname verification guidance、Mbed TLS 2.28 X.509 time API、Mbed TLS external time dependencies
- 使用位置:HTTPS Client 身份验证边界、hostname verification 与 Stage 36 系统时间接入
- 支撑内容:说明 TLS client 需要显式设置预期 hostname 才能完成 certificate hostname authentication,并说明 X.509 validity 检查如何依赖系统 time/date









