Read OSS

curl 命令行工具与测试套件

中级

前置知识

  • 第 1 篇:架构概览
  • 对 libcurl easy 与 multi API 有基本了解
  • 熟悉命令行工具与测试框架

curl 命令行工具与测试套件

前四篇文章我们深入了 libcurl 内部——状态机、连接过滤器、TLS/DNS/连接池基础设施,以及协议处理器。但大多数人接触 curl 的方式是通过命令行工具,而整个技术栈的正确性则由一套近 2000 个测试用例的测试套件来保障。在这篇终章中,我们将梳理 CLI 如何将用户意图转化为 libcurl API 调用,并深入了解支撑这一切运转的测试基础设施。

curl CLI 本身就是一个分量十足的应用程序:约 80 个源文件、250 多个命令行选项、并行传输支持,以及灵活的输出格式化能力。它同时也是一个教科书级别的范例,展示了如何在一个设计良好的库 API 之上构建 CLI 工具。

CLI 启动流程:从 main() 到 operate()

入口点位于 src/tool_main.cmain() 函数出奇地简洁——仅 60 行代码,完成平台初始化后便将控制权交给两个关键函数:

flowchart TD
    main["main(argc, argv)"] --> stderr["tool_init_stderr()"]
    stderr --> win["win32_init() (Windows only)"]
    win --> fds["main_checkfds()"]
    fds --> sigpipe["signal(SIGPIPE, SIG_IGN)"]
    sigpipe --> memdebug["memory_tracking_init()"]
    memdebug --> globalinit["globalconf_init()<br/>→ curl_global_init()"]
    globalinit --> operate["operate(argc, argv)<br/>Main execution"]
    operate --> cleanup["globalconf_free()<br/>→ curl_global_cleanup()"]
    cleanup --> ret["return result"]

第 145–204 行的启动序列在正式工作开始前处理了若干平台相关问题:

  • 文件描述符安全main_checkfds() 确保 stdin/stdout/stderr 处于打开状态——如果它们已被关闭,curl 的网络 socket 可能会意外占用 fd 0-2
  • 屏蔽 SIGPIPE:忽略 SIGPIPE 信号,防止向已断开的管道写入时发生崩溃
  • 全局初始化globalconf_init() 封装了 curl_global_init(),并对全局配置结构进行初始化

真正的执行逻辑在 operate() 中展开,负责解析 .curlrc 文件、处理命令行参数,以及执行传输任务。

选项解析与配置

src/tool_getparam.c 中的选项解析器是工具中最大的文件之一,超过 3100 行。它将 250 多个命令行标志转化为定义在 src/tool_cfgable.h 中的 OperationConfig 结构体。

flowchart LR
    CLI["--header 'X-Custom: foo'<br/>--compressed<br/>-o output.html<br/>https://example.com"] --> Parser["tool_getparam.c<br/>parse_args()"]
    Parser --> Config["OperationConfig<br/>.headers list<br/>.encoding = 'all'<br/>.output = 'output.html'<br/>.url_list → 'https://...'"]
    Config --> Translate["tool_operate.c<br/>→ curl_easy_setopt() calls"]
    Translate --> Easy["CURL *easy handle<br/>CURLOPT_HTTPHEADER<br/>CURLOPT_ACCEPT_ENCODING<br/>CURLOPT_WRITEDATA"]

工具支持两个层级的配置:

  • GlobalConfig:作用于所有 URL,包括并行模式、速率限制和共享缓存等
  • OperationConfig:针对单个 URL 的配置,包括请求方法、请求头、输出文件和认证信息等

