【问题标题】:How can I run a PHP script in the background after a form is submitted?提交表单后如何在后台运行 PHP 脚本?
【发布时间】:2011-06-05 08:09:54
【问题描述】:

问题
我有一个表单,当提交时,它将运行基本代码来处理提交的信息并将其插入数据库以显示在通知网站上。此外,我还有一份已注册通过电子邮件和 SMS 消息接收这些通知的人员列表。该列表目前微不足道(仅推送大约 150 条),但足以导致需要一分钟以上的时间来循环浏览整个订阅者表并发送 150 多封电子邮件。 (由于邮件群发政策,我们的电子邮件服务器的系统管理员要求这些电子邮件单独发送。)

在此期间,发布警报的个人将在表单的最后一页停留近一分钟,而没有任何积极强化他们的通知正在发布。这会导致其他潜在问题,所有可能的解决方案我都觉得不太理想。

  1. 首先,发帖者可能认为服务器滞后并再次单击“提交”按钮,导致脚本重新开始或运行两次。我可以通过使用 JavaScript 禁用按钮并将文本替换为“处理中...”之类的内容来解决此问题,但这并不理想,因为在脚本执行期间用户仍会卡在页面上。 (另外,如果 JavaScript 被禁用,这个问题仍然存在。)

  2. 其次,发帖人可能会在提交表单后过早关闭选项卡或浏览器。脚本将继续在服务器上运行,直到它尝试写回浏览器,但是如果用户随后浏览到我们域中的任何页面(脚本仍在运行时),浏览器会挂起加载页面,直到脚本结束. (这只发生在浏览器的一个选项卡或窗口关闭时,而不是整个浏览器应用程序关闭时。)不过,这并不理想。

(可能的)解决方案
我决定将脚本的“电子邮件”部分拆分为一个单独的文件,以便在发布通知后调用。我本来想在通知成功发布后将其放在确认页面上。但是,用户不会知道这个脚本正在运行,并且任何异常对他们来说都不会很明显;此脚本不会失败。

但是,如果我可以将此脚本作为后台进程运行呢?所以,我的问题是:如何执行 PHP 脚本以作为后台服务触发并完全独立于用户在表单级别执行的操作?

编辑:这个不能被cron'ed。它必须在表单提交的那一刻运行。这些是高优先级通知。此外,运行我们服务器的系统管理员不允许 crons 运行频率超过 5 分钟。

【问题讨论】:

  • 启动一个 cron 作业并在另一个页面上提醒用户他们的请求正在处理中,或者类似的东西。
  • 您想定期(每天或每小时)运行它还是在提交时运行它?我想你可以使用 php 的 exec() 但我从未尝试过。
  • 我只是使用ajax 并在循环内插入带有时间间隔的sleep()(我在我的情况下使用foreach)来运行后台进程。它在我的服务器上完美运行。不确定其他人。我还在循环之前添加ignore_user_abort()set_time_limit()(计算结束时间),以确保脚本不会被我使用的服务器停止,即使根据我的测试,脚本在没有@的情况下完成任务987654327@ 和 set_time_limit().
  • 我看到你已经想出了一个解决方案。我使用gearman 和php 来处理后台任务。如果您遇到大量时间处理和繁重的负载,它是完美的扩展。你应该看看它。
  • 在[stackoverflow](stackoverflow.com/a/46617107/6417678)上关注这个答案

标签: php scripting background


【解决方案1】:

execshell_exec 做一些实验,我发现了一个完美的解决方案!我选择使用shell_exec,这样我就可以记录发生(或未发生)的每个通知过程。 (shell_exec 作为字符串返回,这比使用exec 更容易,将输出分配给变量然后打开要写入的文件。)

我正在使用以下行来调用电子邮件脚本:

shell_exec("/path/to/php /path/to/send_notifications.php '".$post_id."' 'alert' >> /path/to/alert_log/paging.log &");

注意命令末尾的& 很重要(正如@netcoder 所指出的那样)。此 UNIX 命令在后台运行一个进程。

脚本路径后用单引号括起来的额外变量设置为我可以在脚本中调用的$_SERVER['argv'] 变量。

然后电子邮件脚本使用>> 输出到我的日志文件,并将输出如下内容:

[2011-01-07 11:01:26] Alert Notifications Sent for http://alerts.illinoisstate.edu/2049 (SCRIPT: 38.71 seconds)
[2011-01-07 11:01:34] CRITICAL ERROR: Alert Notifications NOT sent for http://alerts.illinoisstate.edu/2049 (SCRIPT: 23.12 seconds)

【讨论】:

  • 谢谢。这救了我的命。
  • php脚本路径后面的$post_id是什么?
  • @Perocat 它是您发送到脚本的变量。在这种情况下,它会发送一个 ID,该 ID 将作为被调用脚本的参数。
  • 我怀疑这是否完美,请参阅stackoverflow.com/questions/2212635/…
  • escape$post_id 有一个待定的编辑建议;我宁愿直接将其转换为一个数字:(int) $post_id。 (呼吁您注意决定哪个更好。)
【解决方案2】:

在 Linux/Unix 服务器上,您可以使用 proc_open 在后台执行作业:

$descriptorspec = array(
   array('pipe', 'r'),               // stdin
   array('file', 'myfile.txt', 'a'), // stdout
   array('pipe', 'w'),               // stderr
);

$proc = proc_open('php email_script.php &', $descriptorspec, $pipes);

& 是这里的重要部分。即使原始脚本已经结束,脚本也会继续。

【讨论】:

  • 当我在任务完成后不调用proc_close 时,此方法有效。 proc_close 使脚本挂起大约 15 到 25 秒。我需要使用管道信息保留日志,因此我必须打开要写入的文件,关闭它,然后关闭 proc 连接。所以,不幸的是,这个解决方案对我不起作用。
  • 当然不要打电话给proc_close,这才是重点!是的,您可以使用proc_open 管道传输到文件。查看更新的答案。
  • @netcoder :如果我不想将结果存储在任何文件中怎么办。标准输出需要进行哪些更改?
【解决方案3】:

在所有答案中,没有人认为fastcgi_finish_request 函数非常简单,调用该函数时,会将所有剩余输出刷新到浏览器并关闭 Fastcgi 会话和 HTTP 连接,同时让脚本在后台运行。

一个例子:

<?php
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
fastcgi_finish_request(); // The user is now disconnected from the script

// Do stuff with received data

【讨论】:

  • 这正是我一直在寻找的,在 shell_exec 中我必须以某种方式将发布的数据传输到执行的脚本中,这在我的情况下本身就是一项耗时的任务,而且也让事情变得非常复杂。
【解决方案4】:

PHPexec("php script.php") 可以做到。

来自Manual

如果一个程序以此启动 功能,以使其继续 后台运行,输出 程序必须重定向到 文件或其他输出流。失败 这样做会导致 PHP 挂起,直到 程序执行结束。

因此,如果您将输出重定向到日志文件(无论如何都是个好主意),您的调用脚本将不会挂起,并且您的电子邮件脚本将在 bg 中运行。

【讨论】:

  • 谢谢,但这不起作用。该脚本在处理第二个文件时仍然“挂起”。
  • 看推荐php.net/manual/en/function.exec.php#80582好像是因为启用了Safe_mode。 unix 下有解决方法。
  • 不幸的是,我们的安装没有启用安全模式。奇怪的是它还挂着。
  • 这行得通:exec($cmd . " > /path/to/file &");
【解决方案5】:

为什么不在脚本上发出 HTTP 请求并忽略响应?

http://php.net/manual/en/function.httprequest-send.php

如果您对脚本提出请求,您需要调用您的网络服务器将在后台运行它,并且您可以(在您的主脚本中)显示一条消息,告诉用户该脚本正在运行。

【讨论】:

    【解决方案6】:

    在后台运行 PHP 脚本的更简单方法是

    php script.php >/dev/null &
    

    脚本将在后台运行,页面也将更快地到达操作页面。

    【讨论】:

    • 我使用过:nohup php script.php >/dev/null & 这是唯一对我有用的解决方案
    【解决方案7】:

    这个怎么样?

    1. 保存表单的 PHP 脚本将标志或某些值保存到数据库或文件中。
    2. 第二个 PHP 脚本会定期轮询此值,如果已设置,它会以同步方式触发电子邮件脚本。

    第二个 PHP 脚本应设置为作为 cron 运行。

    【讨论】:

    • 谢谢,但我已经更新了原始问题。在这种情况下,不能选择 Cron。
    【解决方案8】:

    据我所知,您不能以简单的方式做到这一点(请参阅 fork exec 等(不要在 Windows 下工作)),也许您可​​以反转方法,使用浏览器的背景在 ajax 中发布表单,所以如果帖子仍然有效,您无需等待。
    即使您必须进行长时间的阐述,这也会有所帮助。

    关于发送邮件,总是建议使用假脱机程序,可能是一个本地和快速的 smtp 服务器,它接受您的请求并将它们假脱机到真正的 MTA 或将所有内容放在数据库中,而不是使用假脱机队列的 cron。
    cron 可能位于另一台机器上,将 spooler 作为外部 url 调用:

    * * * * * wget -O /dev/null http://www.example.com/spooler.php
    

    【讨论】:

      【解决方案9】:

      后台 cron 作业听起来是个好主意。

      您需要对机器的 ssh 访问权限才能将脚本作为 cron 运行。

      $ php scriptname.php 运行它。

      【讨论】:

      • 不需要 ssh。许多主机不允许 ssh,但有 cron 支持。
      • 谢谢,但我已经更新了原始问题。在这种情况下,不能选择 Cron。
      【解决方案10】:

      如果您可以通过 ssh 访问服务器并且可以运行您自己的脚本,您可以使用 php 创建一个简单的 fifo 服务器(尽管您必须使用 posix 重新编译 php 以支持 fork)。

      服务器可以用任何东西编写,你可能可以很容易地用 python 来编写。

      或者最简单的解决方案是发送HttpRequest 并且不读取返回数据,但服务器可能会在脚本完成处理之前将其销毁。

      示例服务器:

      <?php
      define('FIFO_PATH', '/home/user/input.queue');
      define('FORK_COUNT', 10);
      
      if(file_exists(FIFO_PATH)) {
          die(FIFO_PATH . ' exists, please delete it and try again.' . "\n");
      }
      
      if(!file_exists(FIFO_PATH) && !posix_mkfifo(FIFO_PATH, 0666)){
          die('Couldn\'t create the listening fifo.' . "\n");
      }
      
      $pids = array();
      $fp = fopen(FIFO_PATH, 'r+');
      for($i = 0; $i < FORK_COUNT; ++$i) {
          $pids[$i] = pcntl_fork();
          if(!$pids[$i]) {
              echo "process(" . posix_getpid() . ", id=$i)\n";
              while(true) {
                  $line = chop(fgets($fp));
                  if($line == 'quit' || $line === false) break;
                  echo "processing (" . posix_getpid() . ", id=$i) :: $line\n";
              //  $data = json_decode($line);
              //  processData($data);
              }
              exit();
          }
      }
      fclose($fp);
      foreach($pids as $pid){
          pcntl_waitpid($pid, $status);
      }
      unlink(FIFO_PATH);
      ?>
      

      示例客户端:

      <?php
      define('FIFO_PATH', '/home/user/input.queue');
      if(!file_exists(FIFO_PATH)) {
          die(FIFO_PATH . ' doesn\'t exist, please make sure the fifo server is running.' . "\n");
      }
      
      function postToQueue($data) {
          $fp = fopen(FIFO_PATH, 'w+');
          stream_set_blocking($fp, false); //don't block
          $data = json_encode($data) . "\n";
          if(fwrite($fp, $data) != strlen($data)) {
              echo "Couldn't the server might be dead or there's a bug somewhere\n";
          }
          fclose($fp);
      }
      $i = 1000;
      while(--$i) {
          postToQueue(array('xx'=>21, 'yy' => array(1,2,3)));
      }
      ?>
      

      【讨论】:

        【解决方案11】:

        假设您在 *nix 平台上运行,请使用 cronphp 可执行文件。

        编辑:

        已经有很多问题要求“在没有 cron 的情况下运行 php”。这是一个:

        Schedule scripts without using CRON

        也就是说,上面的 exec() 答案听起来很有希望:)

        【讨论】:

        • 谢谢,但我已经更新了原始问题。在这种情况下,不能选择 Cron。
        【解决方案12】:

        如果您使用的是 Windows,请研究 proc_openpopen...

        但如果我们在同一台服务器“Linux”上运行 cpanel,那么这是正确的方法:

        #!/usr/bin/php 
        <?php
        $pid = shell_exec("nohup nice php -f            
        'path/to/your/script.php' /dev/null 2>&1 & echo $!");
        While(exec("ps $pid"))
        { //you can also have a streamer here like fprintf,        
         // or fgets
        }
        ?>
        

        如果您怀疑自己可以处理它们,请不要使用fork()curl,这就像滥用您的服务器一样

        最后,在上面调用的script.php 文件中,注意这一点,确保你写了:

        <?php
        ignore_user_abort(TRUE);
        set_time_limit(0);
        ob_start();
        // <-- really optional but this is pure php
        
        //Code to be tested on background
        
        ob_flush(); flush(); 
        //this two do the output process if you need some.        
        //then to make all the logic possible
        
        
        str_repeat(" ",1500); 
        //.for progress bars or loading images
        
        sleep(2); //standard limit
        
        ?>
        

        【讨论】:

        • 很抱歉后来编辑它,因为我正在使用我的手机,实际上它比编写 exec() 脚本更难。大声笑;)
        【解决方案13】:

        对于后台工作人员,我认为您应该尝试这种技术,这将有助于调用尽可能多的页面,所有页面将一次独立运行,而无需等待每个页面响应异步。

        form_action_page.php

             <?php
        
            post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue");
            //post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2");
            //post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue");
            //call as many as pages you like all pages will run at once //independently without waiting for each page response as asynchronous.
        
          //your form db insertion or other code goes here do what ever you want //above code will work as background job this line will direct hit before //above lines response     
                        ?>
                        <?php
        
                        /*
                         * Executes a PHP page asynchronously so the current page does not have to wait for it to     finish running.
                         *  
                         */
                        function post_async($url,$params)
                        {
        
                            $post_string = $params;
        
                            $parts=parse_url($url);
        
                            $fp = fsockopen($parts['host'],
                                isset($parts['port'])?$parts['port']:80,
                                $errno, $errstr, 30);
        
                            $out = "GET ".$parts['path']."?$post_string"." HTTP/1.1\r\n";//you can use POST instead of GET if you like
                            $out.= "Host: ".$parts['host']."\r\n";
                            $out.= "Content-Type: application/x-www-form-urlencoded\r\n";
                            $out.= "Content-Length: ".strlen($post_string)."\r\n";
                            $out.= "Connection: Close\r\n\r\n";
                            fwrite($fp, $out);
                            fclose($fp);
                        }
                        ?>
        

        testpage.php

            <?
            echo $_REQUEST["Keywordname"];//case1 Output > testValue
        //here do your background operations it will not halt main page
            ?>
        

        PS:如果您想将 url 参数作为循环发送,请按照以下答案操作:https://stackoverflow.com/a/41225209/6295712

        【讨论】:

          【解决方案14】:

          在我的例子中,我有 3 个参数,其中一个是字符串 (mensaje):

          exec("C:\wamp\bin\php\php5.5.12\php.exe C:/test/N/trunk/api/v1/Process.php $idTest2 $idTest3 \"$mensaje\" >> c:/log.log &");
          

          在我的 Process.php 中有这段代码:

          if (!isset($argv[1]) || !isset($argv[2]) || !isset($argv[3]))
          {   
              die("Error.");
          } 
          
          $idCurso = $argv[1];
          $idDestino = $argv[2];
          $mensaje = $argv[3];
          

          【讨论】:

            【解决方案15】:

            这对我有用。提这个

            exec(“php asyn.php”.” > /dev/null 2>/dev/null &“);
            

            【讨论】:

            • 如果我有帖子值怎么办?从一个表单,我通过 ajax 和 serialize() 函数发送它。例如,我有 index.php 的表单,我通过 ajax 将值发送到 index2.php 我想在后台执行 index2.php 中的数据发送你有什么建议?谢谢
            【解决方案16】:

            使用Amphp 并行和异步执行作业。

            安装库

            composer require amphp/parallel-functions
            

            代码示例

            <?php
            
            require "vendor/autoload.php";
            
            use Amp\Promise;
            use Amp\ParallelFunctions;
            
            
            echo 'started</br>';
            
            $promises[1] = ParallelFunctions\parallel(function (){
                // Send Email
            })();
            
            $promises[2] = ParallelFunctions\parallel(function (){
                // Send SMS
            })();
            
            
            Promise\wait(Promise\all($promises));
            
            echo 'finished';
            

            对于您的用例,您可以执行以下操作

            <?php
            
            use function Amp\ParallelFunctions\parallelMap;
            use function Amp\Promise\wait;
            
            $responses = wait(parallelMap([
                'a@example.com',
                'b@example.com',
                'c@example.com',
            ], function ($to) {
                return send_mail($to);
            }));
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多