# теория · шаг 3 из 6
Range: докачка и перемотка
Коротко
Клиент вправе попросить не весь файл, а кусок: с такого-то байта по такой-то. На этом стоят докачка после обрыва и перемотка видео - и обе ломаются от одной безобидной настройки.
- Просьба - заголовок
Range, ответ - код 206 иContent-Rangeс границами и полным размером. - nginx сам говорит
Accept-Ranges: bytesна каждый файл с диска. Пропал этот заголовок - клиент даже не попытается докачать. - Кусков можно попросить несколько сразу: ответ станет
multipart/byteranges. - Диапазон за пределами файла - 416, а не 404 и не пустой ответ.
- Сжатие отменяет куски целиком: сжатый ответ отдаётся только полностью.
Если 206, Content-Range и связка со сжатием знакомы - листай до «Теперь сам».
Сначала ответь сам
Пользователь качает дистрибутив на два гигабайта, связь рвётся на восьмидесяти процентах. Он нажимает «продолжить» - и загрузка начинается с нуля.
Файл лежит на диске, nginx настроен обычным образом. Почему клиент не смог докачать?
Потому что кто-то включил сжатие на этот тип файлов (gzipРазбирается в разделе 11, глава «gzip и brotli»: Что сжимать и чем
- nginx пережимает ответ на лету, если браузер сказал, что умеет его
распаковывать). Проверено на стенде: тот же файл в 300 байт, тот же запрос с
Range (-r 0-9 в curl - «дай байты с 0 по 9»), разница только в заголовке
Accept-Encoding: gzip. Браузер присылает его сам всегда, из curl его
добавляют через -H:
curl -sI -r 0-9 localhost/f.txt
HTTP/1.1 206 Partial Content
Server: nginx/1.31.5
Date: Thu, 10 Sep 2026 19:53:00 GMT
Content-Type: text/plain
Content-Length: 10
Last-Modified: Thu, 03 Sep 2026 11:46:11 GMT
Connection: keep-alive
ETag: "6a995e03-12c"
Content-Range: bytes 0-9/300
curl -sI -r 0-9 -H 'Accept-Encoding: gzip' localhost/f.txt
HTTP/1.1 200 OK
Server: nginx/1.31.5
Date: Thu, 10 Sep 2026 19:53:00 GMT
Content-Type: text/plain
Last-Modified: Thu, 03 Sep 2026 11:46:11 GMT
Connection: keep-alive
ETag: W/"6a995e03-12c"
Content-Encoding: gzip
Во втором случае вместо куска пришёл весь файл, сжатый, с кодом 200. И на
обычный запрос без Range сжатый ответ приходит без Accept-Ranges, хотя
несжатый его несёт - замер показал оба. Сервер честно сообщает: кусками этот
ответ не отдаю.
На что это похоже
Библиотека выдаёт книгу двумя способами.
Обычный - книгу целиком: пришёл, взял, унёс. Если по дороге уронил в лужу, возвращаешься и берёшь новую целиком.
Второй - по страницам: «мне со сто сорок первой по двухсотую». Так можно дочитать ровно с того места, где остановился, и не тащить весь том. Но работает это, только пока книга нумерована.
А теперь представь, что перед выдачей текст пересказывают своими словами покороче. Пересказ сам по себе полезен, но нумерация страниц в нём другая, и просьба «мне сто сорок первую» смысла больше не имеет. Ровно это делает сжатие: оно меняет байты, и старые номера перестают на что-либо указывать.
Механизм: просьба и ответ
Правило
Куски возможны, пока байт с номером N в ответе - это байт с номером N в файле. Всё, что меняет содержимое на лету, эту связь рвёт.
Разбор: четыре ответа на четыре просьбы
for r in 0-9 0-9,20-29 99999- 290-; do echo "== Range: bytes=$r"; curl -s -o /dev/null -D- -r $r localhost/f.txt | grep -E 'HTTP|Content-Range|Content-Length|Content-Type'; done
== Range: bytes=0-9
HTTP/1.1 206 Partial Content
Content-Type: text/plain
Content-Length: 10
Content-Range: bytes 0-9/300
== Range: bytes=0-9,20-29
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000000000000000001
Content-Length: 218
== Range: bytes=99999-
HTTP/1.1 416 Requested Range Not Satisfiable
Content-Type: text/html
Content-Length: 197
Content-Range: bytes */300
== Range: bytes=290-
HTTP/1.1 206 Partial Content
Content-Type: text/plain
Content-Length: 10
Content-Range: bytes 290-299/300
-o /dev/null выбрасывает тело, -D- печатает заголовки ответа на экран.
Байты считаются с нуля, и обе границы входят в кусок: 0-9 - это десять
байт, а 290- - «с 290-го до конца».
Что стоит заметить.
Полный размер файла всегда в ответе - после дроби в Content-Range. Клиент
узнаёт его из первого же куска и дальше считает прогресс сам.
Несколько кусков за раз - это другой формат тела. multipart/byteranges
складывает куски с разделителями, как вложения в письме; boundary - и есть
строка-разделитель между кусками. Так браузер тянет
шрифты и метаданные видео: два-три разных места файла одним запросом.
Выход за границу - 416, а не 404. В ответе Content-Range: bytes */300 -
«такого куска нет, а файл длиной 300». Разница смысловая: файл есть, а такого
куска в нём нет. Клиент, получив 416, должен переспросить целиком, а не решить,
что файл пропал.
Разбор: где наивное правило подводит
Сжатие и куски несовместимы. Тут никакой настройки «включить и то и другое» нет: сжатый поток - другие байты, и смещения в нём не совпадают с исходными. На практике это значит: для текстовых файлов выбирают сжатие (они мелкие, докачивать нечего), а для архивов, видео и дистрибутивов - куски. Хорошая новость в том, что такие файлы и так сжаты внутри, и сжимать их второй раз бессмысленно.
Проксируемый ответ nginx кусками не режет. Заголовок Range он передаст
бэкенду, а дальше всё зависит от приложения: умеет - вернёт 206, не умеет - вернёт
200 и весь файл. Проверено: тот же запрос к статике даёт 206, а к бэкенду 200.
Если раздачу больших файлов делает приложение, докачка становится его заботой -
или файлы отдают напрямую с диска, минуя его.
Куски можно и запретить. max_ranges 0; (пишется в http, server или
location; число - сколько кусков разрешено в одном запросе) - и nginx
перестаёт понимать Range,
убрав заодно Accept-Ranges. Директиву иногда ставят против клиентов, которые
дробят загрузку на сотни запросов и создают лишнюю нагрузку. Проверено: ответ
становится обычным 200.
Что ломается без этого
Первое - «докачка не работает, и непонятно почему». Пока не знаешь про связку со
сжатием, ищешь причину в клиенте, в сети, в правах на файл. А достаточно
посмотреть, есть ли в ответе Accept-Ranges.
Второе - видео, которое не перематывается. Плеер перематывает ровно
Range-запросами: просит кусок с нужной секунды. Нет кусков - нет перемотки,
только последовательное воспроизведение с начала.
Зачем это в работе
Проверка занимает секунду:
curl -sI https://shop.local/video/intro.mp4 | grep -i accept-ranges
Есть строка - докачка и перемотка работают. Нет - смотри, не попал ли этот тип
файлов под gzip_types (тип nginx берёт по расширению из mime.types) и не
стоит ли где-нибудь max_ranges 0.
Попросить кусок руками тоже полезно, особенно на большом файле:
curl -s -r 0-99 -o /dev/null -D- https://shop.local/dist/app.tar.gz | grep -i content-range
Флаг -r в curl - это и есть Range. Ответ с Content-Range подтверждает, что
сервер умеет отдавать куски, и заодно показывает полный размер файла, скачав
только эти сто байт: Content-Range: bytes 0-99/300.
Теперь сам
Раздача обновлений: файлы по 300 мегабайт, клиенты на мобильной связи, обрывы
частые. В конфиге для этого каталога стоит gzip on; и gzip_types со всеми
типами подряд. Что произойдёт и что менять?
Докачки не будет: сжатый ответ отдаётся только целиком, и клиент после обрыва
начнёт заново - на мобильной связи это может не закончиться никогда. Сжатие тут
и не нужно: обновления почти наверняка уже в архиве, и второй проход даёт
проценты. Убрать эти типы из gzip_types, проверить, что вернулся
Accept-Ranges: bytes, - и клиенты начнут дотягивать хвост вместо повторной
загрузки.
Главное
Range просит кусок файла, ответ - код 206 с Content-Range, где указаны
границы и полный размер. nginx ставит Accept-Ranges: bytes сам, и по этому
заголовку клиент понимает, что докачка возможна. Несколько кусков за раз дают
multipart/byteranges, выход за границу - 416. Сжатие отменяет куски целиком:
для текста выбирают сжатие, для архивов и видео - куски. Проксируемый ответ
режет не nginx, а приложение.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий