HTML5 服务器发送事件原型 - 模棱两可的错误和重复轮询?

Posted

技术标签:

【中文标题】HTML5 服务器发送事件原型 - 模棱两可的错误和重复轮询?【英文标题】:HTML5 Server-Sent Events prototyping - ambiguous error and repeated polling? 【发布时间】:2012-02-22 16:01:22 【问题描述】:

我正在尝试处理服务器端事件,因为它们完全符合我的要求,并且看起来应该很容易实现,但是我无法克服一个模糊的错误,并且看起来连接反复出现关闭并重新打开。我所尝试的一切都是基于this 和其他教程。

php 是一个单一的脚本:

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');

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

javascript 看起来像这样(在正文负载上运行):

function init() 

    var source;
    if (!!window.EventSource) 
        source = new EventSource('events.php');
        source.addEventListener('message', function(e) 
            document.getElementById('output').innerhtml += e.data + '<br />';
        , false);
        source.addEventListener('open', function(e) 
            document.getElementById('output').innerHTML += 'connection opened<br />';
        , false);
        source.addEventListener('error', function(e) 
            document.getElementById('output').innerHTML += 'error<br />';
        , false);
    
    else 
        alert("Browser doesn't support Server-Sent Events");
    

我搜索了一下,但找不到相关信息

    如果 Apache 需要任何特殊配置来支持服务器发送的事件,并且 如何使用这种设置从服务器发起推送(例如,我可以简单地从 CLI 执行 PHP 脚本来推送到已经连接的浏览器吗?)

如果我在 Chrome (16.0.912.77) 中运行此 JS,它会打开连接,接收时间,然后出现错误(错误对象中没有有用的信息),然后在 3 秒内重新连接并执行相同的过程。在 Firefox (10.0) 中,我得到了相同的行为。

编辑 1:我认为问题可能与我使用的服务器有关,所以我在 vanilla XAMPP 安装上进行了测试,并且出现了同样的错误。一个基本的服务器配置是否应该能够在不修改/额外配置的情况下处理这个问题?

EDIT 2:以下是浏览器的输出示例:

connection opened
server time: 01:47:20
error
connection opened
server time: 01:47:23
error
connection opened
server time: 01:47:26
error

谁能告诉我这是哪里出错了?我看过的教程使 SSE 看起来非常简单。此外,对我上面的两个编号问题的任何答案都会非常有帮助。

谢谢。

【问题讨论】:

你能发布你的定时循环吗,提供的代码没有错误。您是否随时删除 EventSource 对象? @tnt 我要到星期一才能发布代码,但是没有定时循环 - 默认 EventSource 行为是在连接丢失 3 秒后重新连接。单个函数(上面的 init())在体加载时调用一次,创建连接,然后浏览器自行进入其连接-错误-重新连接循环。 来自 W3C:已知旧代理服务器在某些情况下会在短暂超时后断开 HTTP 连接。为了防止此类代理服务器,作者可以每 15 秒左右添加一个注释行(以“:”字符开头)。 @tnt 感谢您的输入,但我不确定这是否适用 - 我的浏览器正在使用接近默认配置的 localhost 直接与 Apache 2.2.14 对话。我假设评论/15 秒建议意味着服务器应该每 15 秒发送一次评论事件,但在我的情况下,在发送单个事件后立即出现连接错误,而不是在连接可能超时的任何明显延迟之后。跨度> 另一条线索:看看 cometd。使用网络套接字,长轮询。看到这个帖子:***.com/questions/603201/using-comet-with-php 【参考方案1】:

问题是你的 php。

根据您编写 php 脚本的方式,每次执行只发送一条消息。直接访问php文件就是这样,用EventSource访问文件也是这样。所以为了让你的php脚本发送多条消息,你需要一个循环。

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');

function sendMsg($id, $msg) 
  echo "id: $id" . PHP_EOL;
  echo "data: $msg" . PHP_EOL;
  echo PHP_EOL;
  ob_flush();
  flush();

while(true) 
  $serverTime = time();
  sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));
  sleep(1);

?>

我已更改您的代码以包含一个无限循环,该循环在每条消息发送后等待 1 秒(按照此处找到的示例:Using server-sent events)。

