我正在开发一个Web应用程序。它将允许用户通过HTTP协议从服务器下载文件。这些文件最大可达4 GB。
这些是我的要求和约束:
我的开发环境:
我的问题是,尽管我已经编写了可以下载大文件的PHP脚本,但我无法有效监控中止下载(浏览器关闭,取消下载,中止互联网连接)。 PHP函数connection_aborted()捕获了所有中止下载量的50%。
因此,我的问题是,如果有任何方法可以真正有效地,精确地监控下载进度和中止下载?那么使用NGINX或LIGHTTPD Web服务器呢?如何为Apache编写自己的LUA或Perl模块,我将在哪里监视PHP输出缓冲区?
我当前的下载脚本:
while(!feof($fileObject))
{
usleep(100000);
//print(@fread($fileObject, $chunkSize));
echo(@fread($fileObject, $chunkSize));
// gradually output buffer to avoid memory problems by downloading large files
ob_flush();
flush();
// check if the client was disconnected
// important for cancelled or interrupted downloads
if (Connection_Aborted())
{
// sent to the database that the connection has been aborted
$result = mysqli_query($dbc, "UPDATE current_downloads SET connection_aborted=TRUE WHERE user_id=1;");
// close the database connection
mysqli_close($dbc);
// close the open file
@fclose($fileObject);
exit(json_encode(array("result" => false, "error" => "Connection with the client was aborted.")));
}
$nLoopCounter++;
$transferred += $chunkSize;
$downloadPercentage = (($nLoopCounter * $chunkSize) / $fileSize) * 100;
$result = mysqli_query($dbc, "UPDATE current_downloads SET progress_percent=$downloadPercentage, transferred=$transferred, connection_aborted=$strConnectionAborted, iteration=$nLoopCounter WHERE user_id=1;");
if($result == false)
{
// close the database connection
mysqli_close($dbc);
// close the file
fclose($handle);
// prepare output message
$outputArray = array("result" => 0, "message" => "Error Processing Database Query");
// output the message
echo json_encode($outputArray);
exit;
}
}
谢谢。
答案 0 :(得分:3)
将您的需求限制纳入帐户,我认为由于各种原因(至少覆盖100%的浏览器)是不可能的(请参阅下面的“hacky”解决方案):
您可以显示下载过程,方法是经常拉出第二页,返回下载脚本可能存储在数据库中的%-Value。但是 - 正如您已经注意到的那样 - PHP没有提供可靠的方法来确定用户是否已中止。
要绕过此问题,您可以执行以下操作:
创建一个download.php
文件,该文件能够以块的形式返回文件。编写一个迭代地拉动所有可用块的javascript,直到下载完成(即download.php?fileId=5&chunk=59
)。然后,Javascript可以组合所有检索到的块,最后渲染完成的文件。
但是,你可以不直接写入硬盘的白色Javascript,意味着:您需要下载所有块以向用户显示“已完成的文件”。如果他停在中间,所有数据都会丢失,这违反了你能够恢复下载的限制。
由于恢复文件下载是必须在客户端实现的任务(您不知何故需要获取已经下载的数据),因此您无法在服务器端执行任何操作。由于JavaScript缺乏直接编写(或读取)硬盘的功能,因此仅使用php / Javascript是不可能的。 (事实上,Javascript中存在ARE文件系统功能,但通常没有浏览器允许它们用于远程站点。)
作为 hacky 解决方案,您可以滥用浏览器缓存来恢复文件下载:
请注意,有多种情况,如果不起作用:
但是,使用此解决方案,最糟糕的情况是缓存/恢复不起作用。
如上所述实施download.php
。以下示例使用固定的ChunkSize为“10”,您可能希望根据需要添加(或者甚至是固定的块大小 - >根据需要修复计算)
<?
header('Cache-Control: max-age=31556926');
$etag = 'a_unique_version_string';
header('ETag: "'.$etag.'"');
$chunkCount = 10;
$file = $_GET["file"]; //ignored in this example
$file = "someImage.jpg";
$chunk = $_GET["chunk"];
$fileSize = filesize($file);
$chunkSize = ceil($fileSize / $chunkCount); //round to whole numbers.
//get required chunk.
$handle = fopen($file, "r");
$start = ($chunk-1) * $chunkSize + ($chunk-1);
$toRead = min($chunkSize+1, $fileSize - $start); //read next chunk or until EOF.
$end = $start + $toRead;
//echo "reading $toRead from $start to $end";
//die();
if (fseek($handle, $start) == 0){
$c = fread($handle, $toRead);
echo $c;
@fclose($handle);
}else{
//error seeking: handle it.
}
?>
现在,任何客户端都可以通过调用url(我在我的服务器上设置一个demo)来下载块,如下所示:
downloading http://dog-net.org/dltest/download.php?file=1&chunk=1
downloading http://dog-net.org/dltest/download.php?file=1&chunk=2
downloading http://dog-net.org/dltest/download.php?file=1&chunk=3
downloading http://dog-net.org/dltest/download.php?file=1&chunk=4
downloading http://dog-net.org/dltest/download.php?file=1&chunk=5
独立的块是没有价值的,所以提到的JavaScript进入游戏。 调用下载时,可以生成以下代码段 。然后它将迭代所有必需的块并将它们“逐个”下载。 如果用户中止,浏览器仍会缓存单个块。含义:每当用户再次开始下载时,已下载的块将在一瞬间完成,尚未请求的块将定期下载
<html>
<head>
<script language="javascript">
var urls = new Array();
urls[0] = "http://dog-net.org/dltest/download.php?file=1&chunk=1";
urls[1] = "http://dog-net.org/dltest/download.php?file=1&chunk=2";
urls[2] = "http://dog-net.org/dltest/download.php?file=1&chunk=3";
urls[3] = "http://dog-net.org/dltest/download.php?file=1&chunk=4";
urls[4] = "http://dog-net.org/dltest/download.php?file=1&chunk=5";
urls[5] = "http://dog-net.org/dltest/download.php?file=1&chunk=6";
urls[6] = "http://dog-net.org/dltest/download.php?file=1&chunk=7";
urls[7] = "http://dog-net.org/dltest/download.php?file=1&chunk=8";
urls[8] = "http://dog-net.org/dltest/download.php?file=1&chunk=9";
urls[9] = "http://dog-net.org/dltest/download.php?file=1&chunk=10";
var fileContent = new Array();
function downloadChunk(chunk){
var url = urls[chunk-1];
console.log("downloading " + url);
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = 'blob';
xhr.onload = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
document.getElementById("log").innerHTML += "downloading " + url + "<br />";
fileContent.push(xhr.response);
document.getElementById("percentage").innerHTML = chunk / urls.length * 100;
if (chunk < urls.length){
downloadChunk(chunk+1);
}else{
finishFile();
}
} else {
console.error(xhr.statusText);
}
}
};
xhr.onerror = function (e) {
console.error(xhr.statusText);
};
xhr.send(null);
}
function finishFile(){
contentType = 'image/jpg'; //TODO: has to be set accordingly!
console.log("Generating file");
var a = document.createElement('a');
var blob = new Blob(fileContent, {'type':contentType, 'endings':'native'});
console.log("File generated. size: " + blob.size);
//Firefox
if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1){
var url = window.URL.createObjectURL(blob);
window.location = url;
}
//IE 11 or chrome?
if (!(window.ActiveXObject) && "ActiveXObject"){
//Chrome:
if (window.chrome){
a.href = window.URL.createObjectURL(blob);
a.download = "download";
a.click();
}else{
//ie 11
window.navigator.msSaveOrOpenBlob(blob, 'download');
}
}
}
function setProgress(chunk){
document.getElementById("percentage").innerHTML = chunk / urls.length * 100;
}
</script>
</head>
<body onload="downloadChunk(1);">
<div id="percentage"></div>
<div id="log"></div>
</body>
</html>
请注意,处理Blobs
痛苦... 我管理它以使其在IE 11,Chrome 32和Firefox 27中运行。没办法Safari到目前为止。此外,我没有检查旧版本。
演示:http://dog-net.org/dltest/(它是一个png图像,所以用paint / irfranview / whatevs打开 - 未设置文件扩展名。)
在第一次调用时,所有文件块将独立下载。在第二次呼叫时,您会注意到,它们完成得非常快,因为所有(已完成的)呼叫都已被浏览器缓存。 (我将缓存时间设置为“永远” - 实际上你不想这样做,但选择大约7天左右!)
您需要自己做的事情:
这只是一个思想,它可能会给你一些想法,如何实现它。
但是,我强烈建议使用基于Flash / Java / Silverlight的客户端实现,因此您的 failave 实现不依赖于Browser Versiosn或任何其他限制!
答案 1 :(得分:2)
我对PHP连接相关问题的最终解决方案是使用Boost.Asio和一个鲜为人知的threadsafe SAPI released by Facebook创建一个Web服务器。下载链接已损坏,但可以在github上找到here。
我在使用Apache和其他Web服务器进行操作时遇到的主要问题是现有SAPI(Fast-CGI,PHP-FPM,mod_apache等)与PHP中连接的相关功能之间的不一致。在我尝试的任何情况下,它们都不可靠,尽管许多其他人声称已经使用了特定配置(操作系统版本,Web服务器版本,SAPI版本,PHP版本等)。
主要问题(正如您所观察到的)是PHP与Apache和其他Web服务器明显隔离。通过使用嵌入式PHP sapi,您可以在PHP和实际套接字连接以及其他网络相关功能之间实现更高级别的协作。这是我能够让PHP与网络服务器携手合作的唯一方式,这正是您所需要的。
然而,在第二个注释中,现在有很多严肃的纯PHP服务浮出水面,因为PHP主要修复了它的垃圾收集问题。使用非阻塞套接字或PHP流可以轻松地创建一个简单的文件服务器,考虑到它将使用异步模式为静态内容提供服务,可能会很快。
如果您觉得这是您的解决方案需要移动的方向,我不介意发布一些Boost.Asio花絮或简单的PHP文件服务。但是,这绝对是可能的。成千上万的服务已经遇到了这个问题。
答案 2 :(得分:1)
您可以使用HTML5 WebSockets实现解决方案。
有一些客户端库(使用JavaScript构建)以易于使用的方式抽象出API。
有服务器端库(使用PHP构建)实现WebSocket服务器。
这样,您就可以进行双向通信,并且可以在服务器端捕获您提到的所有可能事件。
由于时间不足,我不提供代码,但希望这会给出一些指导。
答案 3 :(得分:0)
实际上,没有办法用PHP(服务器端,而不是客户端语言)来真正检测文件下载何时完成。您可以做的最好的事情是在数据库开始时将数据库记录下来。如果您绝对,完全且完全需要知道下载何时完成,则必须执行诸如嵌入Java小程序或使用Flash之类的操作。但是,就用户的可用性而言,通常这不是正确的答案(为什么要求他们安装Java或Flash只是为了下载你的东西?)。
来自here。
您仍然可以尝试更多地了解ignore_user_abort
和connection_aborted
。我可能会以某种方式满足你的需要。但是你不会真正有效和精确地监控下载是否真的结束。