本章导读

卷十三项目四: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概念

架构:下载状态机

空闲启动(URL → HEAD 或首个 GET) →探测
探测无 .part 或 ETag 已变 →全新下载
探测.part 存在且 ETag 匹配(Range: bytes=N-) →续传
全新下载创建 .part(截断) →下载中
续传追加模式打开 .part →下载中
下载中末字节到达(Content-Length 对账)→ 改名交付 →完成
下载中连接断/取消 → .part 保留(下次续传) →中断

探测的两种策略: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 续传协议

1首次GET(无 If-Range)→ 200 全文 → 创建 .part 从 0 写
2重跑(.part 存在)GET + If-Range: <ETag> + Range: bytes=<part大小>-
3服务器无 Range 支持(200 且无 Content-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 章的完整形态):

extlibs/xhttp/examples/http/client_easy/main.c源码 ↗
#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;
}


$ gcc -O1 -DXRT_MODULE_ALL -DXHTTP_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xhttp.h impl.c extlibs/xhttp/examples/http/client_easy/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapiusage: client_easy <http-url>

刚才发生了什么。 ① 装配三步(Engine→Client→GetSync)+ 结果三件套(Response/Status/Body)——下载器的全部 HTTP 词汇已在此。② xdl 在此之上加四件:Range/If-Range 头(换构建器路径)、Body 回调流式(换消费形态)、.part 落盘(加文件层)、改名交付(加原子完成)——增量清晰是分层的价值:第 98 章的"easy 之上有整个运行时"在此兑现为"运行时之上只有下载语义"。③ 验证器必配(HTTPS 形态)——第 85 章纪律。

第二个完整程序:原子写的原语

第二个程序来自 examples/file/whole——原子交付的文件层原语:

examples/file/whole/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/file/whole/main.c -lws2_32 -liphlpapipublished

刚才发生了什么。 ① 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 服务)与项目六(静态服务器收官)继续重放与深化。

常见错误

坑 1:续传不校验 ETag(盲目 Range)

症状:服务器资源已更新(版本发布了)——续传把旧文件的后半拼到新文件的前半:产物哈希不对、模型推理静默出错——最坏的损坏形态(看起来是完整文件)。

原因: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 变——重下 */
坑 2:下载中直接写目标文件

症状:进程被 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");  /* 完成:原子交付 */
坑 3:把"连不上"当"下载失败"不重试

症状:网络抖动一次——下载报错退出;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;重跑续传完成。

进阶:If-Range 全状态机

实现四状态(全新/续传/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→改名——下游只见"没有或完整"
416part≥文件——异常态删 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 个条目)

点击查看完整签名、参数约束、返回值与错误说明