这种类型的循环是我目前正在使用的,它消除了持续的连接断开并每 3 秒重新连接一次。但是(我只在 chrome 中测试过),连接现在只能保持活动 30 秒。我将继续弄清楚为什么会这样,当我找到解决方案时我会发布一个解决方案,但在此之前,这至少应该让你更接近你的目标。

希望对你有帮助,

编辑:

为了让 php 的连接长时间保持打开状态,您需要设置 max_execution_time(感谢 tomfumb)。这至少可以通过三种方式实现:

    如果您可以更改 php.ini,请更改“max_execution_time”的值。这将允许您的所有脚本在您指定的时间内运行。 在您希望长时间运行的脚本中,使用函数 ini_set(key, value),其中 key 是“max_execution_time”,value 是您希望脚本运行的时间(以秒为单位)。 在您希望长时间运行的脚本中,使用函数 set_time_limit(n),其中 n 是您希望脚本运行的秒数。

【讨论】:

太棒了,这正是我想要的。我将睡眠设置为 3 并且连接在 60 秒后断开(可能与 httpd.conf 中的连接超时或 PHP 最大执行时间有关?)。我将其设置为答案,但如果您能找出连接错误,我很乐意为这个问题提供另一个赏金,并按照您的方式发送。我必须承认我的下一个问题是如何在建立初始连接后向客户端发送新事件 - 现在我看到 PHP 必须永远不会停止执行。 谢谢,笨蛋!括号中的可能性正是我需要弄清楚的。我的任何 Apache 配置文件中都没有任何内容,但 max_execution_time 很容易找到。我编辑了我的答案以包括延长每个 EventSource 连接寿命的选项。【参考方案2】:

服务器发送事件只有在涉及到 Javascript 部分时才容易。首先,互联网上很多关于 SSE 的教程都在关闭它们在服务器部分的连接。无论是 PHP 还是 Java 示例。这真的很令人惊讶,因为您得到的只是实现“Ajax 轮询”系统的一种不同方式,该系统具有严格定义的有效负载结构(以及一些次要功能,例如服务器端设置的客户端重试值)。您可以使用几行 jQuery 轻松实现它。那么就不需要SSE了。

根据 SSE 的规范,我想说重试不应该是实现客户端循环的正常方式。对我来说,SSE 是一种单向流式传输方法,它依赖于服务器后端,该后端在将第一个数据推送到客户端后不会关闭连接。

在 Java 中,使用 Servlet3 异步规范来立即释放请求线程并在不同的线程中进行处理/流式处理很有用。到目前为止这有效,但我仍然不喜欢 EventSource 请求的 30 秒连接生命周期。即使我每 5 秒推送一次数据,连接也会在 30 秒后终止(chrome、firefox)。当然,默认情况下 SSE 将在 3 秒后重新连接,但我仍然认为这不是应该的方式。

一个问题是一些 Java MVC 框架没有能力在数据发送后保持连接打开,所以你最终编码到裸 Servlet API。在用 Java 编写原型 24 小时后,我或多或少地感到失望,因为与传统的 jQuery-Ajax 循环相比,收益并没有那么多。并且填充 SSE 功能的问题也存在。

【讨论】:

是的,令人沮丧的是,很多教程作者都假设您在后端使用 Node,而很少考虑如何与其他框架一起工作。 SSE 似乎是一个绝妙的主意,但由于某种原因,它背后的动力似乎有点弱。【参考方案3】:

问题不是服务器端问题,这一切都发生在客户端并且是规范的一部分(我知道这听起来很奇怪)。

http://dev.w3.org/html5/eventsource/

“当用户代理要重新建立连接时,用户代理必须运行以下步骤。这些步骤是异步运行的,而不是作为任务的一部分。(当然,它排队的任务像正常一样运行任务而不是异步的。)”

    排队任务以运行以下步骤:
      如果 readyState 属性设置为 CLOSED,则中止任务。 将 readyState 属性设置为 CONNECTING。 在 EventSource 对象上触发一个名为 error 的简单事件。

我看不出这里有任何错误需要,因此我修改了您的 Init 函数以过滤掉连接时触发的错误事件。

function init() 
                var CONNECTING = 0;
                var source;
                if (!!window.EventSource) 
                    source = new EventSource('events.php');
                    source.addEventListener('message', function (e) 
                        document.getElementById('output').innerHTML += e.data + '';
                    , false);
                    source.addEventListener('open', function (e) 
                        document.getElementById('output').innerHTML += 'connection opened';
                    , false);
                    source.addEventListener('error', function (e) 
                        if (source.readyState != CONNECTING) 
                            document.getElementById('output').innerHTML += 'error';
                        
                    , false);
                
                else 
                    alert("Browser doesn't support Server-Sent Events");
                
            

【讨论】:

我很困惑——你不是在隐藏问题吗?据我了解,EventSource 连接应该保持打开状态,直到我关闭它。这里的问题是连接定期关闭然后重新打开。您说客户端重新建立连接时我看到的行为是正确的,但是为什么首先需要重新建立呢?如果客户端每 3 秒重新连接一次服务器,这与轮询没有什么不同,完全否定了推送的好处。我已经编辑了我的问题以包含浏览器的输出。 好吧,从我读过的内容来看,服务器需要保持连接打开并将数据推送到客户端,在 PHP 的情况下,通过刷新缓冲区。如果服务器没有保持连接,事件源将重试,提交服务器提供的最后一个事件 id,这就是导致观察到的轮询的原因。目前规范还在草稿中,我不知道为什么没有其他事件来区分实际问题和重新连接。 同样基于@unludo 提供的链接,如果您打算保持连接打开,那么对于事件源来说,PHP 听起来并不是一个好的解决方案,因为您需要为每个请求保留一个 PHP 线程。 听起来我们又回到了最初的问题——Apache 是否需要任何特殊配置来支持服务器发送的事件。正如我所说的那样,PHP/线程问题并不困扰我,因为这只是一个开发设置。生产环境可能是基于 java 的。现在我只需要在现有应用程序的上下文中证明该方法,但到目前为止我所证明的是它并不像看起来那么简单 从根本上说,您收到的“错误”事件并不是“真正的”错误。这里实际上没有任何问题需要任何更改/输入。在我看来,这个“错误”是处理这种预期行为的愚蠢方式。这有点苛刻,但肯定会产生误导。也许更好的事件是“重新连接”。这是一个语义问题,它说“错误”,所以它看起来像一个错误,但它可以说什么。这里没有实际错误。【参考方案4】:

我可以看到代码没有实际问题。被选为正确的答案就是不正确的。

这总结了问题中提到的行为(http://www.w3.org/TR/2009/WD-html5-20090212/comms.html):

"如果这样的资源(具有正确的 MIME 类型)完成加载(即接收到整个 HTTP 响应正文或连接本身关闭),用户代理应在等于重新连接的延迟后再次请求事件源资源事件源的时间。这不适用于下面列出的错误情况。"

问题在于流。我之前在 perl 中成功地打开了一个 EventStream;只需发送适当的 HTTP 标头,然后开始发送流数据;永远不要关闭流服务器端。问题是似乎大多数 HTTP 库在打开流后都试图关闭它。这将导致客户端尝试重新连接到完全符合标准的服务器。

这意味着问题似乎可以通过运行一个while循环来解决,原因如下:

A) 代码将继续发送数据,就好像它正在推出一个大文件一样 B) 代码(php 服务器)将永远没有机会尝试关闭连接

但是,这里的问题很明显:要保持数据流处于活动状态,必须发送恒定的数据流。这会导致资源的浪费利用,并否定 SSE 流应该提供的任何好处。

我没有足够的 php 大师知道,但我想 php 服务器中的某些东西/稍后在代码中过早地关闭了流;我必须使用 Perl 在 Socket 级别操作流以使其保持打开状态,因为 HTTP::Response 正在关闭连接,并导致客户端浏览器尝试重新打开连接。在 Mojolicious(另一个 Perl Web 框架)中,这可以通过打开一个 Stream 对象并将超时设置为零来完成,这样流就不会超时。

因此,这里的正确解决方案是不要使用 while 循环;它是调用适当的 php 函数来打开和保持打开一个 php 流。

【讨论】:

【参考方案5】:

我可以通过实现自定义事件循环来做到这一点。似乎这个 html5 功能根本没有准备好,即使与最新版本的 google chrome 也存在兼容性问题。在这里,在 firefox 上工作(无法在 chrome 上正确发送消息):

