本章导读
卷十三项目四:HTTPS 下载器——xd <url> [-o 输出] [-c 续传]:拉取 HTTPS 资源落盘、支持断点续传(服务器支持 Range 时从上次断点继续)、流式写盘(10 GB 文件 MB 级内存)、原子完成(临时文件 + 改名——中途失败不留半文件)。它是 xhttp 客户端全家(第 98-103 章)与文件层(第 45-48 章)的综合运用:easy 层起步(一行 GET 的力量)、响应限额与流式消费(第 99 章 ResponseBodyLimit 与 Body 回调)、Range 续传协议(第 91 章 HTTP 分帧的 Range 语义 + 第 101 章 cache_range 组合层的本机运用)、原子写(第 46 章 WriteAtomic——file/whole 示例的原语)、错误与重试(第 100 章:连接失败的可重试类别判断)。项目一二(第 138/140 章)的七条经验第三次重放——网络形态让它加上远程资源的不确定性维度(服务器可能不支持续传、ETag 可能变化、连接可能中断在 90%)。
143.1引入
需求场景:CI 里要拉一个 2 GB 的模型文件——网络不稳(平均三次断一次)、服务器支持 Range、断了要从断点继续而不是从头再来。朴素做法(浏览器/curl)交互式可用但嵌入自动化流程要解析输出;自己写"open socket + send GET"则 TLS 与 HTTP 的复杂度全部自己扛。xdl 把它做成可组合的命令行件:幂等续传(重复执行同一 URL 自动从断点继续)、完整性校验(ETag/Last-Modified 未变才续传——变了则重新下载:资源已换,续传会拼出损坏文件)、原子交付(下载中是 .part 临时文件、完成才改名——下游永远不会看到半个文件)。
验收标准(写代码前定案):① 完整下载往返正确(哈希与服务器 Content-Length 对账);② 10 GB 流式——内存 MB 级(stats 验证);③ 断点续传:中断后重跑从断点继续(线上只有一个 Range 请求——抓包验证);④ ETag 变化时放弃续传整体重下(不拼损坏文件);⑤ 原子性:任意时刻 kill,磁盘上只有 .part 或完整文件;⑥ 不支持 Range 的服务器优雅降级(整体重下 + 明确提示);⑦ 全部失败路径结构化错误(第 4 章——"连不上"与"证书错"可区分)。
143.2概念
架构:下载状态机
探测的两种策略:HEAD 先行(拿 ETag/Content-Length/Accept-Ranges——一次轻量交互)或直接 GET 带 If-Range(ETag 匹配给 206 续传、不匹配给 200 全新——一次交互完成判断+开始传输)。决策:If-Range——省一个往返且语义由协议保证(服务器端原子判断);HEAD 策略留给扩展(先展示信息再决定下不下)。
模块选型与理由
| 步骤 | 选型 | 理由与备选 |
|---|---|---|
| HTTP 客户端 | xhttp 客户端(第 99 章构建器) | 需要 Range 头与响应限额控制——easy 层(第 98 章)不收自定义头。备选 easy:一行够但不可定制——否决 |
| TLS | 客户端内置(验证器必须) | 第 85 章"不验证不是选项";系统信任库(第 82 章拨号形态) |
| 响应消费 | Body 流式回调 | 内存与文件大小解耦(验收 ②)。备选"整读再写":2 GB 内存——否决 |
| 落盘 | .part + 追加写 | 续传的自然形态;完成时改名(原子交付) |
| 原子完成 | 平台改名(rename) | 同目录改名是原子的(POSIX/Windows 均保证);跨目录不保证——.part 与目标同目录 |
| 断点记录 | .part 文件大小本身 | 文件大小即断点——无需独立状态文件(少一个失步源)。ETag 存 .part.etag 侧车 |
| 进度显示 | Content-Length 百分比 | 已收字节/总字节;无 Content-Length 时显示已收字节 |
If-Range 续传协议
If-Range 的语义(RFC 9110):值是 ETag 或日期——匹配才给 206、不匹配给完整 200——服务器端原子:不存在"验证与传输之间资源变了"的窗口(先 HEAD 后 GET 的策略有这个竞态)。这是续传正确性的协议根基。416 处理:.part 比文件还大——只有损坏或服务器回滚会造成;安全动作是重新下载(删 .part)。
响应消费与限额
第 99 章的客户端响应消费两形态:Body 回调(流式——每段字节到即到即写)与结果缓冲(整读)。下载器选回调:ResponseBodyLimit 设一个防失控上限(比如 Content-Length + 1KB——声明多少收多少,超了即错);回调里顺序追加写 .part(单写者无并发——第 45 章 Full 写保证恰好 N 字节)。进度在回调里累计——Content-Length 已知时百分比、未知时字节数。背压自动:写盘慢 → 回调返回慢 → 客户端停读 → TCP 收缩(第 104 章机制的网络版)——内存恒定的保证。
原子交付与失败清理
.part 与目标文件同目录(目标名.part)——同目录 rename 原子(第 46 章原子写族的近亲:WriteAtomic 是"临时+改名"的文件内版本,这里是跨文件名版本)。失败清理策略:连接中断——.part 保留(下次续传的资源);参数/证书错误——保留(不影响重试);用户 Ctrl-C——保留(标准下载器行为:中断可续)。只有明确"资源已换"(ETag 变)才截断重下。清理例外:--force 显式要求从头(删 .part 全新开始)。
断点续传的状态依据与竞争窗口
续传设计最微妙的部分是状态的权威性:.part 大小是断点、.part.etag 是资源身份——两者组合与服务器对话。竞争窗口分析:窗口一(下载中另一进程同下同 URL)——两个写者追加同一 .part 会交错损坏;对策是锁文件(.part.lock 存在即拒绝启动——进程级互斥;简易但够用;分布式场景超出工具定位)。窗口二(ETag 侧车与 .part 不同步——写 etag 后 kill 在写 part 前)——下次 If-Range 带着旧 ETag 而资源未变:206 续传从 part 实际大小起——天然自愈(part 大小是权威、etag 只是判定依据——判定保守了只是多重下一段,不损坏)。窗口三(文件系统崩溃恢复——part 与 etag 都可能半写)——ext4/NTFS 的日志保证元数据一致;数据半写在 If-Range 判定通过后继续追加——尾部可能损坏,但最终交付前的哈希对账(服务器给 Content-MD5/ETag 时校验)是最后一道闸。三个窗口的分析结论:锁文件挡住真并发、part 大小做权威、哈希做终审——每一层都不依赖上一层完美。
与项目一二经验的对位
七条经验第三次重放,两条升级:验收先行(七条——网络的"远程不确定性"使验收含 ④⑤⑥ 三个失败时点条目);形态决定容器(下载状态是"文件偏移+ETag"——.part 大小即状态,零内存状态);视图即取即弃(Body 回调的片段即到即写——不留缓冲);分层换件(换 HTTP 客户端实现/换存储(写内存/网络出口)是单层);平台事实进设计(同目录改名原子——跨目录不保证);预算先算(吞吐=网络与磁盘的较小者——CPU 在两边都可忽略);测试四象限(加上"服务器行为模拟":正常/不支持 Range/ETag 变/中断在 N% 四种服务端形态——本地 mock 服务器(第 104 章)充当)。
143.3示例
第一个完整程序:HTTPS 拉取的骨架
下面的程序来自 examples/http/client_easy——下载器的 HTTP 面最小闭环(教程第 98 章的完整形态):
#include <stdio.h> #include <string.h> #include <xhttp.h> /* 用一行 GET 便利入口执行请求并输出响应状态与正文。 */ int main(int argc, char** argv) { xnetengineconfig EngineConfig; xhttpclientconfig ClientConfig; xnetengine* pEngine; xhttpclient* pClient; xhttpresult* pResult; const xhttpresponse* pResponse; int iExit = 1; if ( argc != 2 ) { fprintf(stderr, "usage: client_easy <http-url>\n"); return 0; } xrtNetEngineConfigInit(&EngineConfig); pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { goto cleanup_engine; } xrtHttpClientConfigInit(&ClientConfig); pClient = xrtHttpClientCreate(pEngine, &ClientConfig); if ( pClient == NULL ) { goto cleanup_engine; } pResult = xrtHttpClientGetSync( pClient, (xstrview){ argv[1], strlen(argv[1]) }, NULL ); if ( pResult == NULL ) { goto cleanup_client; } pResponse = xrtHttpResultResponse(pResult); if ( pResponse != NULL ) { xbytesview Body = xrtHttpResponseBody(pResponse); printf("status: %u\n", (unsigned)xrtHttpResponseStatus(pResponse)); if ( Body.Size != 0 ) { (void)fwrite(Body.Data, 1, Body.Size, stdout); (void)fputc('\n', stdout); } iExit = 0; } xrtHttpResultDestroy(pResult); cleanup_client: xrtHttpClientDestroy(pClient); cleanup_engine: if ( pEngine != NULL ) { while ( !xrtNetEngineDestroy(pEngine) ) { xrtClearError(); xrtThreadYield(); } } return iExit; }
刚才发生了什么。 ① 装配三步(Engine→Client→GetSync)+ 结果三件套(Response/Status/Body)——下载器的全部 HTTP 词汇已在此。② xdl 在此之上加四件:Range/If-Range 头(换构建器路径)、Body 回调流式(换消费形态)、.part 落盘(加文件层)、改名交付(加原子完成)——增量清晰是分层的价值:第 98 章的"easy 之上有整个运行时"在此兑现为"运行时之上只有下载语义"。③ 验证器必配(HTTPS 形态)——第 85 章纪律。
第二个完整程序:原子写的原语
第二个程序来自 examples/file/whole——原子交付的文件层原语:
#include <stdio.h> #include <xrt.h> */ /* 展示整文件写入、追加、原子替换、复制和移动。 */ int main(void) { static const char sSource[] = "xrt-file-whole-source.tmp"; static const char sCopy[] = "xrt-file-whole-copy.tmp"; static const char sMoved[] = "xrt-file-whole-moved.tmp"; bytes pData; size_t iSize; bool bResult = false; (void)xrtFileDelete(sSource); (void)xrtFileDelete(sCopy); (void)xrtFileDelete(sMoved); xrtClearError(); if ( !xrtFileWriteAll(sSource, XRT_BYTES_LITERAL("first")) || !xrtFileAppend(sSource, XRT_BYTES_LITERAL(" second")) || !xrtFileWriteAtomic(sSource, XRT_BYTES_LITERAL("published")) || !xrtFileCopy(sSource, sCopy, false) || !xrtFileMove(sCopy, sMoved, false) ) { goto cleanup; } pData = xrtFileReadAll(sMoved, &iSize); if ( pData == NULL ) { goto cleanup; } printf("%.*s\n", (int)iSize, (const char*)pData); xrtFree(pData); bResult = true; cleanup: if ( xrtFileExists(sSource) && !xrtFileDelete(sSource) ) { bResult = false; } if ( xrtFileExists(sCopy) && !xrtFileDelete(sCopy) ) { bResult = false; } if ( xrtFileExists(sMoved) && !xrtFileDelete(sMoved) ) { bResult = false; } return bResult ? 0 : 1; }
刚才发生了什么。 ① WriteAtomic 的语义(头注释):写临时文件、冲刷、原子改名——中途失败不留半文件(读者要么看到旧文件要么看到新文件)。② xdl 的完成动作与之同构:.part 下载满 → 改名为目标——下载版的原子发布。③ 对比顺序写(直接写目标):kill 在 90% 时下游看到 90% 的文件——以为完成了(最坏形态:部分模型文件推理出静默错误结果)。原子性不是洁癖是下游正确性。
与全书知识地图的对位
下载器踩过的每一块砖都在书里有正文章:第 4 章(错误三分类驱动动作——连接/证书/资源)、第 45 章(文件打开模式与 Full 写——恰好 N 字节语义)、第 46 章(原子写族——WriteAtomic 的临时+改名哲学跨到文件名级)、第 63 章(地址与端口——URL 的主机解析由拨号层消化)、第 82 章(TLS 客户端拨号——验证器装配)、第 85 章(验证策略——系统信任库的默认形态)、第 91 章(HTTP 分帧——Range 的协议语义与 206/200 判定)、第 98 章(easy 层——本项目的起点形态)、第 99 章(运行时——构建器路径的 Range 头、ResponseBodyLimit、Body 回调、Info 诊断字段(WireBytes/BodyBytes 对账的基础))、第 100 章(自动行为——重试类别的三分类)、第 101 章(cache_range——Range 组合层的本机运用(分片合并是它的本行))、第 104 章(服务端——mock 服务器扮演四种服务端行为)。十三个章节在一个下载器里各就各位——与项目一(十四章节)同构的知识地图验证:项目的深度不在于章节数而在于层间组合的真实性——下载器把"客户端 HTTP×文件×错误模型"三族的边界全部走过一遍。
实现走查:续传核心的源码形态
下载状态机的核心——探测与续传判定——的源码形态(完整程序嵌入两个教学示例之上,本节给出关键段的走查):
/* 探测+开始:If-Range 的原子判定 */ static bool dl_begin(dl_ctx* Ctx, const char* sUrl) { xhttprequest* Req; char Range[64]; size_t iHave = dl_part_size(Ctx); /* .part 大小即断点 */ Req = xrtHttpRequestCreate( XRT_STR_LITERAL("GET"), view(sUrl)); if ( Req == NULL ) { return false; } if ( (iHave > 0u) && Ctx->Etag.Size > 0u ) { /* 有断点有 ETag:If-Range 让服务器原子判定 */ xrtHttpRequestSetHeader(Req, XRT_STR_LITERAL("If-Range"), Ctx->Etag); snprintf(Range, sizeof(Range), "bytes=%zu-", iHave); xrtHttpRequestSetHeader(Req, XRT_STR_LITERAL("Range"), view(Range)); } Ctx->Call = xrtHttpClientDo(Ctx->Client, Req, &Ctx->Options, dl_on_done, Ctx); xrtHttpRequestDestroy(Req); /* 冻结后原请求可毁 */ return Ctx->Call != NULL; } /* 完成回调:206/200 分叉 */ static void dl_on_done(xhttpcall* Call, xhttpcallresult* Res, ptr Ctx) { dl_ctx* C = Ctx; if ( Res->Result != XHTTP_CLIENT_ERROR_NONE ) { dl_fail_keep_part(C); /* 连接类失败:保留 .part */ return; } if ( Res->Response->Status == 206 ) { dl_open_part(C, "ab"); /* 续传:追加 */ } else if ( Res->Response->Status == 200 ) { dl_open_part(C, "wb"); /* 全新或 ETag 变:截断 */ } else if ( Res->Response->Status == 416 ) { dl_discard_part(C); /* 异常态:删 part 重下 */ dl_restart(C); return; } else { dl_fail_http(C, Res->Response->Status); return; } /* 记录新 ETag(下次续传依据)+ 开始流式接收 */ dl_save_etag(C, Res->Response); dl_stream_body(C); /* Body 回调即到即写 */ }
走查要点。 ① dl_begin 的请求组装:有断点才带 If-Range/Range(首次下载是普通 GET——服务器无从判定也不需要)。② HttpRequestDestroy 紧跟 Do——冻结语义(第 99 章)让请求生命周期与调用解耦。③ 完成回调的四路分叉正是状态机的转移表:206 续/200 新/416 弃/其他报错——每条路径一个动作,没有"顺便"。④ dl_fail_keep_part 与 dl_discard_part 的区分是契约"失败保留"的两面:连接类留(续传资源)、状态异常弃(防止永远 416 的死循环)。⑤ Body 流式与进度在 dl_stream_body(回调里追加写+累计——第 99 章消费形态)。
验收走查与测试矩阵
七条验收的测试矩阵设计(第 133 章四象限的下载版):正向——完整下载哈希对账(验收 ①);边界——无 Content-Length 的流式响应(进度降级显示)、空文件(0 字节立即完成)、重定向到 HTTPS(第 100 章跟随+验证不变);负向——404/403(用户错误报告)、证书失败(mock 用错证书——配置错误类)、不支持 Range 的 200(降级路径——验收 ⑥);竞态/中断——在 25%/50%/75% 三点 kill 重跑续传(验收 ③)、kill 后立即检查磁盘只有 .part(验收 ⑤)、ETag 变化的重下(验收 ④——mock 切换资源版本);资源——OOM 逐点(第 134 章:下载路径的分配失败保留 .part 且不损坏)。mock 服务器的行为配置化(正常/无 Range/ETag 变/指定字节断流——第 104 章服务的测试替身形态)让全部矩阵在 CI 离线可跑——网络项目的测试独立性靠服务替身而非真网。
143.4契约
- If-Range 语义:ETag 匹配→206 续传/不匹配→200 全新——服务器端原子,无验证竞态。
- 断点即文件大小:
.part大小是唯一断点状态——无独立状态文件(零失步源)。 - ETag 侧车:
.part.etag存上次 ETag——重跑时构造 If-Range;资源已换则截断重下。 - 416 处理:part ≥ 文件——异常态,删 part 重新下载。
- 同目录改名:完成动作原子;跨目录改名不保证——
.part与目标同目录。 - 流式保证:Body 回调即到即写;ResponseBodyLimit=声明长度+容差——内存与文件大小解耦。
- 失败保留 .part:中断/网络错误保留(续传资源);仅 ETag 变或 --force 才截断。
- 降级路径:无 Range 支持(200 无 Content-Range)——整体下载+提示。
- 验证器必须:HTTPS 形态的默认装配;
--insecure开关不提供(第 85 章"不验证不是选项"的立场)。 - 错误可区分:连接失败(可重试)/证书失败(配置问题)/416(状态异常)——类别驱动动作。
交付形态与 CLI 契约
下载器的 CLI 契约与交付选型:参数面(xdl <url> [-o 输出] [--force] [--timeout 毫秒]——四个参数覆盖全部状态机入口:URL 必选、-o 定目标名(默认取 URL 尾段)、--force 忽略 .part 全新开始、--timeout 覆盖默认预算);退出码契约(0 成功/1 可重试失败(连接类——脚本可退避重跑)/2 不可重试(证书/参数)/3 资源错误(4xx)——退出码是脚本的 API:CI 的重试逻辑按它分支,与第 138 章"工具的验收语言"同源);输出契约(进度到 stderr(可静默)、最终一行"saved <路径> <字节数> <耗时>"到 stdout(可解析)——人机分流)。交付形态:单头(独立工具最易分发——第 132 章消费者选型);裁剪声明 xhttp 客户端+文件闭包(不含服务端/TLS 服务端符号——体积档验证)。与 curl/wget 的边界:xdl 不追求替代通用下载器——它是嵌入 XRT 生态的可组合件(直接复用本书全部知识),教学价值与实用价值合一。
复盘:项目四的可迁移经验
下载器新增的可迁移经验(在项目一二七条之上):远程不确定性是设计维度——本地工具的失败是异常、网络工具的失败是常态(三分类三动作+退避+保留状态让"失败"成为可组合的中间态而非终止);协议给的原子性优先于自己造(If-Range 服务器端原子 vs HEAD+GET 的自造窗口——找到协议原子点并用它);临时名+改名的交付模式(适用于一切"长写入的产物"——下载/序列化大对象/报告生成——观察者世界观里消灭"进行中");文件系统状态优于独立状态文件(.part 大小即断点——少一个失步源;ETag 侧车是辅助而非权威);测试替身让网络项目离线可测(mock 行为配置化——CI 不碰真网)。五条加上原有七条——十二条工程常识将在项目五(WebSocket 服务)与项目六(静态服务器收官)继续重放与深化。
常见错误
症状:服务器资源已更新(版本发布了)——续传把旧文件的后半拼到新文件的前半:产物哈希不对、模型推理静默出错——最坏的损坏形态(看起来是完整文件)。
原因:Range 请求只说"给我第 N 字节起"——不保证资源与上次相同。If-Range 是协议给的原子解:不用它就是自己开竞态窗口(HEAD 验证与 GET 传输之间资源仍可能变)。
/* 盲目续传:资源变了也不知道 */ snprintf(hdr, "Range: bytes=%zu-", part_size()); get(url, hdr); /* 206 回来就续——拼的是新旧混合体 */
/* If-Range:服务器原子判断 */ set_header("If-Range", saved_etag); set_header("Range", "bytes=%zu-", part_size()); if ( status == 206 ) { 续传追加(); } else if ( status == 200 ) { 截断全新(); } /* ETag 变——重下 */
症状:进程被 kill——下游看到 90% 的"目标文件":脚本以为下载完成进入下一步——静默数据损坏。
原因:写目标文件把"进行中"暴露给了观察者。原子交付的形态:写临时名(.part)、完成时改名——观察者的世界观里只有"没有"与"完整"。
f = fopen("model.bin", "w"); /* 直接写目标 */ write_chunks(f); /* kill 在 90%:半文件冒充完成 */
f = fopen("model.bin.part", append_mode); write_chunks(f); /* kill:.part 在——可续传 */ rename("model.bin.part", "model.bin"); /* 完成:原子交付 */
症状:网络抖动一次——下载报错退出;CI 任务失败重跑全部从头(大文件灾难)。
原因:错误类别没驱动动作(第 4 章)。连接类失败(TIMEOUT/AGAIN)是可重试的(指数退避);证书/参数失败不该重试(重试一万次也不行);HTTP 4xx(404/403)是资源问题(报给用户)。三分类三动作——第 100 章重试护栏的下载版。
if ( !download(url) ) { exit(1); } /* 一律失败:抖动=从头再来 */
for ( attempt = 0; attempt < 3; attempt++ ) { if ( download(url) ) { break; } if ( !error_is(xrtGetError(), XERR_TIMEOUT) && !error_is(xrtGetError(), XERR_AGAIN) ) { break; } backoff(attempt); /* 仅可重试类别+退避 */ }
挑战练习的设计意图值得点破:并发分片不是"多开几个连接"那么简单——协议边界有三:各分片的 Range 互不重叠(拼合才正确);服务器可能在分片间变化(严谨做法给每个分片带 If-Range——任一 200 即整体放弃重来);Accept-Ranges: bytes 声明只是必要条件(真给不给 206 要实测——测试矩阵的事)。工程边界:连接数上限(对服务器友好——4-8 片是惯例);单分片失败的重试策略(只重试失败分片——但若 ETag 变则全部重来)。这个练习把第 91 章(Range 协议)与第 101 章(cache_range 的分片合并——本机版)在网络真实环境下重走——组合层的知识在项目里的最终形态。
编程练习
基于 client_easy 加 Body 回调流式写 .part + 完成改名:对本地 mock 服务器(第 104 章)下载 1 MB。验收标准:产物哈希一致;kill 中途只有 .part;重跑续传完成。
实现四状态(全新/续传/ETag 变/416):mock 服务器配置化行为(支持/不支持 Range、固定/变化 ETag)。验收标准:四状态各自走对路径(206 续/200 新/200 重下/删 part);验收 ④⑥ 的机器证明。
多连接分片(Range: bytes=a-b 各拿一段)并行下载到同一 .part 的各自偏移(pwrite 式定位写)。验收标准:分片哈希拼合正确;总吞吐随连接数提升(本地 mock 验证);单分片失败不影响其他(失败分片单独重试)。
项目四完成后,卷十三只剩两章:项目五(WebSocket 服务——第 144 章,连接管理与组播的实战)与项目六(综合静态服务器——第 145 章,全书收官)。回顾四个已完成项目的递进:logstat(工具——一次性流式处理)、configd 设计(服务——长期状态与并发交接)、xdl(网络工具——远程不确定性与原子交付)——每个项目引入一类新的系统约束(数据规模/生命周期/网络),十二到二十章的知识在不同约束下反复锤炼。这种编排验证了一个工程直觉:真实系统的复杂度不在单个技术点,而在技术点之间的约束组合——而教程能做的最好准备就是让你在受控环境里把这些组合各走一遍。最后两章将加上长连接并发(WebSocket)与全栈综合(静态服务器把 HTTP/TLS/文件/缓存全部串起来)——全书的知识螺旋在项目序列里完成最后一圈。
小结
| 知识点 | 速查 |
|---|---|
| 形态 | xdl:幂等续传/完整性校验/原子交付——可组合的命令行件 |
| 续传协议 | If-Range+Range:匹配 206 续/不匹配 200 新——服务器端原子 |
| 断点状态 | .part 大小即断点——零独立状态;ETag 侧车 .part.etag |
| 原子完成 | 同目录 .part→改名——下游只见"没有或完整" |
| 416 | part≥文件——异常态删 part 重下 |
| 流式 | Body 回调即到即写;限额=声明+容差——内存解耦 |
| 失败保留 | 中断留 .part(续传资源);仅 ETag 变/--force 截断 |
| 错误三分类 | 连接类(重试+退避)/证书与参数类(报配置错)/HTTP 4xx(报资源错) |
| 降级 | 无 Range 支持(200 无 Content-Range)——整体下载+明确提示 |
| 验证器 | HTTPS 必配——--insecure 不提供 |
📖 本章涉及的 API 参考手册:xhttp-http_client(101 个条目)xhttp-http_client_easy(9 个条目)file(132 个条目)
点击查看完整签名、参数约束、返回值与错误说明