PHP事件源持续执行

3

我开始使用JavaScript EventSource对象在HTML5中使用推送技术。我对PHP中的工作解决方案非常满意:

$time = 0;
while(true) {
    if(connection_status() != CONNECTION_NORMAL) {
        mysql_close()
        break;
    }
    $result = mysql_query("SELECT `id` FROM `table` WHERE UNIX_TIMESTAMP(`lastUpdate`) > '".$time."'");
    while($row = mysql_fetch_array($result)) {
        echo "data:".$row["id"].PHP_EOL;
        echo PHP_EOL;
        ob_flush();
        flush();
    }
    $time = time();
    sleep(1);
}

突然间,我的WebApp无法访问了,并出现了一个MySQL错误:“连接过多”。

事实证明,在JavaScript中关闭事件源后,MySQL连接没有关闭:

window.onload = function() {
    sse = new EventSource("push.php");
    sse.onmessage = function(event) {
        console.log(event.data.split(":")[1]);
    }
}
window.onbeforeunload = function() {
    sse.close();
}

我猜测PHP脚本没有停止执行。在客户端断开连接之前,有没有办法调用函数(比如die();)?为什么在调用.close();关闭EventSource后我的脚本没有终止?!

谢谢您的帮助!—


似乎每个客户端会话都会启动一个新的服务器端无限循环,带有自己的mysql连接,是吗?我在您发布的示例中没有看到连接开始,所以我不确定。无论如何,“太多连接”错误就是这个原因。服务端服务用于提供数据的可以使用(共享)单个数据库连接(和单个无限循环)。我没有将其发布为答案,因为我手头没有示例。几年来一直没有使用PHP。尽管我在php.net上看到mysql_query有点过时了。他们建议改用MySQLi或PDO_MySQL。 - topr
有点像。每个客户端会话都有自己的mysql连接。连接在无限循环之前启动。真正的问题是,调用EventSource.close()并不会关闭或中止连接本身! - Julian F. Weinert
据我所知,通过push.php提供的内容对于每个客户端都是相同的,不是吗?那么,为什么要针对每个客户端会话连接到数据库并查询,而不是在所有客户端会话之间共享相同的SQL结果呢? 尽管这是更优化的解决方案,但它也可以解决您的问题。 - topr
听起来我应该试一下... 我需要我的脚本在另一个客户端更改数据库时将信息推送到客户端。如何在不为每个客户端建立数据库连接的情况下向所有客户端共享结果?!我需要立即推送信息。 - Julian F. Weinert
我没有进一步的研究,所以无法给出确切的答案。我多年前使用过PHP。但是尝试搜索像“应用程序范围”,“有状态服务”和“定时作业”之类的东西,并添加关键字PHP。也许你可以组合出一些东西。 - topr
6个回答

7
首先,当你将代码放在一个巨大的while(true)循环中时,你的脚本永远不会终止。当你的脚本“执行完毕”时,与数据库的连接将被关闭,但由于你编写了一个死锁,这是不可能发生的...永远不会发生。
这不是一个EventSource的问题,它只是按照它应该做的去做了。真正、可悲的是这是你的错。
问题在于:用户连接到你的网站,EventSource对象被实例化。与服务器的连接建立,并请求返回push.php的返回值。你的服务器按要求运行脚本,这个脚本-再次-什么都没有,只是一个死锁。没有错误,所以它可以一直运行,只要php.ini允许它运行。.close()方法会取消输出流(或者说应该这样),但是你的脚本太忙了,要么执行无限循环,要么休眠。假设脚本因为客户端断开连接而停止,就像假设任何客户端都能干扰服务器所做的事情一样。从安全角度来看,这将是最糟糕的情况。现在,回答你真正的问题:是什么导致了这个问题,如何解决?答案是:头文件。
看看这个小的PHP示例:脚本不是通过无限循环在服务器端保持活动状态,而是通过设置正确的头文件来实现的。
只是作为一个旁注:请尽快放弃mysql_*,它已经被弃用(终于),使用PDOmysqli_*代替。说实话,这并不难。

@Julian:嗯,我在我发布的链接中提供了一个PHP示例。因为头文件将被设置为“keep-alive”和“event-stream”头文件,所以连接到您的DB将一直持续到调用.close()方法为止……就是这样。你不需要任何循环巫术,也不需要连续检查。它的行为类似于node.js:从具有侦听器并大部分时间处于空闲状态的服务器发送分块数据……只需复制粘贴链接中的代码示例(标题下的第一个片段_Server Samples_),然后使用它来测试。 - Elias Van Ootegem
2
嗯...那不起作用。自从我开始使用推送功能以来,我一直在使用"text/event-stream"。当我删除while循环时,事件源对象返回“on error”函数并关闭连接。当我没有循环时,脚本完成执行并终止。然后流就关闭了。头似乎并不影响它。调用.close()仍然不会改变连接状态。想象不出为什么... - Julian F. Weinert
@Julian:错误回调中的readyState是什么?是CLOSED吗?如果是这样,请确保您的PHP代码中没有mysql_close$pdo = null;,同时检查此示例的JS源代码,也许可以从中获得一些灵感。 - Elias Van Ootegem
@Julian:我刚意识到一件事情:你在循环内部清空缓存,EventSource 不同于套接字流,你应该要么以一个整块返回所有输出,要么使用 websocket 进行连续数据传输。 - Elias Van Ootegem
2
@EliasVanOotegem,您在链接中引用的示例并没有通过头部信息来保持连接活跃。事实上,它会在三秒钟后断开连接,而客户端会重新打开连接。 - Ulad Kasach
显示剩余5条评论

