【问题标题】:How do I close a connection early?如何提前关闭连接?
【发布时间】:2010-09-13 09:28:35
【问题描述】:

我正在尝试进行 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() 刷新了输出缓冲区,但它不起作用?

标签: php jquery ajax


【解决方案1】:

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

据推测,它需要的不仅仅是发送关闭标头。


OP 然后确认:是的,这成功了:pointing to user-note #71172 (Nov 2006) 复制到这里:

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

sts at mail dot xubion dot hu 贴出原解决方案:

<?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 月,在 related answer Arctic Fire 中,然后将另外两个用户注释链接到上述注释:

【讨论】:

  • 作者和@Timbo White,是否可以在不知道内容大小的情况下提前关闭连接? IE,无需在关闭前捕获内容。
  • 黑客和蹩脚的网络浏览器仍然可以忽略连接关闭 HTTP 标头,并获得其余的输出......确保接下来的内容不敏感。也许是 ob_start();压制一切:p
  • 添加 fastcgi_finish_request();已经说在上述不起作用的情况下成功关闭了连接。但是,就我而言,它阻止了我的脚本继续执行,因此请谨慎使用。
  • @RichardSmith 因为Connection: close 标头可以被堆栈中的其他软件覆盖,例如,CGI 情况下的反向代理(我观察到 nginx 的这种行为)。请参阅@hanshenrik 对此的回答。通常,Connection: close 在客户端执行,不应被视为此问题的答案。连接应该从 server 端关闭。
【解决方案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 ********/

另外,如果您的网络服务器对输出使用自动 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 谢谢!我暂时无法让它工作,但禁用压缩就可以了。
  • ob_flush() 不是必需的,实际上会导致通知failed to flush buffer。我把它拿出来了,效果很好。
  • 我发现ob_flush()这一行必要的。
  • 除非您使用多个输出缓冲区,否则没有必要。
【解决方案3】:

您可以使用 Fast-CGI 和 PHP-FPM 来使用 fastcgi_end_request() function。这样,您可以在响应已经发送到客户端的情况下继续进行一些处理。

您可以在此处的 PHP 手册中找到此内容:FastCGI Process Manager (FPM);但是该功能没有在手册中进一步记录。这里是PHP-FPM: PHP FastCGI Process Manager Wiki的摘录:


fastcgi_finish_request()

作用域:php函数

类别:优化

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

fastcgi_finish_request()可以调用正在执行的关闭函数。


注意:fastcgi_finish_request() 具有 a quirk,其中对 flushprintecho 的调用将提前终止脚本。

为避免该问题,您可以在fastcgi_finish_request 调用之前或之后调用ignore_user_abort(true)

ignore_user_abort(true);
fastcgi_finish_request();

【讨论】:

  • 这是实际答案!
  • 如果您使用 php-fpm - 只需使用此功能 - 忘记标题和其他所有内容。为我节省了很多时间!
【解决方案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();

【讨论】:

  • 在何种意义上完成?哪个问题需要您完成已接受的答案脚本(哪一个?)以及您的哪些配置差异需要这样做?
  • 这一行:header("Content-Encoding: none"); -> 非常重要。
  • 谢谢,这是此页面上唯一可行的解​​决方案。这应该被批准为答案。
【解决方案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 execute a background process

【讨论】:

  • 取决于你想做什么。如果它正在将一些在当前请求期间确定的数据写入缓存,那么分叉几乎肯定不是更好的解决方案。即使在 OP 的情况下,如果其他解决方案是可靠的,与将大量数据传递给分叉进程相比,它们很可能会节省一些服务器 CPU 周期。
【解决方案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

注意&amp;。 ping 命令将在后台运行,而当前进程继续执行 echo 命令。 它将 ping www.google.com 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 时,您应该很快得到以下响应,而无需等待 ping 命令完成需要 15 秒。

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 &

希望这会有所帮助!

【讨论】:

    【解决方案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 天来尝试解决这个问题。它适用于我的本地开发人员,但不适用于主机。我被冲洗了。你救了我。谢谢!!!!
    【解决方案8】:

    我在共享主机上,fastcgi_finish_request 已设置为完全退出脚本。我也不喜欢connection: close 解决方案。使用它会强制为后续请求建立单独的连接,从而消耗额外的服务器资源。我阅读了Transfer-Encoding: cunked Wikipedia Article 并了解到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。我似乎记得有些服务器在达到最小值之前不会刷新。
    • @skibulk 有些需要 64K 的(未压缩??)填充,例如这是FcgidOutputBufferSize 的默认值,在某些共享服务器上无法覆盖。
    • 谢谢!!!!在浪费了数小时尝试其他所有方法之后,这是在共享主机上对我有用的唯一解决方案。
    【解决方案9】:

    你可以尝试做多线程。

    您可以编写一个脚本来进行系统调用(使用shell_exec),该脚本使用脚本调用php 二进制文件作为参数来完成您的工作。但我认为这不是最安全的方式。也许你可以通过 chrooting php 进程和其他东西来加强东西

    另外,phpclasses 中有一个类可以做到这一点http://www.phpclasses.org/browse/package/3953.html。但是具体的实现我不知道

    【讨论】:

    • 如果您不想等待进程完成,请使用&amp; 字符在后台运行进程。
    【解决方案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 中进行一些并行编程来解决。几周前我在这里问了一个问题:How can one use multi threading in PHP applications

      并得到了很好的答案。我特别喜欢一个。作者提供了一个参考to the Easy Parallel Processing in PHP (Sep 2008; by johnlim) tutorial,它实际上可以很好地解决你的问题,因为我已经用它来处理几天前出现的类似问题。

      【讨论】:

        【解决方案12】:

        Joeri Sebrechts' answer 已关闭,但它会破坏任何在您希望断开连接之前可能被缓冲的现有内容。它没有正确调用ignore_user_abort,导致脚本提前终止。 diyism's answer 很好,但并不普遍适用。例如。一个人可能有更多或更少的输出缓冲区,该答案无法处理,因此它可能根本不适用于您的情况,您不知道为什么。

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

        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 用户注意事项(请自行承担使用风险)。

          快速解决方案

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

          mod_fcgidFcgidOutputBufferSize 配置参数可能是罪魁祸首。我在以下位置找到了这个提示:

          1. this reply of Travers Carter
          2. this blog post of Seumas Mackinnon

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

          FcgidOutputBufferSize 0
          

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

          在上面的(1)中,提到了一个名为“OutputBufferSize”的变量。这是(2)中提到的FcgidOutputBufferSize的旧名称(见upgrade notes in the Apache web page for mod_fcgid)。

          详细信息和第二个解决方案

          上述解决方案禁用 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 page for the corresponding directive 中所述。因此,如果您的环境中设置了另一个值,您可能需要相应地调整此值。

          我的环境

          • WampServer 2.5
          • Apache 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 对我有用的唯一解决方案。
          【解决方案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() 等对 web 服务器运行的 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();
                        }
                    }
                    

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2021-04-03
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多