Ориентация главы

Четвёртый проект тома 13: HTTPS-загрузчик — xd <url> [-o 输出] [-c 续传]: вытянет HTTPS-ресурс на диск, поддерживает возобновление (когда сервер поддерживает Range — продолжение с последней точки), потоковую запись (файл 10 ГБ при памяти уровня МБ), атомарное завершение (временный файл + переименование — неудача посередине не оставляет половину файла). Это комплексное применение всей клиентской семьи xhttp (главы 98-103) и файлового слоя (главы 45-48): старт с easy-слоя (сила однострочного GET), лимиты ответа и потоковое потребление (ResponseBodyLimit и обратный вызов Body главы 99), протокол возобновления Range (семантика Range из фрейминга HTTP главы 91 + локальное применение композитного слоя cache_range главы 101), атомарная запись (WriteAtomic главы 46 — примитив из образца file/whole), ошибки и повторы (глава 100: различение повторяемых категорий отказов соединения). Семь уроков Проектов 1-2 (главы 138/140) проходят третий круг — сетевая форма добавляет измерение неопределённости удалённого ресурса (сервер может не поддерживать возобновление, ETag может смениться, соединение может оборваться на 90%).

143.1引入

Сцена требования: в CI нужно вытащить файл модели на 2 ГБ — сеть неустойчива (в среднем один обрыв на три попытки), сервер поддерживает Range, после обрыва надо продолжать с точки, а не начинать заново. Наивный подход (браузер/curl) годится интерактивно, но встраивание в автоматизацию требует разбора вывода; писать самому «open socket + send GET» — сложность TLS и HTTP целиком на вас. xdl делает из этого комбинируемый командный инструмент: идемпотентное возобновление (повторный запуск того же URL автоматически продолжает с точки), проверка целостности (продолжать, только если ETag/Last-Modified не изменились — изменились: качать заново; ресурс уже другой, возобновление склеит испорченный файл), атомарная поставка (во время загрузки это временный файл .part, по завершении переименование — нижестоящий никогда не увидит половину файла).

Критерии приёмки (зафиксированы до кода): (1) полная загрузка сходится корректно (сверка хеша с Content-Length сервера); (2) потоково 10 ГБ — память уровня МБ (проверка stats); (3) возобновление: после обрыва перезапуск продолжает с точки (в сети лишь один запрос Range — проверка захватом пакетов); (4) при смене ETag возобновление отбрасывается и всё качается заново (не склеивать испорченный файл); (5) атомарность: kill в любой момент — на диске либо .part, либо полный файл; (6) сервер без Range изящно деградирует (полная загрузка + явное сообщение); (7) все пути отказа — структурные ошибки (глава 4 — «не соединяется» и «ошибка сертификата» различимы).

143.2概念

Архитектура: машина состояний загрузки

Простойстарт (URL -> HEAD или первый GET) →Зондирование
Зондирование.part существует и ETag совпадает (Range: bytes=N-) →Возобновление
Возобновлениеоткрыть .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)). Альтернатива «прочитать всё, потом писать»: 2 ГБ памяти — отвергнуто
Запись на диск.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> 206ETag совпал — возобновление (писать с размера part, режим добавления)
4> 200ETag сменился — с нуля (усечь .part, писать с 0)
5> 416part уже >= файла — аномалия (part испорчен? качать заново)
6Сервер без Range (200 без Content-Range)полная загрузка с нуля (сообщение деградации)

Семантика If-Range (RFC 9110): значение — ETag или дата; совпало — 206, не совпало — полный 200 — атомарно на стороне сервера: окна «ресурс изменился между проверкой и передачей» не существует (у стратегии «сначала HEAD, потом GET» это окно есть). Это протокольный фундамент корректности возобновления. Обработка 416: .part больше самого файла — только порча или откат сервера; безопасное действие — качать заново (удалить .part).

Потребление ответа и лимиты

Две формы клиентского потребления главы 99: обратный вызов Body (потоково — каждый кусок байт пишется по прибытии) и буферизация результата (прочитать всё). Загрузчик выбирает обратный вызов: ResponseBodyLimit ставит страховочный потолок (например Content-Length + 1 КБ — сколько объявлено, столько и принимается, сверх — ошибка); в обратном вызове последовательная добавляющая запись в .part (один писатель без конкурентности — Full-запись главы 45 гарантирует ровно N байт). Прогресс накапливается в обратном вызове — при известном Content-Length проценты, без — байты. Противодавление автоматически: диск пишет медленно → обратный вызов возвращается медленно → клиент перестаёт читать → TCP сжимается (сетевая версия механизма главы 104) — гарантия постоянства памяти.

Атомарная поставка и очистка при отказе

.part и целевой файл в одном каталоге (目标名.part) — переименование в одном каталоге атомарно (близкий родственник семейства атомарной записи главы 46: WriteAtomic — это «временный + переименование» на уровне файла, здесь — на уровне имени файла). Политика очистки при отказе: соединение оборвалось — .part сохраняется (ресурс следующего возобновления); ошибки параметров/сертификата — сохранить (не мешает повтору); Ctrl-C пользователя — сохранить (стандартное поведение загрузчиков: прервано — значит возобновимо). Только явная смена ресурса (ETag) усекает и качает заново. Исключение очистки: --force явно требует с нуля (удалить .part и начать заново).

Основание состояния возобновления и окна конкуренции

Самая тонкая часть проекта возобновления — авторитетность состояния: размер .part — точка, .part.etag — идентичность ресурса; пара разговаривает с сервером. Анализ окон конкуренции: окно одно (во время загрузки другой процесс качает тот же URL) — два писателя в один .part дадут чередующуюся порчу; противодействие — файл-замок (наличие .part.lock = отказ в старте — межпроцессная блокировка; просто, но достаточно; распределённые сценарии за пределами инструмента). Окно два (сайдкар ETag рассинхронизирован с .part — kill после записи etag до записи part) — следующий If-Range со старым ETag при неизменном ресурсе: 206 продолжает с фактического размера part — естественное самовосстановление (размер part авторитетен, etag лишь основание решения; перестраховка обойдётся лишним куском, не порчей). Окно три (восстановление файловой системы после сбоя — и part, и etag могут быть наполовину записаны) — журналирование ext4/NTFS гарантирует согласованность метаданных; полузапись данных после прохождения If-Range продолжит добавление — хвост может быть испорчен, но финальная сверка хеша перед поставкой (когда сервер даёт Content-MD5/ETag) — последний шлюз. Итог анализа трёх окон: файл-замок держит настоящую конкурентность, размер part авторитетен, хеш — верховный суд — ни один слой не зависит от идеальности предыдущего.

Сопоставление с уроками Проектов 1-2

Семь уроков проходят третий круг, два усиливаются: приёмка прежде (семь пунктов — «удалённая неопределённость» сети делает приёмку с тремя временными точками отказа (4)(5)(6)); форма решает контейнер (состояние загрузки — «смещение файла + 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>

Что только что произошло. (1) Сборка в три шага (Engine → Client → GetSync) + тройка результата (Response/Status/Body) — весь HTTP-словарь загрузчика уже здесь. (2) xdl добавляет сверху четыре вещи: заголовки Range/If-Range (переход на путь конструктора), потоковый обратный вызов Body (смена формы потребления), запись в .part (плюс файловый слой), переименование при завершении (плюс атомарное завершение) — ясность приращений — ценность слоистости: «над easy — весь рантайм» главы 98 здесь превращается в «над рантаймом — только семантика загрузки». (3) Верификатор обязателен (форма 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

Что только что произошло. (1) Семантика WriteAtomic (комментарий в шапке): записать временный файл, сбросить, атомарно переименовать — неудача посередине не оставляет половину файла (читатель видит либо старый файл, либо новый). (2) Завершающее действие xdl с ним изоморфно: .part скачан до полноты → переименование в цель — атомарная публикация в версии загрузчика. (3) Сравните с последовательной записью (писать прямо в цель): 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-сервер играет четыре серверных поведения). Тринадцать глав каждая на своём месте в одном загрузчике — проверка карты знаний, изоморфная Проекту 1 (четырнадцать глав): глубина проекта не в числе глав, а в подлинности межслойных комбинаций — загрузчик проходит все границы трёх семейств «клиентский 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 пишет по прибытии */
}

Пункты разбора. (1) Сборка запроса в dl_begin: заголовки If-Range/Range ставятся только при наличии точки (первая загрузка — обычный GET — серверу нечего решать, и не нужно). (2) HttpRequestDestroy сразу за Do — семантика замораживания (глава 99) отвязывает жизненный цикл запроса от вызова. (3) Четырёхходовая развилка обратного вызова завершения — таблица переходов машины состояний: 206 продолжает / 200 с нуля / 416 отбрасывает / прочее сообщает — у каждого пути одно действие, без «заодно». (4) Различие dl_fail_keep_part и dl_discard_part — две стороны контракта «неудача сохраняет»: класс соединения сохраняет (ресурс возобновления), аномалия состояния отбрасывает (защита от вечного 416). (5) Потоковый Body и прогресс в dl_stream_body (в обратном вызове добавляющая запись + накопление — форма потребления главы 99).

