我正在尝试进行AJAX调用(通过JQuery),这将启动一个相当长的过程。我希望脚本仅发送一个指示进程已启动的响应,但是JQuery在PHP脚本运行完成之前不会返回响应。

我已经尝试使用“关闭”标头(如下),并带有输出缓冲;似乎都不起作用。有什么猜想吗?还是我需要在JQuery中做这件事?

<?php

echo( "We'll email you as soon as this is done." );

header( "Connection: Close" );

// do some stuff that will take a while

mail( 'dude@thatplace.com', "okay I'm done", 'Yup, all done.' );

?>


评论

您是否使用ob_flush()刷新了输出缓冲区,但没有用?

#1 楼

以下PHP手册页(包括用户注释)建议了有关如何在不结束PHP脚本的情况下关闭与浏览器的TCP连接的多种说明:


连接处理文档

它比发送一个关闭的报头还需要更多的信息。


OP然后确认:是的,这样做的窍门是:指向用户注释#71172(2006年11月)从[PHP] 4.1开始,当修改register_shutdown_function()的行为使其不会自动关闭时,关闭用户浏览器连接并保持php脚本运行一直是一个问题。用户连接。

邮件点xubion点hu上的sts发布了原始解决方案:

<?php
header("Connection: close");
ob_start();
phpinfo();
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush();
flush();
sleep(13);
error_log("do something in the background");
?>


直到您用phpinfo()代替echo('text I want user to see');都可以正常工作在这种情况下,永远不会发送标头!

解决方案是在发送标头信息之前显式关闭输出缓冲并清除缓冲区。示例:

<?php
ob_end_clean();
header("Connection: close");
ignore_user_abort(true); // just to be safe
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 !
// Do processing here 
sleep(30);
echo('Text user will never see');
?>


花了3个小时试图弄清楚这一点,希望对别人有帮助:)

在以下位置进行了测试:


IE 7.5730.11
Mozilla Firefox 1.81


于2010年7月在北极发布了相关答案然后,Fire将上面的两个后续的用户注释链接在一起:


连接处理用户注释#89177(2009年2月)
连接处理用户注释- note#93441(2009年9月)


评论


是的,这确实有效:php.net/manual/en/features.connection-handling.php#71172

– Eric_WVGG
08-09-26在20:14

作者和@Timbo White,是否可以在不知道内容大小的情况下尽早关闭连接? IE,而不必在关闭前捕获内容。

–散装
2014年11月11日在18:05



黑客和糟糕的Web浏览器仍然可以忽略连接关闭的HTTP标头,并获取其余的输出。确保接下来出现的内容不敏感。也许是ob_start();抑制一切:p

– hanshenrik
15年6月2日在8:46

添加fastcgi_finish_request();据说在上述方法不起作用时可以成功关闭连接。但是,就我而言,这阻止了我的脚本继续执行,因此请谨慎使用。

–埃里克·杜贝(EricDubé)
16年6月16日在2:22

@RichardSmith因为Connection:close头可以被堆栈中的其他软件覆盖,例如,对于CGI,反向代理可以覆盖(我观察到nginx的行为)。请参阅@hanshenrik的答案。通常,Connection:close是在客户端执行的,不应视为对此问题的答案。连接应从服务器端关闭。

– 7heo.tk
16年7月21日在15:24



#2 楼

必须发送这两个标头:

Connection: close
Content-Length: n (n = size of output in bytes )


由于您需要知道输出的大小,因此需要缓冲输出,然后将其刷新到浏览器:

// buffer all upcoming output
ob_start();
echo "We'll email you as soon as this is done.";

// get the size of the output
$size = ob_get_length();

// send headers to tell the browser to close the connection
header("Content-Length: $size");
header('Connection: close');

// flush all output
ob_end_flush();
ob_flush();
flush();

// if you're using sessions, this prevents subsequent requests
// from hanging while the background process executes
if (session_id()) session_write_close();

/******** background process starts here ********/


此外,如果您的Web服务器在输出上使用自动gzip压缩(例如,使用mod_deflate的Apache),则此操作将不起作用,因为输出更改,并且Content-Length不再准确。禁用特定脚本的gzip压缩。

有关更多详细信息,请访问http://www.zulius.com/how-to/close-browser-connection-continue-execution

