Самый быстрый способ обслуживания файла с помощью 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 M Y H: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], '.');
}
}
}
?>
8 ответов:
мой предыдущий ответ был частичным и не очень хорошо документированным, вот обновление с кратким изложением решений от него и от других в обсуждении.
решения заказываются от лучшего решения к худшему, но также и от решения, нуждающегося в наибольшем контроле над веб-сервером, к тому, кому нужно меньше. Там, кажется, не простой способ иметь одно решение, которое является одновременно быстрым и работать везде.
использование 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
под 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"
документация для этой функции находится на lighttpd wiki они документ но
X-Sendfile
имя также работаютNginx
на Nginx вы не можете использовать вы должны использовать свой собственный заголовок, который называется
X-Accel-Redirect
. Он включен по умолчанию, и единственное реальное различие заключается в том, что его аргумент должен быть URI, а не файловой системой. Следовательно, необходимо определить место, отмеченное как внутренний в вашей конфигурации, чтобы клиенты не находили реальный url-адрес файла и не переходили непосредственно к нему, их Вики содержит хорошее объяснение этого.символические ссылки и заголовок местоположения
вы могли бы использовать ссылки и перенаправить на них, просто создать символические ссылки на файл со случайными именами, когда пользователь имеет право доступа к файлу и перенаправить пользователя к нему с помощью:
header("Location: " . $url_of_symlink);
очевидно, вам понадобится способ обрезать их либо когда скрипт для их создания вызывается, либо через cron (на машине, если у вас есть доступ или через какой-то сервис webcron в противном случае)
под Apache вы должны быть в состоянии включить
FollowSymLinks
на.htaccess
или в конфигурации apache.управление доступом по IP и заголовку местоположения
другой Хак заключается в создании файлов доступа apache из php, позволяющих явный IP-адрес пользователя. Под apache это означает использование
mod_authz_host
(mod_access
)Allow from
команды.проблема в том, что блокировка доступа к файлу (поскольку несколько пользователей могут захотеть сделать это одновременно) нетривиальна и может привести к тому, что некоторые пользователи будут ждать долгое время. И вам все равно нужно обрезать файл в любом случае.
очевидно, еще одна проблема будет заключаться в том, что несколько человек за одним и тем же IP-адресом могут потенциально получить доступ к файлу.
когда все остальное терпит неудачу
если вы действительно нет никакого способа, чтобы получить ваш веб-сервер, чтобы помочь вам, единственное решение остается readfile он доступен во всех версиях php, которые в настоящее время используются и работают довольно хорошо (но не очень эффективны).
объединение решений
в порядке, лучший способ отправить файл очень быстро, если вы хотите, чтобы ваш php-код был доступен везде, - это иметь настраиваемый параметр где-то, с инструкциями о том, как его активировать в зависимости от интернета сервер и, возможно, автоматическое обнаружение в вашем сценарии установки.
это очень похоже на то, что делается во многих программах для
- чистые URL-адреса (
mod_rewrite
на apache)- функции шифрования (
mcrypt
php модуль)- поддержка Многобайтовых строк (
mbstring
php модуль)
самый быстрый способ: не смотрите в заголовок x-sendfile для nginx, есть аналогичные вещи и для других веб-серверов. Это означает, что вы все еще можете управлять доступом и т. д. В php, но делегировать фактическую отправку файла на веб-сервер, предназначенный для этого.
P. S: Я получаю озноб, просто думая о том, насколько эффективнее использовать это с nginx, по сравнению с чтением и отправкой файла в php. Только подумайте, если 100 человек скачивают файл: с php + apache, будучи щедрым, это, вероятно, 100*15mb = 1.5 GB (приблизительно, стреляйте в меня), ОЗУ прямо там. Nginx просто передаст отправку файла ядру, а затем он будет загружен непосредственно с диска в сетевые буферы. Спиди!
P. P. S: и, с помощью этого метода вы все еще можете сделать все управление доступом, база данных вещи, которые вы хотите.
здесь идет чистое решение 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('~.*=([^,]*).*~', '', $_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 по умолчанию, я подозреваю), так что люди могут приостанавливать / возобновлять загрузки, а также извлекать выгоду из более высоких скоростей загрузки с загрузкой акселераторы. Он также позволяет указать максимальную скорость (в Кбит / с), при которой загрузка (часть) должна подаваться через
Если у вас есть возможность добавить расширения PECL в свой php, вы можете просто использовать функции из Fileinfo package чтобы определить тип контента, а затем отправить соответствующие заголовки...
PHP
Download
функция, упомянутая здесь, вызывала некоторую задержку, прежде чем файл действительно начал загружаться. Я не знаю, было ли это вызвано использованием кэша лака или что, но для меня это помогло удалить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 M Y H: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 M Y H: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; } }