如何在不使用太多内存的情况下强制下载大文件?

Posted

技术标签:

【中文标题】如何在不使用太多内存的情况下强制下载大文件?【英文标题】:How to force download of big files without using too much memory? 【发布时间】:2011-09-05 22:28:33 【问题描述】:

我正在尝试向用户提供大型 zip 文件。当有 2 个并发连接时,服务器会耗尽内存 (RAM)。我将内存量从 300MB 增加到 4GB(Dreamhost VPS),然后它运行良好。

我需要允许超过 2 个并发连接。实际的 4GB 将允许 20 个并发连接(太糟糕了)。

嗯,我正在使用的当前代码需要双倍内存,然后是实际文件大小。这太糟糕了。我想要将文件“流式传输”给用户之类的东西。所以我会分配不超过提供给用户的块。

以下代码是我在 CodeIgniter(php 框架)中使用的代码:

ini_set('memory_limit', '300M'); // it was the maximum amount of memory from my server
set_time_limit(0); // to avoid the connection being terminated by the server when serving bad connection downloads
force_download("download.zip", file_get_contents("../downloads/big_file_80M.zip"));exit;

force_download函数如下(CodeIgniter默认辅助函数):

function force_download($filename = '', $data = '')

    if ($filename == '' OR $data == '')
    
        return FALSE;
    

    // Try to determine if the filename includes a file extension.
    // We need it in order to set the MIME type
    if (FALSE === strpos($filename, '.'))
    
        return FALSE;
    

    // Grab the file extension
    $x = explode('.', $filename);
    $extension = end($x);

    // Load the mime types
    @include(APPPATH.'config/mimes'.EXT);

    // Set a default mime if we can't find it
    if ( ! isset($mimes[$extension]))
    
        $mime = 'application/octet-stream';
    
    else
    
        $mime = (is_array($mimes[$extension])) ? $mimes[$extension][0] : $mimes[$extension];
    

    // Generate the server headers
    if (strpos($_SERVER['HTTP_USER_AGENT'], "MSIE") !== FALSE)
    
        header('Content-Type: "'.$mime.'"');
        header('Content-Disposition: attachment; filename="'.$filename.'"');
        header('Expires: 0');
        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
        header("Content-Transfer-Encoding: binary");
        header('Pragma: public');
        header("Content-Length: ".strlen($data));
    
    else
    
        header('Content-Type: "'.$mime.'"');
        header('Content-Disposition: attachment; filename="'.$filename.'"');
        header("Content-Transfer-Encoding: binary");
        header('Expires: 0');
        header('Pragma: no-cache');
        header("Content-Length: ".strlen($data));
    

    exit($data);

我尝试了一些在 Google 中找到的基于块的代码,但文件交付时总是损坏。可能是因为代码不好。

谁能帮帮我?

【问题讨论】:

您是否尝试过使用标头Location 重定向到文件? 在我看来,您最好只为用户提供指向文件的直接链接... 我忘记告诉您文件位于无法通过网络访问的文件夹中。这是出于安全原因。如果用户通过身份验证过程,我只会提供文件。我会尝试以下建议,并会回来投票选出最佳答案。 感谢文字修改,@p.campbell 我想我昨晚太累了... :) 【参考方案1】:

this thread 中有一些想法。我不知道 readfile() 方法是否会节省内存,但听起来很有希望。

【讨论】:

是的,readfile 确实节省了内存,因为它读取的文件的每个块都直接输出到浏览器而不将其存储在变量中(因此不使用额外的内存,只需要文件块)。 效果很好。我刚刚添加了一些额外的标题,因此 iPhone 不会在下载时发出错误警报。 header('Content-Disposition: attachment; filename="download.zip"'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header("Content-Transfer-Encoding: binary"); header('Pragma: public'); header("Content-Length: ".filesize($filename)); readfile($filename);exit; 我原本以为readfilefpassthru 也可以,但今天遇到了一个问题,readfile 实际上仍将整个文件读入内存。不过,也许这在较新版本的 PHP 中有所改变(我使用的是 5.2)。【参考方案2】:

您正在通过 PHP 发送此文件的内容 ($data)?

如果是这样,处理此问题的每个 Apache 进程最终都会增长到此文件的大小,因为该数据将被缓存。

您唯一的解决方案是不通过 PHP 发送文件内容/数据,而只是将用户重定向到文件系统上的下载 URL。

使用生成的唯一符号链接或隐藏位置。

【讨论】:

【参考方案3】:

我搜索了太多脚本和建议,但对我的 400MB PDF 文件没有任何帮助。我最终使用了 mod_rewrite,这是一个很好用的解决方案 https://unix.stackexchange.com/questions/88874/control-apache-referrer-to-restrict-downloads-in-htaccess-file 该代码只允许从您指定的推荐人处下载,并禁止直接下载

 RewriteEngine On
RewriteCond %HTTP_REFERER !^http://yourdomain.com/.* [NC]
RewriteRule .* - [F]

【讨论】:

【参考方案4】:

您不能将 $data 与其中的整个文件数据一起使用。尝试传递给这个函数,而不是文件的内容,只是它的路径。接下来发送所有标题一次,然后使用 fread() 读取该文件的一部分,回显该块,调用 flush() 并重复。如果同时发送任何其他标头,则最终传输将被破坏。

【讨论】:

readfile 一次读取整个文件,在我的提议中,我使用了fread(也可以使用fgets),因为如果文件将被读取,即 1MB 块,那么当下一个块将分配给相同的变量时,可以释放内存。 抱歉,readfile 以 8K 块读取文件。我实际上已经挖掘了 php 代码,因为这引起了我的兴趣,并证实了这一点。 readfile source, line 1383, php_stream_passthru 在里面定义为_php_stream_passthru (source, line 453) 后面的函数在this file, line 1314 在这种情况下应该在php文档中描述。【参考方案5】:

将大文件符号链接到您的文档根目录(假设它不是唯一的授权文件),然后让 Apache 处理它。 (这样你也可以接受字节范围)

【讨论】:

假设 Apache 启用了 FollowSymLinks 并且 OP 没有使用 PHP 下载脚本来添加安全层,但除此之外是个好主意。【参考方案6】:

SESSION_START(); 之前添加您的ini_set

【讨论】:

以上是关于如何在不使用太多内存的情况下强制下载大文件?的主要内容,如果未能解决你的问题,请参考以下文章

如何在不先加载到 RAM 的情况下将文件加载到 blob 中?

如何在不阻止我的网络服务器的情况下上传大文件?使用 python 和 Amazon Beanstalk/EC2

在 PHP 中不使用太多内存的情况下读取/写入大型 XML

如何在不使用太多内存的情况下播放循环压缩的配乐?

如何在不创建架构的情况下将 CSV 文件加载到 BigQuery

是否可以在不复制的情况下删除大文件的两端?