发送 HTTP 响应后继续 PHP 执行

Posted

技术标签:

【中文标题】发送 HTTP 响应后继续 PHP 执行【英文标题】:Continue PHP execution after sending HTTP response 【发布时间】:2011-04-19 11:32:54 【问题描述】:

如何让 php 5.2(以 apache mod_php 运行)向客户端发送完整的 HTTP 响应,然后再继续执行操作一分钟?

长篇大论:

我有一个 PHP 脚本,它必须执行几个长的数据库请求并发送电子邮件,这需要 45 到 60 秒才能运行。此脚本由我无法控制的应用程序调用。我需要应用程序报告从 PHP 脚本收到的任何错误消息(主要是无效参数错误)。

应用程序的超时延迟小于 45 秒(我不知道确切的值),因此将 PHP 脚本的每次执行都注册为错误。因此,我需要 PHP 尽可能快地向客户端发送完整的 HTTP 响应(理想情况下,输入参数一经验证),然后运行数据库和电子邮件处理。

我正在运行 mod_php,所以pcntl_fork 不可用。我可以通过将要处理的数据保存到数据库并从cron 运行实际过程来解决这个问题,但我正在寻找更短的解决方案。

【问题讨论】:

抱歉,这看起来完全是对 PHP 语言的滥用。 滥用 PHP 语言不如滥用网络服务器进程严重。如果不再涉及 HTTP / web,则没有网络服务器应该忙于它。 系统滥用与否,有时我们必须做我们不喜欢的事情,因为我们无法控制的要求。不会使问题无效,只会使情况变得不幸。 我根本看不出这是滥用。如果是这样,应该有人告诉亚马逊关闭 amazon.com,因为打包和运送订单所涉及的大部分工作都是在购买网络请求完成后进行的。要么这样,要么对 amazon.com 购买请求设置两周的超时时间,并且仅在订单交付给客户后才将响应发送到浏览器。 让我们尽量保留个人意见。请回答问题或去其他地方。 【参考方案1】:

我的“特殊脚本”工具箱中有这个 sn-p,但它丢失了(当时云并不常见),所以我正在寻找它并提出了这个问题,惊讶地发现它不见了,我搜索了更多,然后回到这里发布它:

<?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 有固定的超时时间。所以我承认了银行链接并为他指明了出路,并在他已经离开时做我的事情。

【讨论】:

如果您正在使用会话,我建议在flush(); 之后添加session_write_close();,否则在您的(后台)处理完成之前,您将无法使用您的站点(在同一浏览器选项卡中)。 它在linux上的php5和chrome浏览器上不起作用,chrome在终止连接前等待30秒 ignore_user_abort(); // optional 没有任何效果,如果不传递一个值(布尔值)该函数返回当前设置。 我在共享主机上测试了这个解决方案,等待 30 秒后显示“文本用户永远不会看到”。 应该有ignore_user_abort(true);而不是ignore_user_abort();【参考方案2】:

让处理初始请求的脚本在处理队列中创建一个条目,然后立即返回。然后,创建一个单独的进程(可能通过 cron)定期运行队列中待处理的任何作业。

【讨论】:

这是我最初想到的解决方案。另一方面,为了解决第三方应用程序中的超时而设置处理队列让我感到有点不安。 这个解决方案缺乏并行性......或者需要启动一个工作进程池来为队列服务。我最终发布并断开了对 self-localhost 的 http 请求(以 SomeGuy 在这里描述的方式),以利用现有的 httpd 工作人员池作为后台处理器。【参考方案3】:

你需要的是这种设置

【讨论】:

嗯,但是,根据这个图表,只有在 cron 执行时,状态消息才会被发送回客户端 - 最多 5-10 分钟。无论如何,漂亮的图表! 可以随时请求状态消息 :) 关键是这里有两个独立的进程。但除此之外,谢谢! +1 哇,很棒的图表!但与其用户不断地请求状态,我认为 websockets 更好。【参考方案4】:

可以对自己或任何其他脚本使用“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?&param=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 发送给父。

