【问题标题】:Running a large PHP process on a mobile "mini" browser在移动“迷你”浏览器上运行大型 PHP 进程
【发布时间】:2014-11-06 17:23:31
【问题描述】:

我的客户一直在测试我在他的移动浏览器上制作的脚本......其中之一是 Opera “mini”。在某些时候,一个进程必须运行几分钟,我不知道如何在这个浏览器上处理这个问题。一开始我想显示进度,但此时我只想让浏览器暂停,直到流程完成并在完成时收到通知。

我知道或尝试过的事情:
- Opera mini 不支持 XMLHTTPRequest 2.0。所以你不能那样取得进展。
- 它支持计时器,但只有五秒...所以你不能一直发送 AJAX 请求来检查进度。
- 我试图在等待成功回调的同时只发送一个 AJAX 请求来完成这项工作,但浏览器似乎在很长一段时间后超时了一个 AJAX 请求。
- “你不能把过程分成更小的部分吗?”你会说。我正在这样做,并为每个子运行重新加载页面......直到我意识到缺点:如果你想用浏览器返回,你会看到 50 次相同的页面。

有什么办法处理这个吗??我会很感激任何想法。谢谢!

【问题讨论】:

  • 1) 作业定期在某处写入状态 2) 服务器端 ajax 处理程序读取该状态 3) 客户端定期请求该状态。

标签: php ajax mobile opera


【解决方案1】:

您不能向用户发送分块响应,以便他在进程继续处理新数据的同时继续在其网页上看到结果。

// Turn off output buffering
ini_set('output_buffering', 'off');
// Turn off PHP output compression
ini_set('zlib.output_compression', false);

//Flush (send) the output buffer and turn off output buffering
//ob_end_flush();
while (@ob_end_flush());

// Implicitly flush the buffer(s)
ini_set('implicit_flush', true);
ob_implicit_flush(true);
echo '
<table>
<thead>
<th>Url</th>
<th>Id</th>
<th>Class</th>
</thead>
<tbody>
';
ob_flush();
flush();

您可以通过 Google 了解有关分块响应的更多详细信息。

【讨论】:

  • 谢谢。我在研究时遇到过这样的解决方法。我以前没有使用过这样的东西,我想知道:这总是用来在文档末尾“附加”HTML,对吧?但是,您可以在执行此操作时以某种方式更新进度显示吗? (没有计时器)
  • 是的,你是对的。此方法将分块发送数据,主要用于给人一种页面渲染速度很快的错觉。但如果你回显“数据”; ob_flush();flush();在 for 循环中并连同数据一起,如果您可以用进度更新您的客户端,那么它可以解决您的问题。例如: foreach($data as $key => $item){echo $item . ' ' .$key .'% 完成' ; ob_flush();flush();}.
【解决方案2】:

最近我遇到了类似的问题。发出 Ajax 请求将有 2 个问题。第一个,导航器将被冻结。第二个,大多数服务器会在运行脚本一段时间后引发错误(在某些情况下,通常可以引发 30 秒)。

我第一次处理它的方式是在文件中记录相关数据并将进程划分为较小的进程,并且在每次成功的 ajax 响应时,重新启动下一步直到任务结束,将完成百分比保存到会话变量在每个请求上,以及恢复它的流程的当前步骤,就像这样:

function stepOnTask(){
  ajax.post("file.php", "action:doPartialTask", function(response){
    if ( response.taskFinished ) alert("Task Done"); 
    else{
      showProgress(response.currentProgress);
      stepOnTask();
    }});
}

但这对我的桌面导航器来说真的很紧张,而且它经常崩溃,并不是说你不能同时什么都不做,所以我完全改变了它到另一种方法,在 php 中使用后台进程并保存相关信息(估计时间、开始时间等...)在由已启动进程的 pid 命名的文件中,并每隔 x 秒向该文件发出一次请求以检查并显示进度。

最后一点有点长,如果您不要求我,我不会发布代码,因为我不确定您正在寻找的那种解决方案。

祝你好运。

编辑

PHP 后台进程样式

