橙色网站目前正在讨论一篇关于 Server-Sent Events 的文章,特别是与 WebSockets(以及新兴的WebTransport)相比。文章和讨论都很有见地,但我认为他们遗漏了一个具有相当深远影响的方面。

中介很重要

许多年前,当我在一个超大型网站的基础设施团队工作时,浏览器开始支持 WebSockets,该网站上的各种属性(新闻、体育、娱乐等)都对它提供的可能性感到兴奋. 他们需要扩展 WebSockets 而我们想要支持它们,所以我们询问他们希望我们实现哪个库——实际上是哪个协议。

让我们从空中抽出一个数字,假设我们询问了 20 个房产。问题是我们可能从他们那里得到了十二个不同的答案;他们不同意,因为有很多不同的方式可以使用 WebSockets。

虽然其中一些用途确实是独一无二的,但许多属性只是想要一种高效、可靠的发布/订阅机制来将事件流式传输到浏览器。他们为此使用了长轮询,但发现它不是最佳选择——尤其是因为它有效地消耗了整个 HTTP 连接,而且到那时浏览器开始更严格地限制每个源的可用连接数。

WebSockets 似乎是答案,但 WebSockets 没有标准的发布/订阅;相反,您可以为您的首选语言选择一个库,部署它的服务器端组件并将相应的客户端代码发送到浏览器。

鉴于 Very Big Website 没有单一的语言约定,而且即使是一种语言,WebSockets 库的范围也相当大,所以我们遇到一个问题也就不足为奇了——选择在基础设施中支持哪一个。

对于 CDN,这个问题乘以他们拥有的客户数量。中介无法理解特定于应用程序的语义,因此如果不做一些大的假设,他们就无法为其增加多少价值。

我怀疑这就是为什么大多数 CDN WebSockets 产品实际上是 TCP 层代理的原因;他们将 WebSockets 连接 1:1 传递回配置的源,并且可能提供一些 DDoS 保护和 HTTP/2 连接合并(如果他们喜欢的话)。

遗漏的是根据中介对应用程序的更高级别语义的理解来扩展协议的能力。在应用程序执行 pub/sub 的情况下,中间人无法使用 WebSockets 做很多事情,即使如果他们可以“抓住”那些语义,他们这样做会相对简单。

虽然部署发布/订阅不需要中介,但它将对扩展、可靠性和减少延迟提供巨大帮助。就像中间缓存用于“普通”HTTP 一样。

从本质上讲,这是一个协调问题。拥有许多不同的、有效专有的(即使是开源的)小协议会阻止中介机构为 WebSockets 协议增加价值。

那么,问题是我们如何为 pub/sub 启用中介——在这个过程中,也许使 pub/sub 成为 Web 的标准定义部分?

服务器发送的事件

服务器发送的事件是一种可能性。Fastly 已经允许使用 HTTP 缓存的折叠转发“扇出”SSE,所以实际上我们已经有了一种支持中介的 Web 发布/订阅形式——它只是没有流行起来。为什么不?

有几个潜在的原因。一是HTTP/1.1 的连接限制使 SSE 变得棘手(到了无法工作的地步);实际上,SSE 至少需要 HTTP/2。对于许多 Web 开发人员来说,HTTP/2 相对较新。

即使使用 HTTP/2,当数据包丢失时 TCP 线头阻塞也是可能的,所以如果你需要避免这种情况(例如,你需要尽可能接近“实时”),你将需要 HTTP/ 3,对大多数人来说,这是一个更新、更陌生的东西。

Last-Event-ID此外,此方法中未使用SSE 的机制,这意味着事件可能会在重新连接期间丢失。为了支持这一点,中介(反向代理或 CDN)必须了解事件流并适当地调整其响应。然而,这更像是一个实施问题,而不是协议问题。

这些问题可以随着时间的推移得到解决,因为较新的 HTTP 版本得到更广泛的部署,并且(可能)作为中介更深入地支持 SSE。然而,一些遗留问题更为根本。

其中一个问题(对某些人来说)是 SSE 目前只允许在事件中使用文本内容。Base64 是一种解决方法,但不是很好。理论上,浏览器供应商可以添加对二进制事件的支持,但这是值得怀疑的,因为他们目前的所有关注点都在 WebTransport 上。

此外,即使使用 TLS,SSE 也会遇到防病毒代理和企业防火墙的问题,它们有时会在发送之前缓冲整个 HTTP 响应。橙色站点线程中的一些评论者提到了一些缓解这种情况的策略,显然效果很好。填充响应似乎不是一个很好的解决方案,但如果反病毒软件在识别 SSE 方面变得更加智能,那可能就足够了。不过,这暗示了一个更广泛的问题。

SSE-with-collapsed-forwarding 的高级问题是它是一个隐式解决方案;发布/订阅语义在协议中不是很明确;它们实际上只出现在text/event-stream响应媒体类型中。

因此,您依赖中介以特定方式实现缓存以达到预期效果。这可能不是什么大问题,因为 CDN 和反向代理通常与来源协调得很好,但这不是最佳协议设计。

这些问题都没有排除所有用例的 SSE,但它们确实对其采用产生了摩擦。

扩展 WebSockets / WebTransport

另一种选择是为发布/订阅定义一个新的 WebSockets子协议。从技术上讲,这很简单;该协议将在连接设置期间明确声明:

GET /foo/stream HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: pubsub
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: pubsub

…然后您只需要在该 WebSockets 连接上定义“pub”和“sub”消息。

这不是一个新想法——它已多次提出 。因为 WebSockets 实际上是一块空白的画布,所以在其上设计协议时需要做出很多选择,而且还没有一种方法获得动力。

