Web应用程序:可以监控HTTP文件下载的任何方式

我正在研究一个Web应用程序。 它会让用户通过HTTP协议从服务器上下载文件。 这些文件最多可达4 GB。

这些是我的要求和限制:

  • HTTP文件下载进度%
  • 注册,当HTTP文件下载完成
  • 注册,如果HTTP文件下载崩溃
  • 注册,如果用户取消下载
  • 恢复未完成的文件下载
  • 要能够下载高达4GB的文件
  • 应该仅在客户端实现JavaScript / HTML5,在服务器上实现PHP。
  • 可能不会在客户端使用Java或Flash实现。

我的发展环境:

  • 阿帕奇
  • PHP
  • MySQL的
  • Windows 7的

我的问题是,尽pipe我已经写了PHP脚本,可以下载大文件,我不能有效地监视中止下载(浏览器closures,取消下载,中止互联网连接)。 PHP函数connection_aborted()捕获所有中止下载的cca的50%。

因此,我的问题是,如果有什么办法可以真正高效,精确地监控下载进度和下载中断吗? 那么使用NGINX或LIGHTTPDnetworking服务器怎么样? 如何编写我自己的LUA或Perl模块的Apache,我将监视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; } } 

谢谢。

我最后解决PHP连接相关的问题是使用Boost.Asio和Facebook发布的一个鲜为人知的线程安全SAPI创建一个Web服务器。 下载链接被破坏,但可以在这里找到github。

使用Apache和其他Web服务器进行绑定时遇到的主要问题是现有的SAPI(Fast-CGI,PHP-FPM,mod_apache等)与PHP中相关的相关函数之间的不一致。 在我尝试过的任何情况下,他们根本不可靠,尽管许多人声称已经得到了它的具体配置(操作系统版本,Web服务器版本,SAPI版本,PHP版本等)的工作。

主要的问题(正如你所观察到的)是PHP与Apache和其他Web服务器显着隔离。 通过使用嵌入式PHP sapi,您可以在PHP和实际套接字连接以及其他网络相关功能之间建立更高水平的协作。 这是我能够让PHP与Web服务器一起工作的唯一方法,这非常符合您的需求。

然而,第二个注意事项是,现在有许多严重的纯PHP服务出现,PHP已经解决了垃圾收集问题。 一个简单的文件服务器可以很容易地使用非阻塞套接字或PHP流,并可能会很快考虑到它将服务静态内容使用异步模式。

我不介意发布一些Boost.Asio花絮或一个简单的PHP文件服务,如果你觉得这是你的解决方案需要移动的方向。 但是,这绝对是可能的。 成千上万的服务已经遇到了这个问题。

考虑到你的需求限制,我会说因为各种原因(至少覆盖100%的浏览器)是不可能的(见下面的“hacky”解决方案):

您可以通过经常拉第二个页面显示下载过程,该页面返回您的下载脚本可能存储在数据库中的%-Value。 但是 – 正如你已经注意到的 – PHP不提供可靠的方法来确定用户是否中止。

要绕过这个问题,你可以这样做:

创建一个download.php文件,它能够以块的形式返回文件。 编写一个JavaScript迭代地拉动所有可用的块,直到下载完成(即download.php?fileId=5&chunk=59 )。 然后,Javascript可以组合所有检索的块,并最终呈现完成的文件。

但是,您不能直接写入硬盘,意味着:您需要下载所有的块,以向用户呈现“完成的文件”。 如果他在两者之间停下来,所有的数据都会丢失,这就违反了你能够恢复下载的限制。

由于恢复文件下载是一个必须在客户端实现的任务(你需要拿起已经下载的数据),在服务器端你不能做任何事情。 由于JavaScript缺乏直接编写(或读取)硬盘的功能,因此仅使用php / Javascript是不可能的。 (实际上在JavaScript中有文件系统功能,但是一般来说浏览器不允许它们用于远程站点。)


作为一个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(我在服务器上设置演示)来下载块:

 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进入游戏。 下载被调用时,可以生成以下片段。 然后它将遍历所有需要的块,并将它们“一个接一个”下载。 如果用户中止,浏览器仍然会缓存单个块。 含义:无论何时用户将再次开始下载,已下载的块将在瞬间完成,尚未请求的块将被下载regulary

 <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的工作。到目前为止没有办法。 另外我没有检查旧版本。


演示: http ://dog-net.org/dltest/(它是一个PNG图像,所以用paint / irfranview / whatevs打开 – 文件扩展名未设置。)

首次调用时,所有的文件块将被独立下载。 在Second Call上你会注意到,他们完成得非常快,因为所有已经完成的调用都被浏览器缓存了。 ( 我把缓存时间设置为“永远” – 实际上你不想这样做,但选择7天左右!


你需要自己做的事情:

  • 生成所需的JavaScript-Download-Code(第二个代码段)
  • 为较旧的浏览器版本添加完成文件实施。
  • 检查大文件。 (只能测试高达30 MB)
  • 根据需要将正确的Mime-Type传递给代码片段。
  • 适应你的UI – 造型。
  • 确保文件具有适当的扩展集。

这只是一个想法 ,可能会给你一些想法,如何实现这一点。

不过,我强烈建议使用基于Flash / Java / Silverlight的客户端实现,因此您有一个不依赖于Browser Versiosn或任何其他限制的故障实现。

您可以使用HTML5 WebSockets实施解决方案。

客户端库(使用JavaScript构建)以易于使用的方式抽象出API。

有一些服务器端库(使用PHP构建)来实现WebSocket服务器。

这样,您可以进行双向通信,并且可以在服务器端捕获您提到的所有可能的事件。

由于时间不够,我不提供代码,但希望这给了一些方向。

实际上,PHP(服务器端,而不是客户端语言)无法真正检测文件下载何时完成。 您可以做的最好的事情是在数据库开始时记录下载。 如果你绝对,完全和完全需要知道什么时候下载完成,你将不得不做一些像嵌入Java小程序或使用Flash。 但是,对于用户而言,通常这不是正确的答案(为什么要求他们只安装Java或Flash来从你那里下载东西?)。

从这里 。

您仍然可以尝试了解关于ignore_user_abortconnection_aborted更多信息。 我可能适合你需要什么。 但是,如果下载真的结束,你将无法真正有效和精确地监视。