Web App:可以监控HTTP文件下载的任何方式

时间:2014-02-12 23:13:37

标签: php apache nginx download lighttpd

我正在开发一个Web应用程序。它将允许用户通过HTTP协议从服务器下载文件。这些文件最大可达4 GB。

这些是我的要求和约束:

  • HTTP文件下载进度%
  • 在HTTP文件下载完成时注册
  • 注册,如果HTTP文件下载崩溃
  • 注册,如果用户取消了下载
  • 恢复未完成的文件下载
  • 能够下载最大4GB的文件
  • 应该只在客户端实现JavaScript / HTML5,在服务器上实现PHP。
  • 可能无法在客户端使用Java或Flash实现。

我的开发环境:

  • 的Apache
  • PHP
  • MySQL的
  • Windows 7

我的问题是,尽管我已经编写了可以下载大文件的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;
        }
    }

谢谢。

4 个答案:

答案 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天左右!


您需要自己做的事情:

  • 生成所需的JavaScript-Download-Code(第二个代码段)
  • 为旧浏览器版本添加finishFile-Implementations。
  • 检查大文件。 (仅测试高达30 MB)
  • 将正确的Mime-Type传递给需要的片段。
  • 适应您的UI样式。
  • 确保,文件具有正确的扩展程序集。

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

但是,我强烈建议使用基于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_abortconnection_aborted。我可能会以某种方式满足你的需要。但是你不会真正有效和精确地监控下载是否真的结束。