服务器推送事件和PHP - 是什么触发了服务器上的事件?

124

大家好,

HTML5 Rocks 有一篇很好的 Server-sent Events (SSE) 入门教程:

http://www.html5rocks.com/en/tutorials/eventsource/basics/

但是,我不明白一个重要的概念 - 是什么触发了服务器上的事件导致消息被发送?

换句话说 - 在 HTML5 的示例中 - 服务器只是发送了一个时间戳,而且只发送了一次:

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.
function sendMsg($id, $msg) {
  echo "id: $id" . PHP_EOL;
  echo "data: $msg" . PHP_EOL;
  echo PHP_EOL;
  ob_flush();
  flush();
}
$serverTime = time();
sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));

如果我要构建一个实际的示例,比如类似Facebook的“墙”或股票行情,其中服务器会在某些数据变化时向客户端“推送”新消息,这是如何工作的?

换句话说......PHP脚本是否有一个连续运行的循环,不断检查数据的变化,然后每次找到一个变化就发送一条消息?如果是这样-如何知道何时结束该过程?

或者- PHP脚本是否只是发送消息,然后结束(与HTML5Rocks示例中显示的情况相同)?如果是这样-如何获得连续更新?浏览器是否只是以固定间隔轮询PHP页面?如果是这样-这与编写使用AJAX调用PHP页面的setInterval函数有什么不同?

抱歉-这可能是一个非常幼稚的问题。但我能够找到的所有示例都没有说明这一点。

[更新]

我想我的问题表达不清,请看下面的说明。

假设我有一个网页,应该显示苹果公司股票的最新价格。

当用户首次打开页面时,页面将使用“流”的URL创建一个EventSource。

var source = new EventSource('stream.php');

我的问题是 - "stream.php" 应该如何工作?

是这样吗?(伪代码):

<?php
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache'); // recommended to prevent caching of event data.
    function sendMsg($msg) {
        echo "data: $msg" . PHP_EOL;
        echo PHP_EOL;
        flush();
    }

    while (some condition) {
        // check whether Apple's stock price has changed
        // e.g., by querying a database, or calling a web service
        // if it HAS changed, sendMsg with new price to client
        // otherwise, do nothing (until next loop)
        sleep (n) // wait n seconds until checking again
    }
?>

换句话说,“stream.php”是否会一直保持打开状态,只要客户端与其“连接”?

如果是这样的话 - 那么这是否意味着您运行与并发用户相同数量的线程stream.php?如果是这样的话 - 远程可行吗,或是构建应用程序的适当方式?你如何知道何时可以结束stream.php的实例?

我天真的印象是,如果是这种情况,PHP不适合这种类型的服务器。但是,到目前为止,我看到的所有演示都暗示PHP非常适合此类情况,这就是我如此困惑的原因...


这其中的一个问题是,IE不支持SSE :-/ 另外,我建议阅读这篇文章http://www.prodigyproductionsllc.com/articles/programming/javascript/avoid-using-eventsource-server-sent-events/。我认为他使用端口来避免太多子进程的问题,但总体上看,他的建议是要避免使用SSE。在我看来,这似乎比它值得的麻烦多了。 - PJ Brunet
目前不支持IE11或Android浏览器 http://caniuse.com/eventsource - PJ Brunet
1
如果有人需要 sse php 代码,请访问以下链接:https://github.com/shahzadthathal/server-sent-events-php-example - Muhammad Shahzad
7
我有同样的问题,我深刻理解你所说的“服务器上触发事件的原因是什么”。当你创建一个EventSource('stream.php')对象时,客户端会打开与stream.php的连接,就像通过ajax调用它一样。这个连接触发了你的服务器端代码,并保持连接打开,只要你的服务器端代码有话要说。然后连接关闭,在短暂的延迟后(我想在chrome中是3秒),客户端重新打开连接,这又会触发你的stream.php文件。 - Ahmad Maleki
你能否与我们分享一下你的经验? 你最终找到了任何好的和完整的答案来回答你的问题吗? - Reza Amya
显示剩余3条评论
5个回答

37

"...如果客户端一直连接,那么"stream.php"会保持打开状态吗?"

是的,您的伪代码是一个合理的方法。

"你如何知道何时可以结束 stream.php 的实例?"

在大多数情况下,当用户离开您的站点时会发生这种情况。(Apache 检测到关闭的套接字并终止 PHP 实例。)从服务器端关闭套接字的主要时间是,如果您知道有一段时间没有数据;向客户端发送的最后一条消息是告诉他们在某个特定时间回来。例如,在您的股票流案例中,您可以在晚上8点关闭连接,并告诉客户端在8小时后返回(假设纳斯达克从早上4点到晚上8点进行报价)。周五晚上,您告诉他们周一早上回来。(我正在撰写一本关于SSE的书,并在此主题上专门撰写了几个章节。)

