如何在提交表单后在后台运行PHP脚本?
问题
我有一个表格,在提交时,将运行基本代码来处理提交的信息,并将其插入到数据库中以显示在通知网站上。 另外,我有一个通过电子邮件和短信收到这些通知的人的名单。 这个列表是微不足道的(只需要大约150个),但是这足以导致在整个用户表中循环并发送150多封电子邮件。 (电子邮件按照邮件服务器的系统pipe理员的要求单独发送,因为有大量邮件策略。)
在这段时间内,发布警报的个人将在表单的最后一页坐了近一分钟,没有任何正面的强化,他们的通知被张贴。 这导致了其他潜在的问题,所有有可能的解决办法,我觉得是不太理想的。
-
首先,海报可能认为服务器滞后,再次点击“提交”button,导致脚本重新开始或运行两次。 我可以通过使用JavaScript来禁用button来解决这个问题,并将文本replace为“Processing …”,但是这并不理想,因为用户在脚本执行期间仍然停留在页面上。 (另外,如果禁用JavaScript,则此问题仍然存在。)
-
其次,海报可能会在提交表单后过早closures标签或浏览器。 该脚本将继续在服务器上运行,直到它尝试写回到浏览器,但是,如果用户然后浏览到我们域内的任何页面(而脚本仍在运行),则浏览器挂起加载页面直到脚本结束。 (这只发生在浏览器的标签或窗口closures,而不是整个浏览器应用程序。)但是,这是不太理想的。
(可能的)解决scheme
我已经决定要将脚本的“电子邮件”部分分成一个单独的文件,我可以在通知发布后调用。 我原本以为在通知成功发布之后,把它放在确认页面上。 但是,用户不知道这个脚本正在运行,任何exception对他们来说都是不明显的。 这个脚本不能失败。
但是,如果我可以运行这个脚本作为后台进程呢? 所以,我的问题是:我如何执行PHP脚本作为后台服务触发,并完全独立于用户在表单级别完成的操作?
编辑:这不能 cron'ed。 它必须在提交表单的时候运行。 这些是高优先级的通知。 另外,运行我们服务器的系统pipe理员不允许cron运行超过5分钟。
做exec
和shell_exec
一些实验我发现了一个完美的解决scheme! 我select使用shell_exec
这样我就可以logging发生的每个通知过程(或不)。 ( shell_exec
作为一个string返回,这比使用exec
更容易,将输出分配给一个variables,然后打开一个文件写入。)
我正在使用以下行来调用电子邮件脚本:
shell_exec("/path/to/php /path/to/send_notifications.php '".$post_id."' 'alert' >> /path/to/alert_log/paging.log &");
在命令的最后注意“@ &
是很重要的(正如@netcoder指出的那样)。 这个UNIX命令在后台运行一个进程。
在脚本path之后的单引号中包含的额外variables被设置为我可以在脚本中调用的$_SERVER['argv']
variables。
然后电子邮件脚本使用>>
输出到我的日志文件,并输出如下所示:
[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)
在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);
这里是重要的一点。 即使原始脚本已经结束,脚本也会继续。
PHP exec("php script.php")
可以做到这一点。
从手册 :
如果一个程序是用这个函数启动的,为了继续在后台运行,程序的输出必须被redirect到一个文件或者另一个输出stream。 如果不这样做将导致PHP挂起,直到程序执行结束。
所以如果你把输出redirect到一个日志文件(反正是个好主意),你的调用脚本不会挂起,你的电子邮件脚本将会以bg的forms运行。
为什么不在脚本上做一个HTTP请求而忽略响应呢?
http://php.net/manual/en/function.httprequest-send.php
如果您在脚本上提出请求,则需要调用您的Web服务器将在后台运行,并且您可以(在主脚本中)显示一条消息,告诉用户脚本正在运行。
背景cron的工作听起来像是一个好主意。
您将需要ssh访问机器以cron运行脚本。
$ php scriptname.php
来运行它。
这个怎么样?
- 持有表单的PHP脚本将标志或某个值保存到数据库或文件中。
- 第二个PHP脚本定期轮询该值,如果已设置,则以同步方式触发电子邮件脚本。
第二个PHP脚本应该设置为以cron运行。
因为我知道你不能以简单的方式做到这一点(请参阅叉执行等(不要在Windows下工作)),可能是你可以扭转的方法,使用浏览器的背景张贴在Ajax表单,所以如果post仍然工作你没有等待时间。
这可以帮助,即使你需要做一些长期的阐述。
关于发送邮件,总是build议使用假脱机程序,可能是一个本地&快速smtp服务器,接受您的请求,并将其假脱机到真正的MTA或全部放在一个数据库,比使用一个cron后台队列。
cron可能在另一台机器上调用假脱机程序作为外部url:
* * * * * wget -O /dev/null http://www.example.com/spooler.php
如果你可以通过SSH访问服务器,并且可以运行你自己的脚本,你可以使用PHP做一个简单的fifo服务器(虽然你将不得不用fork
重新编译php的posix
支持)。
服务器可以写在任何东西,你可能可以很容易地在Python中做到这一点。
或者最简单的解决scheme是发送一个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))); } ?>
在后台运行php脚本的更简单的方法是php script.php> / null / dev&
PHP将运行在后台,页面也将达到动作页面更快..
假设你正在* nix平台上运行,使用cron
和php
可执行文件。
编辑:
SO上有很多问题要求“不用cron运行php”。 这里有一个:
安排脚本而不使用CRON
这就是说,上面的exec()答案听起来非常有前途:)
如果你在Windows上,研究proc_open
或popen
…
但是,如果我们在运行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 ?>
在我的情况下,我有3个参数,其中一个是string(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];
对于后台工作者,我认为你应该尝试这种技术,它将有助于打电话多达你喜欢的页面,所有页面将独立运行一次,而无需等待每个页面的响应为asynchronous。
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
在所有的答案中,没有一个考虑过简单的fastcgi_finish_request函数,当被调用时,将所有剩余的输出刷新到浏览器并closuresFastcgi会话和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,