我在这里找到了 - http://linuxportal.ru/forums/index.php/t/22951/

【讨论】:

这种方法(稍作修改)是我发现从 apache 的 mod_php 创建后台任务的唯一可行的解​​决方案 没有 启动单独操作系统进程的开销 - 这将占用并使用其中一个已经存在的 httpd 工作人员 在父脚本的fread($socketToChild, $dataSize)中,$dataSize来自哪里?您是否需要确切知道从套接字输出的数据量(包括标头的大小)?我一定是错过了什么。【参考方案5】:

如果调用文件服务器上的脚本来执行,就好像它是在命令行中触发的一样?您可以使用 PHP 的 exec 来做到这一点。

【讨论】:

+1,Gearman 之类的东西已经为它设置好了(但其他/自己的解决方案当然同样有效)。 exec() 通常是共享/托管空间中的问题。加上巨大的安全风险。【参考方案6】:

您可以使用 PHP 函数 register-shutdown-function,该函数将在脚本完成与浏览器的对话后执行某些操作。

另请参阅ignore_user_abort - 但如果您使用 register_shutdown_function,则不需要此功能。在同一页面上,set_time_limit(0) 将防止您的脚本超时。

【讨论】:

显然,根据文档,自 4.1.0 以来,在脚本完成对话框之前调用了 register_shutdown_function。但是,您的其他链接包含一个有希望的评论:php.net/manual/en/features.connection-handling.php#89177 我会尝试更深入地研究这一点并在这里报告。【参考方案7】:

对于这个简单的任务,使用队列、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?

【讨论】:

您可能需要禁用 Apache 中发生的额外缓冲:&lt;?php apache_setenv('no-gzip', 1); ini_set('zlib.output_compression', 0); ini_set('implicit_flush', 1);?&gt;【参考方案8】:

您可以在服务器和服务器之间创建一个 http 请求。 (不需要浏览器)。 创建后台 http 请求的秘诀是设置一个非常小的超时,因此忽略响应。

这是我为此目的使用的一个工作函数:

五月 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

【讨论】:

【参考方案9】:

可以为此使用 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参数。

【讨论】:

我一直在寻找这个问题的解决方案已经有一段时间了,这个可行!非常感谢。其他解决方案可能适用于特定场景,除非您对网络服务器的控制有限且无法派生新进程;我通常在商业网络服务器上找到的配置。这个解决方案仍然有效!一个重要的补充。对于 UNIX 系统,您需要添加 curl_setopt($ch, CURLOPT_NOSIGNAL, 1); 以使超时 explanation。 终于,正版!【参考方案10】:

您可以将这些函数拆分为三个脚本。 1.启动进程并通过exec或command调用第二个,这也可以通过http调用运行。 2.第二个将运行数据库处理,最后将启动最后一个 3. 最后一封会发邮件

【讨论】:

【参考方案11】:

呸,我误解了你的要求。看起来他们实际上是:

脚本从您无法控制的外部来源接收输入 脚本处理和验证输入,让外部应用知道它们是否正确并终止会话。 脚本启动一个长时间运行的进程。

在这种情况下,是的,使用外部作业队列和/或 cron 会起作用。验证输入后,将作业详细信息插入队列,然后退出。然后可以运行另一个脚本,从队列中获取作业详细信息,并启动更长的过程。 Alex Howansky 的想法是正确的。

抱歉,我承认我第一次略读了一点。

【讨论】:

【参考方案12】:

我建议在最后产生一个新的异步请求,而不是继续与用户一起处理。

您可以使用此处的答案生成另一个请求: Asynchronous PHP calls?

【讨论】:

【参考方案13】:

在您的 Apache php.ini 配置文件中,确保禁用输出缓冲:

output_buffering = off

【讨论】:

以上是关于发送 HTTP 响应后继续 PHP 执行的主要内容,如果未能解决你的问题,请参考以下文章

发送响应后继续执行(Cloud Functions for Firebase)

php提前输出响应及注意问题

HTTP状态码

常用的HTTP状态码

HTTP状态码

HTTP状态码