20 February 2012

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

PHP
В одном из проектов мне понадобилось разобрать 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;
  }
}
?>
Tags:rfc2616httprangephp
Hubs: PHP
+22
25.1k 163
Comments 9
Top of the last 24 hours