Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Диапазонные запросы и стриминг больших файлов

Range-запросы: отдача больших файлов частями

HTTP Range: 206 Partial Content, Content-Range, Accept-Ranges, возобновляемая загрузка, параллельное скачивание и реализация на сервере.

Прямой ответ

Диапазонный запрос позволяет клиенту получить не весь файл, а заданный фрагмент. Сервер отвечает кодом 206 Partial Content и заголовком Content-Range, указывающим, какая часть возвращена.

GET /files/video.mp4 HTTP/1.1
Range: bytes=1048576-2097151

HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 1048576-2097151/104857600
Content-Length: 1048576

Три заголовка образуют механизм:

Заголовок Направление Назначение
Accept-Ranges: bytes Ответ Сервер поддерживает диапазоны
Range: bytes=старт-конец Запрос Запрашиваемый фрагмент
Content-Range: bytes старт-конец/всего Ответ Фактический фрагмент и размер файла

Если диапазон недостижим, сервер возвращает 416 Range Not Satisfiable с указанием действительной длины.

Что это даёт на практике:

  1. Возобновляемая загрузка. После обрыва клиент запрашивает продолжение: curl -C - -O или wget -c`.
  2. Параллельное скачивание. Клиент делит файл на части и получает их одновременно, полнее используя полосу.
  3. Потоковое видео. Плеер запрашивает фрагменты по мере воспроизведения и переходит к нужному моменту без загрузки всего файла.
  4. Экономия памяти сервера. Отдаётся только запрошенный фрагмент.

Реализация на сервере:

from fastapi.responses import StreamingResponse
import os, re

@app.get("/files/{file_id}")
def get_file(file_id: str, range_header: str | None = Header(None)):
    path = resolve_safe(file_id)
    size = os.path.getsize(path)

    if range_header and (m := re.match(r"bytes=(\d+)-(\d*)", range_header)):
        start = int(m.group(1))
        end = int(m.group(2)) if m.group(2) else size - 1
        end = min(end, size - 1)
        if start > end:
            return Response(status_code=416,
                            headers={"Content-Range": f"bytes */{size}"})
        length = end - start + 1
        return StreamingResponse(
            iter_chunks(path, start, length),
            status_code=206,
            media_type="application/octet-stream",
            headers={
                "Content-Range": f"bytes {start}-{end}/{size}",
                "Accept-Ranges": "bytes",
                "Content-Length": str(length),
            },
        )
    return FileResponse(path, headers={"Accept-Ranges": "bytes"})


def iter_chunks(path: str, start: int, length: int, chunk: int = 256 * 1024):
    with open(path, "rb") as f:
        f.seek(start)
        remaining = length
        while remaining > 0 and (block := f.read(min(chunk, remaining))):
            remaining -= len(block)
            yield block

Ключевые моменты: seek вместо чтения с начала, генератор вместо загрузки файла в память, явный Content-Length и корректный Content-Range.

Практика: надёжная отдача больших файлов

1. Не отдавайте файлы из приложения без причины. Nginx делает это эффективнее через sendfile, не вовлекая процесс приложения:

location /media/ {
    internal;                          # только через X-Accel-Redirect
    sendfile on;
    aio threads;
}

Приложение проверяет права и возвращает заголовок X-Accel-Redirect с внутренним путём, а отдачу выполняет прокси — см. статью про nginx.

2. Проверяйте права до отдачи фрагмента. Диапазонный запрос не отменяет авторизацию; иначе доступ к части файла даёт доступ ко всему.

3. Используйте ETag вместе с If-Range. Если файл изменился между запросами частей, клиент получит фрагменты разных версий. Условный запрос защищает от этого.

4. Учитывайте поведение прокси. Некоторые прокси и CDN кэшируют только полные ответы. Для корректной работы с кэшем нужны Accept-Ranges и согласованный ETag.

5. Ограничивайте число одновременных отдач. Параллельная загрузка одним клиентом создаёт много соединений; лимит задаётся на уровне прокси.

6. Следите за таймаутами. Отдача большого файла на медленном канале занимает минуты; стандартный таймаут прокси обрывает передачу.

7. Проверяйте поддержку на реальном клиенте. Отсутствие Accept-Ranges в ответе заставляет клиента скачивать файл целиком. Проверка выполняется одним запросом с заголовком Range и ожиданием кода 206 в ответе.

8. Выбирайте размер блока осознанно. Слишком мелкий блок увеличивает число системных вызовов и накладные расходы, слишком крупный — повышает потребление памяти и задержку первого байта. Практичный диапазон — от 64 до 512 КБ; для медленных каналов удобнее меньший размер, чтобы клиент раньше начинал получать данные.

9. Не кэшируйте частичные ответы без явной настройки. Промежуточный кэш, получив ответ 206, по умолчанию не может собрать из фрагментов полный объект. Для кэшируемого контента отдачу диапазонов настраивают на уровне прокси или CDN, а не в приложении.

Рекомендации:

  1. Отдавайте потоково всегда. Даже без поддержки диапазонов чтение файла в память неоправданно; о механизмах давления в потоках — в статье про backpressure.
  2. Тестируйте возобновление. Обрыв на середине и продолжение — обязательный сценарий проверки.
  3. Логируйте код 206 отдельно от 200. Иначе в статистике невозможно отличить полную отдачу от частичной.
  4. Не смешивайте отдачу файлов с приёмом. Нагрузка на канал разная; лимиты и таймауты настраиваются отдельно — см. статью про загрузку файлов.
  5. Проверяйте корректность Content-Range. Ошибка в длине приводит к тому, что клиент считает файл повреждённым.

Практический сценарий: видео не перематывалось, а загружалось целиком

Контекст: плеер на странице начинал воспроизведение только после полной загрузки файла объёмом 1,2 ГБ. Перемотка не работала.

  1. Диагностика. В ответе сервера отсутствовал заголовок Accept-Ranges, а запрос с Range возвращал 200 с полным телом вместо 206.
  2. Причина. Файлы отдавались обработчиком приложения, который читал объект целиком и возвращал его как единый ответ. Поддержка диапазонов реализована не была.
  3. Проверка прокси. Nginx передавал ответ без изменений, поскольку proxy_pass на приложение не включает автоматическую обработку диапазонов для динамических ответов.
  4. Действия. (а) Перевели отдачу на схему X-Accel-Redirect: приложение проверяет права, а файл отдаёт nginx с sendfile on; (б) добавили Accept-Ranges: bytes в ответы; (в) для файлов, которые всё же отдаёт приложение, реализовали обработку Range с кодом 206; (г) увеличили proxy_read_timeout для длинных отдач; (д) добавили проверку в тесты: запрос с Range обязан возвращать 206 и корректный Content-Range.
  5. Результат. Воспроизведение начиналось сразу, перемотка работает, объём переданных данных сократился, поскольку пользователь больше не загружает файл целиком.

Вывод: отсутствие поддержки диапазонов превращает просмотр в полную загрузку файла. Проверка сводится к одному запросу с заголовком Range и ожиданию кода 206.

FAQ

Что вернёт сервер без поддержки диапазонов? Ответ 200 с полным телом. Клиент обязан это обработать: отсутствие 206 означает, что нужно скачать файл целиком.

Как запросить несколько диапазонов одним запросом? Через Range: bytes=0-99, 200-299 — сервер ответит multipart/byteranges. На практике используется редко из-за сложности разбора.

Работает ли Range для сжатых ответов? Проблематично: диапазон применяется к сжатому телу, поэтому клиент не может распаковать фрагмент отдельно. Для больших файлов сжатие на лету отключают.

Обязан ли клиент поддерживать диапазоны? Нет. Клиент может игнорировать Accept-Ranges и запрашивать файл целиком, поэтому сервер обязан корректно обрабатывать и обычные запросы. Отсутствие заголовка Range — нормальный случай, а не ошибка.

Как возобновить загрузку с клиента? curl -C - -O <url> или wget -c <url>. Обе команды запрашивают продолжение с последнего полученного байта.

Источники: RFC 9110 (range requests), MDN (Range, 206) — ссылки в шапке.