Ориентация главы
Четвёртый проект тома 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概念
Архитектура: машина состояний загрузки
Две стратегии зондирования: 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
Семантика 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 учебника):
#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; }
Что только что произошло. (1) Сборка в три шага (Engine → Client → GetSync) + тройка результата (Response/Status/Body) — весь HTTP-словарь загрузчика уже здесь. (2) xdl добавляет сверху четыре вещи: заголовки Range/If-Range (переход на путь конструктора), потоковый обратный вызов Body (смена формы потребления), запись в .part (плюс файловый слой), переименование при завершении (плюс атомарное завершение) — ясность приращений — ценность слоистости: «над easy — весь рантайм» главы 98 здесь превращается в «над рантаймом — только семантика загрузки». (3) Верификатор обязателен (форма 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; }
Что только что произошло. (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 (финальный статический сервер).
Типичные ошибки
Симптом: ресурс на сервере обновился (вышла версия) — возобновление приклеивает заднюю половину старого файла к передней половине нового: хеш продукта не сходится, модель молчаливо ошибается — худшая форма порчи (выглядит полным файлом).
Причина: запрос 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 сменился — качать заново */
Симптом: процесс убит — нижестоящий видит 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 всё заново). Это упражнение заново проходит главу 90 (протокол Range) и главу 100 (склейка сегментов cache_range — локальная версия) в настоящей сетевой среде — финальная форма знания композитного слоя внутри проекта.
Практические задания
На базе client_easy добавьте потоковую запись в .part через обратный вызов Body + переименование по завершении: скачать 1 МБ у локального mock-сервера (глава 104). Критерии приёмки: хеш продукта совпадает; kill посередине — на диске только .part; перезапуск завершает возобновление.
Реализуйте четыре состояния (с нуля / возобновление / смена 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 → переименование в одном каталоге — нижестоящий видит «нет или полностью» |
| 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки