如何在PHP中实现事件监听

8

这是我的问题:我有一个脚本(我们称其为comet.php),它被一个AJAX客户端脚本请求,并等待像这样发生的更改:

while(no_changes){
    usleep(100000);
    //check for changes
}

我不太喜欢这个,它不太可扩展,并且(在我看来)是“坏实践”。我想用信号量或任何并发编程技术来改进这种行为。你能给我一些如何处理它的提示吗?(我知道,这不是一个简短的答案,但一个起点就足够了。)
编辑:LibEvent 怎么样?

你想要实现什么?通常的方法是让客户端定期调用脚本以检查更改。你想在服务器端执行有什么原因吗? - JJJ
4
PHP并不是你应该用于COMET的语言。使用Node.js或其他可以异步工作的语言(例如Python Tornado或Greenlets)。通过在基于线程/进程的Web服务器上运行PHP,你会有很大的开销。 - ThiefMaster
@Juhana,这样做的原因是为了避免定期检查更改并采用反向Ajax解决方案。@thiefMaster我知道有一些COMET服务器解决方案,但我真的认为一个PHP后端是可能的,只要我将我的业务逻辑编写在PHP中,避免代码重复,那么它会更好。您能否请解释一下为什么PHP COMET后端会导致过度负荷? - ArtoAle
我能理解,但是避免定期检查的原因是什么? - JJJ
是的,它会产生很多网络开销,这取决于您检查新更新的频率。 - ArtoAle
5个回答

14
你可以使用 ZeroMQ 解决这个问题。
ZeroMQ 是一个库,为连接(线程、进程甚至是独立的机器)提供了超强的套接字。
我假设您正在尝试将数据从服务器推送到客户端。那么,一个不错的方法就是使用 EventSource API(有可用的 polyfill )。 client.js 通过 EventSource 连接到 stream.php。
var stream = new EventSource('stream.php');

stream.addEventListener('debug', function (event) {
    var data = JSON.parse(event.data);
    console.log([event.type, data]);
});

stream.addEventListener('message', function (event) {
    var data = JSON.parse(event.data);
    console.log([event.type, data]);
});

路由器.php

这是一个长期运行的进程,它监听传入的消息并将它们发送给任何正在监听的人。

<?php

$context = new ZMQContext();

$pull = $context->getSocket(ZMQ::SOCKET_PULL);
$pull->bind("tcp://*:5555");

$pub = $context->getSocket(ZMQ::SOCKET_PUB);
$pub->bind("tcp://*:5556");

while (true) {
    $msg = $pull->recv();
    echo "publishing received message $msg\n";
    $pub->send($msg);
}

stream.php

每个连接到网站的用户都会获得自己的stream.php。这个脚本是长时间运行的并且等待来自路由器的任何消息。一旦它收到新消息,它就会以EventSource格式输出此消息。

<?php

$context = new ZMQContext();

$sock = $context->getSocket(ZMQ::SOCKET_SUB);
$sock->setSockOpt(ZMQ::SOCKOPT_SUBSCRIBE, "");
$sock->connect("tcp://127.0.0.1:5556");

set_time_limit(0);
ini_set('memory_limit', '512M');

header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");

while (true) {
    $msg = $sock->recv();
    $event = json_decode($msg, true);
    if (isset($event['type'])) {
        echo "event: {$event['type']}\n";
    }
    $data = json_encode($event['data']);
    echo "data: $data\n\n";
    ob_flush();
    flush();
}

要向所有用户发送消息,只需将其发送到路由器即可。然后,路由器将把该消息分发给所有正在侦听的流。以下是一个示例:

<?php

$context = new ZMQContext();

$sock = $context->getSocket(ZMQ::SOCKET_PUSH);
$sock->connect("tcp://127.0.0.1:5555");

$msg = json_encode(array('type' => 'debug', 'data' => array('foo', 'bar', 'baz')));
$sock->send($msg);

$msg = json_encode(array('data' => array('foo', 'bar', 'baz')));
$sock->send($msg);

这可以证明实时编程并不需要使用 node.js,PHP 也完全可以胜任。

除此之外,socket.io 是一个非常好的实现方式。而且你可以通过 ZeroMQ 轻松地将它连接到你的 PHP 代码中。

