服务器推送事件是如何工作的?

59

我理解服务器发送事件(EventSource)的概念:

  • 客户端通过EventSource连接到端点
  • 客户端只需侦听从端点发送的消息

我的疑惑在于它在服务器上的工作原理。我看过不同的示例,但最先想到的是Mozilla的示例:http://hacks.mozilla.org/2011/06/a-wall-powered-by-eventsource-and-server-sent-events/

现在这可能只是一个糟糕的示例,但从我理解的角度来看,服务器端的工作方式是有道理的:

  • 数据存储库(比如数据库)中发生变化
  • 服务器端脚本每N秒轮询数据存储库
  • 如果轮询脚本注意到变化,则向客户端发送服务器发送的事件

这样讲清楚了吗?从最基本的角度来看,它确实是这样工作的吗?

2个回答

82

HTML5 doctor网站有一篇关于服务器发送事件的优秀文章,但是我也会在这里提供一个(相对)简短的摘要。

服务器发送事件本质上是一种长时间运行的HTTP连接,使用了特殊的MIME类型(text/event-stream)以及提供了EventSource API的用户代理。这些因素共同构成了服务器和客户端之间的单向连接,可以从服务器向客户端发送消息。

在服务器端,操作非常简单。你只需要设置以下HTTP头即可:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

一定要用代码200回应,而不是204或其他任何代码,因为这会导致兼容的用户代理断开连接。另外,请确保不要在服务器端结束连接。现在,您可以自由地开始向该连接推送消息。在nodejs(使用express)中,可能如下所示:

app.get("/my-stream", function(req, res) {
    res.status(200)
       .set({ "content-type"  : "text/event-stream"
            , "cache-control" : "no-cache"
            , "connection"    : "keep-alive"
            })

    res.write("data: Hello, world!\n\n")
})

在客户端,您只需使用EventSource API,如您所指出的:

var source = new EventSource("/my-stream")
source.addEventListener("message", function(message) {
    console.log(message.data)
})

基本上就是这样了。

实际上,服务器和客户端通过一种相互约定的方式保持连接活跃。只要服务器认为必要,就会让连接保持活跃。如果服务器想要终止连接,它可以选择在客户端下次尝试连接时使用 204 No Content 进行响应来终止连接。这将导致客户端停止尝试重新连接。我不确定是否有一种方式可以以告知客户端不要重新连接的方式结束连接,从而跳过客户端尝试重新连接一次。

正如前面提到的,客户端也会保持连接活跃,并在连接断开时进行重新连接。重新连接的算法在规范中进行了说明,非常简单明了。

然而,我到目前为止几乎没有涉及的一个极其重要的部分是 MIME 类型。MIME 类型定义了传输中消息的格式。请注意,它并不规定消息内容的格式,而只规定消息本身的结构。MIME 类型非常简单明了。消息本质上是信息键值对。键必须是预定义的一组键之一:

  • id - 消息的 ID
  • data - 实际数据
  • event - 事件类型
  • retry - 用户代理在重试失败的连接之前应等待的毫秒数

其他任何键都应该被忽略。消息是通过使用两个换行符 \n\n 来分隔的。

以下是一个有效的消息(最后的换行符添加了冗余):

data: Hello, world!
\n

客户端将看到:Hello, world!

如此:

data: Hello,
data: world!
\n
客户端将看到如下内容:Hello,\nworld!
这基本上概括了服务器推送事件的内容:一个长时间运行的非缓存 http 连接、一个 MIME 类型和一个简单的 JavaScript API。
如果您想了解更多信息,我强烈建议阅读规范。它很简短,并且描述得非常清楚(尽管服务器端的要求可能需要更好地总结一下)。我强烈建议阅读它以获得特定 HTTP 状态代码的预期行为的信息,例如。

4
这段话提到了客户端和连接如何工作的详细信息,但如果需要将事件发送到数据库进行更改,我想只能在服务器上不断轮询数据库了。这应该是提问者所问的问题吧?那么,有没有更高效的服务器端解决方案呢?显然,在小型企业服务器上进行循环检查数据库变化的连接数量一旦增多,就会开始拖慢速度。 - benedict_w
2
客户端只在服务器决定发送消息时才会收到消息,它不会轮询任何内容。服务器可以自行确定何时以及为什么发送消息。这可能在服务器端使用轮询或适合的任何技术来实现。服务器上的连接可以被池化,并且可以通过每个连接发送一条消息,从而给您提供一种广播功能。因此,服务器可以是与DB的单个连接,但广播到连接到服务器的大量客户端。我认为问题比那更加普遍。 - Marcus Stade
4
那个重试的毫秒位——太聪明了!救了我的狗命! - rob_james
好的,解释得很好,但我仍然不明白如何更新内容。你是说每次想要更新流时都必须更新服务器文件吗? 如果我想要更新一个关于降雨积累的天气页面。每10分钟雨水就会积累。它可能会下大雨20分钟,然后在另外20分钟内变成小雨。 所以如果在前10分钟雨量为1英寸,那么在接下来的10分钟内它将达到2.7英寸,然后减缓10分钟并达到3英寸。服务器从哪里接收到这些信息呢?在发生之前我们无法知道确切的数量。 - user2585548
关于关闭连接:如果我想防止浏览器重新连接,我只需发送特定的服务器事件(例如事件:close),并在客户端上监听该事件,然后调用close方法即可。 - Simon
显示剩余3条评论

6

您还需要确保调用res.flushHeaders(),否则Node.js不会在您调用res.end()之前发送HTTP头。请参见此教程以获取完整示例。


教程讲解了_response_的flushHeaders而不是请求的flushHeaders。然后你(和教程犯了同样的错误)链接到了Node.js的_request_的flushHeaders。这是正确的链接:https://nodejs.org/api/http.html#responseflushheaders - icc97

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