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

9
我正在尝试向用户提供大型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上找到的基于块的代码,但文件总是无法正常传递。可能是因为代码有问题。是否有人可以帮我解决这个问题?

1
你尝试过使用Location头重定向到文件吗? - Daniel
听起来你最好直接给用户提供文件的直接链接... - NotMe
我忘了告诉你,这些文件位于一个无法通过网络访问的文件夹中。这是出于安全考虑。只有在用户通过身份验证流程后,才会提供文件服务。我将尝试下面的建议,并回来投票选择最佳答案。 - Leandro Alves
感谢您的文本修订,@p.campbell。我想昨晚我太累了... :) - Leandro Alves
6个回答

3

这个讨论串中提出了一些想法。我不确定readfile()方法是否会节省内存,但它听起来很有前途。


是的,readfile确实节省内存,因为它直接将读取的每个文件块输出到浏览器,而不将其存储在变量中(因此不会使用额外的内存,只需使用文件块所需的内存)。 - Carlos Campderrós
1
那个很好用。我只是添加了一些额外的头文件,这样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; - Leandro Alves
2
我最初认为readfilefpassthru也可以,但今天遇到了一个问题,似乎readfile实际上仍然将整个文件读入内存。也许在PHP的新版本中已经改变了这一点(我使用的是5.2)。 - Eric Petroelje

3

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

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

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

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


1

0

你不能在$data中使用整个文件数据。尝试将文件的内容而不是路径传递给此函数。然后发送所有头信息,之后使用fread()读取文件的一部分,echo该块,调用flush()并重复此过程。如果同时发送了任何其他头信息,那么最终传输将会损坏。


“readfile” 一次性读取整个文件,而在我的方案中我使用了“fread”(也可以使用“fgets”),因为如果文件将被分块为1MB,则当下一个块被分配给相同的变量时,那些内存可以被释放。 - Arek Jablonski
1
抱歉,readfile 每次读取 8K 的文件块。我实际上已经深入研究了 PHP 代码,因为这让我很感兴趣,并进行了确认。readfile 源码,第 1383 行,其中的 php_stream_passthru 被定义为 _php_stream_passthru (源码,第 453 行),而后者函数在 此文件,第 1314 行 中。 - Carlos Campderrós
在这种情况下,应该在PHP文档中进行描述。 - Arek Jablonski

0

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


假设Apache已启用FollowSymLinks,并且OP没有使用PHP下载脚本添加安全层,否则这是一个好主意。 - Marc B

0

SESSION_START(); 之前添加你的 ini_set


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接