我建议这变得更加困难,因为所有这些提案都在创建与 HTTP 完全不同的东西——它们不是构建在顶部,而是要求您购买新协议。

所以,这又是一个协调问题;如果我们能让每个人都同意以一种方式来做,它应该会奏效。到目前为止,这还没有发生。

我怀疑部分原因是中介不能仅仅改变他们实现协议的方式而不可能破坏许多依赖它们的站点,因此正在寻找一个非常稳定的解决方案。同时,开源库不受这些限制,并希望灵活地改进它们的操作方式。

扩展 HTTP

第三个选项是扩展HTTP 的核心语义以包括发布/订阅。这具有使这些语义对协议和中介更加明确的优点。

当我仔细研究这个时,我通常认为它是一个名为 的新 HTTP 方法SUB,以及一个名为 的新非最终(即1xx)状态代码PUB。因为单个请求可以有许多非最终响应,所以可以将事件映射到每个非最终响应,如下所示:

SUB /foo/stream HTTP/2
Host: example.com
HTTP/2 105 PUB
Date: Sun, 20 Feb 2022 04:36:53 GMT
My-App-Data: 54
Last-Event-ID: 1

HTTP/2 105 PUB
Date: Sun, 20 Feb 2022 04:37:05 GMT
My-App-Data: 42
Last-Event-ID: 2

HTTP/2 105 PUB
Date: Sun, 20 Feb 2022 04:38:31 GMT
My-App-Data: 36
Last-Event-ID: 3

在这里,一个SUBto/foo/stream导致三个事件,每个事件都在My-App-DataHTTP 标头中携带它们的数据。使用标头是因为1xx响应不能包含正文内容。

这里的开销非常小(尤其是在 HTTP/2 和 HTTP/3 中)。与其他方法一样,客户端将负责维护连接并在必要时重新建立连接。我们甚至可以为“keepalive”事件标准化一个 HTTP 状态代码来帮助:

HTTP/2 106 no-operation
Date: Sun, 20 Feb 2022 04:39:31 GMT

有些人可能会担心1xx无法互操作。然而,谷歌的工作103 (Early Hints)表明它们是网络兼容的,前提是连接是加密的。

另一个潜在的反对意见是 HTTP 标头通常被认为是文本的,导致与 SSE 相同的内容限制。但是,HTTP 标头可以是二进制的(尽管这可能无法广泛互操作)。然而,结构化字段可能会提供一条出路——已经讨论过创建它们的二进制编码,在这种情况下,API 可以提供对二进制数据的直接访问。最终。

对于非最终响应如何与资源状态相关,也存在一些架构/哲学问题。然而,由于我们正在定义一个新方法,它不必如此,所以这不应该是一个阻碍。

这种方法与 SSE 非常相似——事实上,它可以通过EventSource API进行一些小的调整来访问。不同之处在于,在线路上的语义非常明确,因此 HTTP 中介如果愿意,将能够理解和支持它们。

在边缘运行代码

另一种方法是在网络的“边缘”运行代码,其方式类似于大多数 WebSockets 协议在浏览器中运行代码的方式。

这是一项非常新的功能;直到最近,CDN 仅在边缘提供缓存和处理标头字段等服务。然而,现在,像Fastly Compute@EdgeCloudflare WorkersAkamai EdgeWorkers这样的解决方案允许您在它们的中介中编写用于处理协议的代码。

这代表了我们对协议功能的看法发生了巨大转变。但是,它们非常新,还不能互操作;如果您为其中之一编写代码,则在至少进行一些重写的​​情况下,它不一定能在其他一个上运行。

此外,这些网络对其内部拓扑和状态有深刻的了解,并且可以使用它来细粒度地通知协议级决策。这让我质疑,在这些平台之一上编写代码来执行此功能是否是一个好主意,因为它可以更好地作为其中一个广泛有用部分提供

选择最好的前进道路

作为一个 HTTP 人,我有偏见:我的主要兴趣是确保 HTTP 协议提供尽可能多的价值,以保持它的相关性并保留社区在其中所做的大量投资。这意味着要确保该协议基于普遍实施的标准提供丰富的功能、良好的效率和良好的互操作性。

相比之下,WebSockets 提供协议功能的方法是让它们出现在开源实现中,而不是在开放标准中指定。因为服务器开始在客户端部署代码,所以效果很好——你选择像socket.io这样的库,部署服务器和客户端组件,它就可以正常工作——但客户端和服务器之间的协议本质上是专有的。

这是因为在其核心,WebSockets 提供了一个非常低的抽象:有效的“Web 的 TCP”(WS 支持者自己使用的词)。作为开发人员,您使用的抽象不再是 WebSocket,它是由您选择的库提供的。

考虑到这一点,对我来说,为 HTTP 中的 pub/sub 定义基于标准的函数比 WebSockets(或 WebTransport)更有意义。

然而,我不确定这会发生,所以最好的前进道路是不确定的。我们是否应该扩展和改进 SSE(如果我们可以让浏览器支持)?因为 WebSockets 子协议不需要浏览器支持,所以这条路可能更实用——但我们能否在单个提案背后获得足够的动力?或者,我们能否让浏览器实现新的PUBHTTP 方法?还是边缘计算平台会融合并使所有这些都变得不必要?

我认为答案很重要,不仅因为 pub/sub 是一种广泛有用的模式,还因为它可能为我们提供了一种在 Web 上引入其他高级协议功能的途径,这些功能可以从其架构(包括中介)中获益。

Server-Sent Events、WebSocket 和 HTTP
标签: