PHP Conference Japan 2024

连接处理

在 PHP 内部,维护着连接状态。有 4 种可能的状态

  • 0 - NORMAL
  • 1 - ABORTED
  • 2 - TIMEOUT
  • 3 - ABORTED 和 TIMEOUT

当 PHP 脚本正常运行时,NORMAL 状态处于活动状态。如果远程客户端断开连接,则 ABORTED 状态标志将被打开。远程客户端断开连接通常是由用户点击其停止按钮引起的。如果达到 PHP 强制执行的时间限制(请参阅 set_time_limit()),则 TIMEOUT 状态标志将被打开。

您可以决定是否希望客户端断开连接导致脚本中止。有时,即使没有远程浏览器接收输出,也需要始终让脚本运行到完成。但是,默认行为是在远程客户端断开连接时中止脚本。此行为可以通过 ignore_user_abort php.ini 指令以及相应的 php_value ignore_user_abort Apache httpd.conf 指令或 ignore_user_abort() 函数进行设置。如果您没有告诉 PHP 忽略用户中止,并且用户中止了,则您的脚本将终止。唯一的例外是,如果您使用 register_shutdown_function() 注册了关闭函数。使用关闭函数时,当远程用户点击其停止按钮时,您的脚本下次尝试输出内容时,PHP 将检测到连接已被中止,并且会调用关闭函数。此关闭函数也会在脚本正常终止结束时被调用,因此,为了在客户端断开连接时执行不同的操作,您可以使用 connection_aborted() 函数。如果连接被中止,此函数将返回 true

您的脚本也可能被内置的脚本计时器终止。默认超时时间为 30 秒。可以使用 max_execution_time php.ini 指令或相应的 php_value max_execution_time Apache httpd.conf 指令以及 set_time_limit() 函数更改它。当计时器到期时,脚本将被中止,并且与上述客户端断开连接的情况一样,如果已注册关闭函数,则将调用它。在此关闭函数中,您可以通过调用 connection_status() 函数来检查是否是由超时导致调用关闭函数的。如果超时导致调用关闭函数,此函数将返回 2。

需要注意的是,ABORTED 和 TIMEOUT 状态可以同时处于活动状态。如果您告诉 PHP 忽略用户中止,则可能发生这种情况。PHP 仍然会注意到用户可能已断开连接的事实,但脚本将继续运行。如果随后达到时间限制,它将被中止,并且您的关闭函数(如果有)将被调用。此时,您会发现 connection_status() 返回 3。

添加注释

用户贡献的注释 11 条注释

tom lgold2003 at gmail dot com
15 年前
嗨,感谢 arr1,这对我很实用,当需要快速返回给用户然后执行其他操作时。

在使用代码时,它几乎让我抓狂,我发现另一件事可能会影响代码

Content-Encoding: gzip

这是因为 zlib 已开启,内容将被压缩。但这不会输出缓冲区,直到所有输出都结束。

因此,可能需要发送标头以防止此问题。

现在,代码变成

<?php
ob_end_clean
();
header("Connection: close\r\n");
header("Content-Encoding: none\r\n");
ignore_user_abort(true); // 可选
ob_start();
echo (
'用户将看到的文本');
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush(); // 奇怪的行为,不起作用
flush(); // 除非两者都被调用!
ob_end_clean();

// 在这里进行处理
sleep(5);

echo(
'用户永远不会看到的文本');
// 执行一些处理
?>
arr1 at hotmail dot co dot uk
18 年前
在保持 PHP 脚本运行的同时关闭用户的浏览器连接一直是 4.1 以来存在的问题,当时 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("在后台执行某些操作");
?>

这可以正常工作,直到您将 phpinfo() 替换为
echo ('我想让用户看到的文本'); 在这种情况下,标头永远不会发送!

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

示例

<?php
ob_end_clean
();
header("Connection: close");
ignore_user_abort(); // 可选
ob_start();
echo (
'用户将看到的文本');
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush(); // 奇怪的行为,不起作用
flush(); // 除非两者都调用!
// 在这里进行处理
sleep(30);
echo(
'用户永远不会看到的文本');
?>

花了3个小时才弄明白,希望对其他人有所帮助 :)

已在以下浏览器中测试
IE 7.5730.11
Mozilla Firefox 1.81
Lee
20年前
最后一条评论中提到的要点并不总是正确的。

如果用户的连接在订单处理脚本(例如确认用户的信用卡/将他们添加到数据库等)的过程中断开(由于他们的ISP断网、网络故障……等等),并且您的脚本尝试发送回输出(例如,“正在预处理订单”或任何其他类型的确认),那么您的脚本将中止——这可能会导致您的处理过程出现问题。

我有一个订单脚本,它将数据添加到InnoDB数据库(通过MySQL),并且只有在成功完成时才提交事务。如果没有使用ignore_user_abort(),我曾经遇到过用户在处理阶段连接断开的情况……他们的卡被扣款了,但他们没有添加到我的本地数据库中。

因此,如果您正在处理敏感的事务,无论用户是否在另一端“观看”,忽略任何中止都是安全的。
mheumann at comciencia dot cl
11年前
我在使重定向工作方面遇到了很多问题,之后我的脚本应该在后台继续工作。重定向到我网站上的另一个页面只会在我完成原始页面的处理后才能工作。

我终于发现了问题所在
会话只有在脚本的最后才会被PHP关闭,并且由于对会话数据的访问被锁定以防止多个页面同时写入它,因此新页面必须等到原始处理完成后才能加载。

