Самый быстрый способ подачи файла с помощью PHP

Я пытаюсь собрать функцию, которая получает путь к файлу, определяет, что это такое, устанавливает соответствующие заголовки и служит ему так же, как и Apache.

Причина, по которой я делаю это, – это то, что мне нужно использовать PHP для обработки некоторой информации о запросе перед тем, как обслуживать файл.

Скорость критическая

virtual () не является опцией

Должен работать в среде совместного размещения, где пользователь не имеет контроля над веб-сервером (Apache / nginx и т. Д.),

Вот что у меня до сих пор:

File::output($path); <?php class File { static function output($path) { // Check if the file exists if(!File::exists($path)) { header('HTTP/1.0 404 Not Found'); exit(); } // Set the content-type header header('Content-Type: '.File::mimeType($path)); // Handle caching $fileModificationTime = gmdate('D, d MYH:i:s', File::modificationTime($path)).' GMT'; $headers = getallheaders(); if(isset($headers['If-Modified-Since']) && $headers['If-Modified-Since'] == $fileModificationTime) { header('HTTP/1.1 304 Not Modified'); exit(); } header('Last-Modified: '.$fileModificationTime); // Read the file readfile($path); exit(); } static function mimeType($path) { preg_match("|\.([a-z0-9]{2,4})$|i", $path, $fileSuffix); switch(strtolower($fileSuffix[1])) { case 'js' : return 'application/x-javascript'; case 'json' : return 'application/json'; case 'jpg' : case 'jpeg' : case 'jpe' : return 'image/jpg'; case 'png' : case 'gif' : case 'bmp' : case 'tiff' : return 'image/'.strtolower($fileSuffix[1]); case 'css' : return 'text/css'; case 'xml' : return 'application/xml'; case 'doc' : case 'docx' : return 'application/msword'; case 'xls' : case 'xlt' : case 'xlm' : case 'xld' : case 'xla' : case 'xlc' : case 'xlw' : case 'xll' : return 'application/vnd.ms-excel'; case 'ppt' : case 'pps' : return 'application/vnd.ms-powerpoint'; case 'rtf' : return 'application/rtf'; case 'pdf' : return 'application/pdf'; case 'html' : case 'htm' : case 'php' : return 'text/html'; case 'txt' : return 'text/plain'; case 'mpeg' : case 'mpg' : case 'mpe' : return 'video/mpeg'; case 'mp3' : return 'audio/mpeg3'; case 'wav' : return 'audio/wav'; case 'aiff' : case 'aif' : return 'audio/aiff'; case 'avi' : return 'video/msvideo'; case 'wmv' : return 'video/x-ms-wmv'; case 'mov' : return 'video/quicktime'; case 'zip' : return 'application/zip'; case 'tar' : return 'application/x-tar'; case 'swf' : return 'application/x-shockwave-flash'; default : if(function_exists('mime_content_type')) { $fileSuffix = mime_content_type($path); } return 'unknown/' . trim($fileSuffix[0], '.'); } } } ?> 

    Мой предыдущий ответ был частичным и не совсем документированным, вот обновленная информация с резюме решений от него и других в обсуждении.

    Решения заказываются от лучшего решения до наихудшего, но также и от решения, требующего наибольшего контроля над веб-сервером, которому требуется меньше. Кажется, нет простого способа иметь одно решение, которое быстро и повсеместно работает.


    Использование заголовка X-SendFile

    Как документировано другими, это на самом деле лучший способ. Основа заключается в том, что вы выполняете свой контроль доступа в php, а затем вместо отправки файла сами указываете веб-серверу это сделать.

    Основной PHP-код:

     header("X-Sendfile: $file_name"); header("Content-type: application/octet-stream"); header('Content-Disposition: attachment; filename="' . basename($file_name) . '"'); 

    Где $file_name – полный путь к файловой системе.

    Основная проблема с этим решением заключается в том, что он должен быть разрешен веб-сервером и либо не установлен по умолчанию (apache), либо неактивен по умолчанию (lighttpd), либо нуждается в конкретной конфигурации (nginx).

    апаш

    Под apache, если вы используете mod_php, вам необходимо установить модуль с именем mod_xsendfile, а затем настроить его (либо в конфигурации apache, либо через .htaccess, если вы разрешите это)

     XSendFile on XSendFilePath /home/www/example.com/htdocs/files/ 

    С помощью этого модуля путь к файлу может быть либо абсолютным, либо относительным к указанному XSendFilePath .

    Lighttpd

    Mod_fastcgi поддерживает это при настройке с помощью

     "allow-x-send-file" => "enable" 

    Документация для этой функции находится на wiki lighttpd, они документируют заголовок X-LIGHTTPD-send-file но также имя X-Sendfile

    Nginx

    На Nginx вы не можете использовать заголовок X-Sendfile вы должны использовать свой собственный заголовок, который называется X-Accel-Redirect . Он включен по умолчанию, и единственная реальная разница в том, что его аргумент должен быть URI, а не файловой системой. Следствием является то, что вы должны определить местоположение, помеченное как внутреннее в вашей конфигурации, чтобы клиенты не находили истинный URL-адрес файла и не переходили непосредственно к нему, их вики содержит хорошее объяснение этого.

    Заголовок Symlinks и Location

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

     header("Location: " . $url_of_symlink); 

    Очевидно, вам понадобится способ обрезать их либо при вызове сценария для их создания, либо через cron (на машине, если у вас есть доступ или через какую-либо службу webcron)

    Под apache вы должны иметь возможность включить FollowSymLinks в .htaccess или в конфигурации apache.

    Контроль доступа по IP и заголовку местоположения

    Другой взлом – это создание файлов доступа apache с php, позволяющих явный IP-адрес пользователя. Под apache это означает использование mod_access ( mod_access ) Allow from команд.

    Проблема в том, что блокировка доступа к файлу (поскольку несколько пользователей могут захотеть сделать это в одно и то же время) является нетривиальной и может привести к тому, что некоторые пользователи ожидают долгое время. И все равно вам все равно нужно обрезать файл.

    Очевидно, что еще одна проблема заключается в том, что несколько человек за одним и тем же IP могут потенциально получить доступ к файлу.

    Когда все остальное не работает

    Если у вас действительно нет способа заставить ваш веб-сервер помочь вам, единственным оставшимся решением является readfile, он доступен во всех версиях PHP, которые в настоящее время используются и работают очень хорошо (но не очень эффективны).


    Объединение решений

    В лучшем случае лучший способ отправить файл очень быстро, если вы хотите, чтобы ваш php-код был полезен повсюду, должен иметь где-то настраиваемый параметр с инструкциями о том, как его активировать, в зависимости от веб-сервера и, возможно, автоматическое обнаружение в вашей установке скрипт.

    Это очень похоже на то, что сделано во многих программах для

    • Очистить URL ( mod_rewrite на apache)
    • Функции mcrypt ( mcrypt php module)
    • Поддержка многобайтовой строки (модуль mbstring php)

    Самый быстрый способ: не надо. Посмотрите в заголовок x-sendfile для nginx , есть и другие вещи для других веб-серверов. Это означает, что вы все еще можете управлять доступом и т. Д. В php, но делегировать фактическую отправку файла на веб-сервер, предназначенный для этого.

    PS: Я получаю озноб, просто думая о том, насколько эффективнее это использовать с nginx, по сравнению с чтением и отправкой файла в php. Подумайте, если 100 человек загружают файл: с php + apache, будучи щедрым, возможно, 100 * 15 мб = 1,5 ГБ (примерно, стрелять мне), там, где есть. Nginx просто передаст файл в ядро, а затем загрузится непосредственно с диска в сетевые буферы. Speedy!

    PPS: И с помощью этого метода вы все равно можете выполнять все функции контроля доступа, необходимые для базы данных.

    Здесь идет чистое PHP-решение. Я адаптировал следующую функцию из своей личной структуры :

     function Download($path, $speed = null, $multipart = true) { while (ob_get_level() > 0) { ob_end_clean(); } if (is_file($path = realpath($path)) === true) { $file = @fopen($path, 'rb'); $size = sprintf('%u', filesize($path)); $speed = (empty($speed) === true) ? 1024 : floatval($speed); if (is_resource($file) === true) { set_time_limit(0); if (strlen(session_id()) > 0) { session_write_close(); } if ($multipart === true) { $range = array(0, $size - 1); if (array_key_exists('HTTP_RANGE', $_SERVER) === true) { $range = array_map('intval', explode('-', preg_replace('~.*=([^,]*).*~', '$1', $_SERVER['HTTP_RANGE']))); if (empty($range[1]) === true) { $range[1] = $size - 1; } foreach ($range as $key => $value) { $range[$key] = max(0, min($value, $size - 1)); } if (($range[0] > 0) || ($range[1] < ($size - 1))) { header(sprintf('%s %03u %s', 'HTTP/1.1', 206, 'Partial Content'), true, 206); } } header('Accept-Ranges: bytes'); header('Content-Range: bytes ' . sprintf('%u-%u/%u', $range[0], $range[1], $size)); } else { $range = array(0, $size - 1); } header('Pragma: public'); header('Cache-Control: public, no-cache'); header('Content-Type: application/octet-stream'); header('Content-Length: ' . sprintf('%u', $range[1] - $range[0] + 1)); header('Content-Disposition: attachment; filename="' . basename($path) . '"'); header('Content-Transfer-Encoding: binary'); if ($range[0] > 0) { fseek($file, $range[0]); } while ((feof($file) !== true) && (connection_status() === CONNECTION_NORMAL)) { echo fread($file, round($speed * 1024)); flush(); sleep(1); } fclose($file); } exit(); } else { header(sprintf('%s %03u %s', 'HTTP/1.1', 404, 'Not Found'), true, 404); } return false; } 

    Код так же эффективен, как и может быть, он закрывает обработчик сеанса, так что другие скрипты PHP могут запускаться одновременно для одного и того же пользователя / сеанса. Он также поддерживает загрузку загрузок в диапазонах (что также делает Apache по умолчанию, как я подозреваю), чтобы люди могли приостановить / возобновить загрузку, а также получить более высокую скорость загрузки с помощью ускорителей загрузки. Он также позволяет указать максимальную скорость (в килобайтах), при которой загрузка (часть) должна быть подана с помощью аргумента $speed .

     header('Location: ' . $path); exit(0); 

    Пусть Apache сделает для вас работу.

    если у вас есть возможность добавить расширения PECL на ваш php, вы можете просто использовать функции из пакета Fileinfo, чтобы определить тип содержимого, а затем отправить соответствующие заголовки …

    Функция Download PHP, упомянутая здесь, вызывала некоторую задержку, прежде чем файл начал загружаться. Я не знаю, было ли это вызвано использованием кеша лака или что, но для меня это помогло удалить sleep(1); полностью и установить $speed до 1024 . Теперь он работает без каких-либо проблем, как быстро, как черт. Возможно, вы тоже можете изменить эту функцию, потому что я видел, как она использовалась по всему Интернету.

    Лучшая реализация, с поддержкой кеша, настраиваемые заголовки HTTP.

     serveStaticFile($fn, array( 'headers'=>array( 'Content-Type' => 'image/x-icon', 'Cache-Control' => 'public, max-age=604800', 'Expires' => gmdate("D, d MYH:i:s", time() + 30 * 86400) . " GMT", ) )); function serveStaticFile($path, $options = array()) { $path = realpath($path); if (is_file($path)) { if(session_id()) session_write_close(); header_remove(); set_time_limit(0); $size = filesize($path); $lastModifiedTime = filemtime($path); $fp = @fopen($path, 'rb'); $range = array(0, $size - 1); header('Last-Modified: ' . gmdate("D, d MYH:i:s", $lastModifiedTime)." GMT"); if (( ! empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModifiedTime ) ) { header("HTTP/1.1 304 Not Modified", true, 304); return true; } if (isset($_SERVER['HTTP_RANGE'])) { //$valid = preg_match('^bytes=\d*-\d*(,\d*-\d*)*$', $_SERVER['HTTP_RANGE']); if(substr($_SERVER['HTTP_RANGE'], 0, 6) != 'bytes=') { header('HTTP/1.1 416 Requested Range Not Satisfiable', true, 416); header('Content-Range: bytes */' . $size); // Required in 416. return false; } $ranges = explode(',', substr($_SERVER['HTTP_RANGE'], 6)); $range = explode('-', $ranges[0]); // to do: only support the first range now. if ($range[0] === '') $range[0] = 0; if ($range[1] === '') $range[1] = $size - 1; if (($range[0] >= 0) && ($range[1] <= $size - 1) && ($range[0] <= $range[1])) { header('HTTP/1.1 206 Partial Content', true, 206); header('Content-Range: bytes ' . sprintf('%u-%u/%u', $range[0], $range[1], $size)); } else { header('HTTP/1.1 416 Requested Range Not Satisfiable', true, 416); header('Content-Range: bytes */' . $size); return false; } } $contentLength = $range[1] - $range[0] + 1; //header('Content-Disposition: attachment; filename="xxxxx"'); $headers = array( 'Accept-Ranges' => 'bytes', 'Content-Length' => $contentLength, 'Content-Type' => 'application/octet-stream', ); if(!empty($options['headers'])) { $headers = array_merge($headers, $options['headers']); } foreach($headers as $k=>$v) { header("$k: $v", true); } if ($range[0] > 0) { fseek($fp, $range[0]); } $sentSize = 0; while (!feof($fp) && (connection_status() === CONNECTION_NORMAL)) { $readingSize = $contentLength - $sentSize; $readingSize = min($readingSize, 512 * 1024); if($readingSize <= 0) break; $data = fread($fp, $readingSize); if(!$data) break; $sentSize += strlen($data); echo $data; flush(); } fclose($fp); return true; } else { header('HTTP/1.1 404 Not Found', true, 404); return false; } } 

    Если вы хотите скрыть место, где находится файл, и люди с определенными привилегиями могут загрузить файл, тогда рекомендуется использовать PHP как ретрансляцию, и вы должны пожертвовать некоторым временем процессора, чтобы получить больше безопасности и контроля.