"...如果是这种情况,PHP不是这种服务器的合适技术。但迄今为止我看到的所有演示都表明PHP非常适合这一点,这就是我感到困惑的原因..."

嗯,人们认为 PHP 不适合普通网站的技术,他们是对的:如果您用 C++ 替换整个 LAMP 堆栈,就可以使用更少的内存和 CPU 周期来完成。但是,尽管如此,PHP 可以很好地支持大多数现有网站。它是一种非常适合 Web 工作的语言,具有熟悉的类 C 语法和众多库,而且也是一种让管理者感到舒适的语言,因为有很多 PHP 程序员可供聘用,有很多书籍和其他资源,以及一些大型用例(例如 Facebook 和 Wikipedia)。这些基本上就是您选择 PHP 作为流媒体技术的原因。

典型的设置不是每个PHP实例都连接到NASDAQ,而是另外一个进程与NASDAQ建立单一连接,或者可能是集群中每台机器都与NASDAQ建立单一连接。然后将价格推入SQL/NoSQL服务器或共享内存中。然后PHP只需要轮询共享内存(或数据库)并推送数据。或者有一个数据采集服务器,每个PHP实例都打开一个套接字连接到该服务器。数据采集服务器在接收到更新时向其每个PHP客户端推送更新,它们也会将该数据推送给其客户端。
使用Apache+PHP进行流处理的主要可扩展性问题是每个Apache进程的内存。当达到硬件的内存限制时,可以做出增加集群中另一个机器的业务决策,或者将Apache排除在循环之外,编写专用HTTP服务器。后者可以使用PHP完成,因此所有现有的知识和代码都可以重复使用,或者可以用另一种语言重写整个应用程序。纯开发人员会使用C++编写专用简化HTTP服务器。管理人员会增加另一台机器。

由于每个Apache连接进程都会消耗内存,使用Nginx是否更好? - Zhang Buzz
@ZhangBuzz,当我提到Apache+PHP时,实际上指的是“Web服务器+ PHP进程”,因此使用不同的Web服务器并没有什么区别。 - Darren Cook
也许像这样的服务器?https://github.com/hoaproject/Eventsource 或 https://github.com/hhxsv5/php-sse - Enrique
请注意,Nginx 可能更有效,因为它使用的内存更少:https://blog.webfaction.com/2008/12/a-little-holiday-present-10000-reqssec-with-nginx-2/ - Enrique

36

服务器推送事件是从服务器端实时更新到客户端的技术。在第一个示例中,连接不会保持,客户端每3秒尝试重新连接,与Ajax轮询没有区别。

因此,为了使连接持久化,需要在循环中包装代码,并不断检查是否有更新。

PHP是基于线程的,更多的连接用户会让服务器耗尽资源。可以通过控制脚本执行时间来解决这个问题,并在超过一定时间(例如10分钟)后结束脚本运行。 EventSource API将自动重新连接,因此延迟在可接受范围内。

此外,请查看我的PHP服务器推送事件库,您可以更好地了解如何在PHP中使用服务器推送事件并使其更容易编码。


8
您能详细说明“可以通过控制脚本的执行时间并在超过一定时间后结束脚本来解决这个问题”吗?如果您有大量用户,关闭连接真的会显著改善资源使用情况吗?因为用户只会在3秒钟内重新连接。 - Luke
可能是关于while循环不会永远运行的问题。 - Avatar
1
如果用户数量没有改变,我怀疑每10分钟重新建立连接不会改变正在使用的线程数 - 或许在你的 "while(true){" 循环中放置一个 "if ( connection_aborted() ) break;" 可以停止循环,当用户离开网站时。 - will

4

我注意到SSE技术会定期向客户端发送延迟数据(类似于将从客户端页面进行Ajax轮询数据的操作颠倒过来)。为了克服这个问题,我编写了一个名为sseServer.php的页面:

<?php
        session_start();
        header('Content-Type: text/event-stream');
        header('Cache-Control: no-cache'); // recommended to prevent caching of event data
        require 'sse.php';
        if ($_POST['message'] != ""){
                $_SESSION['message'] = $_POST['message'];
                $_SESSION['serverTime'] = time();
        }
        sendMsg($_SESSION['serverTime'], $_SESSION['message'] );
?>

而 sse.php 是:

<?php
function sendMsg($id, $msg) {
  echo "id: $id" . PHP_EOL;
  echo "data: $msg" . PHP_EOL;
  echo PHP_EOL;
  ob_flush();
  flush();
}
?>

