服务器发送事件、WebSocket和HTTP

2022-02-20 19:52:43

orange网站目前正在讨论一篇关于服务器发送事件的文章,尤其是与WebSocket(以及新兴的WebTransport)的比较。这篇文章和讨论都很有见地,但我认为他们忽略了一个具有相当深刻影响的方面。

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

让我们拿出一个数字,假设我们问了20处房产。问题是我们可能从他们那里得到了12个不同的答案;他们不同意,因为有很多不同的方法可以使用WebSocket。

虽然其中一些用途是真正独特的,但许多属性只是想要一种高效、可靠的发布/订阅机制,将事件流式传输到浏览器。他们对此使用了长时间的轮询,并发现它不太理想——特别是因为它实际上占用了整个HTTP连接,而到那时,浏览器开始更严格地限制每个来源的可用连接数。

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

考虑到非常大的网站没有单一的语言约定,而且WebSockets库的规模甚至在一种语言中也是相当大的,所以我们遇到了一个问题也就不足为奇了——在基础设施中选择支持哪种语言。

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

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

遗漏的是基于中介体对应用程序更高级语义的理解来扩展协议的能力。在应用程序执行pub/sub的情况下,中介机构不能对WebSocket做太多事情,尽管如果他们能够“抓住”这些语义,那么这样做相对简单。

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

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

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

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

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

即使使用HTTP/2,当数据包丢失时,TCP行首阻塞也是可能的,因此如果您需要避免这种情况(例如,您需要尽可能接近“实时”),您将需要HTTP/3,这对大多数人来说更新、更陌生。

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

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

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

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

SSE的高级别问题是,它是一种隐式解决方案;协议中的pub/sub语义不是很明确;它们实际上只出现在文本/事件流响应媒体类型中。

因此,您需要依靠中间层以特定的方式实现缓存,以达到预期的效果。这可能没什么大不了的,因为CDN和反向代理通常与源站协调得很好,但这不是最佳的协议设计。

所有这些问题都不排除SSE适用于所有用例,但它们确实会对其采用产生摩擦。

另一个选择是为pub/sub定义一个新的WebSockets子协议;该协议将在连接设置期间明确声明:

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

这不是一个新的想法——它已经被提出过很多次。由于WebSockets实际上是一块空白画布,因此在其上设计协议时需要做出很多选择,而且目前还没有一种实现方法。

我认为这会变得更加困难,因为所有这些提议都是在创建完全独立于HTTP的东西——它们不是在上面构建的,而是要求您购买新的协议。

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

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

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

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

HTTP/2 105 PUBDate:Sun,2022年2月20日04:36:53 GMTMy应用程序数据:54上次事件ID:1HTTP/2 105 PUBDate:Sun,2022年2月20日04:37:05 GMTMy应用程序数据:42上次事件ID:2HTTP/2 105 PUBDate:Sun,2022年2月20日04:38:31 GMTMy应用程序数据:36上次事件ID:3

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

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

有些人可能会担心1xx不可互操作。然而,谷歌在103(早期提示)上的工作表明,只要连接是加密的,它们是网络兼容的。

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

对于非最终响应如何与资源状态相关,也存在一些建筑/哲学方面的担忧。然而,因为我们正在定义一个新方法,所以它不必这样做,所以这不应该成为一个阻碍。

这种方法与SSE非常相似——实际上,只需稍加调整,就可以通过EventSource API访问它。不同之处在于,语义在网络上非常明确,因此HTTP中介机构如果愿意,将能够理解和支持它们。

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

这是一种非常新的能力;直到最近,CDN还只在边缘提供缓存和头字段操作等服务。然而现在,像Fastly这样的解决方案Compute@EdgeCloudFlare Workers和Akamai EdgeWorkers允许您在其中介中编写处理协议的代码。

这代表着我们对协议功能的看法发生了巨大变化。然而,它们是非常新的,还不能互操作;如果你为其中一个编写代码,如果不进行至少一些重写,它就不一定能在另一个上工作。

此外,这些网络对其内部拓扑结构和状态有着深刻的理解,并可以利用它来精确地通知协议级别的决策。这让我怀疑,如果可以更好地将其作为一个广泛有用的部分提供,那么在这些平台之上编写代码来执行此功能是否是一个好主意。

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

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

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

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

然而,我还远不能确定这会发生,所以最好的前进道路还不确定。我们是否应该扩展和完善SSE(如果我们能让浏览器加入的话)?因为WebSockets子协议不需要浏览器购买,所以这条路可能更实用——但我们能在一个提案背后获得足够的动力吗?或者,我们可以让浏览器实现一个新的PUB-HTTP方法吗?或者边缘计算平台会聚在一起,使所有这些都变得不必要?

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