评论


如果您的服务器压缩输出,则可以使用header(“ Content-Encoding:none \ r \ n”);将其禁用。这样,Apache将不会对其进行压缩。

– GDmac
2012-3-18在16:03



@GDmac谢谢!我暂时无法使用它,但是禁用压缩可以解决问题。

–反应堆
2013年9月16日16:02

ob_flush()是不必要的,实际上会导致通知未能刷新缓冲区。我将其取出,效果很好。

–利维
2013年9月29日14:02



我发现ob_flush()行是必需的。

– Deebster
2014年4月4日在11:46

#3 楼

您可以将Fast-CGI与PHP-FPM结合使用以使用fastcgi_end_request()函数。这样,您可以在响应已发送到客户端的情况下继续进行一些处理。


如何使用fastcgi_finish_request()的示例(2010年11月)

您可以在PHP手册中找到此文件:FastCGI流程管理器(FPM);但是该功能没有在手册中进一步记录。以下是PHP-FPM的摘录:PHP FastCGI Process Manager Wiki:


fastcgi_finish_request()

范围:php函数

类别:优化

此功能使您可以加快某些php查询的实施。当脚本执行过程中有一些不影响服务器响应的操作时,可以进行加速。例如,可以在页面形成并传递到Web服务器之后将会话保存在memcached中。 fastcgi_finish_request()是php功能,可停止响应输出。 Web服务器立即开始“缓慢而悲伤地”将响应传输到客户端,同时php可以在查询的上下文中做很多有用的事情,例如保存会话,转换下载的视频,处理各种内容。统计信息等。

fastcgi_finish_request()可以调用正在执行的关闭功能。

要避免该问题,可以在fastcgi_finish_request()调用之前或之后立即调用flush

ignore_user_abort(true);
fastcgi_finish_request();


评论


这是实际的答案!

– Kirill Titov
15年7月9日在8:07

如果您使用的是php-fpm-只需使用此功能-则无需考虑标头和其他所有内容。节省了我很多时间!

–罗斯
16年7月28日在18:42

#4 楼

完整版本:

ignore_user_abort(true);//avoid apache to kill the php running
ob_start();//start buffer output

echo "show something to user";
session_write_close();//close session file on server side to avoid blocking other requests

header("Content-Encoding: none");//send header to avoid the browser side to take content as gzip format
header("Content-Length: ".ob_get_length());//send length header
header("Connection: close");//or redirect to some url: header('Location: http://www.google.com');
ob_end_flush();flush();//really send content, can't change the order:1.ob buffer to normal buffer, 2.normal buffer to output

//continue do something on server side
ob_start();
sleep(5);//the user won't wait for the 5 seconds
echo 'for diyism';//user can't see this
file_put_contents('/tmp/process.log', ob_get_contents());
ob_end_clean();


评论


在哪个意义上完成?哪个问题需要您完成可接受的答案脚本(哪个?),并且您的哪些配置差异使此操作必要?

– hakre
13年8月24日在6:39

这行:header(“ Content-Encoding:none”); ->非常重要。

– Bobby Tables
2013年9月25日14:36在

谢谢,这是此页面上唯一可行的解​​决方案。应该将其作为答案。

–user494599
16-09-15在9:11

#5 楼

更好的解决方案是派生后台进程。在unix / linux上非常简单:

<?php
echo "We'll email you as soon as this is done.";
system("php somestuff.php dude@thatplace.com >/dev/null &");
?>


您应该看一下这个问题以获取更好的示例:

PHP执行后台进程

评论


取决于您要做什么。如果将当前请求期间确定的某些数据写入高速缓存,则分叉几乎肯定不是更好的解决方案。即使在OP的情况下,如果其他解决方案都是可靠的,则与将大量数据传递给派生进程相比,它们也可以节省一些服务器CPU周期。

–杰克
10月4日23:16

#6 楼

假设您具有Linux服务器和root用户访问权限,请尝试此操作。这是我找到的最简单的解决方案。

为以下文件创建一个新目录,并赋予其完整权限。 (稍后我们将使其更安全。)

mkdir test
chmod -R 777 test
cd test


将其放在名为bgping的文件中。

echo starting bgping
ping -c 15 www.google.com > dump.txt &
echo ending bgping


注意&。当当前进程进入echo命令时,ping命令将在后台运行。
它将对www.google.com进行15次ping操作,这大约需要15秒。

执行可执行文件。

chmod 777 bgping


将其放在名为bgtest.php的文件中。

<?php

echo "start bgtest.php\n";
exec('./bgping', $output, $result)."\n";
echo "output:".print_r($output,true)."\n";
echo "result:".print_r($result,true)."\n";
echo "end bgtest.php\n";

?>


当您请求bgtest时。在浏览器中的php中,您应该迅速获得以下响应,而无需等待大约15秒即可完成ping命令。

start bgtest.php
output:Array
(
    [0] => starting bgping
    [1] => ending bgping
)

result:0
end bgtest.php


ping命令应现在正在服务器上运行。代替ping命令,您可以运行PHP脚本:

php -n -f largejob.php > dump.txt &


希望有帮助!

#7 楼

这是对Timbo代码的修改,可用于gzip压缩。

// buffer all upcoming output
if(!ob_start("ob_gzhandler")){
    define('NO_GZ_BUFFER', true);
    ob_start();
}
echo "We'll email you as soon as this is done.";

//Flush here before getting content length if ob_gzhandler was used.
if(!defined('NO_GZ_BUFFER')){
    ob_end_flush();
}

// get the size of the output
$size = ob_get_length();

// send headers to tell the browser to close the connection
header("Content-Length: $size");
header('Connection: close');

// flush all output
ob_end_flush();
ob_flush();
flush();

// if you're using sessions, this prevents subsequent requests
// from hanging while the background process executes
if (session_id()) session_write_close();

/******** background process starts here ********/


评论


你是上帝。我已经工作了2天,试图解决这个问题。它对我的本地开发人员有效,但对主机无效。我被水管了。你救了我。谢谢!!!!

–乍得考德威尔
4月13日在16:14



#8 楼

我在共享主机上,并且已将fastcgi_finish_request设置为完全退出脚本。我也不喜欢connection: close解决方案。使用它会强制为后续请求建立单独的连接,从而浪费额外的服务器资源。我阅读了Transfer-Encoding: cunked维基百科文章,并了解到0\r\n\r\n终止了响应。我尚未在浏览器的版本和设备上进行全面测试,但是它可以在我当前使用的所有4种浏览器上运行。

// Disable automatic compression
// @ini_set('zlib.output_compression', 'Off');
// @ini_set('output_buffering', 'Off');
// @ini_set('output_handler', '');
// @apache_setenv('no-gzip', 1);

// Chunked Transfer-Encoding & Gzip Content-Encoding
function ob_chunked_gzhandler($buffer, $phase) {
    if (!headers_sent()) header('Transfer-Encoding: chunked');
    $buffer = ob_gzhandler($buffer, $phase);
    return dechex(strlen($buffer))."\r\n$buffer\r\n";
}

ob_start('ob_chunked_gzhandler');

// First Chunk
echo "Hello World";
ob_flush();

// Second Chunk
echo ", Grand World";
ob_flush();

ob_end_clean();

// Terminating Chunk
echo "\x30\r\n\r\n";
ob_flush();
flush();

// Post Processing should not be displayed
for($i=0; $i<10; $i++) {
    print("Post-Processing");
    sleep(1);
}


评论


多亏了您的良好回答,我才意识到使用连接:关闭是多么愚蠢(和不必要)。我猜有些人对服务器的基本知识并不熟悉。

–贾斯汀
18年3月13日在9:15

@Justin我很久以前写了这个。再次查看它,我应该指出,可能有必要将这些块填充到4KB。我似乎记得有些服务器在达到最低要求之前不会刷新。

–散装
18年3月13日在13:21

@skibulk有些需要64K的(未压缩??)填充,例如这是FcgidOutputBufferSize的默认设置,不能在某些共享服务器上覆盖。

–杰克
10月5日,0:47

#9 楼

您可以尝试进行多线程处理。

您可以编写一个脚本来进行系统调用(使用shell_exec),该脚本使用该脚本调用php二进制文件作为参数来完成工作。但是我认为这不是最安全的方法。也许您可以通过更改php进程和其他内容来整理内容。或者,在phpclasses上有一个可以做到这一点的类http://www.phpclasses.org/browse/package/3953.html。但是我不知道实现的细节

