如果客户端断开连接,则停止执行 PHP 脚本

Posted

技术标签:

【中文标题】如果客户端断开连接,则停止执行 PHP 脚本【英文标题】:Stop PHP script from executing if client disconnects 【发布时间】:2014-10-11 09:15:46 【问题描述】:

我遇到了以下问题。

程序中的客户端向服务器发送 HTTPS 操作。服务器获取所需的操作和所有参数。但是,当被调用的 php 脚本正在运行时,客户端会断开连接。

脚本回显响应并且客户端没有得到它不应该得到的任何东西。因为这是重要的操作,所以如果这种情况发生,客户端应该总是得到我想要停止 PHP 脚本执行的响应。

整个脚本都在 mysql 事务中,所以如果它死了,数据库回滚就会发生,这是必不可少的。

我把这段代码放在了脚本的最后

ob_end_clean();

header("Connection: close");

ignore_user_abort();

ob_start();

echo $response->asXML();

$size = ob_get_length();

header("Content-Length: $size");

ob_end_flush();

flush();

if ( connection_aborted() )
        log('Connection aborted');
else
        log('Connection is fine');

但 connection_aborted 始终返回 0(正常连接),因此脚本始终执行。 connection_status 也没有帮助,所以我不知道还能尝试什么。

编辑:

我发现这仅在使用 AJAX 请求调用脚本时才有效。当使用常规 HTTP 调用它时,它显示连接已死。但是当我用new XMLHttpRequest() 调用它时,它永远不会发现连接已关闭。

它是异步请求。

所以我设法缩小范围。但我仍然不知道如何解决它。

【问题讨论】:

php.net/manual/en/function.connection-aborted.php#111167 @IssamZoli 它仍然不起作用。在我将 ob_flush() 和 flush() 放在 connection_aborted() 之前,我得到 0 作为输出。我也在刷新前回显响应,这对我也没有帮助。 试试if (connection_aborted())而不是if (connection_aborted()==1) @IssamZoli 我已经在这样做 if (connection_aborted() ) 试试这个php.net/manual/en/function.connection-status.php#43273 【参考方案1】:

经过一些实验,您似乎需要对连接进行多次刷新(写入)以让 PHP 检测到连接已中止。但是,您似乎希望将输出写为一大块 XML 数据。我们可以通过对 HTTP 请求使用chunked 传输编码来解决没有更多数据要发送的问题。

ob_implicit_flush(true);
ignore_user_abort(true);

while (ob_get_level()) 
    ob_end_clean();


header('Content-Type: text/xml; charset=utf-8');
header('Transfer-Encoding: chunked');
header('Connection: close');

// ----- Do your MySQL processing here. -----

// Sleep so there's some time to abort the connection. Obviously this wouldn't be
// here in production code, but is here to simulate time to process the request.
sleep(3);

// Put XML content into $xml. If output buffering is used, dump contents to $xml.
//$xml = $response->asXML();
$xml = '<?xml version="1.0"?><test/>';
$size = strlen($xml);

// If connection is aborted before here, connection_status() will not return 0.

echo dechex($size), "\r\n", $xml, "\r\n"; // Write content chunk.

echo "0\r\n\r\n"; // Write closing chunk, detecting if connection closed.

if (0 !== connection_status()) 
    error_log('Connection aborted.');
    // Rollback MySQL transaction.
 else 
    error_log('Connection successful.');
    // Commit MySQL transaction.

ob_implicit_flush(true) 告诉 PHP 在每个 echo 语句之后进行刷新。在使用echo 发送每个块后检查连接状态。如果连接在第一条语句之前关闭,connection_status() 应该返回一个非零值。

如果在 php.ini 中打开压缩,您可能需要在脚本顶部使用 ini_set('zlib.output_compression', 0) 来关闭输出压缩,否则它将充当另一个外部缓冲区,connection_status() 可能总是返回 0


编辑:Transfer-Encoding: chunked 标头修改了浏览器期望在 HTTP 响应中接收数据的方式。然后浏览器期望以[Hexadecimal number of bytes in chunk\r\ndata\r\n] 形式接收数据。例如,您可以发送字符串“这是一个示例字符串”。通过回显1a\r\nThis is an example string.\r\n 作为一个块。浏览器只会显示字符串,而不是1a,并保持连接打开,直到收到一个空块,即0\r\n\r\n


编辑:我修改了答案中的代码,使其作为独立脚本运行,而不是依赖于由 OP 代码定义的 $response。也就是说,我用$xml = $response-&gt;asXML(); 注释了该行并用$xml = '&lt;?xml version="1.0"?&gt;&lt;test/&gt;'; 替换它作为概念证明。

【讨论】:

