【问题标题】:Serving large files with PHP使用 PHP 处理大文件
【发布时间】:2010-09-30 18:27:24
【问题描述】:

所以我试图通过 PHP 脚本提供大文件,它们不在网络可访问的目录中,所以这是我能想到的最好的方式来提供对它们的访问。

我能想到的提供此文件的唯一方法是将其加载到内存中(fopen、fread 等),将标头数据设置为正确的 MIME 类型,然后仅回显文件的全部内容文件。

问题在于,我必须一次将这些约 700MB 的文件加载到内存中,并将整个文件保存在那里直到下载完成。如果我可以在我需要的部分下载时流式传输,那就太好了。

有什么想法吗?

【问题讨论】:

标签: php apache


【解决方案1】:

您不需要阅读整个内容 - 只需进入一个循环读取它,例如 32Kb 块并将其作为输出发送。更好的是,使用 fpassthru 为您做同样的事情......

$name = 'mybigfile.zip';
$fp = fopen($name, 'rb');

// send the right headers
header("Content-Type: application/zip");
header("Content-Length: " . filesize($name));

// dump the file and stop the script
fpassthru($fp);
exit;

行数更少如果你使用readfile,不需要fopen调用...

$name = 'mybigfile.zip';

// send the right headers
header("Content-Type: application/zip");
header("Content-Length: " . filesize($name));

// dump the file and stop the script
readfile($name);
exit;

如果您想变得更可爱,您可以支持Content-Range 标头,它允许客户端请求您文件的特定字节范围。这对于向 Adob​​e Acrobat 提供 PDF 文件特别有用,Adobe Acrobat 只请求呈现当前页面所需的文件块。有点牵扯,不过see this for an example

【讨论】:

  • 谢谢!我没有意识到我可以通过 Adob​​e 摆脱 Content-Range。
  • fpassthru() 一直被reported 视为内存占用,因为它将整个文件加载到内存中。不适合大文件。
  • readfilefpassthru 都因我的内存相关错误而失败:Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 1883504640 bytes)...
【解决方案2】:

使用 php 发送大文件的最佳方式是 X-Sendfile 标头。它允许网络服务器通过像sendfile(2) 这样的零拷贝机制更快地提供文件。它由带有 plugin 的 lighttpd 和 apache 支持。

例子:

$file = "/absolute/path/to/file"; // can be protected by .htaccess
header('X-Sendfile: '.$file);
header('Content-type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($file).'"');
// other headers ...
exit;

服务器读取X-Sendfile标头并发送文件。

【讨论】:

    【解决方案3】:

    虽然fpassthru() 过去一直是我的首选,但如果您只是将文件原样转储到客户端,PHP 手册实际上建议* 使用readfile()

    * "如果您只想将文件的内容转储到输出缓冲区,而不需要先修改它或寻找特定的偏移量,您可能需要使用 readfile(),它可以为您节省 fopen( ) 称呼。” ——PHP manual

    【讨论】:

      【解决方案4】:

      如果由于路径不在您的 Web 服务目录 (htdocs) 中而导致 Web 服务器无法访问您的文件,那么您可以在您的 Web 服务目录中创建指向该文件夹的符号链接 (symlink) 以避免传递所有流量

      你可以这样做

      ln -s /home/files/big_files_folder /home/www/htdocs
      

      使用 php 提供静态文件会慢很多,如果流量大,内存消耗会很大,可能无法处理大量请求。

      【讨论】:

        【解决方案5】:

        看看fpassthru()。在较新版本的 PHP 中,这应该提供文件而不会将它们保存在内存中,如 this comment 状态。

        【讨论】:

          【解决方案6】:

          奇怪,fpassthru() 和 readfile() 都没有为我做,总是出现内存错误。 我求助于使用不带“f”的 passthru():

          $name = 'mybigfile.zip';
          // send the right headers
          header("Content-Type: application/zip");
          header("Content-Length: " . filesize($name));
          // dump the file and stop the script
          passthru('/bin/cat '.$filename);
          exit;
          

          这会执行 'cat' Unix 命令并将其输出发送到浏览器。

          对 slim 的评论:您只是不将符号链接放在某处的原因是网络空间是安全的。

          【讨论】:

            【解决方案7】:

            fpassthru() 的一个好处是这个函数不仅可以处理文件,还可以处理任何有效的句柄。以插座为例。

            并且 readfile() 必须快一点,因为使用操作系统缓存机制,如果可能的话(就像 file_get_contents() 一样)。

            还有一个提示。 fpassthru() 保持句柄打开,直到客户端获取内容(在慢速连接上可能需要相当长的时间),因此如果可以并行写入此文件,则必须使用一些锁定机制。

            【讨论】:

              【解决方案8】:

              Python 的答案都很好。但是,您是否有任何理由不能创建包含指向实际文件的符号链接的 Web 可访问目录?它可能需要一些额外的服务器配置,但它应该可以工作。

              【讨论】:

                【解决方案9】:

                如果你想把它做好,单靠 PHP 是做不到的。您可能希望使用 Nginx 的 X-Accel-Redirect (推荐) 或 Apache 的 X-Sendfile 来提供文件,它们正是为此目的而构建的。

                我将在此答案中包含一些在此 article 上找到的文本。

                为什么不用 PHP 提供文件:

                • 天真地完成,文件被读入内存,然后被提供。如果 文件很大,这可能会导致您的服务器内存不足。
                • 缓存标头通常设置不正确。这会导致网络浏览器 即使文件没有更改,也要多次重新下载文件。
                • 通常不支持 HEAD 请求和范围请求 自动支持。如果文件很大,则提供此类文件 绑定一个工作进程或线程。这可能会导致饥饿,如果 可用的工人有限。增加工人数量 可能会导致服务器内存不足。

                NGINX 可以正确处理所有这些事情。因此,让我们在应用程序中处理权限检查,并让 NGINX 为实际文件提供服务。这就是内部重定向的用武之地。这个想法很简单:您可以在提供常规文件时像往常一样配置位置条目。

                将此添加到您的 nginx 服务器块:

                location /protected_files/ {
                    internal;
                    alias /var/www/my_folder_with_protected_files/;
                }
                

                在您的项目中,需要 HTTP Foundation 包:

                composer require symfony/http-foundation

                使用 Nginx 在 PHP 中提供文件:

                use Symfony\Component\HttpFoundation\BinaryFileResponse;
                
                $real_path = '/var/www/my_folder_with_protected_files/foo.pdf';
                $x_accel_redirect_path = '/protected_files/foo.pdf';
                
                BinaryFileResponse::trustXSendfileTypeHeader();
                $response = new BinaryFileResponse( $real_path );
                $response->headers->set( 'X-Accel-Redirect', $accel_file );
                $response->sendHeaders();
                exit;
                

                这应该是您入门所需的基本知识。

                这是一个提供内联 PDF 的更完整示例:

                use Symfony\Component\HttpFoundation\BinaryFileResponse;
                use Symfony\Component\HttpFoundation\File\File;
                use Symfony\Component\HttpFoundation\ResponseHeaderBag;
                
                $real_path = '/var/www/my_folder_with_protected_files/foo.pdf';
                $x_accel_redirect_path = '/protected_files/foo.pdf';
                
                $file = new File( $file_path );
                
                BinaryFileResponse::trustXSendfileTypeHeader();
                $response = new BinaryFileResponse( $file_path );
                $response->setImmutable( true );
                $response->setPublic();
                $response->setAutoEtag();
                $response->setAutoLastModified();
                $response->headers->set( 'Content-Type', 'application/pdf' );
                $response->headers->set( 'Content-Length', $file->getSize() );
                $response->headers->set( 'X-Sendfile-Type', 'X-Accel-Redirect' );
                $response->headers->set( 'X-Accel-Redirect', $accel_file );
                $response->headers->set( 'X-Accel-Expires', 60 * 60 * 24 * 90 ); // 90 days
                $response->headers->set( 'X-Accel-Limit-Rate', 10485760 ); // 10mb/s
                $response->headers->set( 'X-Accel-Buffering', 'yes' );
                $response->setContentDisposition( ResponseHeaderBag::DISPOSITION_INLINE, basename( $file_path ) ); // view in browser. Change to DISPOSITION_ATTACHMENT to download
                $response->sendHeaders();
                exit;
                

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2014-05-23
                  • 2012-03-09
                  • 1970-01-01
                  • 2011-03-11
                  • 2013-10-23
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多