浏览器获得服务器实时消息的五种技术方案

doMore 314 2024-05-09

参考文章:https://rxdb.info/articles/websockets-sse-polling-webrtc-webtransport.html

前言

对于现在的网络应用程序来说,从服务器向客户端发送消息的能力是不可或缺的。多年来,根据这种需求开发了很多种方法,每一种都有自己的优点和缺点。最初,长轮询(long-polling) 是唯一可用的方式。随后,WebSockets 取而代之,为双向通信提供了更好的方案。在 WebSockets 之后,Server-Sent Events(SSE)为服务器到客户端的单向通信提供了一种更简单的办法。展望未来,WebTransport 协议有望通过提供更高效、灵活和可扩展的方法,进一步彻底改变这一局面。对于小众用例,WebRTC 也可用于服务器-客户端事件。

长轮询

长轮询是第一种 “黑客 ”技术,可在浏览器中通过 HTTP 启用 服务器-客户端 消息传递方法。该技术通过正常的 XHR 请求模拟服务器推送通信。与传统的轮询(客户端以固定的时间间隔反复向服务器请求数据)不同,长轮询建立了一个与服务器的连接,该连接一直保持打开状态,直到有新数据可用。一旦服务器获得新信息,就会向客户端发送响应,然后关闭连接。收到服务器的响应后,客户端会立即发起新的请求,整个过程重复进行。这种方法可以更即时地更新数据,减少不必要的网络流量和服务器负载。不过,这种方法仍然会造成通信延迟,而且效率低于 WebSockets 等其他实时技术。

WebSockets

WebSockets 通过客户端和服务器之间的单个长期连接提供全双工通信通道。这项技术使浏览器和服务器能够交换数据,而无需 HTTP 请求-响应周期的开销,从而为即时聊天、游戏或金融交易平台等应用的实时数据传输提供了便利。WebSockets 允许双方在建立连接后独立发送数据,是传统 HTTP 的一大进步,非常适合需要低延迟和高频率更新的应用场景。

虽然 WebSocket API 的基本原理很容易使用,但在生产中却显得相当复杂。套接字(Socket)可能会失去连接,因此必须相应地重新创建。尤其是检测连接是否仍然可用,可能非常棘手。大多数情况下,你需要添加一个 “乒(ping)乓(png)心跳 ”来确保打开的连接没有关闭。这也是大多数人在 WebSockets 上使用 Socket.IO 等三方库的原因,这些库可以处理所有这些情况,甚至在需要时提供长时间轮询的后备功能。

Server-Send Events

服务器发送事件(SSE)提供了一种通过 HTTP 向客户端推送服务器更新的标准方式。与 WebSockets 不同,SSE 专为服务器到客户端的单向通信而设计,因此非常适合实时新闻、体育比赛比分或客户端需要实时更新而无需向服务器发送数据的任何情况。

上述介绍的 WebSocket 也可以做到,但是 WebSocket 更适合双方需要相互通信的场景,比如聊天室APP。而且 WebSocket 方案比较重,需要改动更多代码、使用不同的协议(非 HTTP )。

使用 SSE 创建用于接收事件的连接非常简单。在浏览器的客户端,你可以用生成事件的服务器端脚本的 URL 初始化一个 EventSource 实例。监听消息需要将事件处理程序直接附加到 EventSource 实例。应用程序接口区分了通用消息事件和命名事件,从而实现了更有条理的通信。以下是如何在 JavaScript 中进行设置:

// Connecting to the server-side event stream
const evtSource = new EventSource("https://example.com/events");

// Handling generic message events
evtSource.onmessage = event => {
    console.log('got message: ' + event.data);
};

SSE 其实还是基于 HTTP 协议的,因此我们寻常使用的 HTTP Client Library 和 HTTP Server Library 仍然适用。唯一需要注意的是 HTTP response 里header Content-Type 的值是 text/event-stream
数据格式方面, SSE 使用的是 UTF8 编码的文本格式。
使用两个换行符来分隔前后的消息,每条消息支持4种属性,属性名和属性值之间用冒号区分,新起一行(即使用一个换行符)创建下一个属性。

数据示例

id: D420B6E8-2F51-4235-B778-5C1C494681E8
event: city-notification
retry: 3000
data: Plainville

id: 3AC67AA0-2852-4D30-9103-23CEF4B43D6D
event: city-notification
retry: 3000
data: Bellevue

上面有两条消息(两个 server sent event),两条消息之间有一个空行 —— 即 'Plainville' 与下一个 'id' 之间有2个换行符。

每条消息都有4个属性:

  • id - 当前消息的 id
  • event - 当前消息的类型,根据业务需要自行定义。比如例子里是 city-notification ,如果是天气推送,这个值可以是 weather;新闻推送,这个值可以是 news ...
  • data - 这条消息的内容,只能为文本类型 —— 可以像例子里直接一个字符串,也可以是JSON字符串或者自定义的字符串格式
  • retry - 值必须是数字(非数字自动忽略), 不同于其他的属性,这个属性跟当前消息无关,而是跟这次SSE连接相关 —— 如果连接中断,客户端应该间隔多少毫秒再尝试重新连接。
    只有上述四种属性是合法,其他属性都会被忽略。

WebTransport

WebTransport 是一种更先进的 API,旨在实现网络客户端和服务器之间高效、低延迟的通信。它利用 HTTP/3 QUIC 协议实现了多种数据传输功能,如通过多个流以可靠或不可靠的方式发送数据,甚至允许不按顺序发送数据。这使得 WebTransport 成为需要高性能网络的应用(如实时游戏、实时流媒体和协作平台)的强大工具。不过,值得注意的是,WebTransport 目前只是一个工作草案,尚未得到广泛采用。截至目前(2024 年 3 月),WebTransport 仍处于工作草案阶段,尚未得到广泛支持。

个人猜测:即使 WebTransport 将得到广泛支持,其 API 的使用也非常复杂,人们很可能会在 WebTransport 的基础上构建库,而不是直接在应用程序的源代码中使用它。

WebRTC

WebRTC (网络实时通信)是一个开源项目和 API 标准,可直接在网络浏览器和移动应用程序中实现实时通信(RTC)功能,而无需复杂的服务器基础设施或安装额外的插件。它支持点对点连接,可在浏览器之间进行流式音频、视频和数据交换。WebRTC 设计用于穿越 NAT 和防火墙,利用 ICE、STUN 和 TURN 等协议在对等网络之间建立连接。

虽然 WebRTC 可用于客户端与客户端之间的交互,但它也可用于服务器与客户端之间的通信,即服务器只是模拟客户端。这种方法只适用于小范围的使用案例。

问题在于,要让 WebRTC 起作用,无论如何都需要一个信令服务器,然后再通过 websockets、SSE 或 WebTransport 运行。这就违背了使用 WebRTC 替代这些技术的初衷。

技术的局限性

双向发送数据

只有 WebSockets 和 WebTransport 允许双向发送数据,因此您可以在同一连接上接收服务器数据并发送客户端数据。

虽然理论上长轮询(Long-Polling)也可以做到,但并不推荐这样做,因为向现有的长轮询连接发送 “新 ”数据无论如何都需要进行额外的 http 请求。因此,你可以在不中断长轮询连接的情况下,通过额外的 http 请求直接从客户端向服务器发送数据。

服务器发送事件(Server-Sent-Events)不支持向服务器发送任何附加数据。你只能执行初始请求,而且即使在初始请求中,你也不能使用本地事件源 API 在 http-body 中发送类似于 POST 的数据。取而代之的是,你必须将所有数据放在 url 参数中,这被认为是一种不利于安全的做法,因为凭据可能会泄露到服务器日志、代理和缓存中。为了解决这个问题,RxDB 使用 eventsource polyfill 代替本地 EventSource API。该库增加了发送自定义 http 头信息等额外功能。此外,微软还提供了一个库,允许发送正文数据并使用 POST 请求代替 GET。

每个域6个请求限制

大多数现代浏览器允许每个域有六个连接,这限制了所有稳定的 服务器-客户端 消息传递方法的可用性。6 个连接的限制甚至可以在浏览器标签页中共享,因此当在多个标签页中打开同一个页面时,它们必须相互共享 6 个连接池。这一限制是 HTTP/1.1-RFC 的一部分(它甚至定义了更低的连接数,即只有两个连接)。

