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

Kir*_*met 93 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], '.');
    }
}
}
?>
Run Code Online (Sandbox Code Playgroud)

Jul*_*lia 135

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

解决方案从最佳解决方案到最差解决方案,也从需要最大程度控制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) . '"');
Run Code Online (Sandbox Code Playgroud)

$file_name文件系统上的完整路径在哪里.

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

阿帕奇

在apache下如果你使用mod_php你需要安装一个名为mod_xsendfile的模块然后配置它(如果你允许的话,可以在apache config或.htaccess中)

XSendFile on
XSendFilePath /home/www/example.com/htdocs/files/
Run Code Online (Sandbox Code Playgroud)

使用此模块,文件路径可以是绝对路径,也可以是指定路径XSendFilePath.

Lighttpd的

配置时,mod_fastcgi支持此功能

"allow-x-send-file" => "enable" 
Run Code Online (Sandbox Code Playgroud)

该功能的文档位于lighttpd wiki上,它们记录了X-LIGHTTPD-send-file标题,但X-Sendfile名称也有效

Nginx的

在Nginx上,您无法使用X-Sendfile标头,您必须使用自己的名称标头X-Accel-Redirect.它默认启用,唯一真正的区别是它的参数应该是URI而不是文件系统.结果是您必须在配置中定义标记为内部的位置,以避免客户端找到真实的文件URL并直接转到它,他们的wiki包含对此的一个很好的解释.

符号链接和位置标题

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

header("Location: " . $url_of_symlink);
Run Code Online (Sandbox Code Playgroud)

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

在Apache你需要能够使FollowSymLinks.htaccess或Apache的配置.

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

另一个黑客是从php生成apache访问文件,允许显式用户IP.在apache下它意味着using mod_authz_host(mod_access)Allow from命令.

问题是锁定对文件的访问(因为多个用户可能希望同时执行此操作)是非常重要的,并且可能导致一些用户等待很长时间.而且你仍然需要修剪文件.

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

当其他一切都失败了

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


结合解决方案

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

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

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

  • 这样的动作没问题,您需要小心的事情是发送内容(打印,回显),因为标头必须先于任何内容,然后在发送此标头之后执行操作,这不是立即重定向,而是在发送后进行编码在大多数情况下都会执行,但您无法保证浏览器不会断开连接。 (2认同)

Jor*_*rds 33

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

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

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

  • 我要补充一点,这也适用于Apache:http://www.jasny.net/articles/how-i-php-x-sendfile/.您可以使脚本嗅出服务器并发送相应的标头.如果不存在(并且用户根据问题无法控制服务器),则回退到正常的`readfile()` (4认同)

Ali*_*xel 22

这是一个纯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;
}
Run Code Online (Sandbox Code Playgroud)

代码尽可能高效,它关闭会话处理程序,以便其他PHP脚本可以同时为同一个用户/会话运行.它还支持在范围内提供下载服务(这也是我默认的Apache所做的事情),因此人们可以暂停/恢复下载,并且还可以通过下载加速器获得更高的下载速度.它还允许您指定应通过$speed参数提供下载(部分)的最大速度(以Kbps为单位).

  • 显然,如果您不能使用X-Sendfile或其中一个变体让内核发送文件,这只是一个好主意.您应该能够使用[http://php.net/manual/en/function.eio-sendfile.php](PHP的eio_sendfile()]调用替换上面的feof()/ fread()循环,这样就完成了PHP中的东西.这不像在内核中直接执行那么快,因为在PHP中生成的任何输出仍然必须通过Web服务器进程返回,但它比在PHP代码中执行它要快得多. . (2认同)

amp*_*ine 13

header('Location: ' . $path);
exit(0);
Run Code Online (Sandbox Code Playgroud)

让Apache为您完成工作.

  • 这比x-sendfile方法简单,但不能限制对文件的访问,只说登录的人.如果你不需要这样做,那就太好了! (12认同)
  • @UltimateBrent所有人都可以访问该位置..而且,由于来自客户端,因此检查检查完全没有安全性 (7认同)