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

通过PHP从外部Web服务流式传输大型文件,php,memory,soap,file,download,Php,Memory,Soap,File,Download,我目前正在使用外部SOAPWeb服务,允许分块下载/上传二进制文件(应该允许更大的文件)。我需要允许最终用户使用my PHP应用程序通过浏览器下载文件。为小文件提供服务效果很好,但25MB以上的文件会导致web服务器内存不足 我正在使用原生PHP Soap客户端(不支持MTOM),并通过提交表单请求下载。目前,web服务器似乎正在尝试在向浏览器输出任何内容之前下载整个文件(例如,在通过PHP处理整个文件之后,“下载”提示才会显示) 我的方法看起来是这样的(很抱歉,如果它很混乱,我已经对这个问题进

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

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

我的方法看起来是这样的(很抱歉,如果它很混乱,我已经对这个问题进行了一段时间的黑客攻击)

公共函数下载()
{
$file\u info\u from\u ws…//假设安装程序来自$\u请求参数
//不知道是否需要这些
gc_enable();
设置时间限制(0);
@apache_setenv('no-gzip',1);
@ini_集('zlib.output_compression',0);
//文件信息
$filesize=$file\u info\u from\u ws->get\u filesize();
$fileid=$file\u info\u from\u ws->get\u id();
$filename=$file\u info\u from\u ws->get\u name();
$offset=0;
$chunksize=(1024*1024);
//清除任何以前的数据
ob_clean();
ob_start();
//输出标题
标题(“内容类型:应用程序/八位字节流”);
标题('Content-Length:'.$filesize);
标题(“内容传输编码:二进制”);
标题('Content-Disposition:attachment;filename=“.”.$filename.'”);
标题('Accept-Ranges:bytes');
而($offset<$filesize)
{
$chunk=$this->dl_service()->下载_chunked_文件($fileid,$offset,$chunksize);
如果($chunk)
{
//立刻发出回声
$chunk->render();
$offset+=$chunksize;
unset($chunk);//这不应该触发GC吗?
ob_flush();
}
}
ob_end_flush();
}
所以我的主要问题是: 通过PHP将外部资源(Webservices、DB等)中的大型二进制块输出给最终用户的最佳方式是什么?最好不要过多地占用内存/CPU

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


这可能会有所帮助。这也可能改变你做任何事情的方式。

我觉得自己很傻。事实证明,这只是另一种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);
}
公共函数下载()
{
$file\u info…//假定从WS或DB初始化
//允许长时间运行进程
设置时间限制(0);
//文件信息
$filesize=$file\u info->get\u filesize();
$fileid=$file\u info->get\u id();
$filename=$file_info->get_name();
$offset=0;
$chunksize=(1024*1024);
//清除任何以前的数据
ob_clean();
ob_start();
//输出标题以通知浏览器这是下载
标题(“内容类型:应用程序/八位字节流”);
标题('Content-Length:'.$filesize);
标题('Content-Disposition:attachment;filename=“.”.$filename.'”);
而($offset<$filesize)
{
//从服务中检索区块
$chunk=$this->dl_service()->下载_chunked_文件($fileid,$offset,$chunksize);
如果($chunk)
{
//立刻发出回声
$chunk->render();
//注:冲洗顺序很重要
//将数据刷新到输出缓冲区
ob_flush();
//将写缓冲区直接刷新到浏览器
冲洗();
//清理并准备下一个请求
$offset+=$chunksize;
未结算($);
}
}
//立即退出脚本以防止其他输出损坏文件
出口(0);
}

fopen可以打开外部URL。所以它应该是有效的。。。理论上,不幸的是,这个API不能被URL直接调用。因为WebService是一个非常复杂的enterprise-ysoap服务,而download\u chunk方法实际上需要相当复杂的对象作为参数,这真是太糟糕了。这是我讨厌肥皂的主要原因之一。我听说了!这可能是一种痛苦。谢谢你的努力。我想你给了我一个主意。如果成功的话,我会发布结果。我最终能够使用php://temp 通过WS提供的流进行传递,通过在内存耗尽时写入光盘解决了内存不足错误。但我仍然有高记忆力的根本原因。有关更多信息,请参阅我的答案。您也可以使用ob_implicit_flush,而不是每次都调用flush()。
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);
}