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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/3697748/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-25 10:45:09  来源:igfitidea点击:

Fastest Way to Serve a File Using PHP

phpperformancefile-iox-sendfile

提问by Kirk Ouimet

I'm trying to put together a function that receives a file path, identifies what it is, sets the appropriate headers, and serves it just like Apache would.

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

The reason I am doing this is because I need to use PHP to process some information about the request before serving the file.

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

Speed is critical

速度至关重要

virtual() isn't an option

virtual() 不是一个选项

Must work in a shared hosting environment where the user has no control of the web server (Apache/nginx, etc)

必须在用户无法控制 Web 服务器(Apache/nginx 等)的共享托管环境中工作

Here's what I've got so far:

这是我到目前为止所得到的:

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], '.');
    }
}
}
?>

回答by Julien Roncaglia

My previous answer was partial and not well documented, here is an update with a summary of the solutions from it and from others in the discussion.

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

The solutions are ordered from best solution to worst but also from the solution needing the most control over the web server to the one needing the less. There don't seem to be an easy way to have one solution that is both fast and work everywhere.

解决方案从最好的解决方案到最差的解决方案排序,但也从需要对 Web 服务器进行最多控制的解决方案到需要较少的解决方案。似乎没有一种简单的方法可以让一个解决方案既快速又适用于任何地方。



Using the X-SendFile header

使用 X-SendFile 标头

As documented by others it's actually the best way. The basis is that you do your access control in php and then instead of sending the file yourself you tell the web server to do it.

正如其他人所记录的那样,这实际上是最好的方法。基础是您在 php 中进行访问控制,然后您不是自己发送文件,而是告诉 Web 服务器执行此操作。

The basic php code is :

基本的php代码是:

header("X-Sendfile: $file_name");
header("Content-type: application/octet-stream");
header('Content-Disposition: attachment; filename="' . basename($file_name) . '"');

Where $file_nameis the full path on the file system.

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

The main problem with this solution is that it need to be allowed by the web server and either isn't installed by default (apache), isn't active by default (lighttpd) or need a specific configuration (nginx).

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

Apache

阿帕奇

Under apache if you use mod_php you need to install a module called mod_xsendfilethen configure it (either in apache config or .htaccess if you allow it)

在 apache 下,如果您使用 mod_php,则需要安装一个名为mod_xsendfile的模块,然后对其进行配置(如果允许,则在 apache 配置或 .htaccess 中)

XSendFile on
XSendFilePath /home/www/example.com/htdocs/files/

With this module the file path could either be absolute or relative to the specified XSendFilePath.

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

Lighttpd

轻量级

The mod_fastcgi support this when configured with

mod_fastcgi 在配置时支持这个

"allow-x-send-file" => "enable" 

The documentation for the feature is on the lighttpd wikithey document the X-LIGHTTPD-send-fileheader but the X-Sendfilename also work

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

Nginx

nginx

On Nginx you can't use the X-Sendfileheader you must use their own header that is named X-Accel-Redirect. It is enabled by default and the only real difference is that it's argument should be an URI not a file system. The consequence is that you must define a location marked as internal in your configuration to avoid clients finding the real file url and going directly to it, their wiki contains a good explanationof this.

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

Symlinks and Location header

符号链接和位置标题

You could use symlinksand redirect to them, just create symlinks to your file with random names when an user is authorized to access a file and redirect the user to it using:

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

header("Location: " . $url_of_symlink);

Obviously you'll need a way to prune them either when the script to create them is called or via cron (on the machine if you have access or via some webcron service otherwise)

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

Under apache you need to be able to enable FollowSymLinksin a .htaccessor in the apache config.

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

Access control by IP and Location header

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

Another hack is to generate apache access files from php allowing the explicit user IP. Under apache it mean using mod_authz_host(mod_access) Allow fromcommands.

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

The problem is that locking access to the file (as multiple users may want to do this at the same time) is non trivial and could lead to some users waiting a long time. And you still need to prune the file anyway.

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

Obviously another problem would be that multiple people behind the same IP could potentially access the file.

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

When everything else fail

当其他一切都失败时

If you really don't have any way to get your web server to help you, the only solution remaining is readfileit's available in all php versions currently in use and work pretty well (but isn't really efficient).

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



Combining solutions

组合解决方案

In fine, the best way to send a file really fast if you want your php code to be usable everywhere is to have a configurable option somewhere, with instructions on how to activate it depending on the web server and maybe an auto detection in your install script.

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

It is pretty similar to what is done in a lot of software for

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

  • Clean urls (mod_rewriteon apache)
  • Crypto functions (mcryptphp module)
  • Multibyte string support (mbstringphp module)
  • 清理网址(mod_rewrite在 apache 上)
  • 加密函数(mcryptphp 模块)
  • 多字节字符串支持(mbstringphp 模块)

回答by Jords

The fastest way: Don't. Look into the x-sendfile header for nginx, there are similar things for other web servers also. This means that you can still do access control etc in php but delegate the actual sending of the file to a web server designed for that.

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

P.S: I get chills just thinking about how much more efficient using this with nginx is, compared to reading and sending the file in php. Just think if 100 people are downloading a file: With php + apache, being generous, thats probably 100*15mb = 1.5GB (approx, shoot me), of ram right there. Nginx will just hand off sending the file to the kernel, and then it's loaded directly from the disk into the network buffers. Speedy!

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

P.P.S: And, with this method you can still do all the access control, database stuff you want.

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

回答by Alix Axel

Here goes a pure PHP solution. I've adapted the following function from my personal framework:

这是一个纯 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('~.*=([^,]*).*~', '', $_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;
}

The code is as efficient as it can be, it closes the session handler so that other PHP scripts can run concurrently for the same user / session. It also supports serving downloads in ranges (which is also what Apache does by default I suspect), so that people can pause/resume downloads and also benefit from higher download speeds with download accelerators. It also allows you to specify the maximum speed (in Kbps) at which the download (part) should be served via the $speedargument.

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

回答by amphetamachine

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

Let Apache do the work for you.

让 Apache 为您完成工作。

回答by shawn

A better implementation, with cache support, customized http headers.

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

回答by Samuel Dauzon

I coded a very simple function to serve files with PHP and automatic MIME type detection :

我编写了一个非常简单的函数来提供带有 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);
}

Usage

用法

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

回答by user1601422

The PHP Downloadfunction mentioned here was causing some delay before the file actually started to download. I don't know if this was caused by using varnish cache or what, but for me it helped to remove the sleep(1);completely and set $speedto 1024. Now it works without any problem as is fast as hell. Maybe you could modify that function too, because I saw it used all over the internet.

Download此处提到的 PHP函数在文件实际开始下载之前造成了一些延迟。我不知道这是通过使用清漆缓存或什么原因造成的,但对我来说帮助的去除sleep(1);彻底,并设置$speed1024。现在它可以毫无问题地工作,就像地狱一样快。也许你也可以修改那个函数,因为我看到它在互联网上被广泛使用。

回答by Andreas Linden

if you have the possibility to add PECL extensions to your php you can simply use the functions from the Fileinfo packageto determine the content-type and then send the proper headers...

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