Class BackgroundScript{
    public $win_path = "C:\\xampp\\htdocs\\www\\yourProject";
    public $unix_path = "/home/yourFolder/yourProject.com";
    public $script = NULL;
    public $command = NULL;
    public $pid = NULL;
    public $start_time = NULL;
    public $estimated_time = NULL;
    public $ellapsed_time = NULL;
    public $status_file = NULL;

    public function __construct($script = ""){
        $this->script = $script;
        if ( self::get_os() == "windows" ){
            $this->command = "start /b C:\\xampp\\php\\php.exe ".$this->win_path."\\".$this->script;
        }else{
            $this->command = "php ".$this->unix_path."/".$this->script;
        }
    }

    public static function conPID($pid){
        if ( file_exists(dirname(__FILE__)."/pids/".$pid.".json") ){
            $bgScript = new BackgroundScript();
            $process_info = json_decode(file_get_contents(dirname(__FILE__)."/pids/".$pid.".json"));
            foreach ( $process_info as $key=>$val ){
                $bgScript->$key = $val;
            }
            return $bgScript;
        }else {
            return false;
        }
    }

    public static function get_os(){
        if ( substr(php_uname(), 0, 7) == "Windows" ) return "windows";
        else return "unix";
    }

    public function saveToFile(){
        $path_to_pfolder = self::get_os()=="windows"? $this->win_path."\\pids":$this->unix_path."/pids";

        if ( !( file_exists($path_to_pfolder) && is_dir($path_to_pfolder)) ){
            mkdir($path_to_pfolder);
        }
        $fileHandler = fopen($path_to_pfolder."/".$this->pid.".json", "w");
        $this->status_file = $path_to_pfolder."/".$this->pid.".json";

        fwrite($fileHandler, json_encode($this));
        fclose($fileHandler);

        return $this->status_file;
    }

    public function removeFile(){
        $path_to_pfolder = self::get_os()=="windows"? $this->win_path."\\pids":$this->unix_path."/pids";
        unlink($path_to_pfolder."/".$this->pid.".json");
    }

    public function run($outputFile = '/dev/null'){
        if ( self::get_os() == "windows" ){
            $desc = array(
               0 => array("pipe", "r"),  // stdin es una tubería usada por el hijo para lectura
               1 => array("pipe", "w"),  // stdout es una tubería usada por el hijo para escritura
            );

            //proc_get_status devuelve el pid del proceso que lanza al proceso, o sea, del padre, y hay que hacer algo más para obtener el pid real del proceso que genera el archivo
            $p = proc_open($this->command, $desc, $pipes);
            $status = proc_get_status($p);
            $ppid = $status["pid"];

            //Ya tenemos el pid del padre, ahora buscamos el del último proceso hijo, que será el que acabamos de lanzar, y lo guardamos
            $output = array_filter(explode(" ", shell_exec("wmic process get parentprocessid,processid | find \"$ppid\"")));
            array_pop($output);
            $this->pid = end($output);

            //Cerramos el proceso padre, esto es lo que hará que no se nos quede pillada la aplicación mientras el "servidor" trabaja.
            proc_close($p);
        } else{
            //En unix e ma facilico
             $this->pid =  trim(shell_exec(sprintf('%s > %s 2>&1 & echo $!', $this->command,  $outputFile)));
        }
        $this->ellapsed_time = 0;
        $this->start_time = date("Y-m-d H:i:s");

        return $this->saveToFile();
    }

    public function isRunning()
    {
        try {
            $result = shell_exec(sprintf('ps %d', $this->pid));
            if(count(preg_split("/\n/", $result)) > 2) {
                return true;
            }
        } catch(Exception $e) {}

        return false;
    }

    public function kill(){
        $this->removeFile();
        if ( self::get_os() == "windows" ){
            shell_exec(" taskkill /PID ".$this->pid);
        } else{
            // shell_exec(sprintf('kill %d 2>&1', $this->pid));
            shell_exec(sprintf('kill '.$this->pid));
        }
    }

    public function getPid(){
        return $this->pid;
    }

    public static function getAll(){
        $path_to_pfolder = self::get_os()=="windows"? self::$win_path."\\pids":self::$unix_path."/pids";

        if ( !( file_exists($path_to_pfolder) && is_dir($path_to_pfolder)) ){
            return array();
        }   
        $archivos = scandir($path_to_pfolder);
        $processes = array();

        foreach ($archivos as $archivo){
            if ( is_file($path_to_pfolder."/".$archivo) ){
                $json = file_get_contents($path_to_pfolder."/".$archivo);
                $info = json_decode($json);
                $process = new BackgroundScript();
                foreach ( $info as $key=>$val ){
                    $process->$key = $val;
                }
                $processes[] = $process;
            }
        }

        return $processes;
    }

    public function view(){
        $segundos_estimados = $this->estimated_time;
        $segundos_transcurridos = time() - strtotime($this->start_time);
        $segundos_restantes = max($segundos_estimados - $segundos_transcurridos, 0);

        /*
        $minutos_estimados = floor($segundos_estimados/60);
        $segundos_estimados = $segundos_estimados - $minutos_estimados*60;

        $minutos_restantes = floor($segundos_restantes/60);
        $segundos_restantes = $segundos_restantes - $minutos_restantes*60;
        */

        $estimado =  date("i:s", strtotime("1983-09-23 00:00:00")+$segundos_estimados);
        $restante = date("i:s", strtotime("1983-09-23 00:00:00")+$segundos_restantes);

        if (!$segundos_estimados){
            $html="<a>".$this->nombre_diario."
                    <!--<br>Tiempo Estimado: <span class='estimado'>Calculando</span>-->
                    <br>Tiempo Restante: <span class='restante' data-time='".$segundos_restantes."'>Calculando</span></a>";
        }elseif (!$segundos_transcurridos){
                $html="<a>".$this->nombre_diario."
                    <!--<br>Tiempo Estimado: <span class='estimado'>Guardando</span>-->
                    <br>Tiempo Restante: <span class='restante' data-time='".$segundos_restantes."'>Guardando</span></a>";
        }else{  
                $html="<a>".$this->nombre_diario."
                    <!--<br>Tiempo Estimado: <span class='estimado'>".$estimado."</span>-->
                    <br>Tiempo Restante: <span class='restante' data-time='".$segundos_restantes."'>".$restante."</span></a>";
        }
        return $html;
    }
}