4

我曾经遇到了完全相同的问题,据我所知,原因是Apache没有终止PHP脚本,直到我再次尝试通过关闭的套接字连接写入数据。

我的数据库没有任何更改,因此没有输出。

为了解决这个问题,我只需在while循环中简单地打印一个事件源注释,例如:

echo ": heartbeat\n\n";
ob_flush();
flush();

如果套接字连接关闭,这将导致脚本被终止。


是的,这个方法可以工作。但相比之下,它会产生“巨大”的网络开销。这并不比不断轮询好多少... - Julian F. Weinert
取决于您发送频率的高低。您不需要在每个循环中都这样做。您可以每隔15分钟左右执行一次...重要的是脚本终止,而不是立即终止。 - Marcel Gwerder
同意,你说得完全正确。但我总是试图把事情做得完美;) 所以你明白为什么这个并不“好吃” :D 但是,谢谢你的建议,这肯定使它可行,即使不完美。我只是认为PHP不是做到完美所需的正确工具。 - Julian F. Weinert
我也喜欢把事情做到完美,我明白你的意思。但是,如果能够像整个应用程序一样使用PHP来完成这个任务,会更容易一些。如果我必须使用其他工具来完成事件源部分,我可能会转向WebSockets。 - Marcel Gwerder

0
正如Elias Van Ootegem所指出的那样,您的脚本从未终止,因此您有一堆MySQL连接处于活动状态。更令人担忧的是,您有一堆资源被使用,以PHP循环的形式运行,而没有尽头!
不幸的是,保持连接的解决方案并不是“headers”。在这个链接由Elias引用的示例中,php脚本终止了,客户端javascript实际上必须重新打开连接。您可以使用以下代码进行测试。
source.addEventListener('open', function(e) {
  // Connection was opened.
    console.log("Opening new connection");
}, false);

如果你在github上跟随他的例子,在实现中,作者实际上使用了一个在X秒后终止的循环。请参见脚本底部的do循环和注释。

解决方案是给你的循环设定寿命。

通过定义循环的限制,例如X次迭代或X时间后触发循环结束或die();,你将使得如果客户端断开连接,脚本保持活动状态的时间有限。如果客户端在X时间后仍然存在,他们将简单地重新连接。


0
$time = 0;
while(true) { 
    if(connection_status() != CONNECTION_NORMAL) {
        mysql_close()
        break;
    }
    $result = mysql_query("SELECT id FROM table WHERE UNIX_TIMESTAMP(lastUpdate) > '".$time."'");
    while($row = mysql_fetch_array($result)) {
        $rect[] = $row;
    }
    for($i-0;$i echo "data:".implode('',$disp)."\n\n"; $time = time(); sleep(1);
}

0

1.

window.onbeforeunload = function() {
    sse.close();
}

这不是必须的,EventSource 将在页面卸载时关闭。

  1. 即使您找不到一种在 PHP 上检测断开连接的方法,您也可以在 N 秒后停止执行,EventSource 将自动重新连接。

http://www.php.net/manual/ru/function.connection-aborted.php 该页面上的评论说,在 connection_status 之前应使用 "flush",无论如何,您应在 N 秒后断开连接,因为您无法检测到 "bad" 断开连接。

3. echo "retry:1000\n"; // 使用此选项告诉 EventSource 重新连接的延迟时间(以毫秒为单位)


谢谢,那是一种解决方法。但似乎也不是正确的方式,因为当 PHP 脚本停止执行时,EventStream 会收到错误信息。 - Julian F. Weinert
而且?出现此错误是可以的,您应该在错误处理程序中检查readyState,如果它为EventSource.CONNECTED-那么ES将尝试重新连接。 - 4esn0k

0

你可能想尝试将这个添加到循环中:

if(connection_status() != CONNECTION_NORMAL)
{
    break;
}

但是 PHP 应该在客户端断开连接时停止。


我原本以为PHP也应该停止。我尝试了你的解决方案,使用if语句,然后是mysql_close();,再然后是break;。但这并没有帮助。当我去MySQL客户端并输入SHOW PROCESSLIST时,我的WebApps用户仍然连接着。每次重新加载页面时,它都会再次连接一次...也许这是JS EventSource.clos();的问题? - Julian F. Weinert
如果使用if(connection_aborted())作为检查条件会更好吗? - Nathan

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