评论


而且,如果您不想等待该过程完成,请使用&字符在后台运行该过程。

–利亚姆
08年9月26日在9:32

#10 楼

TL; DR答案:

ignore_user_abort(true); //Safety measure so that the user doesn't stop the script too early.

$content = 'Hello World!'; //The content that will be sent to the browser.

header('Content-Length: ' . strlen($content)); //The browser will close the connection when the size of the content reaches "Content-Length", in this case, immediately.

ob_start(); //Content past this point...

echo $content;

//...will be sent to the browser (the output buffer gets flushed) when this code executes.
ob_end_flush();
ob_flush();
flush();

if(session_id())
{
    session_write_close(); //Closes writing to the output buffer.
}

//Anything past this point will be ran without involving the browser.


功能答案:

ignore_user_abort(true);

function sendAndAbort($content)
{
    header('Content-Length: ' . strlen($content));

    ob_start();

    echo $content;

    ob_end_flush();
    ob_flush();
    flush();
}

sendAndAbort('Hello World!');

//Anything past this point will be ran without involving the browser.


#11 楼

您的问题可以通过在php中进行一些并行编程来解决。几周前,我在这里问了一个问题:如何在PHP应用程序中使用多线程

,并获得了不错的答案。我特别喜欢一个。笔者参考了PHP中的Easy Parallel Processing(轻松并行处理)(2008年9月; johnlim),该教程实际上可以很好地解决您的问题,因为我已经使用它来解决了几天前出现的类似问题。 br />

#12 楼

Joeri Sebrechts的答案很接近,但是它破坏了在您希望断开连接之前可能已缓冲的所有现有内容。它没有正确调用ignore_user_abort,从而使脚本过早终止。 diyism的答案很好,但不能普遍适用。例如。一个人可能有更多或更少的输出缓冲区,而该缓冲区无法解决该问题,因此它可能无法在您的情况下正常工作,您也不知道为什么。

