【问题标题】:Fastest Way to Serve a File Using PHP使用 PHP 提供文件的最快方法
【发布时间】:2011-04-11 11:55:23
【问题描述】:

我正在尝试组合一个函数来接收文件路径,识别它是什么,设置适当的标题,并像 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], '.');
    }
}
}
?>

【问题讨论】:

  • 你为什么不让 Apache 这样做呢?它总是比启动 PHP 解释器快得多...
  • 我需要处理请求并在输出文件之前将一些信息存储在数据库中。
  • 我可以建议一种无需更昂贵的正则表达式即可获得扩展的方法:$extension = end(explode(".", $pathToFile)),或者您可以使用 substr 和 strrpos:$extension = substr($pathToFile, strrpos($pathToFile, '.'))。此外,作为mime_content_type() 的后备方案,您可以尝试系统调用:$mimetype = exec("file -bi '$pathToFile'", $output);
  • 你说的最快是什么意思?最快的下载时间?

标签: php performance file-io x-sendfile


【解决方案1】:

我编写了一个非常简单的函数来使用 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");

【讨论】:

    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

      这里提到的 PHP Download 函数在文件实际开始下载之前造成了一些延迟。我不知道这是否是由使用清漆缓存或什么引起的,但对我来说,它有助于完全删除sleep(1); 并将$speed 设置为1024。现在它可以毫无问题地运行,速度非常快。也许你也可以修改那个函数,因为我看到它在整个互联网上都被使用了。

      【讨论】:

        【解决方案4】:

        这里有一个纯 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 为单位)。

        【讨论】:

        • 显然,如果您不能使用 X-Sendfile 或其变体之一让内核发送文件,这只是一个好主意。您应该能够用 [php.net/manual/en/function.eio-sendfile.php](PHP's eio_sendfile()] 调用替换上面的 feof()/fread() 循环,这在 PHP 中完成了同样的事情。这不像直接在内核中那样快,因为在 PHP 中生成的任何输出仍然必须通过 web 服务器进程返回,但它会比在 PHP 代码中快得多。
        • @BrianC:当然可以,但是您不能使用 X-Sendfile 限制速度或多部分功能(可能不可用),eio 也不总是可用的。尽管如此,+1 并不知道那个 pecl 扩展。 =)
        • 支持 transfer-encoding:chunked 和 content-encoding:gzip 会有用吗?
        • 为什么是$size = sprintf('%u', filesize($path))
        • @Alix Axel 谢谢 ;)
        【解决方案5】:

        我之前的回答是片面的,没有很好的记录,这里是一个更新,其中包含来自它和讨论中其他人的解决方案的摘要。

        解决方案的顺序是从最佳解决方案到最差解决方案,但也从最需要控制 Web 服务器的解决方案到需要较少的解决方案。似乎没有一种简单的方法来获得一种既快速又适用于任何地方的解决方案。


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

        此解决方案的主要问题是它需要被 Web 服务器允许,并且默认情况下未安装 (apache)、默认情况下不活动 (lighttpd) 或需要特定配置 (nginx)。

        阿帕奇

        在 apache 下,如果你使用 mod_php,你需要安装一个名为 mod_xsendfile 的模块然后配置它(在 apache config 或 .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 名称也可以使用

        Nginx

        在 Nginx 上,您不能使用 X-Sendfile 标头,您必须使用它们自己的标头 X-Accel-Redirect。它是默认启用的,唯一真正的区别是它的参数应该是一个 URI 而不是文件系统。结果是您必须在配置中定义一个标记为内部的位置,以避免客户端找到真正的文件 url 并直接访问它,他们的 wiki 包含 a good explanation

        符号链接和位置标头

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

        header("Location: " . $url_of_symlink);
        

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

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

        通过 IP 和 Location 标头进行访问控制

        另一个技巧是从 php 生成允许显式用户 IP 的 apache 访问文件。在 apache 下意味着使用 mod_authz_host (mod_access) Allow from 命令。

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

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

        当一切都失败时

        如果你真的没有办法让你的网络服务器帮助你,唯一剩下的解决方案是readfile 它在当前使用的所有 php 版本中都可用并且工作得很好(但不是很有效) .


        组合解决方案

        好吧,如果您希望您的 php 代码在任何地方都可用,那么快速发送文件的最佳方法是在某处有一个可配置的选项,其中包含有关如何根据 Web 服务器激活它的说明,也许还有自动检测在您的安装脚本中。

        这与许多软件中所做的非常相似

        • 干净的网址(mod_rewrite 在 apache 上)
        • 加密函数(mcrypt php 模块)
        • 多字节字符串支持(mbstring php 模块)

        【讨论】:

        • 在做header("Location: " . $path);之前做一些PHP工作(检查cookie/其他GET/POST参数对数据库)有什么问题吗?
        • 这种操作没问题,你需要注意的是发送内容(打印,回显),因为标头必须在任何内容之前,并且在发送此标头之后执行操作,它不是立即重定向和代码大部分时间都会执行,但您不能保证浏览器不会断开连接。
        • Jords:我不知道 apache 也支持这个,有时间我会把这个添加到我的答案中。唯一的问题是我没有统一(例如 X-Accel-Redirect nginx),因此如果服务器不支持它,则需要第二个解决方案。但我应该将它添加到我的答案中。
        • 我在哪里可以允许 .htaccess 控制 XSendFilePath?
        • @Keyne 我不认为你可以。 tn123.org/mod_xsendfile 没有在 XSendFilePath 选项的上下文中列出 .htaccess
        【解决方案6】:
        header('Location: ' . $path);
        exit(0);
        

        让 Apache 为您完成这项工作。

        【讨论】:

        • 这比 x-sendfile 方法更简单,但不能限制对文件的访问,也就是说只有登录的人。如果您不需要这样做,那就太好了!
        • 还使用 mod_rewrite 添加引荐来源网址检查。
        • 您可以在传递标头之前进行身份验证。这样你也不会在 PHP 的内存中注入大量的东西。
        • @UltimateBrent 该位置仍然必须可供所有人访问。而且由于它来自客户端,所以引用检查根本没有安全性
        • @Jimbo 您将如何检查的用户令牌?用 PHP 吗?突然间,您的解决方案正在递归。
        【解决方案7】:

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

        P.S:与在 php.ini 中读取和发送文件相比,与 nginx 一起使用它的效率高出多少,我感到不寒而栗。试想一下,如果有 100 个人正在下载一个文件:使用 php + apache,很慷慨,那可能是 100*15mb = 1.5GB(大约,射击我),内存就在那里。 Nginx 只会将文件发送到内核,然后直接从磁盘加载到网络缓冲区中。快!

        P.P.S:而且,使用这种方法,您仍然可以进行所有您想要的访问控制和数据库操作。

        【讨论】:

        • 让我补充一点,这也适用于 Apache:jasny.net/articles/how-i-php-x-sendfile。您可以让脚本嗅出服务器并发送适当的标头。如果不存在(并且用户无法根据问题控制服务器),则回退到正常的readfile()
        • 现在这真是太棒了——我一直讨厌提高我的虚拟主机中的内存限制,以便 PHP 可以提供一个文件,而我不应该这样做。我很快就会尝试一下。
        • 对于信用到期的信用,Lighttpd 是第一个实现此功能的网络服务器(其余的复制了它,这很好,因为这是一个好主意。但是在信用到期时给予信用)...
        • 这个答案不断得到支持,但它在网络服务器及其设置不受用户控制的环境中不起作用。
        • 在我发布此答案后,您实际上已将其添加到您的问题中。如果性能是一个问题,那么 Web 服务器必须在您的控制范围内。
        【解决方案8】:

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

        【讨论】:

        • /bump,你提到过这种可能性吗? :)
        猜你喜欢
        • 2014-05-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-06-13
        • 2013-01-12
        相关资源
        最近更新 更多