var source;

function Body_Load(event) 
    loopEvent();


function loopEvent() 
    if (source == undefined) 
        source = new EventSource("event/message.php");
    
    source.onmessage = function(event) 
        _e("out").value = event.data;
        loopEvent();
    

附: : _e 是调用document.getElementById(id)的函数;

【讨论】:

【参考方案6】:

根据规范,3 秒重新连接是在连接关闭时设计的。理论上,带有循环的 PHP 应该会停止这种情况,但 PHP 脚本将无限期地运行并浪费资源。由于这个问题,您应该尽量避免将 apache 和 php 用于 SSE。

标准的 http 响应应在响应发送后关闭连接。您可以使用标题“connection: keep-alive”来更改它,它应该告诉浏览器连接应该保持打开状态,尽管如果您使用代理,这可能会导致问题。

node.js 或类似的东西是用于 SSE 而不是 apache/php 的更好引擎,因为它基本上是 JavaScript,所以很容易掌握。

【讨论】:

【参考方案7】:

服务器发送事件,顾名思义,如果它必须每三秒重新连接一次以从服务器检索数据,那么数据应该从服务器传输到客户端,那么它与其他轮询机制没有什么不同。SSE 的目的是提醒客户端一旦有客户端不知道的新数据。由于服务器关闭连接,即使标头保持活动状态,除了在无限循环中运行 php 脚本之外别无他法,但有相当多的线程睡眠以防止服务器负担。直到现在我看不到任何其他出路,它比每 3 秒向服务器发送一次垃圾邮件来获取新数据要好。

【讨论】:

【参考方案8】:

我也在尝试同样的事情。取得了不同程度的成功。

    在使用 Firefox 时遇到了同样的问题,运行与提到的相同的 js 代码。 使用 nginx 服务器和一些退出的 PHP(即没有持续循环),只有在 PHP 退出后,我才能从 firefox 将消息返回到“请求”。 在 PHP.exe 中将 PHP 作为脚本运行,在 concole 上一切正常,刷新时会打印刺痛。但是,在 PHP 完成之前,Nginx 不会发送数据。尝试添加额外的 \r\n\r\n 和 flush() 或 ob_flush() 没有帮助。 没有数据推送,如 Wireshark 日志中所示,只是对 GET 的延迟响应数据包。

读到我需要一个用于 Nginx 的“推送”模块,该模块需要从源代码重新构建。

所以这绝对是 Nginx 的问题。

    使用“C”中的套接字,我能够按预期将数据推送到 Firefox,并且套接字保持打开状态,并且没有丢失任何消息。但是,这样做的缺点是我需要为 page.html 提供服务,并且由于跨站点 URL 问题,来自同一套接字或 firefox 的事件/流将无法连接。在某些情况下有一些方法可以解决这个问题,但不适用于菜单系统中的 iframe。这种方法确实证明了 SSE 确实可以与 firefox 一起使用,并且在 wireshark 日志中有推送的数据包。其中选项 1 只有请求/回复数据包。

说了这么多,我还是没有解决办法。我试图删除 PHP 和 Nginx 上的缓冲。但在 PHP 完成之前仍然一无所获。尝试了不同的标题选项,例如块也没有帮助。 我不想在'C'中编写一个完整的http服务器,但这似乎是目前对我有用的唯一选择。 我即将尝试 Apache,但大多数文章表明这​​比 Nginx 在这项工作上更糟糕。

【讨论】:

自从发帖以来,我发现了一些链接。/写了很多帮助。 自从发帖以来,我发现了一些链接。/写了很多帮助。 [html5doctor.com/server-sent-events/] 关于这个主题的大量示例页面和有用信息。 pushmodule.slact.net Nginx 的推送模块。

以上是关于HTML5 服务器发送事件原型 - 模棱两可的错误和重复轮询?的主要内容,如果未能解决你的问题,请参考以下文章

Grails 服务器发送事件

html html5服务器发送事件,推送

HTML5 SSE

HTML5Server-Sent服务器发送事件

在浏览器中调试 HTML5 服务器发送的事件

服务器发送的事件和 php - 啥触发服务器上的事件?