使用PHP提供文件的最快方法

时间:2010-09-13 03:47:48

标签: php performance file-io x-sendfile

我正在尝试将一个接收文件路径的函数组合在一起,识别它是什么,设置适当的头文件,并像Apache那样提供服务。

我这样做的原因是因为我需要在提供文件之前使用PHP来处理有关请求的一些信息。

速度至关重要

virtual()不是一个选项

必须在用户无法控制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 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], '.');
    }
}
}
?>

9 个答案:

答案 0 :(得分:133)

我之前的回答是不完整的,没有详细记录,这里有一个更新,其中包含了解决方案的摘要以及讨论中的其他人。

解决方案从最佳解决方案到最差解决方案,也从需要最大程度控制Web服务器的解决方案到需要更少控制的解决方案。似乎没有一种简单的方法可以让一个既快速又适用的解决方案。


使用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是文件系统上的完整路径。

此解决方案的主要问题是需要Web服务器允许,默认情况下不安装(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" 

该功能的文档位于lighttpd wiki X-LIGHTTPD-send-file标题X-Sendfile标题,但X-Sendfile名称也可用

Nginx的

在Nginx上,您无法使用X-Accel-Redirect标头,您必须使用名为header("Location: " . $url_of_symlink); 的标头。它默认启用,唯一真正的区别是它的参数应该是URI而不是文件系统。结果是您必须在配置中定义标记为内部的位置,以避免客户端找到真实的文件URL并直接转到它,他们的wiki包含a good explanation

符号链接和位置标题

您可以使用symlinks并重定向到它们,只需在用户被授权访问文件并使用以下方法将用户重定向到该文件时,使用随机名称为您的文件创建符号链接:

mod_access

显然,你需要一种方法来修剪它们,无论是调用创建它们的脚本还是通过cron(如果你有访问权限,可以在机器上或通过某些webcron服务)

在apache下,您需要能够在FollowSymLinks或apache配置中启用.htaccess

按IP和位置标题进行访问控制

另一个黑客是从php生成apache访问文件,允许显式用户IP。在apache下,它意味着使用mod_authz_hostAllow frommod_rewrite命令。

问题在于锁定对文件的访问权限(因为多个用户可能希望同时执行此操作)并非易事,并且可能导致某些用户等待很长时间。而且你还是需要修剪文件。

显然,另一个问题是同一IP背后的多个人可能会访问该文件。

当其他一切都失败时

如果你真的没有办法让你的网络服务器帮助你,唯一剩下的解决方案是readfile它可以在当前使用的所有php版本中使用并且运行良好(但实际上并不是这样)有效)。


组合解决方案

很好,如果你希望你的php代码可以在任何地方使用,那么发送文件的最好方法是在某个地方有一个可配置的选项,有关如何激活它的说明,具体取决于web服务器和自动检测在您的安装脚本中。

它与许多软件中的内容非常相似

  • 清理网址(apache上为mcrypt
  • 加密函数(mbstring php模块)
  • 多字节字符串支持({{1}} php模块)

答案 1 :(得分:33)

最快的方式:不要。查看x-sendfile header for nginx,其他Web服务器也有类似的东西。这意味着您仍然可以在php中进行访问控制等,但将实际发送的文件委托给为此设计的Web服务器。

P.S:与在php中读取和发送文件相比,我在考虑使用nginx时效率更高的效率让我感到畏缩。试想是否有100个人正在下载文件:使用php + apache,慷慨,那可能是100 * 15mb = 1.5GB(约,拍我),ram就在那里。 Nginx只是将文件发送到内核,然后直接从磁盘加载到网络缓冲区。迅速!

P.P.S:并且,使用此方法,您仍然可以执行所需的所有访问控制,数据库内容。

答案 2 :(得分:20)

这是一个纯PHP解决方案。我已经改编了以下函数from my personal framework

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参数提供下载(部分)的最大速度(以Kbps为单位)。

答案 3 :(得分:13)

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

让Apache为您完成工作。

答案 4 :(得分:0)

如果你有可能在你的php中添加PECL扩展,你可以简单地使用Fileinfo package中的函数来确定内容类型,然后发送正确的标题......

答案 5 :(得分:0)

此处提到的PHP Download函数在文件实际开始下载之前导致了一些延迟。我不知道这是否是由使用清漆缓存造成的,但对我而言,它有助于完全删除sleep(1);并将$speed设置为1024。现在它的运行没有任何问题,就像地狱一样快。也许你也可以修改这个功能,因为我看到它在互联网上使用过。

答案 6 :(得分:0)

更好的实现,具有缓存支持,自定义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;
    }
}

答案 7 :(得分:0)

我编写了一个非常简单的函数来使用PHP和自动MIME类型检测来提供文件:

function serve_file($filepath, $new_filename=null) {
    $filename = basename($filepath);
    if (!$new_filename) {
        $new_filename = $filename;
    }
    $mime_type = mime_content_type($filepath);
    header('Content-type: '.$mime_type);
    header('Content-Disposition: attachment; filename="downloaded.pdf"');
    readfile($filepath);
}

用法

serve_file("/no_apache/invoice243.pdf");

答案 8 :(得分:-1)

如果您希望隐藏文件所在的位置,并且具有特定权限的人可以下载该文件,那么最好使用PHP作为中继,并且您必须牺牲一些CPU时间来获得更多安全性和控制。