多状态机——每次传输是如何执行的
前置知识
- ›第 1 篇:架构概览与 curl 代码库导读
- ›了解事件驱动编程与状态机模式
- ›熟悉非阻塞 I/O 的基本概念
多状态机——每次传输是如何执行的
无论你是使用简单的阻塞式 curl_easy_perform(),还是通过 curl_multi_perform() 管理数百个并发下载,每一次 curl 传输背后驱动它的都是同一个状态机。这可以说是 libcurl 中最重要的架构决策:整个库只有一个执行引擎,所有代码路径最终都汇聚于此。
本文将通过阅读源码来验证这一点,然后逐一解析传输过程中经历的 16 个状态,最后深入剖析负责协调每次状态转换的 multi_runsingle() 函数。
curl_easy_perform 的秘密:底层全是 multi
大多数开发者最初接触 libcurl 都是从 easy API 开始的:创建句柄、设置选项、调用 curl_easy_perform()。这看起来是一次简单的同步函数调用。但打开 lib/easy.c,这层表象便不攻自破:
sequenceDiagram
participant App as Application
participant EP as curl_easy_perform
participant IP as easy_perform (static)
participant Multi as Hidden Curl_multi
participant ET as easy_transfer
App->>EP: curl_easy_perform(handle)
EP->>IP: easy_perform(data, FALSE)
IP->>IP: Check for existing multi_easy
alt No existing multi
IP->>Multi: Curl_multi_handle(16, 1, 3, 7, 3)
end
IP->>Multi: curl_multi_add_handle(multi, data)
IP->>ET: easy_transfer(multi)
loop until done
ET->>Multi: curl_multi_perform()
ET->>Multi: curl_multi_poll()
end
IP->>Multi: curl_multi_remove_handle(multi, data)
IP-->>App: CURLcode result
源码第 722 行的注释一语道破:
"设计思路:该函数创建一个 multi 句柄,将 easy 句柄添加进去,循环调用 curl_multi_perform() 直到传输完成,然后移除 easy 句柄、销毁 multi 句柄,并返回 easy 句柄的结果码。"
公开入口 curl_easy_perform() 只有三行,全部委托给静态函数 easy_perform()。后者会复用之前创建的 multi 句柄(存储在 data->multi_easy 中),或者新建一个。接着将 easy 句柄加入 multi,运行阻塞式的传输循环,完成后再移除句柄。
这一设计有一个深远的影响:对 multi 状态机的每一次缺陷修复、性能优化或功能增强,easy API 的用户都能自动受益。 根本不存在需要单独维护的"easy 模式"代码路径。
提示:
easy_perform()创建的 multi 句柄刻意保持精简——Curl_multi_handle(16, 1, 3, 7, 3)使用最小的哈希表尺寸,因为它始终只管理一个传输。但它仍是一个功能完整的 multi 句柄,拥有独立的连接池、DNS 缓存和定时器树。
CURLMstate:传输的 16 个状态
CURLMstate 枚举 定义了传输可能处于的所有状态。让我们把它们映射到真实的网络阶段:
stateDiagram-v2
[*] --> INIT
INIT --> SETUP: pretransfer init
SETUP --> CONNECT: set timeouts
CONNECT --> PENDING: no connections available
PENDING --> CONNECT: slot opens up
CONNECT --> CONNECTING: DNS + TCP in progress
CONNECTING --> PROTOCONNECT: TCP connected
PROTOCONNECT --> PROTOCONNECTING: async protocol handshake
PROTOCONNECTING --> DO: protocol ready
PROTOCONNECT --> DO: immediate completion
DO --> DOING: async request send
DOING --> DOING_MORE: need 2nd connection (FTP)
DOING --> DID: request sent
DOING_MORE --> DID: 2nd connection ready
DID --> PERFORMING: transfer data
PERFORMING --> RATELIMITING: rate limit hit
RATELIMITING --> PERFORMING: timer expired
PERFORMING --> DONE: transfer complete
DONE --> COMPLETED: cleanup
DONE --> INIT: wildcard/redirect
COMPLETED --> MSGSENT: result posted
| 状态 | 阶段 | 发生了什么 |
|---|---|---|
INIT |
准备 | Curl_pretransfer() — 校验配置、重置计数器 |
SETUP |
准备 | 设置操作计时器和连接超时 |
CONNECT |
连接 | DNS 解析 + 查找/创建连接 |
PENDING |
等待 | 已排队——已达最大连接数 |
CONNECTING |
连接 | TCP/QUIC 连接通过连接过滤器进行中 |
PROTOCONNECT |
协议 | 协议层连接(如 FTP 登录) |
PROTOCONNECTING |
协议 | 异步协议连接的延续 |
DO |
请求 | 发送请求(HTTP GET、FTP RETR 等) |
DOING |
请求 | 异步请求发送的延续 |
DOING_MORE |
请求 | 辅助连接建立(FTP 数据通道) |
DID |
请求 | 请求完全发出,准备接收响应 |
PERFORMING |
传输 | 数据流式传输——主传输循环 |
RATELIMITING |
传输 | 因超出速率限制而暂停 |
DONE |
清理 | 传输后处理,决定是否复用连接 |
COMPLETED |
完成 | 传输结束,结果可读取 |
MSGSENT |
完成 | 完成消息已发布给应用程序 |
INIT 和 SETUP 是瞬态——multi_runsingle() 会立即穿越它们。真正的核心工作发生在 CONNECTING(连接过滤器构建 I/O 栈)、DO(协议处理器发送请求)以及 PERFORMING(实际数据传输)这三个状态中。
multi_runsingle:核心 switch 语句
multi_runsingle() 是 libcurl 的心脏所在。这个约 320 行的函数,本质上是一个 do { switch(data->mstate) { ... } } while(...) 循环,每次调用都会将某个传输在当前状态中向前推进,并可能将其推入下一个状态。
flowchart TD
Entry[multi_runsingle called] --> Check{Check mstate}
Check --> INIT[INIT: Curl_pretransfer]
INIT --> SETUP[SETUP: set timeouts]
SETUP --> CONNECT[CONNECT: state_connect]
CONNECT --> |connected| PROTOCONNECT
CONNECT --> |async| CONNECTING[CONNECTING: Curl_conn_connect]
CONNECTING --> |done| PROTOCONNECT[PROTOCONNECT: protocol_connect]
PROTOCONNECT --> |immediate| DO[DO: state_do]
PROTOCONNECT --> |async| PCING[PROTOCONNECTING]
PCING --> DO
DO --> |immediate| DID
DO --> |async| DOING[DOING: protocol_doing]
DOING --> |more needed| DOING_MORE[DOING_MORE: multi_do_more]
DOING --> DID[DID: check sockets]
DOING_MORE --> DID
DID --> PERFORMING[PERFORMING: state_performing]
PERFORMING --> |rate limit| RATELIMITING[RATELIMITING]
RATELIMITING --> PERFORMING
PERFORMING --> |done| DONEST[DONE: multi_done]
DONEST --> COMPLETED[COMPLETED]
COMPLETED --> MSGSENT[MSGSENT: handle_completed]
接下来我们逐一追踪 switch 语句中的关键状态转换,从 第 2519 行 开始:
INIT → SETUP → CONNECT:这三个状态通过 FALLTHROUGH() 宏连续穿透。MSTATE_INIT 调用 Curl_pretransfer() 校验配置;MSTATE_SETUP 启动计时器;MSTATE_CONNECT 调用 state_connect(),后者再调用 url.c 中的 Curl_connect() 来查找连接池中的现有连接或新建一个。
CONNECTING:在 第 2551 行,传输等待连接过滤器完成工作。Curl_conn_connect() 驱动过滤器链(DNS 解析、TCP 连接、TLS 握手——均由第 3 篇介绍的连接过滤器架构处理)。当 connected 返回 TRUE 时,状态推进到 PROTOCONNECT。
DO:辅助函数 state_do() 调用协议处理器的 do_it 方法——对 HTTP 来说是发送请求头,对 FTP 来说是发送 RETR 命令。如果协议支持 do_more 机制(由 conn->bits.do_more 标识),状态机会经由 DOING_MORE 路由。
PERFORMING:在 第 2702 行,state_performing() 调用 Curl_sendrecv() 驱动实际的数据传输。响应体的流式接收、上传数据的发送以及进度回调的触发,都发生在这里。
底部的 do/while 循环(第 2771 行)在 mresult == CURLM_CALL_MULTI_PERFORM 时持续迭代,使瞬态状态得以连续穿透,无需返回给调用方。
传输循环与 SingleRequest
当状态机进入 PERFORMING 时,控制权转移到 lib/transfer.c 中的数据传输引擎。Curl_sendrecv() 是核心工作函数——它检查传输需要什么样的 I/O(发送、接收或两者兼有),并驱动数据在连接中流动。
每个传输的请求级状态存储在 struct SingleRequest 中,以 data->req 的形式内嵌。这个结构体追踪以下信息:
- 字节计数:
bytecount(已接收)、writebytecount(已发送)、size(预期大小)、maxdownload(限制) - 响应元数据:
httpcode、httpversion、头部计数 - 客户端栈:
writer_stack(处理响应数据,包括解压缩、分块解码等)和reader_stack(处理请求数据,包括分块编码等) - 完成标志:
done、download_done、upload_done、eos_written
sequenceDiagram
participant SM as State Machine
participant SR as Curl_sendrecv
participant CW as Client Writers
participant CF as Connection Filters
participant CR as Client Readers
SM->>SR: state_performing() → Curl_sendrecv()
SR->>CF: Curl_cf_recv(data, buf)
CF-->>SR: received bytes
SR->>CW: Curl_client_write(data, BODY, buf)
CW->>CW: decompress → decode chunked → write callback
SR->>CR: Curl_client_read(data, buf)
CR->>CR: encode chunked → read callback
SR->>CF: Curl_cf_send(data, buf)
SR-->>SM: CURLE_OK or done
每次传输开始时,Curl_req_hard_reset() 会重置 SingleRequest。发生重定向时,状态机从 DONE 循环回到 INIT(或 SETUP),触发一个全新的 SingleRequest,同时可能复用原有连接。
并发、待处理传输与速率限制
multi 句柄不只是驱动单个传输——它同时协调多个传输。Curl_multi 结构体 维护四个位集合来对传输进行分类:
process:正在被处理的传输dirty:由事件触发、需要立即执行的传输pending:因连接数达到上限而等待中的传输msgsent:结果已发布、传输已完成的句柄
每次调用 curl_multi_perform() 时,它会遍历所有 dirty 和 process 状态的传输,为每个传输调用 multi_runsingle()。process_pending_handles() 函数会周期性地检查待处理的传输是否可以晋升为活跃状态——例如,当一个多路复用的 HTTP/2 连接建立成功后,就可以唤醒那些等待连接的传输。
当传输尝试连接但 Curl_cpool_check_limits() 返回 CPOOL_LIMIT_DEST 或 CPOOL_LIMIT_TOTAL 时,传输进入 MSTATE_PENDING 状态,空闲等待,直到另一个传输释放出连接槽位。
MSTATE_RATELIMITING 的用途则有所不同。当设置了 CURLOPT_MAX_RECV_SPEED_LARGE 或 CURLOPT_MAX_SEND_SPEED_LARGE 且传输速率超过限制时,state_performing() 会带着一个计算好的定时器跳转到 RATELIMITING 状态。定时器触发后,传输回到 PERFORMING 并继续传输数据。
提示: 调试并发传输问题时,可以启用
CURLOPT_VERBOSE并观察日志中的状态转换记录。每次状态变化都会通过multistate()写入日志,该函数调用Curl_trc_M()——这是近期版本引入的 curl 结构化追踪系统。
完成路径:DONE → COMPLETED → MSGSENT
当 Curl_sendrecv() 发出完成信号(接收到全部预期字节或遇到错误),状态机进入 DONE。在 第 2706 行,multi_done() 负责处理传输后的清理工作:
- 调用协议处理器的
done()方法 - 决定连接是否可以复用(归还连接池)还是必须关闭
- 将传输从连接上解绑
对于 FTP 通配符传输,DONE 可以循环回到 INIT 继续处理通配符匹配的下一个文件。其他情况则转入 COMPLETED。
COMPLETED 是一个终止状态,传输在此等待。当 multi_runsingle() 检测到 MSTATE_COMPLETED 时,会调用 handle_completed(),向 multi 的消息列表投递一条 CURLMSG_DONE 消息,并将传输移入 MSGSENT。应用程序通过 curl_multi_info_read() 获取这些消息。
下一步
现在我们已经了解了每一次传输——无论通过 easy API 还是 multi API 发起——是如何流经同一个状态机的。但我们对最有趣的部分一笔带过了:CONNECTING 状态下调用 Curl_conn_connect() 时,内部究竟发生了什么?一个连接是如何从"一无所有"走到"TCP 连接建立 + TLS 握手完成 + HTTP/2 协商成功"的?
答案是连接过滤器——curl 的可组合 I/O 架构——这正是第 3 篇的主题。