Приёмочный разбор и тестовая матрица

Дизайн тестовой матрицы семи критериев (загрузочная версия четырёх квадрантов главы 133): позитив — полная загрузка со сверкой хеша (критерий (1)); границы — потоковый ответ без Content-Length (прогресс деградирует в байты), пустой файл (0 байт, мгновенное завершение), редирект на HTTPS (глава 100: следование + неизменная проверка); негатив — 404/403 (отчёт пользовательской ошибки), неудача сертификата (mock с неверным сертификатом — класс ошибки конфигурации), 200 без Range (путь деградации — критерий (6)); гонки/прерывания — kill в трёх точках 25%/50%/75% и перезапуск с возобновлением (критерий (3)), сразу после kill проверка диска: только .part (критерий (5)), повторная загрузка при смене ETag (критерий (4) — 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 (прямое переиспользование всех знаний книги), учебная и практическая ценность в одном.

Ретроспектива: переносимые уроки Проекта 4

Новые переносимые уроки загрузчика (поверх семи уроков Проектов 1-2): удалённая неопределённость — измерение проектирования — отказ локального инструмента аномалия, отказ сетевого норма (три класса три действия + откат + сохранение состояния делают «отказ» комбинируемым промежуточным состоянием, а не концом); протокольная атомарность прежде самодельной (If-Range атомарен на сервере против самодельного окна HEAD+GET — найдите атомарную точку протокола и используйте её); паттерн поставки «временное имя + переименование» (годится для любого «продукта долгой записи» — загрузка / сериализация большого объекта / генерация отчёта — устраняет «в процессе» из мировоззрения наблюдателя); состояние файловой системы лучше отдельного файла состояния (размер .part и есть точка — на один источник рассинхрона меньше; сайдкар ETag — помощник, не авторитет); тестовые двойники делают сетевой проект офлайн-тестируемым (конфигурируемость mock — CI не трогает настоящую сеть). Пять плюс исходные семь — двенадцать инженерных аксиом продолжат круги в Проекте 5 (сервис WebSocket) и Проекте 6 (финальный статический сервер).

Типичные ошибки

Ловушка 1: возобновление без проверки ETag (слепой Range):Ловушка 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: запись прямо в целевой файл во время загрузки:Ловушка 2: запись прямо в целевой файл во время загрузки

Симптом: процесс убит — нижестоящий видит 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: «не соединяется» принимается за «загрузка не удалась» без повтора:Ловушка 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 всё заново). Это упражнение заново проходит главу 90 (протокол Range) и главу 100 (склейка сегментов cache_range — локальная версия) в настоящей сетевой среде — финальная форма знания композитного слоя внутри проекта.

Практические задания

Базовое: полный контур загрузки

На базе client_easy добавьте потоковую запись в .part через обратный вызов Body + переименование по завершении: скачать 1 МБ у локального mock-сервера (глава 104). Критерии приёмки: хеш продукта совпадает; kill посередине — на диске только .part; перезапуск завершает возобновление.

Продвинутое: полная машина состояний If-Range

Реализуйте четыре состояния (с нуля / возобновление / смена ETag / 416): конфигурируемое поведение mock-сервера (поддерживает / не поддерживает Range, фиксированный / меняющийся ETag). Критерии приёмки: каждое состояние идёт своим путём (206 продолжает / 200 с нуля / 200 качает заново / удалить part); машинное доказательство критериев (4)(6).

Вызов: конкурентная загрузка сегментами

Многосегментная загрузка с несколькими соединениями (Range: bytes=a-b — каждому свой отрезок) параллельно в один .part по своим смещениям (позиционная запись в духе pwrite). Критерии приёмки: хеши сегментов склеиваются корректно; общая пропускная способность растёт с числом соединений (проверка на локальном mock); отказ одного сегмента не мешает другим (упавший сегмент повторяется отдельно).

Итоги перед финалом книги

После Проекта 4 в томе 13 остались две главы: Проект 5 (сервис WebSocket — глава 144, практика управления соединениями и веерной рассылки) и Проект 6 (комплексный статический сервер — глава 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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки