如何让PHP 5.2(作为apache mod_php运行)向客户端发送完整的HTTP响应,然后继续执行操作一分钟?
长篇故事:
我有一个PHP脚本,必须执行一些长数据库请求并发送电子邮件,这需要45到60秒才能运行。该脚本由我无法控制的应用程序调用。我需要应用程序报告从PHP脚本收到的任何错误消息(主要是无效的参数错误)。
应用程序的超时延迟小于45秒(我不知道确切的值),因此将每次执行PHP脚本都记录为错误。因此,我需要PHP尽可能快地将完整的HTTP响应发送到客户端(理想情况下,只要输入参数已经过验证),然后运行数据库和电子邮件处理。
我正在运行mod_php,因此pcntl_fork
不可用。我可以通过将要处理的数据保存到数据库并从cron
运行实际过程来解决这个问题,但我正在寻找更短的解决方案。
答案 0 :(得分:46)
我在“特殊脚本”工具箱中有这个片段,但它丢失了(当时云不常见),所以我正在搜索它并提出这个问题,惊讶地发现它丢失了,我搜索了更多,并回到这里发布它:
<?php
ob_end_clean();
header("Connection: close");
ignore_user_abort(); // optional
ob_start();
echo ('Text the user will see');
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush(); // Strange behaviour, will not work
flush(); // Unless both are called !
session_write_close(); // Added a line suggested in the comment
// Do processing here
sleep(30);
echo('Text user will never see');
?>
我实际上在很少的地方使用它。它完全有道理:银行链接正在返回成功支付的请求,我必须调用大量服务并在发生这种情况时处理大量数据。这有时需要10秒以上,但是banklink有固定的超时时间。因此,我承认银联并告诉他出路,并在他已经离开时做我的事情。
答案 1 :(得分:25)
让处理初始请求的脚本在处理队列中创建一个条目,然后立即返回。然后,创建一个单独的进程(可能通过cron),定期运行队列中待处理的任何作业。
答案 2 :(得分:7)
您需要的是这种设置
答案 3 :(得分:6)
可以对自己或任何其他脚本使用“http fork”。我的意思是这样的:
// parent sript, called by user request from browser
// create socket for calling child script
$socketToChild = fsockopen("localhost", 80);
// HTTP-packet building; header first
$msgToChild = "POST /sript.php?¶m=value&<more params> HTTP/1.0\n";
$msgToChild .= "Host: localhost\n";
$postData = "Any data for child as POST-query";
$msgToChild .= "Content-Length: ".strlen($postData)."\n\n";
// header done, glue with data
$msgToChild .= $postData;
// send packet no oneself www-server - new process will be created to handle our query
fwrite($socketToChild, $msgToChild);
// wait and read answer from child
$data = fread($socketToChild, $dataSize);
// close connection to child
fclose($socketToChild);
...
现在是儿童剧本:
// parse HTTP-query somewhere and somehow before this point
// "disable partial output" or
// "enable buffering" to give out all at once later
ob_start();
// "say hello" to client (parent script in this case) disconnection
// before child ends - we need not care about it
ignore_user_abort(1);
// we will work forever
set_time_limit(0);
// we need to say something to parent to stop its waiting
// it could be something useful like client ID or just "OK"
...
echo $reply;
// push buffer to parent
ob_flush();
// parent gets our answer and disconnects
// but we can work "in background" :)
...
主要思想是:
如果您需要与孩子互动 - 您可以使用DB作为“通信媒介”:父母可以阅读孩子状态并写入命令,孩子可以阅读命令和写入状态。如果您需要多个子脚本 - 您应该在用户端保留子ID以区别对象,并在每次要检查相应子项的状态时将该ID发送给父级。
答案 4 :(得分:5)
如果调用文件服务器上的脚本执行就像在命令行中触发一样?您可以使用PHP的exec。
执行此操作答案 5 :(得分:4)
您可以使用PHP函数register-shutdown-function,该函数将在脚本完成与浏览器的对话后执行。
另请参阅ignore_user_abort - 但如果使用register_shutdown_function,则不需要此功能。在同一页面上,set_time_limit(0)
会阻止您的脚本超时。
答案 6 :(得分:3)
使用队列,exec或cron对这个简单的任务来说太过分了。 没有理由不留在同一个脚本中。 这种组合对我很有用:
ignore_user_abort(true);
$response = "some response";
header("Connection: close");
header("Content-Length: " . mb_strlen($response));
echo $response;
flush(); // releasing the browser from waiting
// continue the script with the slow processing here...
阅读更多内容: How to continue process after responding to ajax request in PHP?
答案 7 :(得分:2)
您可以在服务器和服务器之间创建http请求。 (不需要浏览器)。 创建后台http请求的秘诀是设置一个非常小的超时,因此忽略响应。
这是我用于该目的的一个功能函数:
MAY 31 PHP异步后台请求 在PHP中创建异步请求的另一种方法(模拟后台模式)。
/**
* Another way to make asyncronous (o como se escriba asincrono!) request with php
* Con esto se puede simpular un fork en PHP.. nada que envidarle a javita ni C++
* Esta vez usando fsockopen
* @author PHPepe
* @param unknown_type $url
* @param unknown_type $params
*/
function phpepe_async($url, $params = array()) {
$post_params = array();
foreach ($params as $key => &$val) {
if (is_array($val)) $val = implode(',', $val);
$post_params[] = $key.'='.urlencode($val);
}
$post_string = implode('&', $post_params);
$parts=parse_url($url);
$fp = fsockopen($parts['host'],
isset($parts['port'])?$parts['port']:80,
$errno, $errstr, 30);
$out = "POST ".$parts['path']." HTTP/1.1\r\n";
$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";
if (isset($post_string)) $out.= $post_string;
fwrite($fp, $out);
fclose($fp);
}
// Usage:
phpepe_async("http://192.168.1.110/pepe/feng_scripts/phprequest/fork2.php");
有关详细信息,请查看 http://www.phpepe.com/2011/05/php-asynchronous-background-request.html
答案 8 :(得分:1)
可以使用cURL,超时很短。这将是您的主要文件:
<?php>
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://example.com/processor.php");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 10); //just some very short timeout
curl_exec($ch);
curl_close($ch);
?>
这是您的处理器文件:
<?php
ignore_user_abort(true); //very important!
for($x = 0; $x < 10; $x++) //do some very time-consuming task
sleep(10);
?>
正如您所看到的,上面的脚本将在短时间后超时(在这种情况下为10毫秒)。 CURLOPT_TIMEOUT_MS
可能不会像这样工作,在这种情况下,它将等同于curl_setopt($ch, CURLOPT_TIMEOUT, 1)
。
因此,当访问处理器文件时,无论用户(即调用文件)是否中止连接,它都将执行其任务。
当然,您也可以在页面之间传递GET或POST参数。
答案 9 :(得分:0)
呸,我误解了你的要求。看起来他们实际上是:
在这种情况下,然后是,使用外部作业队列和/或cron将起作用。验证输入后,将作业详细信息插入队列,然后退出。然后可以运行另一个脚本,从队列中获取作业详细信息,然后启动更长的过程。 Alex Howansky有正确的想法。
对不起,我承认我第一次撇去了一下。
答案 10 :(得分:0)
您可以将这些功能拆分为三个脚本。
1.启动进程并通过exec或command
调用第二个进程,这也可以通过http调用运行。
2.第二个将运行数据库处理,最后将启动最后一个
3.最后一个将通过电子邮件发送
答案 11 :(得分:0)
我建议最后生成一个新的异步请求,而不是继续与用户进行该过程。
您可以使用以下答案生成其他请求: Asynchronous PHP calls?
答案 12 :(得分:0)
在Apache php.ini
配置文件中,确保禁用输出缓冲:
output_buffering = off