另请参阅


3
请注意:我最近进行了一些基准测试。在某个点上,这种方法不会很好地扩展。我尝试在短时间内使用几百个连接的客户端发送几千条消息。如果您使用Apache,由于线程之间的上下文切换次数过多,它将变慢。在某个时刻,Node或Nginx-push-stream模块将成为更好的工具来完成此操作。尽管如此,使用PHP也是可能的,并且可以正常工作。 - igorw
1
只是喜欢这种简便易行和非常详细的示例。6年过去了,依然是一个简单而现代的答案。谢谢! - Emil Borconi

4

这取决于你在服务器端脚本中做什么。有些情况下,除了上述方法,你别无选择。

然而,如果你要调用一个函数等待某个事件发生,你可以使用这个技巧来避免竞争,而不是使用 usleep()(这被认为是一种“不好的做法”)。

比如说,你正在等待来自文件或其他阻塞流的数据,你可以这样做:

while (($str = fgets($fp)) === FALSE) continue;
// Handle the event here

实际上,PHP并不是执行此类操作的最佳语言。但有些情况下(我知道,因为我自己处理过这种情况),PHP是唯一的选择。


1
实际上,我想通过删除while中的noop来减少CPU开销(这就是为什么有usleep()调用) - ArtoAle
2
@ArtoAle 在PHP中你不能没有类似于JavaScript的事件处理程序等待事件的发生而不使用某种循环。但是通过以上所述的方法,让 fgets() 调用进行等待,而不是检查-睡眠-检查-睡眠-检查...在你的示例中,如果事件发生在 usleep()期间,则必须等待睡眠结束才能处理它。通过将(阻塞)检查作为循环条件,一旦发生事件,fgets() 就会立即返回,然后您就可以立即处理它。 - DaveRandom
好的,你是对的。问题在于使用System V IPC时,我可以使用阻塞调用来获取信号量(这会导致与您提出的解决方案相同的结果)。问题是我不知道如何使用信号量来实现“事件式”的应用程序行为。 - ArtoAle
抱歉,我不能帮你,信号量是我从未尝试实现的东西。你具体想做什么?你正在等待什么事件? - DaveRandom
嗯...比如说要在聊天频道上发送一条新消息(当然,重新实现聊天引擎并不是最好的选择,这只是举个例子)。 - ArtoAle
所以你可能会有一些守护进程来收集消息,然后当有一个可用的消息时,守护进程会向 PHP 进程发送一个信号量,PHP 进程会从数据库中获取消息并将其推送到客户端? - DaveRandom

2
尽管我很喜欢PHP,但我必须说PHP并不是完成这项任务的最佳选择。对于这种情况,Node.js要好得多,而且它的扩展性非常好。如果您了解JS知识,那么实现也相当简单。
现在,如果您不想浪费CPU周期,就必须创建一个PHP脚本,该脚本将连接到某个特定端口上的服务器。指定的服务器应在所选端口上侦听连接,并每隔一段时间检查您想要检查的内容(例如新帖子的db条目),然后将消息分派给每个已连接的客户端,告知新条目已准备好。
现在,在PHP中实现此事件队列架构并不是很困难,但使用Node.js和Socket.IO进行此操作只需要5分钟,而且无需担心它是否适用于大多数浏览器。

0

你需要一个实时库。

其中一个例子是 Ratchet http://socketo.me/

负责发布订阅的部分在 http://socketo.me/docs/wamp 中有讨论。

这里的限制是 PHP 也需要是初始化可变数据的那个。

换句话说,这不会神奇地让你订阅 MySQL 更新。但是如果你可以编辑 MySQL 设置代码,那么就可以在那里添加发布部分。


0

我同意这里的共识,即PHP不是解决此问题的最佳方案。您确实需要寻找专用的实时技术来解决从服务器向客户端传递数据的异步问题。听起来您正在尝试实现HTTP-长轮询,这并不容易在跨浏览器上解决。Comet产品的开发人员已经多次解决了这个问题,因此我建议您考虑使用Comet解决方案,甚至更好的是WebSocket解决方案,并为旧版浏览器提供回退支持。

我建议您让PHP执行其擅长的Web应用程序功能,并选择专用解决方案来处理实时、事件驱动和异步功能。


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