我有以下问题:
客户端从程序发送一个HTTPS操作到服务器。服务器获取所需的操作和所有参数。但随后客户端断开连接,而PHP脚本被称为是工作。
脚本回显响应,客户端没有得到任何它不应该得到的东西。因为它是重要的行动,客户端应该总是得到响应,我想阻止PHP脚本执行,如果这种情况发生。
整个脚本是在MySQL事务中,所以如果它死了,db回滚将发生,这是必要的。
我把这段代码放在脚本
的最后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()
调用它时,它永远不会发现连接是关闭的。
它是异步请求。
所以我设法缩小了范围。但我还是不知道该怎么修
经过一些实验,似乎您需要对连接进行多次刷新(写),以使PHP检测到连接已终止。然而,您似乎希望将输出写成一大块XML数据。我们可以通过使用chunked
传输编码为HTTP请求绕过没有更多数据要发送的问题。
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
语句之后进行flush。在echo
发送每个chunk后检查连接状态。如果连接在第一个语句之前关闭,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]
的形式接收数据块。因此,例如,您可以通过回显1a'r'nThis is an example string.'r'n
将字符串"This is an example string."作为块发送。浏览器将只显示字符串,而不显示1a
,并保持连接打开,直到它接收到一个空块,即0'r'n'r'n
。
编辑:我修改了我的答案中的代码,使其作为一个独立的脚本工作,而不是依赖于$response
被OP的代码定义。也就是说,我用$xml = $response->asXML();
注释了这行,并将其替换为$xml = '<?xml version="1.0"?><test/>';
作为概念证明。
我认为你不能依赖connection_aborted()函数:有一些用户报告(https://stackoverflow.com/a/23530884/3908097,替代PHP函数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时。
我使用memcache是为了简单。也可以使用MySQL表。
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)
,如果没有它,执行将在脚本输出处结束,即echo
或flush()
。