命令行上的每个 URL(或配置文件中的每个 URL)都会对应一个独立的 OperationConfig--next 标志用于分隔不同 URL 的配置,而 URL 通配符(例如 http://example.com/[1-100].html)则可以从单个配置生成多个传输任务。

提示: 工具的选项解析代码是查找 CURLOPT_*--flag 对应关系的最佳参考。如果你不确定某个 curl 标志在内部的作用,在 tool_getparam.c 中搜索该标志,顺着 curl_easy_setopt() 调用往下追踪即可。

传输执行与回调

选项解析完成后,operate() 负责协调实际的传输过程。对于每个 URL,它会创建一个 easy handle,通过 curl_easy_setopt() 完成配置,然后根据模式选择:直接执行(顺序模式),或将其添加到 multi handle 中(使用 --parallel 时的并行模式)。

工具注册了若干回调来处理 I/O:

  • 写入回调src/tool_cb_wrt.c):根据 -o--output-dir 等标志,将响应数据写入文件、stdout 或 /dev/null。负责在首次写入时创建输出文件、遵循 --create-dirs 指令,以及管理多 URL 场景下的输出。

  • 响应头回调:处理响应头的展示,支持 -i(包含响应头)、-D(转储响应头)和 -w(写出格式)。

  • 进度回调:驱动进度条(-#)或进度计的显示。

  • 调试回调:使用 --verbose--trace 时,捕获并格式化协议级别的详细信息。

sequenceDiagram
    participant Op as operate()
    participant Easy as Easy Handle
    participant Multi as Multi Handle
    participant CB as Callbacks

    Op->>Easy: curl_easy_init()
    Op->>Easy: curl_easy_setopt(URL, ...)
    Op->>Easy: curl_easy_setopt(WRITEFUNCTION, tool_write_cb)
    Op->>Easy: curl_easy_setopt(HEADERFUNCTION, tool_header_cb)
    alt Sequential mode
        Op->>Easy: curl_easy_perform()
    else Parallel mode (--parallel)
        Op->>Multi: curl_multi_add_handle(easy)
        loop until all done
            Op->>Multi: curl_multi_perform()
            Op->>Multi: curl_multi_poll()
            Op->>Multi: curl_multi_info_read()
        end
    end
    Easy->>CB: Write callback with body data
    Easy->>CB: Header callback with headers

并行传输时,工具创建单个 multi handle 并将所有 easy handle 加入其中。--parallel-max 标志(默认值:50)通过 CURLMOPT_MAX_HOST_CONNECTIONSCURLMOPT_MAXCONNECTS 控制并发数量。工具还会在所有并行传输之间共享连接池和 cookie jar。

测试套件架构:runtests.pl 与测试数据格式

curl 的测试套件是开源项目中最严格的测试体系之一。测试框架 tests/runtests.pl 是一个体量庞大的 Perl 脚本,负责协调测试执行、管理测试服务器以及验证结果。

flowchart TD
    subgraph "Test Harness"
        RP[runtests.pl]
    end
    subgraph "Test Servers"
        HS[HTTP Server<br/>sws]
        FS[FTP Server<br/>ftpserver.pl]
        SS[SMTP/POP3/IMAP<br/>servers]
        PS[SOCKS Proxy]
    end
    subgraph "Test Data"
        TD["tests/data/test1..N<br/>XML format"]
    end
    subgraph "Test Subjects"
        CT[curl CLI tool]
        LT[tests/libtest/<br/>C programs]
        UT[tests/unit/<br/>Unit tests]
    end

    RP --> HS
    RP --> FS
    RP --> SS
    RP --> PS
    RP --> TD
    TD --> RP
    RP --> CT
    RP --> LT
    RP --> UT

每个测试用例都是 tests/data/ 目录下的一个 XML 文件。来看 tests/data/test1——最简单的 HTTP GET 测试:

<testcase>
<info>
<keywords>
HTTP
HTTP GET
</keywords>
</info>

<reply>
<data>
HTTP/1.1 200 OK
Content-Length: 6
Content-Type: text/html

-foo-
</data>
</reply>

<client>
<server>
http
</server>
<name>
HTTP GET
</name>
<command>
http://%HOSTIP:%HTTPPORT/%TESTNUMBER
</command>
</client>

<verify>
<protocol>
GET /%TESTNUMBER HTTP/1.1
Host: %HOSTIP:%HTTPPORT
User-Agent: curl/%VERSION
Accept: */*
</protocol>
</verify>
</testcase>

格式包含以下几个部分:

  • <reply>:测试服务器应返回的响应内容
  • <client>:要执行的命令、所需的服务器,以及测试元数据
  • <verify>:预期结果——发送的精确协议字节、响应数据和退出码

%HOSTIP%HTTPPORT%TESTNUMBER%VERSION 等变量会在运行时被替换,使测试可以跨环境运行,无需硬编码任何值。

测试框架会在每个测试开始前启动所需的测试服务器,运行 curl 命令(或测试程序),捕获输出,并与预期结果进行比对。服务器日志会被检查,以验证 curl 发送的精确字节——这能发现仅靠功能测试难以察觉的协议细节错误。

库测试、单元测试与测试服务器

测试基础设施不止于 CLI 测试。

库测试(tests/libtest/

这些 C 程序直接测试 libcurl 的 API。每个程序都会创建 easy/multi handle、设置选项、执行传输,并以编程方式验证结果。它们覆盖了 CLI 工具难以复现的场景——例如 multi API 事件循环、共享 handle,以及连接复用行为。

单元测试(tests/unit/tests/tunit/

单元测试专注于单个内部函数。它们直接链接 libcurl 的内部符号(编译时定义 UNITTESTS,这会屏蔽 CLI 的 main()——注意 tool_main.c 第 74 行#ifndef UNITTESTS 守卫),从而能够对 URL 解析、动态缓冲区、哈希表操作等内部函数进行隔离测试。

测试服务器(tests/server/

curl 内置了自己的测试服务器——HTTP、FTP、SMTP、POP3、IMAP 和 SOCKS 协议的小型实现。这些不是生产级服务器,而是专门用于返回预设响应并记录每一个接收到的字节。其中 HTTP 测试服务器(sws)可以模拟慢响应、连接中断、特定响应头组合以及 HTTP/2 行为。

提示: 编写新测试用例时,建议从 tests/data/ 中找一个与目标场景接近的现有测试复制修改。XML 格式有许多可选节用于控制服务器行为,现有的测试本身就是了解所有可能性的最佳文档。

测试理念

curl 的测试理念值得特别一提。项目追求的是协议级别的验证,而非单纯的功能测试。不只是检查"有没有拿到正确的文件",测试还会验证"有没有在网络上发送完全正确的字节"。这能捕获响应头格式化、编码以及协议合规性方面的回归问题,而这类问题往往会被高层测试所遗漏。

测试编号具有重要意义:它们永远不会被复用。如果测试 1234 被删除,该编号将永久空缺。这避免了在引用测试编号的 bug 报告和 CI 日志中产生混淆。

系列总结

在这六篇文章中,我们从宏观视角一路深入到 vtable 的具体条目,梳理了 curl 的完整架构:

  1. 架构概览:双产品拆分、目录结构、核心数据结构,以及三大支柱
  2. Multi 状态机:每个传输如何通过 multi_runsingle() 在 16 个状态间流转
  3. 连接过滤器:从 TCP 到 TLS 再到 HTTP/2 的可组合 I/O 栈
  4. TLS、DNS 与连接池:后端抽象层与连接复用基础设施
  5. 协议处理器:两层 scheme/protocol 设计,以及 HTTP/FTP 深度解析
  6. CLI 工具与测试:命令行如何转化为 API 调用,以及整个栈如何被验证

贯穿始终的核心模式是基于 vtable 的抽象Curl_ssl 对应 TLS 后端,Curl_cftype 对应连接过滤器,Curl_protocol 对应协议处理器。curl 证明了 C 语言尽管不具备语言层面的多态,仍然可以通过严格使用函数指针表和命名约定,实现清晰、可扩展的架构。

这份代码库值得细细研读。下次你执行 curl https://example.com 时,你会清楚地知道每一步发生了什么:main()operate()curl_easy_perform() → 隐式 multi handle → 状态机 → 连接过滤器链 → TLS 握手 → HTTP 协议处理器 → 客户端写入栈 → 你的终端。全程约 130 个组织良好的 C 文件,每一步的代码都清晰可查,等待着你去探索。