您好,感谢您的重播。我在回显 dechex($size) 和 0 时遇到问题,因为它损坏了 xml 数据,因此客户端报告错误并且无法正确读取数据。 要查看我写的实际工作,请将我的答案中的代码复制并粘贴到一个空的 PHP 脚本中,然后将 $xml = $response-&gt;asXML(); 替换为 $xml = '&lt;?xml version="1.0"?&gt;&lt;test/&gt;';。将其放入您的真实脚本中,它应该可以满足您的需求。 我已经放了标头 header('Transfer-Encoding: chunked');但就像我说的我有 dechex($size) 生成的字符串“c6”,开头的新行也是一个问题,因为 xml 不在实体的开头。 您可能还需要将ini_set('zlib.output_compression', 0); 添加到脚本的顶部,因为这将缓冲输出并可能干扰Transfer-Encoding 标头。您正在使用的其他一些代码也可能正在设置Transfer-Encoding 标头,可能是生成$response 对象的任何内容。你能告诉我正在服务的实际标题是什么吗?另外,您能否尝试使用我的答案中的确切代码来查看是否有效(如果无效,请尝试添加ini_set('zlib.output_compression', 0);【参考方案2】:

我认为你不能依赖 connection_aborted() 函数:有一些用户报告(https://***.com/a/23530884/3908097,Alternative to PHP Function connection_aborted()?)它不可靠。还要考虑发生网络超时的情况。

我建议另一个想法,我认为它更可靠。很简单:向客户端发送 XML 数据,然后等待一段时间 确认:客户端在获取数据时发送另一个带有确认的 HTTP 请求。 确认后,您可以提交交易。如果超时 - 回滚。

这个想法只需要对服务器的 php 和客户端的代码进行微小的修改。

修改了你的php代码(test1.php):

ob_end_clean();
header("Connection: close");
ignore_user_abort(true);
ob_start();

echo $response->asXML();

$size = ob_get_length();
header("Content-Length: $size");

// *** added lines ***
$serial = rand();
$memcache_obj = memcache_connect('localhost', 11211);
$memcache_obj->set("test1.$serial", 0);
header("X-test1-serial: $serial");
// ***

ob_end_flush();
flush();

// *** changed code ***
$ok = 0;
$tmout = 10*1000; // timeout in milliseconds
// wait until confirmation flag changes to 1 or timeout occurs
while (!($ok = intval($memcache_obj->get("test1.$serial"))))

    if (connection_aborted()) break; // then no wait
    if (($tmout = $tmout - 10) <= 0) break;
    usleep(10000); // wait 10 milliseconds

$memcache_obj->delete("test1.$serial");

if ($ok) 
   error_log('User informed');
   // commit transaction
 else 
   error_log('Timeout');
   // rollback transaction

处理客户端确认的代码(user_ack.php):

<?php
header("Content-type: text/plain; charset=utf-8");
$serial = intval($_GET['serial']);

$memcache_obj = memcache_connect('localhost', 11211);
$memcache_obj->replace("test1.$serial", 1);
echo $serial;

最后是客户端的代码(test1.html)进行测试:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Test connection</title>
<script type="text/javascript">

function user_ack(serial) 
    var httpReq = new XMLHttpRequest();
    httpReq.open("GET", "user_ack.php?serial=" + encodeURIComponent(serial), true);
    httpReq.onreadystatechange = function () 
        if (httpReq.readyState == 4) 
            if (httpReq.status == 200) 
                // confirmation successful
                document.getElementById("ack").textContent = httpReq.responseText;
            
        
    
    httpReq.send("");


function get_xml() 
    var httpReq = new XMLHttpRequest();
    httpReq.open("POST", "test1.php", true);
    httpReq.onreadystatechange = function () 
        if (httpReq.readyState == 4) 
            if (httpReq.status == 200) 
                // send confirmation
                var serial = httpReq.getResponseHeader("X-test1-serial");
                user_ack(serial);

                // ... process XML ...
                document.getElementById("xml").textContent = httpReq.responseText;
            
        
    
    httpReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    httpReq.send("id=abc"); // parameters to test1.php


</script>
</head>
<body>
<p>XML from server:</p>
<div id="xml"></div>

<p>Confirmation serial: <span id="ack"></span></p>
<p><input type="button" value="XML download test" onclick="get_xml();"/></p>
</body>
</html>

我使用 memcached 来存储 test1.php 和 user_ack.php 之间通信的确认标志值。这个值的名称对于每个连接必须是唯一的,所以我使用$serial = rand(); 来生成名称:"test1.$serial"。创建时确认标志的值为 0。确认后user_ack.php 将其值更改为 1。

因为user_ack.php 必须知道标志的值名称,所以客户端发送带有$serial 值的确认。 $serial 的值在自定义 HTTP 标头中从服务器获取:X-test1-serial 获取 XML 时。

为了简单起见,我使用内存缓存。 MySQL 表也可以用于此目的。

【讨论】:

【参考方案3】:

只有当 PHP 尝试向浏览器发送一些输出时,它才能检测到连接被中止。尝试输出一些空白字符,flush() 输出,然后测试connection_status()。您可能必须输出足够的字符才能让缓冲区通过,在我的情况下 64 kb 有效。

ob_end_flush();
flush();
sleep(10);
ignore_user_abort(true);
echo str_repeat(' ', 1024*64);
flush();

if ( connection_status() != 0 )
    die();

注意我添加了ignore_user_abort(true),没有它,执行将在脚本输出处结束,即echoflush()

【讨论】:

还是不行。在我将 ob_flush() 和 flush() 放在 connection_status 和 connection_aborted() 之前,我得到 0 作为两个函数的输出。我也在刷新前回显响应,这对我也没有帮助。 Apache 可能有自己的缓冲区。我刚刚进行了实验性测试,我会更新我的答案。 我使用的是 lighttpd 而不是 apache,但我也会对此进行测试并告诉你 您是否尝试将输出从 64kb 增加到更多?

以上是关于如果客户端断开连接,则停止执行 PHP 脚本的主要内容,如果未能解决你的问题,请参考以下文章

NettyIO 断开客户端与服务器的连接

PHP 函数 ignore_user_abort()

PHP 如果断开连接则重新连接的mysql连接

如何断开wincc与数据库的连接

如果网络连接断开,则重新启动 systemd 服务

source端在hdmi断开连接多久会停止连接