通过PHP从外部Web服务流式传输大文件
问题描述:
我目前正在使用允许分块下载/上传二进制文件(应允许更大文件)的外部SOAP Web服务。我需要允许最终用户使用我的PHP应用程序通过浏览器下载文件。提供小文件效果很好,但25MB +文件会导致Web服务器耗尽内存。通过PHP从外部Web服务流式传输大文件
我正在使用原生PHP Soap Client(无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方法的每个循环之后内存不会被释放?
答
嗯,我感觉很傻。这原来只是另一个PHP主题。显然,即使我用ob_flush
来刷新输出缓冲区(我认为)应该已经将标题和块发送到浏览器,但是在脚本完成之前,标题和输出实际上并没有被刷新到浏览器。
即使输出自刷新,您仍然必须明确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);
}
答
尝试这对我的作品
public function testUpload(){
$response = array();
if (empty($_FILES) || $_FILES['file']['error']) {
$response["code"] = 2;
$response["message"] = "failed to move uploaded file";
echo json_encode($response);
}
$chunk = isset($_REQUEST["chunk"]) ? intval($_REQUEST["chunk"]) : 0;
$chunks = isset($_REQUEST["chunks"]) ? intval($_REQUEST["chunks"]) : 0;
$fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : $_FILES["file"]["name"];
$filePath = "uploads/$fileName";
// Open temp file
$out = @fopen("{$filePath}.part", $chunk == 0 ? "wb" : "ab");
if ($out) {
// Read binary input stream and append it to temp file
$in = @fopen($_FILES['file']['tmp_name'], "rb");
if ($in) {
while ($buff = fread($in, 4096))
fwrite($out, $buff);
} else
$response["code"] = 2;
$response["message"] = "Oops! Failed to open input Stream error occurred.";
echo json_encode($response);
@fclose($in);
@fclose($out);
@unlink($_FILES['file']['tmp_name']);
} else
$response["code"] = 2;
$response["message"] = "Oops! Failed to open output error occurred.";
echo json_encode($response);
// Check if file has been uploaded
if (!$chunks || $chunk == $chunks - 1) {
// Strip the temp .part suffix off
rename("{$filePath}.part", $filePath);
}
$response["code"] = 2;
$response["message"] = "successfully uploaded";
echo json_encode($response);
}
的fopen可以打开外部URL。所以它应该在理论上起作用。 – DampeS8N 2010-12-09 19:20:18
不幸的是,API不能直接由URL调用。因为WebService是一个非常复杂的enterprise-y SOAP服务,并且download_chunk方法实际上需要相当复杂的对象作为参数。 – 2010-12-09 19:27:52