【问题标题】:PHP readfile() and large filesPHP readfile() 和大文件
【发布时间】:2012-07-31 23:19:11
【问题描述】:

当使用 readfile() -- 在 Apache 上使用 PHP -- 是文件立即读入 Apache 的输出缓冲区并且 PHP 脚本执行完成,还是 PHP 脚本执行等到客户端完成下载文件(或服务器超时,以先发生者为准)?

更长的背景故事:

我有一个包含大量大型 mp3 文件的网站(当地教会的布道)。并非音频存档中的所有文件都允许下载,因此 /sermon/{filename}.mp3 路径被重写以真正执行 /sermon.php?filename={filename} 如果允许下载文件,则内容类型设置为“audio/mpeg”并使用 readfile() 流式传输文件。我一直在抱怨(几乎完全来自通过 3G 流式传输下载的 iPhone 用户)文件没有完全下载,或者在大约 10 或 15 分钟后被切断。当我从使用 readfile() 流式传输文件切换到简单地重定向到文件时 -- header("Location: $file_url"); ——所有的抱怨都消失了(我什至检查了一些以前可以可靠地按需重现问题的用户)。

这让我怀疑在使用 readfile() 时,PHP 脚本引擎一直在使用,直到文件完全下载,但我找不到任何证实或否认这一理论的参考资料。我承认我在 ASP.NET 世界中更自在,dotNet 等效于 readfile() 立即将整个文件推送到 IIS 输出缓冲区,因此 ASP.NET 执行管道可以独立于文件的传递完成到最终客户端... PHP+Apache 是否有与此行为等效的方法?

【问题讨论】:

  • 您是否尝试在使用 readfile 时禁用 php 执行时间限制?这也可以回答您的问题 - 如果您允许无限的执行时间并且 readfile 版本不会中止,那么您就有了答案。
  • 我在共享主机环境中运行,因此我认为无法更改此类设置。在进行更多阅读时,看起来使用 PHP 读取文件以输出的所有可能方式都会让脚本一直运行,直到下载完成。如果我想要“在 Web 服务器处理将文件获取到客户端时刷新并完成执行”功能,看起来我需要将其重新编写为 ASP.NET 应用程序。

标签: php apache


【解决方案1】:

在执行 readfile() 时,您可能仍然有 PHP output buffering 处于活动状态。检查:

if (ob_get_level()) ob_end_clean();

while (ob_get_level()) ob_end_clean();

这样,剩下的唯一输出缓冲区应该是 apache 的输出缓冲区,请参阅 SendBufferSize 了解 apache 调整。

编辑

您还可以查看mod_xsendfile (an SO post on such usage, PHP + apache + x-sendfile),这样您只需告诉网络服务器您已完成安全检查,现在他可以传递文件了。

