使用PHP服务文件的最快方法


98

我试图将一个函数接收一个文件路径,确定它是什么,设置适当的标头,并像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], '.');
    }
}
}
?>

10
您为什么不让Apache执行此操作?它总是比启动PHP解释器快得多……
Billy ONeal 2010年

4
在输出文件之前,我需要处理请求并将一些信息存储在数据库中。
Kirk Ouimet 2010年

3
我是否可以建议一种无需使用更昂贵的正则表达式即可获得扩展名的方法$extension = end(explode(".", $pathToFile)),也可以使用substr和strrpos:来实现$extension = substr($pathToFile, strrpos($pathToFile, '.'))。另外,作为回mime_content_type()$mimetype = exec("file -bi '$pathToFile'", $output);
退至

最快是什么意思?下载时间最快?
Alix Axel

Answers:


140

我以前的回答是不完整的,没有很好的记录,这里是更新,其中总结了该解决方案以及讨论中其他解决方案。

从最佳解决方案到最差解决方案的顺序排列,也从需要对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 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并直接进入该URL,他们的Wiki 对此做了很好的解释

符号链接和位置标题

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

header("Location: " . $url_of_symlink);

显然,当调用创建它们的脚本时或通过cron(在您可以访问的机器上,或者通过某些webcron服务),您将需要一种修剪它们的方法。

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

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

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

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

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

当其他一切都失败了

如果您真的没有办法让您的Web服务器来帮助您,那么剩下的唯一解决方案就是readfile,它可以在当前使用的所有php版本中使用,并且运行良好(但效率不高)。


结合解决方案

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

它与许多软件中完成的工作非常相似

  • 清理网址(mod_rewrite针对Apache)
  • 加密函数(mcryptphp模块)
  • 多字节字符串支持(mbstringphp模块)

在进行某些PHP工作之前是否有任何问题(针对数据库检查Cookie /其他GET / POST参数)header("Location: " . $path);
Afriza N. Arief

2
这样的动作没问题,您需要注意的是发送内容(打印,回显),因为标头必须先于任何内容发送,然后在发送此标头之后执行操作,这不是立即重定向,而是发送后的代码在大多数情况下都会执行,但您无法保证浏览器不会断开连接。
朱利安·隆卡利亚

Jords:我不知道apache也支持这一点,如果有时间,我会将其添加到我的答案中。唯一的问题是我不是统一的(例如X-Accel-Redirect nginx),因此如果服务器不支持它,则需要第二种解决方案。但我应该将其添加到我的答案中。
朱利安·隆卡利亚

我在哪里可以允许.htaccess来控制XSendFilePath?
Keyne Viana

1
@凯恩,我不认为你可以。tn123.org/mod_xsendfile确实在为XSendFilePath选项的情况下没有列表的.htaccess
cheshirekow

33

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

PS:让我感到不寒而栗的是,与在php中读取和发送文件相比,在nginx上使用它的效率要高得多。试想一下,如果有100个人正在下载文件:使用php + apache,那么慷慨,那大概是100 * 15mb = 1.5GB(大约,给我射击),就在那儿。Nginx只会将文件发送到内核,然后将其直接从磁盘加载到网络缓冲区中。迅速!

PPS:而且,使用这种方法,您仍然可以执行所有访问控制以及所需的数据库工作。


4
让我补充说一下,Apache也存在:jasny.net/articles/how-i-php-x-sendfile。您可以使脚本嗅探服务器并发送适当的标头。如果不存在(根据问题用户无法控制服务器),请恢复正常状态readfile()
Fanis Hatzidakis

现在,这真是太棒了-我一直讨厌增加虚拟主机中的内存限制,以使PHP可以提供文件,而我不必这样做。我很快就会尝试。
格雷格W

1
对于需要信用的信用,Lighttpd是第一个实现此功能的Web服务器(其余服务器复制了它,这很好,因为这是个好主意。但是在应信用的地方给予信用)...
ircmaxell 2010年

1
这个答案一直在不断增加,但是在Web服务器及其设置不受用户控制的环境中将无法使用。
Kirk Ouimet 2010年

在我发布此答案后,您实际上将其添加到了您的问题中。如果性能是一个问题,那么Web服务器必须在您的控制范围内。
乔德斯,2010年

23

这里有一个纯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;
}

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


2
显然,这是一个好主意,如果您不能使用X-Sendfile或其变体之一来让内核发送文件。您应该能够用[ php.net/manual/en/function.eio-sendfile.php](PHP的 eio_sendfile()调用)替换上面的feof()/ fread()循环,这在PHP中完成了同样的事情。这是没有那么快,直接在内核中做,在PHP中生成的任何输出还是要过重新从Web服务器的过程,但是这将是一个宏大的速度远比在PHP代码做的。
布莱恩ç

@BrianC:可以,但是您不能用X-Sendfile限制速度或多部分功能(这可能不可用),eio并且也不总是可用。+1仍然不知道该pecl扩展名。=)
Alix Axel

支持transfer-encoding:chunked和content-encoding:gzip是否有用?
2014年

为什么$size = sprintf('%u', filesize($path))
Svish

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

让Apache为您完成工作。


12
这比x-sendfile方法更简单,但不能限制访问文件,也就是说只能登录。如果您不需要这样做,那就太好了!
Jords

还可以使用mod_rewrite添加引荐来源网址检查。
sanmai 2010年

1
您可以在传递标题之前进行身份验证。这样一来,您就不会再通过PHP的内存抽很多东西。
布伦特

7
@UltimateBrent位置仍然是对所有人开放。而一指检查是根本没有安全性,因为它是来自客户端
岛之风Skaar

@Jimbo您要检查方式的用户令牌?使用PHP?突然,您的解决方案在递归。
Mark Amery 2014年

1

具有缓存支持和自定义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;
    }
}


0

Download此处提到的PHP 函数在实际开始下载文件之前造成了一些延迟。我不知道这是否是由使用清漆缓存或什么引起的,但是对我来说,它有助于sleep(1);完全删除并将其设置$speed1024。现在它可以像地狱般快速地工作,而没有任何问题。也许您也可以修改该功能,因为我看到了它在整个Internet上的使用。


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");
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.