此功能允许您随时断开连接( (只要尚未发送标头),并保留到目前为止生成的内容。默认情况下,额外的处理时间是无限的。

function disconnect_continue_processing($time_limit = null) {
    ignore_user_abort(true);
    session_write_close();
    set_time_limit((int) $time_limit);//defaults to no limit
    while (ob_get_level() > 1) {//only keep the last buffer if nested
        ob_end_flush();
    }
    $last_buffer = ob_get_level();
    $length = $last_buffer ? ob_get_length() : 0;
    header("Content-Length: $length");
    header('Connection: close');
    if ($last_buffer) {
        ob_end_flush();
    }
    flush();
}


如果您还需要额外的内存,请在调用此函数之前分配它。

#13 楼

给mod_fcgid用户的注意事项(请您自担风险)。但是,如果使用mod_fcgid,则可能会发现此解决方案无法单独使用。换句话说,当调用flush函数时,与客户端的连接不会关闭。

可能要怪mod_fcgid的FcgidOutputBufferSize配置参数。我在以下文章中找到了这个技巧:




此回复Travers Carter和

此博客帖子Seumas Mackinnon。

阅读完上述内容后,您可能会得出一个结论,即快速的解决方案是添加该行(请参阅最后的“示例虚拟主机”):

FcgidOutputBufferSize 0


在您的Apache配置文件(例如,httpd.conf),FCGI配置文件(例如,fcgid.conf)或虚拟主机文件(例如,httpd-vhosts.conf)中。


在上面的(1)中,提到了一个名为“ OutputBufferSize”的变量。这是(2)中提到的FcgidOutputBufferSize的旧名称(有关mod_fcgid,请参见Apache网页中的升级说明)。


详细信息和第二个解决方案
/>以上解决方案禁用了mod_fcgid对整个服务器或特定虚拟主机执行的缓冲。这可能会导致您的网站的性能下降。另一方面,由于PHP本身执行缓冲,因此情况可能并非如此。

如果您不想禁用mod_fcgid的缓冲,则还有另一种解决方案...您可以强制使用此缓冲刷新代码。

下面的代码仅基于Joeri Sebrechts提出的解决方案来实现:

<?php
    ob_end_clean();
    header("Connection: close");
    ignore_user_abort(true); // just to be safe
    ob_start();
    echo('Text the user will see');

    echo(str_repeat(' ', 65537)); // [+] Line added: Fill up mod_fcgi's buffer.

    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush(); // Strange behaviour, will not work
    flush(); // Unless both are called !
    // Do processing here 
    sleep(30);
    echo('Text user will never see');
?>


所添加的代码行本质上是在填充mod_fcgi的缓冲区,从而迫使其刷新。之所以选择数字“ 65537”,是因为FcgidOutputBufferSize变量的默认值为“ 65536”,如相应指令的Apache Web页面中所述。因此,如果您的环境中设置了另一个值,则可能需要相应地调整此值。

我的环境


WampServer 2.5
Apache 2.4。 9
PHP 5.5.19 VC11,x86,无线程安全
mod_fcgid / 2.3.9
Windows 7 Professional x64

虚拟主机示例

<VirtualHost *:80>
    DocumentRoot "d:/wamp/www/example"
    ServerName example.local

    FcgidOutputBufferSize 0

    <Directory "d:/wamp/www/example">
        Require all granted
    </Directory>
</VirtualHost>


评论


我尝试了许多解决方案。这是使用mod_fcgid的唯一解决方案。

– Tsounabe
18年5月23日在18:40



#14 楼

这对我有用

//avoid apache to kill the php running
ignore_user_abort(true);
//start buffer output
ob_start();

echo "show something to user1";
//close session file on server side to avoid blocking other requests
session_write_close();

//send length header
header("Content-Length: ".ob_get_length());
header("Connection: close");
//really send content, can't change the order:
//1.ob buffer to normal buffer,
//2.normal buffer to output
ob_end_flush();
flush();
//continue do something on server side
ob_start();
//replace it with the background task
sleep(20);


#15 楼

好的,所以基本上jQuery进行XHR请求的方式,甚至ob_flush方法也将不起作用,因为您无法在每个onreadystatechange上运行一个函数。 jQuery检查状态,然后选择要采取的正确操作(完成,错误,成功,超时)。尽管我找不到参考资料,但我记得听说它不适用于所有XHR实现。
我认为一种对您有用的方法是在ob_flush和永久帧轮询之间进行交叉。 />
<?php
 function wrap($str)
 {
  return "<script>{$str}</script>";
 };

 ob_start(); // begin buffering output
 echo wrap("console.log('test1');");
 ob_flush(); // push current buffer
 flush(); // this flush actually pushed to the browser
 $t = time();
 while($t > (time() - 3)) {} // wait 3 seconds
 echo wrap("console.log('test2');");
?>

<html>
 <body>
  <iframe src="ob.php"></iframe>
 </body>
</html>


由于脚本是内联执行的,因此刷新缓冲区后,您就可以执行了。为了使此功能有用,请将console.log更改为在主脚本设置中定义的回调方法,以接收数据并对其执行操作。希望这可以帮助。干杯,摩根。

#16 楼

另一种解决方案是将作业添加到队列中,并创建一个cron脚本来检查新作业并运行它们。

我最近不得不这样做,以避开共享主机施加的限制- exec()等已被网络服务器运行的PHP禁用,但可以在Shell脚本中运行。

#17 楼

如果flush()功能不起作用。您必须在php.ini中设置下一个选项,例如:

output_buffering = Off  
zlib.output_compression = Off  


#18 楼

最新的工作解决方案

    // client can see outputs if any
    ignore_user_abort(true);
    ob_start();
    echo "success";
    $buffer_size = ob_get_length();
    session_write_close();
    header("Content-Encoding: none");
    header("Content-Length: $buffer_size");
    header("Connection: close");
    ob_end_flush();
    ob_flush();
    flush();

    sleep(2);
    ob_start();
    // client cannot see the result of code below


#19 楼

在尝试了该线程的许多不同解决方案之后(没有一个对我有用),我在PHP.net官方页面上找到了解决方案:

function sendResponse($response) {
    ob_end_clean();
    header("Connection: close\r\n");
    header("Content-Encoding: none\r\n");
    ignore_user_abort(true);
    ob_start();

    echo $response; // Actual response that will be sent to the user

    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush();
    flush();
    if (ob_get_contents()) {
        ob_end_clean();
    }
}