如何提早关闭连接?


99

我正在尝试进行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()刷新了输出缓冲区,但没有成功?
Vinko Vrsalovic

Answers:


87

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

据说它比发送关闭标头还需要更多。


然后 OP确认:是的,这成功了:指向复制在此处的用户注释#71172(2006年11月)

自[PHP] 4.1起,在保持用户php脚本运行的同时关闭用户浏览器连接一直是一个问题。 register_shutdown_function()当时修改,使其不会自动关闭用户连接。

邮件点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月,在一个相关的答案中, Arctic Fire随后将两个跟进的用户注释链接到了上面的一个:



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

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

3
添加fastcgi_finish_request(); 据说当以上方法不起作用时,可以成功关闭连接。但是,就我而言,它阻止了脚本继续执行,因此请谨慎使用。
埃里克·杜贝(EricDubé)

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

56

有必要发送以下两个标头:

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压缩(例如,Apache使用mod_deflate),则将无法正常工作,因为输出的实际大小已更改,并且Content-Length不再准确。禁用gzip压缩特定脚本。

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


16
如果您的服务器压缩了输出,则可以使用以下header("Content-Encoding: none\r\n");方式禁用它: apache不会压缩它。
GDmac 2012年

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

ob_flush()是不必要的,实际上会引起通知failed to flush buffer。我将其取出,效果很好。
Levi

2
我发现这ob_flush()条线必要的。
Deebster 2014年

21

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

您可以在以下PHP手册中找到它:FastCGI Process Manager(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,对print,或的调用echo将尽早终止脚本。

为避免该问题,您可以在通话ignore_user_abort(true)之前或之后立即fastcgi_finish_request拨打电话:

ignore_user_abort(true);
fastcgi_finish_request();

3
这是实际的答案!
Kirill Titov

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

17

完整版本:

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

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

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

6

更好的解决方案是派生后台进程。在unix / linux上,这是相当简单的:

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

您应该查看此问题以获取更好的示例:

PHP执行后台进程


4

假设您具有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进行ping操作15次,这大约需要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命令现在应该在服务器上运行。您可以运行PHP脚本来代替ping命令:

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

希望这可以帮助!


4

这是对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天,试图解决这个问题。它对我的本地开发人员有效,但对主机无效。我被水管了。你救了我。谢谢!!!!
乍得考德威尔

3

我在共享主机上,fastcgi_finish_request并设置为完全退出脚本。我也不喜欢该connection: close解决方案。使用它会强制为后续请求建立单独的连接,从而浪费额外的服务器资源。我阅读了Transfer-Encoding: cunked Wikipedia文章,并了解到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);
}

多亏了您的良好回答,我才意识到使用连接:关闭是多么愚蠢(也是不必要的)。我猜有些人不熟悉服务器的具体细节。
贾斯汀

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

2

您可以尝试执行多线程。

您可以制作一个脚本来进行系统调用(使用shell_exec),该脚本使用该脚本调用php二进制文件作为参数来完成您的工作。但是我认为这不是最安全的方法。也许您可以通过更改php进程和其他内容来整理内容

另外,phpclasses上有一个可以做到这一点的类http://www.phpclasses.org/browse/package/3953.html。但我不知道实施的细节


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

2

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.


1

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();
}

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


1

给mod_fcgid用户的注意事项(请使用,后果自负)。

快速解决方案

Joeri Sebrechts的公认答案确实有用。但是,如果使用mod_fcgid,则可能会发现此解决方案无法单独使用。换句话说,调用flush函数时,与客户端的连接不会关闭。

可能要怪mod_fcgidFcgidOutputBufferSize配置参数。我在以下地方找到了这个技巧:

  1. Travers Carter的回复
  2. Seumas Mackinnon的这篇博客文章

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

FcgidOutputBufferSize 0

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

在上面的(1)中,提到了一个名为“ OutputBufferSize”的变量。这是FcgidOutputBufferSize(2)中提到的旧名称(有关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
  • 阿帕奇2.4.9
  • PHP 5.5.19 VC11,x86,非线程安全
  • mod_fcgid / 2.3.9
  • Windows 7专业版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

1

这对我有用

//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);

0

好的,所以基本上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更改为在主脚本设置中定义的回调方法,以接收数据并对其执行操作。希望这可以帮助。干杯,摩根。


0

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

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


0

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

output_buffering = Off  
zlib.output_compression = Off  

0

最新的工作解决方案

    // 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

0

在尝试了该线程的许多不同解决方案(没有一个对我有用)之后,我在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();
    }
}
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.