解决方案
使用session_write_close()在重定向时手动关闭会话

<?php
ignore_user_abort
(true);
set_time_limit(0);

$strURL = "在此处放置您的重定向URL";
header("Location: $strURL", true);
header("Connection: close", true);
header("Content-Encoding: none\r\n");
header("Content-Length: 0", true);

flush();
ob_flush();

session_write_close();

// 继续处理...

sleep(100);
exit;
?>

但请注意
确保您的脚本在session_write_close()之后(即在您的后台处理代码中)不会写入会话。那将不起作用。另外,避免读取,请记住,下一个脚本可能已经修改了数据。

因此,请尝试在重定向之前读取您需要的数据。
Marco
7年前
由于某种原因,此处未列出的CONNECTION_XXX常量为

0 = CONNECTION_NORMAL
1 = CONNECTION_ABORTED
2 = CONNECTION_TIMEOUT
3 = CONNECTION_ABORTED & CONNECTION_TIMEOUT

数字3实际上是这样测试的
if (CONNECTION_ABORTED & CONNECTION_TIMEOUT)
echo '连接已中止并超时';
a1n2ton at gmail dot com
14年前
PHP在连接中止时更改目录,因此以下代码将无法按预期工作

<?php
function abort()
{
if(
connection_aborted())
unlink('file.ini');
}
register_shutdown_function('abort');
?>

实际上,它会删除apache的根目录中的文件,因此如果您想在中止时取消链接脚本目录中的文件或写入文件,则必须存储目录
<?php
function abort()
{
global
$dsd;
if(
connection_aborted())
unlink($dsd.'/file.ini');
}
register_shutdown_function('abort');
$dsd=getcwd();
?>
Ilya Penyaev
12年前
当尝试使我的脚本将客户端重定向到另一个URL然后继续处理时,我陷入了困境。原因是php-fpm。所有可能的缓冲区刷新都不起作用,除非我调用fastcgi_finish_request();

例如

<?php
// 重定向...
ignore_user_abort(true);
header("Location: ".$redirectUrl, true);
header("Connection: close", true);
header("Content-Length: 0", true);
ob_end_flush();
flush();
fastcgi_finish_request(); // 使用php-fpm时很重要!

sleep (5); // 用户不会感觉到这个睡眠,因为他已经离开了

// 在用户重定向后执行一些工作
?>
匿名用户
12年前
此简单函数输出一个字符串并关闭连接。它考虑使用“ob_gzhandler”进行压缩

我花了一段时间才将所有这些整合在一起,主要是因为像某些人在这里提到的那样将编码设置为none不起作用。

<?php
function outputStringAndCloseConnection2($stringToOutput)
{
set_time_limit(0);
ignore_user_abort(true);
// 缓冲所有即将输出的内容 - 确保我们关心压缩:
if(!ob_start("ob_gzhandler"))
ob_start();
echo
$stringToOutput;
// 获取输出的大小
$size = ob_get_length();
// 发送标头以告诉浏览器关闭连接
header("Content-Length: $size");
header('Connection: close');
// 刷新所有输出
ob_end_flush();
ob_flush();
flush();
// 关闭当前会话
if (session_id()) session_write_close();
}
?>
匿名用户
17年前
关于来自以下用户的帖子
arr1 at hotmail dot co dot uk

如果您使用/编写会话,则需要在此之前执行此操作
(否则它不起作用)

session_write_close();

如果需要

ignore_user_abort(TRUE);
而不是ignore_user_abort();
pulstar at mail dot com
21年前
这些函数非常有用,例如,如果您需要控制网站访问者何时下单,并且需要检查他们是否没有两次点击提交按钮或在点击提交按钮后立即取消提交。
如果您的访问者在提交后点击停止按钮,您的脚本可能会在注册产品的过程中停止,并且不会完成列表,从而导致数据库中出现不一致。
使用ignore_user_abort()函数,您可以使您的脚本顺利完成所有操作,然后您可以使用register_shutdown_function()和connection_aborted()检查访问者是否取消了提交或丢失了连接。如果他们确实取消了,您可以将订单设置为未确认,当访问者返回时,您可以再次显示旧订单。
为了防止双击提交按钮,您可以使用javascript禁用它,或者在您的脚本中,您可以为该订单设置一个标志,该标志将被记录到数据库中。在接受新提交之前,脚本将检查是否之前没有放置过相同的订单,并拒绝它。这将正常工作,因为脚本在之前已经完成了工作。
请注意,如果您在脚本开头使用 ob_start("callback_function"),您可以指定一个回调函数,该函数在脚本结束时充当关闭函数的作用,并且还可以让您在将生成的页面发送给访问者之前对其进行处理。
Jean Charles MAMMANA
16 年前
connection_status() 仅当客户端正常断开连接(使用停止按钮)时才会返回 ABORTED 状态。在这种情况下,浏览器会发送 RST TCP 数据包通知 PHP 连接已关闭。
但是……如果连接因网络故障(例如 wifi 链接断开)而停止,脚本将无法知道客户端已断开连接 :(

我尝试使用 fopen("php://output") 和 stream_select() 对写入进行检测以检测写锁定(由于缓冲区已满),但 php 给了我这个错误:“无法将类型为 Output 的流表示为可 select() 的描述符”

所以我不知道如何正确检测网络故障连接……
To Top