【讨论】:

    【解决方案2】:

    您可以做一些事情(我没有报告您需要发送的所有标头,可能与您当前在脚本中拥有的标头相同):

    set_time_limit(0);  //as already mention
    readfile($filename);
    exit(0);
    

    passthru('/bin/cat '.$filename);
    exit(0);
    

    //When you enable mod_xsendfile in Apache
    header("X-Sendfile: $filename");
    

    //mainly to use for remove files
    $handle = fopen($filename, "rb");
    echo stream_get_contents($handle);
    fclose($handle);
    

    $handle = fopen($filename, "rb");
    while (!feof($handle)){
        //I would suggest to do some checking
        //to see if the user is still downloading or if they closed the connection
        echo fread($handle, 8192);
    }
    fclose($handle);
    

    【讨论】:

    • 我注意到没有人真正知道set_time_limit(0); 是如何工作的。 set_time_limit(0); 有 bo 在循环中,因为它在每次迭代时将脚本超时重置为默认值。把set_time_limit(0);放在代码块的开头是完全没用的。
    【解决方案3】:

    脚本将一直运行,直到用户完成文件下载。最简单、最有效且确实可行的解决方案是重定向用户:

    header("Location: /real/path/to/file");
    exit;
    

    但这可能会泄露文件的位置。使用 .htaccess 文件对可能并非所有人都下载的文件进行密码保护是个好主意,但也许您使用数据库来确定访问权限,这是没有选择的。

    另一种可能的解决方案是将 PHP 的最大执行时间设置为 0,从而禁用限制:

    set_time_limit(0);
    

    不过,您的主机可能不允许这样做。 PHP 也先将文件读入内存,然后通过 Apache 的输出缓冲区,最后到达网络。让用户直接下载文件效率更高,并且没有PHP的最大执行时间等限制。

    编辑:您经常收到 iPhone 用户抱怨的原因可能是他们的连接速度较慢(例如 3G)。

    【讨论】:

    • 我现在正在使用 header("Location: ... ") 它确实揭示了文件的位置(我现在只需要管理两个档案:一个可通过网络访问的公共文件存储和非- 可访问的私人商店。不理想,但可以完成工作)。
    【解决方案4】:

    通过 php 下载文件效率不高,使用重定向是可行的方法。如果您不想暴露文件的位置,或者文件不在公共位置,那么请查看内部重定向,这里有一篇关于它的帖子,Can I tell Apache to do an internal redirect from PHP?

    【讨论】:

    • 重定向确实会显示位置。
    • @Luc 如果重定向是内部的,则不会
    • 哦,对了!我的错 :) 我认为唯一的问题是 PHP 不能做到这一点。如果您已经在使用 htaccess,您还可以简单地保护您不想被所有人下载的文件,这样您就可以使用文件的真实位置而无需重定向。
    • 使用链接中建议的virtual() 仍然需要运行 PHP 脚本直到下载完成,并且它可能会根据 PHP 外部的时间限制终止(例如在 Apache/CGI 级别) 你无法控制共享服务器。
    【解决方案5】:

    尝试改用 stream_copy_to_stream()。我发现它的问题比 readfile() 少。

    set_time_limit(0);
    $stdout = fopen('php://output', 'w');
    $bfname = basename($fname);
    
    header("Content-type: application/octet-stream");
    header("Content-Disposition: attachment; filename=\"$bfname\"");
    
    $filein = fopen($fname, 'r');
    stream_copy_to_stream($filein, $stdout);
    
    fclose($filein);
    fclose($stdout);
    

    【讨论】:

    • 非常简单的解决方案,不需要麻烦的 X-Sendfile,而且效果很好。这正是我在搞砸了一个多小时后所需要的。谢谢!
    • 这样可以避免超大文件的 PHP 内存限制,但是脚本仍然需要运行直到下载完成,并且它可能会根据 PHP 外部的时间限制终止(例如在Apache/CGI 级别),您无法在共享服务器上控制。
    【解决方案6】:

    在 Apache 下,有一个很好的 elgant 解决方案,根本不涉及 php:

    只需将 .htaccess 配置文件放入包含要下载的文件的文件夹中,内容如下:

    <Files *.*>
    ForceType applicaton/octet-stream
    </Files>
    

    这告诉 Apache 提供此文件夹(及其所有子文件夹)中的所有文件以供下载,而不是直接在浏览器中显示它们。

    【讨论】:

      【解决方案7】:

      见下面网址

      http://php.net/manual/en/function.readfile.php

      <?php
      $file = 'monkey.gif';
      
      if (file_exists($file)) {
          header('Content-Description: File Transfer');
          header('Content-Type: application/octet-stream');
          header('Content-Disposition: attachment; filename='.basename($file));
          header('Content-Transfer-Encoding: binary');
          header('Expires: 0');
          header('Cache-Control: must-revalidate');
          header('Pragma: public');
          header('Content-Length: ' . filesize($file));
          ob_clean();
          flush();
          readfile($file);
          exit;
      }
      ?>
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-11-12
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多