好的,我知道代码可能看起来有点糟糕,但它可以工作。

现在,我将向您展示我的使用方式,您必须根据自己的风格调整它。

我有一个名为 controller.php 的文件,它处理我的项目中的所有操作,看起来很像这样:

if (isset($_POST) && isset($_POST["action"]) ) $action= $_POST["action"];
else $action= $argv[1];

switch ($action) {
    case "performTask1":{   
       task1();
    }
    break;

    case "performTask2":{   
       task2();
    }
    break;

    case "performTask2inBackground":
    {
        $process = new BackgroundScript("controller.php performTask2");
        $response["file_with_info"] = $process->run();  
    }
    break;

    echo json_encode($response);
}

仅此而已。

当然,在课程开始时您必须更改 win_path 和 unix_path 以匹配您自己的机器路径到项目。我同时使用它们,所以我的本地测试环境和真实服务器版本工作相同。还没有 mac 版本:P(希望你不需要它)。

另外需要注意的是,如果您的 php 文件夹位于不同的路径中,您可能必须在构造函数中更改构建变量“command”的路径。

将在项目的根目录创建一个名为“pid”的目录,以保存包含信息的文件,名称为 {pid_of_the_process}.json。请注意,在您的过程中,您可以在此文件中填写有用的信息,否则,它将没有有用的信息。

执行此操作的正确方法是,在您的脚本中执行以下操作:

...
do{
   doLotsOfThings();
   $bgScript= BackgroundScript::conPID(getmypid());
   $bgScript->estimated_time = recalculate_estimated_time();
   $bgScript->ellapsed_time = recalculate_remaining_time();
   $bgScript->saveToFile();
 } while($whatever)
 //END
 $process->kill();

要在任何时候出于任何目的检索有关正在运行的进程的信息,您可以使用BackgroundScript::getAll();,以显示进程的估计剩余时间的快照,例如,这就是为什么留下“视图”方法的原因,这可能对你没有用,但我用它来检索状态并按需向用户显示剩余时间。

出于调试目的,我建议您找到非常需要的 php 错误日志文件,因为您不会有直接的浏览器反馈,请记住,您可以简单地将生成的命令粘贴到控制台并运行该过程,如果您想要了解正在发生的事情的第一手信息。

最后,我想感谢@FlorianEckerstorfer,他的后台进程库帮助我开发了我在此处发布的解决方案。

【讨论】:

  • 出于某种原因,即使我以前使用过它,我也没有想到这一点:分成块并为每个成功响应简单地执行一个新的 AJAX 调用......所以不涉及计时器并且不再需要 AJAX 调用。谢谢!
  • 值得一试,但它经常在我出色的计算机上冻结谷歌浏览器,所以对 Opera mini 不乐观,让我知道它是否有效。
  • 不幸的是……不,没有用。虽然我无法在任何地方找到确认信息,但 Opera mini 似乎在页面加载 5 秒后取消了任何 AJAX 请求。浏览器只是拒绝以任何方式使用处理器超过那个时间。
  • 我将发布调用后台进程的代码,它可能会对您有所帮助,但它有点大。
【解决方案3】:

如果您不需要服务器响应,您的页面可以尝试加载一些 1x1px 的图像。这个 img 是 php 脚本,它返回这个 img 然后重置连接。但是使用 ignore_user_abort(true) 脚本仍然可以继续工作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-04-11
    • 2015-01-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多