最快的方式来使用PHP服务文件
我试图把一个接收文件path的函数放在一起,标识它是什么,设置适当的头文件,并像Apache那样服务它。
我这样做的原因是因为我需要使用PHP在提供文件之前处理一些有关请求的信息。
速度至关重要
虚拟()不是一个选项
必须在用户无法控制Web服务器的共享主机环境中工作(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], '.'); } } } ?>
我之前的回答是部分的,没有很好的logging,这里是一个更新,其中有来自其他人的解决scheme的总结。
这些解决scheme从最好的解决scheme到最差的解决scheme,也是从需要对Web服务器进行最多控制的解决scheme到需要更less的解决scheme。 似乎没有一种简单的方法可以使一个解决scheme既快速又无处不在。
使用X-SendFile标题
正如其他人所记载,这实际上是最好的方法。 基础是,你在php中进行访问控制,然后不要自己发送文件,而是告诉Web服务器去做。
基本的PHP代码是:
header("X-Sendfile: $file_name"); header("Content-type: application/octet-stream"); header('Content-Disposition: attachment; filename="' . basename($file_name) . '"');
其中$file_name
是文件系统上的完整path。
这个解决scheme的主要问题是它需要被Web服务器所允许,并且默认情况下(apache)没有被安装,缺省情况下(lighttpd)没有被激活,或者需要特定的configuration(nginx)。
阿帕奇
在Apache下,如果你使用mod_php,你需要安装一个名为mod_xsendfile的模块,然后configuration它(如果你允许的话,可以在apacheconfiguration文件或者.htaccess文件中)
XSendFile on XSendFilePath /home/www/example.com/htdocs/files/
使用这个模块,文件path可以是绝对的或者相对于指定的XSendFilePath
。
Lighttpd的
mod_fastcgi在configuration时支持
"allow-x-send-file" => "enable"
该function的文档位于lighttpd wiki上,它们logging了X-LIGHTTPD-send-file
标题,但X-Sendfile
名称也起作用
Nginx的
在Nginx上,您不能使用X-Sendfile
头,您必须使用自己的名为X-Accel-Redirect
。 它是默认启用的,唯一真正的区别是它的参数应该是一个URI而不是文件系统。 其结果是,你必须在你的configuration中定义一个标记为内部的位置,以避免客户端find真正的文件url并直接进入,他们的wiki包含了一个很好的解释 。
符号链接和位置标题
您可以使用符号链接并redirect到它们,只需在用户被授权访问文件并使用以下命令将用户redirect到文件时,使用随机名称创build符号链接:
header("Location: " . $url_of_symlink);
显然你需要一种方法来修剪它们,当创build它们的脚本被调用时,或者通过cron(在机器上,如果你有访问权限,或者通过一些webcron服务,否则)
在apache下,您需要能够在.htaccess
或apacheconfiguration中启用FollowSymLinks
。
通过IP和位置标题进行访问控制
另一个黑客是从PHP生成Apache访问文件,允许明确的用户IP。 在Apache下意味着使用mod_authz_host
( mod_access
) Allow from
命令。
问题在于,locking文件的访问权限(因为多个用户可能想同时执行此操作)不是微不足道的,可能会导致一些用户长时间等待。 而且你仍然需要修剪文件。
显然,另一个问题是同一个IP后面的多个人可能会访问这个文件。
当一切都失败了
如果你真的没有办法让你的web服务器来帮助你,唯一的解决scheme是readfile它可以在所有的php版本中使用,并且工作得很好(但是效率不高)。
结合解决scheme
总的来说,如果你希望你的php代码能够在任何地方都可用,发送文件的最好方法是在某个地方configuration一个可configuration选项,以及如何根据networking服务器激活它的说明,或者在你的安装中自动检测脚本。
这与许多软件所做的很相似
- 清理url(Apache上的
mod_rewrite
) - encryptionfunction(
mcrypt
php模块) - 多字节string支持(
mbstring
php模块)
最快的方法:不要。 看看nginx的x-sendfile标题 ,其他web服务器也有类似的东西。 这意味着你仍然可以在php中进行访问控制等,但委托文件的实际发送到为此devise的Web服务器。
PS:我只是想着用nginx做这个比用PHP读取和发送文件更有效率。 只是想,如果有100人下载文件:用PHP + Apache,慷慨,这可能是100 * 15mb = 1.5GB(大约射击我),在那里的公羊。 Nginx将把文件发送到内核,然后直接从磁盘加载到networking缓冲区。 迅速!
PPS:用这种方法,你仍然可以做所有的访问控制,你想要的数据库的东西。
这里是一个纯粹的PHP解决scheme。 我从我的个人框架中调整了以下function:
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; }
代码是尽可能高效的,它会closures会话处理程序,以便其他PHP脚本可以同时运行在同一个用户/会话中。 它还支持范围内的服务下载(这也是Apache默认情况下我所怀疑的),以便人们可以暂停/恢复下载,也可以通过下载加速器获得更高的下载速度。 它还允许您指定下载(部分)应通过$speed
参数提供的最大速度(以Kbps为单位)。
header('Location: ' . $path); exit(0);
让Apache为你做好工作。
如果你有可能添加PECL扩展到你的PHP,你可以简单地使用Fileinfo包中的函数来确定内容types,然后发送适当的头文件…
这里提到的PHP Download
function在文件实际开始下载之前造成了一些延迟。 我不知道这是由于使用清漆caching或什么,但对我来说,它有助于消除sleep(1);
完全设置$speed
为1024
。 现在它没有任何问题,像地狱一样快。 也许你可以修改这个function,因为我看到它在互联网上使用。
更好的实现,支持caching,自定义的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作为中继是一个不错的主意,您必须牺牲一些CPU时间来获得更多的安全性和控制权。