引自 RFC 2616 - 第 8.1.4 节:
"Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. A proxy SHOULD use up to 2*N connections to another server or proxy, where N is the number of simultaneously active users. These guidelines are intended to improve HTTP response times and avoid congestion."

“使用持久连接的客户端应限制与特定服务器的同时连接数。单用户客户端不应与任何服务器或代理保持超过 2 个连接。代理应最多使用 2*N 个连接到另一个服务器或代理,其中 N 为同时活动的用户数量。这些准则旨在改善 HTTP 响应时间,避免拥塞。

虽然这一策略可以防止网站所有者利用其访问者对其他网站进行 D-DOS 操作,但当合法使用情况下需要多个连接来处理服务器-客户端通信时,这就会成为一个大问题。为了解决这一限制,您必须使用 HTTP/2 或 HTTP/3,浏览器将只为每个域打开一个连接,然后使用多路复用技术通过单个连接运行所有数据。虽然这样可以提供几乎无限量的并行连接,但 SETTINGS_MAX_CONCURRENT_STREAMS 设置会限制实际连接数。大多数配置的默认并发流数为 100。

理论上,浏览器也可以增加连接数限制,至少对于特定的 API(如 EventSource),但这些问题已被 chromium 和 Firefox 标记为 “不会修复”。

手机应用程序无法保持开放连接

对于在 Android 和 iOS 等操作系统上运行的手机应用程序来说,保持开放连接(如 WebSockets 等使用的连接)是一项重大挑战。手机操作系统的设计目的是在应用程序闲置一段时间后自动将其移至后台,从而有效关闭所有打开的连接。这种行为是操作系统资源管理策略的一部分,目的是节省电池和优化性能。因此,开发人员通常依赖手机推送通知作为从服务器向客户端发送数据的高效可靠方法。推送通知允许服务器向应用程序发出新数据警报,提示操作或更新,而无需持续开放连接。

代理和防火墙

在企业环境(又称 “工作环境”)中,通常很难在基础设施中安装 WebSocket 服务器,因为许多代理和防火墙会阻止非HTTP 连接。因此,使用服务器发送事件(Server-Sent-Events)提供了更简便的企业集成方式。此外,长轮询只使用纯 HTTP 请求,也是一种选择。

延迟

  • WebSockets: 通过单个持久连接进行全双工通信,因此延迟最低。非常适合需要即时数据交换的实时应用。
  • 服务器发送事件(SSE): 也能为服务器到客户端的通信提供低延迟,但在没有额外 HTTP 请求的情况下,无法将信息发送回服务器。
  • 长时间轮询: 延迟较高,因为每次数据传输都需要建立新的 HTTP 连接,因此实时更新的效率较低。此外,当客户端仍在打开新连接时,服务器要发送事件的情况也可能发生。在这种情况下,延迟会大大增加。
  • WebTransport 承诺提供与 WebSockets 类似的低延迟,并具有利用 HTTP/3 协议实现更高效的多路复用和拥塞控制的额外优势。

吞吐量

WebSockets: 但吞吐量会受到反向压力的影响,即客户端处理数据的速度赶不上服务器发送数据的速度。
服务器发送事件(SSE): 可高效地向许多客户端广播信息,开销比 WebSockets 少,从而可能提高服务器到客户端单向通信的吞吐量。
长时间轮询: 由于频繁打开和关闭连接会消耗更多服务器资源,因此吞吐量一般较低。
WebTransport: 预计可在单个连接内支持单向和双向流的高吞吐量,在需要多个流的情况下性能优于 WebSockets。

可扩展性和服务器负载

WebSockets 维持大量 WebSocket 连接会大大增加服务器负载,可能会影响拥有众多用户的应用程序的可扩展性。
服务器发送的事件(SSE): 与 WebSockets 相比,它使用的是 “正常的 ”HTTP 请求,而无需像 WebSockets 那样运行协议更新,因此使用的连接开销更少。
长轮询: 由于频繁建立连接会产生较高的服务器负载,因此它的扩展性最差,只能作为一种备用机制。
WebTransport 利用 HTTP/3 在处理连接和流方面的高效性,设计为高度可扩展,与 WebSockets 和 SSE 相比,可减少服务器负载。