请注意,在sseServer.php文件中,我启动了一个会话并使用了一个会话变量!以克服这个问题。
此外,每当我想要“更新”消息时,我通过Ajax调用sseServer.php(发送请求并设置variable message的值)。
现在在jQuery(javascript)中,我做了如下操作: 1)声明一个全局变量var timeStamp=0; 2)使用下一个算法:
if(typeof(EventSource)!=="undefined"){
        var source=new EventSource("sseServer.php");
        source.onmessage=function(event)
        if ((timeStamp!=event.lastEventId) && (timeStamp!=0)){
                /* this is initialization */
                timeStamp=event.lastEventId;
                $.notify("Please refresh "+event.data, "info");
        } else {
                if (timeStamp==0){
                         timeStamp=event.lastEventId;
                }
        } /* fi */

} else {
        document.getElementById("result").innerHTML="Sorry, your browser does not support server-sent events...";
} /* fi */

在这一行代码中:$.notify("Please refresh "+event.data, "info");,你可以处理消息。
对于我的情况,我使用了jQuery notify来发送通知。
您可以使用POSIX PIPES或DB表代替通过POST传递“消息”,因为sseServer.php执行类似于“无限循环”的操作。
我当时的问题是,上面的代码仅向配对(调用sseServer.php的客户端)发送“消息”,而不是所有客户端,因此我将更改技术,并从要触发“消息”的页面进行DB更新,然后sseServer.php获取它从DB表中而不是通过POST获取消息。
我希望我已经帮到你了!

3
这是一个关于应用程序结构的问题。实时事件是你要从一开始就考虑的东西,这样你可以围绕它设计你的应用程序。如果你编写了一个仅使用字符串查询运行一堆随机mysql(i)_query方法的应用程序,并且没有通过任何中间人传递它们,那么很多时候你就只能重新编写你的应用程序或者不断地进行服务器端轮询。
然而,如果你将实体作为对象管理并通过某种中间类传递它们,你就可以钩入该过程。看看这个例子:
<?php
class MyQueryManager {
    public function find($myObject, $objectId) {
        // Issue a select query against the database to get this object
    }

    public function save($myObject) {
        // Issue a query that saves the object to the database
        // Fire a new "save" event for the type of object passed to this method
    }

    public function delete($myObject) {
        // Fire a "delete" event for the type of object
    }
}

在您的应用程序中,当您准备好保存时:

<?php
$someObject = $queryManager->find("MyObjectName", 1);
$someObject->setDateTimeUpdated(time());
$queryManager->save($someObject);

这并不是最优雅的示例,但它可以作为一个不错的基础。你可以钩入实际的持久层来处理触发这些事件。然后你可以立即得到它们(尽可能实时),而不会使服务器负载过重(因为你无需不断查询数据库以查看是否有更改)。
显然,你不能通过这种方式捕获对数据库的手动更改 - 但如果你经常手动进行任何数据库操作,则应该:
- 解决需要进行手动更改的问题 - 构建一个工具来加快这个过程,并触发这些事件

4
Colin - 感谢你的答复。我的问题不太清楚- 但这并不是我真正想问的。我本意是问...如果您使用PHP作为您的"服务器" - 那么您从客户端调用的PHP脚本是否需要在客户端连接期间运行整个时间?这是否意味着,如果您有1,000个并发用户,您需要运行1,000个独立的线程来运行1,000个并发实例的PHP脚本?这可行吗?而且,当要结束php脚本(假设它正在循环以保持“活动状态”)时,该如何知道? - mattstuehler

-9

基本上,PHP不是适合这种事情的技术。 是的,你可以让它工作,但在高负载下会是一场灾难。我们运行股票服务器,通过Websockets向数万用户发送股票变化信号 - 如果我们使用php...好吧,我们可以,但那些自制的循环 - 只是一场噩梦。每个连接都会在服务器上创建一个单独的进程,或者您必须从某种数据库中处理连接。

简单地使用nodejs和socket.io。它将让您在几天内轻松启动并运行服务器。Nodejs也有自己的限制,但对于Websockets(和SSE)连接,现在它是最强大的技术。

而且- SSE并不像看起来那么好。与Websockets唯一的优势是数据包被本地压缩(ws未经压缩),但缺点是SSE是单向连接。如果用户想要添加另一个股票符号进行订阅,他将不得不发出ajax请求(包括所有与源控制相关的麻烦,并且请求将很慢)。在Websockets中,客户端和服务器在一个已打开的连接中双向通信,因此如果用户发送交易信号或订阅报价,他只需在已打开的连接中发送一个字符串。而且速度很快。


3
你可以基本上像在Node.js中使用事件循环一样使用React.php。 - Matěj Koubík
6
虽然说指出PHP不是最好的选择是好的,但我认为你至少应该包含OP所要求的内容。 - user1267177

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