PHP关闭连接早期。如果输出任何内容,则脚本将挂起

7
我将尽早关闭与客户端的连接,代码如下:

static public function early_close( $output )
{
   ignore_user_abort(true);
   echo $output;

   // Disable gzip compression in apache, as it can result in this request being buffered until it is complete,
   // regardless of other settings.
   if (function_exists('apache_setenv')) {
       apache_setenv('no-gzip', 1);
   }

    // 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');
    header("Content-Encoding: none"); // To disable Apache compressing anything

    // IF PHP-FM
    // fastcgi_finish_request();

    // flush all output
    if( ob_get_level() > 0 )
    {
        ob_end_flush();
        ob_get_level()? ob_flush():null;
        flush();
    }

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

功能正常,但是在这个事件之后,如果某些脚本输出任何内容(无论是通过echo还是添加新的头),则脚本将从那一点停止执行。
我尝试在早期关闭后启动输出缓冲区,然后丢弃它,但它不起作用:

Server::early_close();
ob_start();
heavy_work();
ob_clean();

有什么想法吗?
使用php 5.3.x


一个简单的 die() 是被禁止的吗? - Anthony
在这里使用die()是没有帮助的,因为要求是一旦服务器接收到执行heavy_work的指令就立即回复客户端。客户端不需要等待heavy_work结束。如果将Server::early_close()替换为die(),则可以避免执行heavy_work() - Pherrymason
1
我忍不住要问,为什么你在 heavy_work() 中所做的任何事情都会输出或修改头信息?请求的响应已经在 flush() 时发送,因此您无法响应任何端点? - tomahaug
你为什么要尝试提前关闭连接?你想在不影响用户的情况下执行繁重的后台任务吗? - Toto
@tomahaug 我不打算从那一点输出任何东西。你说的是对的,但有时我会意外地留下一个 echo,有时 php 会抛出输出字符串的错误,如果我使用某些库输出一些东西...... 我知道我可以通过删除所有的 echo 和禁用错误输出来避免所有这些问题,但我想知道为什么我的脚本停止了,以及是否可以“捕获”在重负载工作中完成的任何输出,以便不会停止进程执行。 - Pherrymason
显示剩余2条评论
4个回答

2
经典的代码如下所示:

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 !

// Do processing here
sleep(30);

echo('Text user will never see');

否则,如果您想进行异步调用,我建议阅读以下内容:PHP中异步进程的方法

我正确地关闭了连接,但如果我的繁重工作中有任何输出,脚本就会停止执行,我想知道为什么以及是否可以避免这种情况。 - Pherrymason
你确定这是关于输出的问题吗?难道没有超时/内存限制等等吗? - Toto
是的,当我在“繁重的工作”中放置一个回声时,它不会结束执行,它只会停在执行回声的同一行。当这个回声被注释掉时,脚本成功结束。 - Pherrymason
如果您测试以上确切内容,它是否有效?如果是,请重新开始编写您的代码,并逐步添加(每次进行测试),直到它“中断”。 :) - Toto
1
您的代码似乎对我无效,我不得不在开头添加 ob_start。一旦我这样做了,我测试了一下,代码到达了结尾,所以我猜我在代码中有其他引起问题的东西。因为您让我意识到php正确处理了这个问题,所以我给您发放奖金。非常感谢。 - Pherrymason
显示剩余2条评论

1

我认为你不应该采用这种方式。为了提高可用性,Http请求应尽可能简短。

如果需要进行一些“重型处理”,可以使用某种类型的队列进行“调度”。服务器上的一个单独的进程/守护程序可以从队列中获取这些作业并执行它们。然后,http应用程序可以检查是否仍有等待处理/已启动/完成的作业。

有许多库可用于实现此目的:Gearman, ØMQ, RabbitMQ 等。

Http请求并不适合长时间操作,这就是为什么在尝试这样做时会遇到各种问题的原因 :)

更新

如果无法在服务器上使用库(如Gearman等),可以构建自己的基于文件或数据库的队列,从应用程序中将“命令”推入队列,并使用cronjob读取该队列并执行这些任务。


这就是为什么我会尽快关闭连接的原因。 - Pherrymason
1
你依赖于进程持续运行以执行所需操作,但你无法保证这将成功,也无法向用户报告失败。 - Jasper N. Brouwer
使用后台任务解决方案(即使是自己实现的)来执行+1操作比依赖于孤立的HTTP请求更可靠,更稳定。 - AD7six
我同意这不是一个干净的解决方案(我计划使用一个库/开发自己的库来更好地处理这个问题),我只是好奇一个简单的回声怎么能让我的程序执行停顿。 - Pherrymason
你考虑过不走这条路吗?你需要做你需要做的事情。有时候,仅仅假设脚本会按时运行是不可能的。 - asiby
我收回之前的说法。它在MAMP(我的本地开发环境)上根本无法工作。我现在正在调查原因。可能是因为客户端和服务器都在同一个IP(localhost)上。 - asiby

1
在 echo $output; 后面需要添加 echo chr(0);。发送一个空字节会强制浏览器断开连接。另外,我假设 Server::early_close() 之前有 ob_start() 吗?如果没有,你需要它才能使 ob_get_length 正常工作。

是的,有一个 ob_start() 调用。 - Pherrymason

1
根据您迄今为止的评论,我建议用AJAX请求替换您当前的解决方案。不要提前关闭连接并在服务器上继续处理,而是按照通常方式提供响应,并添加一个AJAX请求以在客户端加载页面后进行任何其他处理。这将完全消除多余输出的问题,您还可以向用户发送任何成功/失败消息。
另一个解决方案是将您的工作排队到表格或内存中,并设置定时任务在后台处理。

这个脚本已经通过ajax调用请求了。我发现如果没有提前关闭,请求会超时。 - Pherrymason
哦,那样的话你可以延长超时时间而不是早期关闭连接。虽然如果你有长时间运行的后台进程,我强烈建议使用 cron(同样具有长超时时间)。 - Matt S

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接