使用jQuery实现PHP cURL下载进度

5
我不知道如何使用php每次更新时推送进度。为了清晰起见,我将写一个例子。
jQuery:
function uploadMovieDownload(link){
    $.post("php/downloadmovie.php", { source:link }, function(json){ console.log(json); });
}

uploadMovieDownload(url);

PHP (php/downloadmovie.php):
session_start();
ob_start();
date_default_timezone_set("Europe/Bucharest");
ini_set('display_errors',true);

require_once(dirname(__FILE__)."/functions.php");


$url = $_POST['source'];
$headers = getHeaders($url);
$url = $headers['url'];
$path = dirname(__FILE__)."/temp/test.mp4";

$fp = fopen ($path, 'w+');
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $url );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, false );
curl_setopt( $ch, CURLOPT_PROGRESSFUNCTION, 'progress' );
curl_setopt( $ch, CURLOPT_NOPROGRESS, false );
curl_setopt( $ch, CURLOPT_BINARYTRANSFER, true );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 100 );
curl_setopt( $ch, CURLOPT_FILE, $fp );
curl_exec( $ch );
curl_close( $ch );
fclose( $fp );


function progress($resource,$download_size, $downloaded, $upload_size, $uploaded){
    if($download_size > 0) echo $downloaded / $download_size  * 100;
    ob_flush();
    flush();
}

echo "Done";
ob_flush();
flush();

我遇到的问题是它在下载完成后才返回进度,而不是在下载时推送进度。如果您有任何建议,提前感谢您。

你好,你解决了这个问题吗? - Ashish
一些想法:1. 负载均衡器可能会无意中保存渐进式响应。2. 为了让某些浏览器正确运行,您需要立即发送一定数量的字节。3. 您需要返回HTML,并将数据方法地包装在<script>标签中,以使用类似于此的简单COMET方法。4. 我会考虑重新实现,使用临时文件存储百分比并ping另一个简单的脚本从文件中获取该值,绕过上述脚本。5. "SSE",也称为EventSource,是跨浏览器的,并且易于在PHP中实现,以替换#4中的ping操作... - dandavis
4个回答

10

使用该方法,您将遇到各种缓存问题,例如PHP输出缓冲?那就会有问题。您是否在Web服务器(如Nginx / Apache / Lighttpd / 任何其他服务器)后面?那也会有问题。浏览器是否缓存输出内容?(所有主流浏览器都这样做),那也会有问题。

我建议另一种替代方案,它将避免所有这些问题:使用$_SESSION来存储下载百分比,并使用XMLHttpRequests查询百分比(实际上通过WebSockets查询百分比会更佳,无延迟、使用的带宽更少等,但难以实现)

downloadmovie.php

<?php 
require_once(dirname(__FILE__)."/functions.php");


$url = $_POST['source'];
$headers = getHeaders($url);
$url = $headers['url'];
//after validation of input
session_start();
$_SESSION['download_percentage']=0.0;//initialize it
session_write_close();
fastcgi_finish_request();//or if you're not using fcgi, some equivalent..

$path = dirname(__FILE__)."/temp/test.mp4";

$fp = fopen ($path, 'w+');
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $url );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, false );
curl_setopt( $ch, CURLOPT_PROGRESSFUNCTION, 'progress' );
curl_setopt( $ch, CURLOPT_NOPROGRESS, false );
curl_setopt( $ch, CURLOPT_BINARYTRANSFER, true );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 100 );
curl_setopt( $ch, CURLOPT_FILE, $fp );
curl_exec( $ch );
curl_close( $ch );
fclose( $fp );


function progress($resource,$download_size, $downloaded, $upload_size, $uploaded){
    $percentage=$download_size==0? 0.0 : (($downloaded/$download_size)*100);
    session_start();
    $_SESSION['download_percentage']=$percentage;
    session_write_close();
}

获取进度的getProgress.xhr.php

<?php 
if(""===session_id()){
session_start();
}
echo $_SESSION['download_percentage']??'?';

然后在浏览器中监视进度:

(function checkProgress() {
    "use strict";
    var xhr = new XMLHttpRequest();
    xhr.open("GET", "getProgress.xhr.php");
    xhr.addEventListener("readystatechange", function(ev) {
        var xhr = ev.target;
        if (xhr.readyState !== 4) {
            return;
        }
        console.log(xhr.responseText + " percent downloaded!");
        if (xhr.responseText === "100") {
            return; /*stop checking for progress when its finished*/
        }
        setTimeout(checkProgress, 1000); //<<check for progress every 1 second

    });
    xhr.send();
})();

重要编辑:正如@drew010指出的那样,如果没有session_write_close();每次curl更新值时session_start()不会起作用,已将其修复。


2
我认为这是最好的解决方案,但不要忘记在每次更新后使用session_write_close,然后再次使用session_start,否则Ajax请求将会被锁定并阻止使用相同会话的后续请求,直到会话关闭。 - drew010
为什么要在session_start中包装一个if,直接调用它,第一件事就是检查来自cookie的session_id是否存在,如果存在,则知道要加载它。 - Barkermn01
@drew010 哇,我不知道那个,谢谢!我马上更新代码。 - hanshenrik
@MartinBarker 我不知道在实际的php文件被调用之前会运行什么,但是有些网站会在auto_prepend_file php.ini指令或类似的地方开始会话。在这种情况下,如果没有检查是否已经开始,调用session_begin()将会导致一个错误(我相信是E_NOTICE?)-- 刚刚检查过,没错:Notice: A session had already been started - ignoring session_start() in C:\WTServer\WWW\sesstest\2.php on line 4 - hanshenrik
如果我尝试在没有启动会话的情况下使用 $_SESSION,即使会话已经存在,它对我也不起作用... - Barkermn01

1

Quote from cURL download progress in PHP

echo "<pre>";
echo "Loading ...";

ob_flush();
flush();

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://stackoverflow.com");
//curl_setopt($ch, CURLOPT_BUFFERSIZE,128);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, 'progress');
curl_setopt($ch, CURLOPT_NOPROGRESS, false); // needed to make progress function work
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
$html = curl_exec($ch);
curl_close($ch);


function progress($resource,$download_size, $downloaded, $upload_size, $uploaded)
{
    if($download_size > 0)
         echo $downloaded / $download_size  * 100;
    ob_flush();
    flush();
    sleep(1); // just to see effect
}

echo "Done";
ob_flush();
flush();

?>


0

不要使用ob_flush(),只需使用flush()。(删除所有的ob_函数)。 此外,考虑修改缓冲区大小:

curl_setopt($ch, CURLOPT_BUFFERSIZE, 16000);

尝试不同的大小。


0

当所有脚本完成时,PHP将显示结果。这就是你只看到完成进度的原因。

您可以在后台执行curl下载,并将进度写入会话、数据库、会话或内存(如redis、memchche)。然后在客户端每隔很短的时间读取进程。

您还可以使用Iframe来实现此功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接