通过PHP从外部Web服务流式传输大型文件

时间:2010-12-09 19:08:41

标签: php memory soap file download

我目前正在使用外部SOAP Web服务,允许分块下载/上传二进制文件(应该允许更大的文件)。我需要允许最终用户使用我的PHP应用程序通过浏览器下载文件。提供小文件效果很好,但25MB +文件会导致Web服务器内存不足。

我正在使用本机PHP Soap客户端(没有MTOM支持),并通过提交表单请求下载。目前,似乎Web服务器在向浏览器输出任何内容之前尝试下载整个文件(例如,直到整个文件通过PHP处理后才显示“下载”提示符。)

我的方法看起来像这样(对不起,如果它很乱,我一直在讨论这个问题一段时间)。

public function download()
{
    $file_info_from_ws ... //Assume setup from $_REQUEST params

    //Don't know if these are needed
    gc_enable();
    set_time_limit(0);
    @apache_setenv('no-gzip', 1);
    @ini_set('zlib.output_compression', 0);

    //File Info
    $filesize = $file_info_from_ws->get_filesize();
    $fileid = $file_info_from_ws->get_id();
    $filename = $file_info_from_ws->get_name();
    $offset = 0;
    $chunksize = (1024 * 1024);

    //Clear any previous data
    ob_clean();
    ob_start();

    //Output headers
    header('Content-Type: application/octet-stream');
    header('Content-Length: ' . $filesize);
    header('Content-Transfer-Encoding: binary');
    header('Content-Disposition: attachment; filename="' . $filename . '"');
    header('Accept-Ranges: bytes');

    while($offset < $filesize)
    {
      $chunk = $this->dl_service()->download_chunked_file($fileid, $offset, $chunksize);
      if($chunk)
      {
        //Immediately echo out the stream
        $chunk->render();
        $offset += $chunksize;
        unset($chunk); //Shouldn't this trigger GC?
        ob_flush();
      }
    }
    ob_end_flush();
}

所以我的主要问题是: 从外部资源(Web服务,数据库等)通过PHP向最终用户输出大型二进制块的最佳方法是什么?最好不要过多地杀死内存/ CPU。

我对以下内容感到好奇:
为什么在第一次输出后弹出“下载”提示? 为什么在about方法中的每个循环后都没有释放内存?

2 个答案:

答案 0 :(得分:1)

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

这可能有所帮助。它也可能改变你想做的一切。

答案 1 :(得分:1)

我觉得很傻。事实证明这只是另一个PHP主义。显然,即使我用ob_flush刷新输出缓冲区(我认为)应该已经将标题和块发送到浏览器,但是在脚本完成之前,标题和输出实际上并没有被刷新到浏览器。

即使输出是self被刷新,你仍然必须显式flush PHP的写缓冲区和Web服务器回到客户端。不执行此操作会导致内存扩展,并且在整个下载完成之前不会显示下载提示。

以下是工作方法的一个版本:

public function download()
{
    $file_info ... //Assume init'ed from WS or DB

    //Allow for long running process
    set_time_limit(0);

    //File Info
    $filesize = $file_info->get_filesize();
    $fileid = $file_info->get_id();
    $filename = $file_info->get_name();
    $offset = 0;
    $chunksize = (1024 * 1024);

    //Clear any previous data
    ob_clean();
    ob_start();

    //Output headers to notify browser it's a download
    header('Content-Type: application/octet-stream');
    header('Content-Length: ' . $filesize);
    header('Content-Disposition: attachment; filename="' . $filename . '"');

    while($offset < $filesize)
    {
      //Retrieve chunk from service
      $chunk = $this->dl_service()->download_chunked_file($fileid, $offset, $chunksize);
      if($chunk)
      {
        //Immediately echo out the stream
        $chunk->render();
        //NOTE: The order of flushing IS IMPORTANT
        //Flush the data to the output buffer
        ob_flush(); 
        //Flush the write buffer directly to the browser
        flush();
        //Cleanup and prepare next request
        $offset += $chunksize;
        unset($chunk);
      }
    }
    //Exit the script immediately to prevent other output from corrupting the file
    exit(0);
}