提交表单后,如何在后台运行PHP脚本?


104

问题
我有一个表单,提交后将运行基本代码来处理提交的信息,并将其插入数据库中以显示在通知网站上。此外,我还有一个名单,他们注册了通过电子邮件和SMS消息接收这些通知的人员。此列表暂时是微不足道的(仅推送约150个),但是足以导致花费一分钟以上的时间来遍历整个订户表并发送150多封电子邮件。(由于批量电子邮件政策,电子邮件是根据我们的电子邮件服务器的系统管理员的要求单独发送的。)

在此期间,发布警报的个人将坐在表单的最后一页上将近一分钟,而不会对发布通知进行任何积极的强化。这导致了其他潜在的问题,我认为所有可能的解决方案都不理想。

  1. 首先,发布者可能认为服务器落后,然后再次单击“提交”按钮,导致脚本重新开始或运行两次。我可以通过使用JavaScript禁用按钮并替换文本来表示类似“正在处理...”的方法来解决此问题,但这并不理想,因为在执行脚本的过程中,用户仍然会停留在页面上。(此外,如果禁用了JavaScript,则此问题仍然存在。)

  2. 其次,张贴者在提交表格后可能会过早关闭标签或浏览器。该脚本将一直在服务器上运行,直到尝试写回浏览器为止,但是,如果用户随后浏览到我们域中的任何页面(在脚本仍在运行时),浏览器就会挂起加载页面,直到脚本结束。(只有在关闭浏览器的选项卡或窗口,而不是关闭整个浏览器应用程序时,才会发生这种情况。)但是,这并不理想。

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

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

编辑:不能被cron'ed。它必须在提交表单后立即运行。这些是高优先级的通知。另外,运行我们的服务器的系统管理员不允许cron超过5分钟的时间运行。


开始一项cron作业,并在另一页上警告用户正在处理他们的请求或类似的请求。
埃文·穆拉斯基

1
您要定期运行(每天或每小时)还是提交运行?我想您可以使用php,exec()但我从未尝试过。
西蒙(Simon)

2
我只是简单地在循环中使用ajax并插入sleep()时间间隔(在我的情况下使用foreach)来运行后台进程。它在我的服务器上完美运行。不确定其他人。我还要在循环之前添加ignore_user_abort()set_time_limit()(具有计算的结束时间),以确保脚本不会被我使用的服务器停止,即使根据我的测试,该脚本也会在没有ignore_user_abort()and的情况下完成任务set_time_limit()
2013年

我看到您已经想出了一个解决方案。我使用gearmanphp来处理后台任务。如果您遇到大量的时间处理和繁重的工作,则非常适合扩展。您应该看看它。
安德烈·加西亚

按照第[计算器]这个答案(stackoverflow.com/a/46617107/6417678
Joshy弗朗西斯

Answers:


89

做了一些实验用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)

谢谢。这救了我一命。
利亚姆·贝利2013年

$post_idphp脚本的路径之后是什么?
Perocat

@Perocat,这是您要发送到脚本的变量。在这种情况下,它将发送一个ID,该ID将成为被调用脚本的参数。
2014年

我怀疑这样做的工作很好,看到stackoverflow.com/questions/2212635/...
symcbean

1
有一个未决的编辑建议可以逃脱$post_id;我宁愿直接将其转换为数字:(int) $post_id。(请引起您的注意,以决定哪个更好。)
Al.G.

19

在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连接。因此,不幸的是,此解决方案对我不起作用。
Michael Irigoyen

3
当然,您不打电话proc_close,这就是重点!是的,您可以使用管道传输到文件proc_open。查看最新答案。
netcoder 2011年

@netcoder:如果我不想将结果存储在任何文件中怎么办。stdout需要哪些更改?
naggarwal11 2015年

9

在所有答案中,没有人考虑过非常简单的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中,我必须以某种方式将已发布的数据传输到执行的脚本,这对我而言本身是一项耗时的任务,并且使事情变得非常复杂。
奥兰则布

7

PHP exec("php script.php")可以做到。

手册

如果使用此功能启动程序,则要使其在后台继续运行,必须将程序的输出重定向到文件或其他输出流。否则,PHP会挂起,直到程序执行结束。

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


1
谢谢,但这没有用。脚本在处理第二个文件时仍然“挂起”。
Michael Irigoyen

查看推荐的php.net/manual/en/function.exec.php#80582似乎是因为启用了Safe_mode。在UNIX下有一种解决方法。
西蒙(Simon)

不幸的是,我们的安装未启用安全模式。奇怪的是它仍然挂着。
Michael Irigoyen


4

这个怎么样?

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

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


谢谢,但我已经更新了原始问题。在这种情况下,不能使用Cron。
Michael Irigoyen

4

据我所知,您无法以简单的方式执行此操作(请参阅fork exec等(在Windows下不起作用)),也许您可​​以逆转此方法,请使用浏览器的背景在ajax中发布表单,因此,如果发布仍然工作,您没有等待时间。
即使您需要进行长时间的详细说明,这也会有所帮助。

关于发送邮件,总是建议使用后台处理程序,它可能是一台本地且快速的smtp服务器,可以接受您的请求,然后将其后台处理到真正的MTA或全部放入数据库中,而不是使用后台处理队列的cron。
Cron可能在另一台将后台处理程序作为外部URL调用的机器上:

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

3

后台cron作业听起来很不错。

您需要对计算机的ssh访问权限才能以cron身份运行脚本。

$ php scriptname.php 运行它。


1
无需ssh。许多主机禁止ssh,但具有cron支持。
Teson

谢谢,但我已经更新了原始问题。在这种情况下,不能使用Cron。
Michael Irigoyen

3

如果您可以通过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)));
}
?>


2

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

编辑:

已经有很多问题要求在SO上“不带cron运行php”。这是一个:

调度脚本而不使用CRON

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


谢谢,但我已经更新了原始问题。在这种情况下,不能使用Cron。
Michael Irigoyen

2

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

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

#!/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()脚本要困难得多。大声笑;)
我是ArbZ 2014年

2

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

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:如果您想将网址参数作为循环发送,请遵循以下答案:https : //stackoverflow.com/a/41225209/6295712


0

就我而言,我有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];

0

这对我有用。提这个

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

如果我有邮寄值怎么办?从表单,然后通过ajax和serialize()函数发送它 。例如,我index.php 使用表单,并通过ajax将值发送到index2.php,我想在后台执行index2.php中的数据发送,这是什么建议?谢谢
khalid JA '18

0

使用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);
}));
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.