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

时间:2014-08-18 12:45:15

标签: php mysql ajax https connection

我遇到了以下问题。

程序客户端向服务器发送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(NORMAL CONNECTION),所以脚本总是执行。 connection_status也没有帮助,所以我不知道还有什么可以尝试。

编辑:

我发现只有在使用AJAX请求调用脚本时,这才会起作用。当使用常规HTTP调用它时,它表明连接已经死亡。但是,当我用new XMLHttpRequest()调用它时,它从未发现连接已关闭。

异步请求。

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

3 个答案:

答案 0 :(得分:4)

经过一些实验,您似乎需要对连接进行多次刷新(写入),以便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\n{data}\r\n]形式的块的形式接收数据。例如,您可以发送字符串“This is a example string”。通过回显1a\r\nThis is an example string.\r\n作为一个块。浏览器只显示字符串,而不是1a,并保持连接打开等待,直到收到空块,即0\r\n\r\n


编辑:我修改了我的答案中的代码,使其作为独立脚本运行,而不是依赖于OP代码定义的$response。也就是说,我使用$xml = $response->asXML();对该行进行了评论,并将其替换为$xml = '<?xml version="1.0"?><test/>';作为概念验证。

答案 1 :(得分:1)

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

答案 2 :(得分:1)

我认为你不能依赖connection_aborted()函数:有一些用户报告(https://stackoverflow.com/a/23530884/3908097Alternative 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标头中获取表单服务器:获取XML时为X-test1-serial

为简单起见,我使用memcache。 MySQL表也可以用于此目的。