Разбираем HTTP Range по стандарту

    В одном из проектов мне понадобилось разобрать HTTP Range запрос, чтобы добавить поддержку загрузки файлов по частям. В сети полно различных примеров, но я так и не нашел ни одной полной реализации RFC 2616. Один код не учитывал, что диапазонов может быть несколько, другой, что стандарт допускает запросы больше размера документа, третий не различает синтаксически правильный и недостижимый запрос, как рекомендует стандарт. Поэтому я решил написать свою реализацию и поделиться со всеми. Подробности и пример реализации на PHP под катом.

    Как гласит стандарт, запрос диапазона состоит из двух частей: размерность диапазона и список правил выборки. Единственная размерность диапазонов определенная в RFC 2616 – байты. Также, необходимо учесть, что в одном и том же заголовке Range может быть сразу несколько диапазонов, указанных через запятую.

    Существует два варианта выборки диапазона HTTP клиентом.

    Первый — указание начальной и конечной позиции в теле документа. Первая позиция начинается с нуля. Последняя позиция ОБЯЗАНА быть больше или равна первой позиции в запросе. В противном случае, согласно стандарту, этот заголовок реализация ОБЯЗАНА игнорировать. Если последняя позиция отсутствует или ее значение больше или равно размеру документа, то последней позицией считается текущий размер документа в байтах, уменьшенный на 1. По стандарту, это не является ошибкой, так как позволяет клиенту запрашивать часть документа, не зная его размер заранее.

    Например, для документа размером 10 байт, bytes=1-9 запрашивает 9 байт, начиная со второго и заканчивая последним байтом тела документа.

    Второй — выборка последних N байт тела документа. Если размер документа меньше, чем указанный в запросе, то будет выбран весь документ. Например, bytes=-2 запрашивает последние 2 байта.

    После завершения обработки всех диапазонов серверу СЛЕДУЕТ определить, есть ли хоть один диапазон, который содержит не нулевое количество байт. Если таких нет, то серверу СЛЕДУЕТ ответить клиенту 416 (Requested range not satisfiable), иначе 206 (Partial Content).

    Реализация считается «условно совместимой», если она не выполняет условия СЛЕДУЕТ (SHOULD). Таким образом, полная обработка запроса Range может быть достигнута при выполнении всех условий стандарта.

    Пример реализации на PHP:

    <?php namespace HTTP;
    /*
     * Copyright (c) 2012, aignospam@gmail.com
     * http://www.opensource.org/licenses/bsd-license.php
     */
    
    /**
     * Parse HTTP Range header
     * http://tools.ietf.org/html/rfc2616#section-14.35
     * return array of Range on success
     *        false on syntactically invalid byte-range-spec
     *        empty array on unsatisfiable bytes-range-set
     * @param int $entity_body_length
     * @param string range_header
     * @return array|bool
     */
    function parse_range_request($entity_body_length, $range_header)
    {
      $range_list = array();
    
      if ($entity_body_length == 0) {
        return $range_list; // mark unsatisfiable
      }
    
      // The only range unit defined by HTTP/1.1 is "bytes". HTTP/1.1
      // implementations MAY ignore ranges specified using other units.
      // Range unit "bytes" is case-insensitive
      if (preg_match('/^bytes=([^;]+)/i', $range_header, $match)) {
        $range_set = $match[1];
      } else {
        return false;
      }
    
      // Wherever this construct is used, null elements are allowed, but do
      // not contribute to the count of elements present. That is,
      // "(element), , (element) " is permitted, but counts as only two elements.
      $range_spec_list = preg_split('/,/', $range_set, null, PREG_SPLIT_NO_EMPTY);
    
      foreach ($range_spec_list as $range_spec) {
        $range_spec = trim($range_spec);
    
        if (preg_match('/^(\d+)\-$/', $range_spec, $match)) {
          $first_byte_pos = $match[1];
    
          if ($first_byte_pos > $entity_body_length) {
            continue;
          }
    
          $first_pos = $first_byte_pos;
          $last_pos = $entity_body_length - 1;
        } elseif (preg_match('/^(\d+)\-(\d+)$/', $range_spec, $match)) {
          $first_byte_pos = $match[1];
          $last_byte_pos = $match[2];
    
          // If the last-byte-pos value is present, it MUST be greater than or
          // equal to the first-byte-pos in that byte-range-spec
          if ($last_byte_pos < $first_byte_pos) {
            return false;
          }
    
          $first_pos = $first_byte_pos;
          $last_pos = min($entity_body_length - 1, $last_byte_pos);
        } elseif (preg_match('/^\-(\d+)$/', $range_spec, $match)) {
          $suffix_length = $match[1];
    
          if ($suffix_length == 0) {
            continue;
          }
    
          $first_pos = $entity_body_length - min($entity_body_length, $suffix_length);
          $last_pos = $entity_body_length - 1;
        } else {
          return false;
        }
    
        $range_list[] = new Range($first_pos, $last_pos);
      }
    
      return $range_list;
    }
    
    class Range
    {
      private $_first_pos;
      private $_last_pos;
    
      public function __construct($first_pos, $last_pos) {
        $this->_first_pos = $first_pos;
        $this->_last_pos = $last_pos;
      }
    
      public function get_first_pos()
      {
        return $this->_first_pos;
      }
    
      public function get_last_pos()
      {
        return $this->_last_pos;
      }
    }
    ?>
    
    • +22
    • 16,8k
    • 9
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 9
    • 0
      Хотелось бы увидеть рабочее демо.
    • 0
      preg_match('/^bytes=(.+)$/i', $range_header, $match)
      Разве HTTP не допускает перечислить через точку с запятой после этого поля ещё несколько? Что-то вроде bytes=0-200; x-something-custom=x-value?
      preg_split('/,/', $range_set, null, PREG_SPLIT_NO_EMPTY)
      array_filter(explode(',', $range_set), 'strlen')
      • 0
        Спасибо, поправил. Про комментарий забыл совсем.
        • 0
          /^bytes=([^;]+)$/i

          Надо ещё доллар в конце убрать.
          • 0
            Да, спасибо, только что прогнал тесты, сам заметил.
      • 0
        public function get_first_pos()
        {
        return $this->_first_pos;
        }

        public function get_last_pos()
        {
        return $this->_last_pos;
        }
        Зачем методы на каждое свойство, если есть __get? Почему класс, а не массив?
        • 0
          Не думаю, что это сильно важно в данном случае. У меня испольуется класс, так как Range еще умеет разбивать на чанки (делать выравнивание по границе блока). Просто удалил лишнее из кода.
        • +1
          Краткое содержание поста
          str_replace('bytes=', '', $_SERVER['HTTP_RANGE'])

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.