探讨了 WebSockets、Server-Sent Events、Long-Polling、WebRTC 和 WebTransport 在实时 Web 应用中的关键作用和优劣势。通过比较它们的性能、优势和局限性,为开发者在构建实时 Web 应用时提供了建议。同时介绍了 RxDB 复制协议的兼容性实践经验。
译者:@飘飘
作者:@RxDB
原文:https://rxdb.info/articles/websockets-sse-polling-webrtc-webtransport.html
对于现代实时网络应用程序来说,从服务器向客户端发送事件的能力是必不可少的。这一需求促使人们在过去的几年中开发了几种方法,每种方法都有其自身的优势和局限性。最初,唯一可用的方法是长轮询。随后,WebSockets 取而代之,为双向通信提供了更强大的解决方案。继 WebSockets 之后,Server-Sent Events(SSE)提供了一种更简单的从服务器到客户端的单向通信方法。展望未来,WebTransport 协议有望进一步改变这一局面,提供更高效、灵活和可扩展的方法。对于特定的用例,WebRTC 也可能被考虑用于服务器到客户端的事件传输。
本文旨在深入探讨这些技术,比较它们的性能,突出它们的优势和局限性,并针对各种使用场景提出建议,帮助开发人员在构建实时网络应用时做出明智的决策。这是作者在将 RxDB 复制协议与各种后端技术兼容时积累经验的浓缩总结。
![WebSockets、服务器推送事件、Long-Polling、WebRTC、WebTransport对比](https://www.nxrte.com/wp-content/themes/justnews/themer/assets/images/lazy.png)
长轮询(Long-Polling)是什么?
长轮询是第一个可以在浏览器中通过 HTTP 实现服务器 – 客户端消息传输的 “黑客” 技术。该技术通过普通的 XHR 请求模拟服务器推送通信。与传统的轮询(客户端以固定的时间间隔反复向服务器请求数据)不同,长轮询会建立一个与服务器的连接,并保持该连接打开,直到有新的数据可用。一旦服务器有了新的信息,就会向客户端发送响应,然后关闭连接。客户端在收到服务器的响应后立即发起新的请求,然后重复上述过程。这种方法可以更即时地更新数据,减少不必要的网络流量和服务器负载。不过,这种方法仍然会造成通信延迟,并且不如其他如 WebSockets 之类的实时技术高效。
// long-polling in a JavaScript client
function longPoll() {
fetch(http://example.com/poll)
.then(response => response.json())
.then(data => {
console.log("Received data:", data);
longPoll(); // Immediately establish a new long polling request
})
.catch(error => {
/**
* Errors can appear in normal conditions when a
* connection timeout is reached or when the client goes offline.
* On errors we just restart the polling after some delay.
*/
setTimeout(longPoll, 10000);
});
}
longPoll(); // Initiate the long polling
前端端实现长轮询非常简单,如上面的代码所示。然而在后端,可能存在多个困难,以确保客户端接收到所有事件,并在客户端正在重新连接时不会错过更新。
WebSockets 是什么?
WebSockets 可以在客户端和服务器之间建立一个持久的连接,提供全双工通信通道。这项技术使浏览器和服务器能够在不经过 HTTP 请求 – 响应循环的开销的情况下交换数据,为实时数据传输提供了便利,例如实时聊天、游戏或金融交易平台等应用。与传统的 HTTP 相比,WebSockets 的一大进步在于允许双方在连接建立后独立发送数据,这使得它非常适合需要低延迟和高频率更新的场景。
// WebSocket in a JavaScript client
const socket = new WebSocket(ws://example.com);
socket.onopen = function(event) {
console.log(Connection established);
// Sending a message to the server
socket.send(Hello Server!);
};
socket.onmessage = function(event) {
console.log(Message from server:, event.data);
};
虽然 WebSocket API 的基本用法很容易,但在生产环境中却显得相当复杂。一个套接字可能会丢失连接,必须相应地重新创建。尤其是检测一个连接是否可用,可能非常棘手。通常,您会添加一个 ping-and-pong 心跳机制,以确保已打开的连接不会被关闭。正是因为这些复杂性,大多数人在 WebSockets 之上使用 Socket.IO 等库,这些库处理所有这些情况,甚至在需要时提供对长轮询的回退。
服务器发送事件(SSE)是什么?
服务器发送事件(SSE)提供了一种通过 HTTP 将服务器更新推送到客户端的标准方式。与 WebSockets 不同,SSE 仅用于服务器到客户端的单向通信,因此非常适合用于实时新闻推送、体育比分等场景,以及任何需要客户端实时更新但无需向服务器发送数据的情况。
你可以把 Server-Sent-Events 看作是一个单一的 HTTP 请求,在这个请求中,后端不会一次性发送整个正响应体,而是保持连接开放,每次有事件需要发送给客户端时,只发送一条记录。
使用 SSE 创建接收事件的连接非常简单。在浏览器客户端,您可以使用服务器端脚本生成事件的 URL 初始化一个 EventSource 实例。
监听消息需要将事件处理程序直接附加到 EventSource
实例上。API 区分了通用消息事件和命名事件,这使得通信更加结构化。以下是在 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);
};
与 WebSockets 不同的是,EventSource 会在连接丢失时自动重新连接。
在服务器端,您的脚本必须将 Content-Type
标头设置为 text/event-stream
,并根据 SSE 规范格式化每条信息。这包括指定事件类型、数据有效载荷以及事件 ID 和重试时间等可选字段。
下面是如何在 Node.js Express 应用程序中设置一个简单的 SSE 端点:
import express from express;
const app = express();
const PORT = process.env.PORT || 3000;
app.get(/events, (req, res) => {
res.writeHead(200, {
Content-Type: text/event-stream,
Cache-Control: no-cache,
Connection: keep-alive,
});
const sendEvent = (data) => {
// all message lines must be prefixed with data:
const formattedData = `data: ${JSON.stringify(data)}\n\n`;
res.write(formattedData);
};
// Send an event every 2 seconds
const intervalId = setInterval(() => {
const message = {
time: new Date().toTimeString(),
message: Hello from the server!,
};
sendEvent(message);
}, 2000);
// Clean up when the connection is closed
req.on(close, () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
WebTransport API 是什么?
WebTransport 是一种先进的 API,旨在旨在为 Web 客户端和服务器之间提供高效、低延迟的通信。它利用 HTTP/3 QUIC 协议实现了多种数据传输功能,例如以可靠和不可靠的方式同时传输多条数据流,甚至允许数据以乱序的方式发送。这使得 WebTransport 成为需要高性能网络的应用程序(如实时游戏、流媒体和协作平台)的强大工具。不过,值得注意的是,WebTransport 目前仍然只是一个工作草案,尚未得到广泛采用。截至目前(2024 年 3 月),WebTransport 仍处于工作草案阶段,尚未得到广泛支持。您还不能在 Safari 浏览器中使用 WebTransport,Node.js 中也没有原生支持。这限制了 WebTransport 在不同平台和环境中的可用性。
即使 WebTransport 在未来得到广泛支持,其 API 的使用也非常复杂,人们很可能会在 WebTransport 的基础上构建库,而不是直接在应用程序的源代码中使用它。
什么是 WebRTC?
WebRTC (网络实时通信)是一个开源项目和 API 标准,它使实时通信(RTC)功能可以直接在网页浏览器和移动应用程序中使用,无需复杂的服务器基础架构或安装额外的插件。它支持浏览器之间的点对点连接,用于流式传输音频、视频和数据交换。WebRTC 设计用于在网络地址转换(NAT)和防火墙环境中工作,并使用 ICE、STUN 和 TURN 等协议在对等方之间建立连接。
尽管 WebRTC 旨在用于客户端之间的交互,但它也可用于服务器与客户端之间的通信,即服务器只是模拟客户端。这种方法仅适用于特定的用例,因此在接下来的讨论中,我们将不考虑 WebRTC 作为一种选择。
问题是,要让 WebRTC 正常工作,仍然需要一个信号服务器,而这个服务器又需要通过 WebSocket、SSE 或 WebTransport 来运行。这样一来,使用 WebRTC 来替代这些技术的意义也就不大了。
技术的局限性
双向传输数据
只有 WebSockets 和 WebTransport 允许双向发送数据,这样您可以在同一连接上接收服务器数据并向客户端发送数据。
虽然理论上可以使用长轮询(Long-Polling)技术实现这一点,但不推荐这样做,因为向现有的长轮询连接发送 “新” 数据仍需要进行额外的 HTTP 请求。因此,你可以直接从客户端向服务器发送数据,同时发送一个额外的 HTTP 请求,而不会中断长轮询连接。
服务器发送事件(Server-Sent-Events)不支持向服务器发送任何附加数据。你只能执行初始请求,甚至在默认情况下,您也无法使用原生的 EventSource API 向 HTTP 主体发送类似 POST 的数据。相反,您必须将所有数据放入 URL 参数中,这在安全性方面被视为一种不良做法,因为凭据可能会泄露到服务器日志、代理和缓存中。为了解决这个问题,RxDB 使用 eventsource polyfill 代替本地 EventSource API。该库增加了发送自定义 http 头信息等额外功能。此外,微软还提供了一个库,允许发送正文数据并使用 POST 请求代替 GET。
每个域 6 个请求的限制
大多数现代浏览器允许每个域建立六个连接,这限制了所有稳定的服务器到客户端消息传递方法的可用性。6 个连接的限制甚至可以在浏览器标签页中共享,因此当您在多个标签页中打开同一个页面时,它们必须相互共享 6 个连接池。这一限制是 HTTP/1.1-RFC 的一部分(它甚至定义了更低的连接数,即只有两个连接)。
来自 RFC 2616 – 第 8.1.4 节:“使用持久连接的客户端应该限制与某个服务器同时保持的连接数量。单用户客户端不应该与任何服务器或代理同时保持超过 2 个连接。代理应该使用多达 2*N 个连接与另一台服务器或代理通信,其中 N 是同时活跃用户的数量。这些指导方针旨在改善 HTTP 响应时间并避免拥塞。”
虽然这种策略有助于防止网站所有者利用其访客进行分布式拒绝服务攻击(D-DOS)其他网站,但在需要多个连接来处理服务器与客户端之间的通信的合法用例中,这可能会成为一个大问题。为了解决这一限制,您必须使用 HTTP/2 或 HTTP/3,浏览器将只为每个域名只打开一个连接,然后使用多路复用技术通过单个连接运行所有数据。虽然这为您提供了几乎无限数量的并发连接,但有一个 SETTINGS_MAX_CONCURRENT_STREAMS 设置限制了实际连接的数量。大多数配置的默认值为 100 个并发流。
理论上,浏览器也可以增加连接数限制,至少对于特定的 API(如 EventSource),但这些问题已被 chromium 和 Firefox 标记为 “不会修复”。
降低浏览器应用程序的连接数
当你构建一个浏览器应用程序时,你必须假定用户不仅会打开一次应用程序,还会在多个浏览器标签页中同时打开多个应用程序。默认情况下,你可能会为每个标签页打开一个服务器流连接,但这通常是不必要的。相反,你只需打开一个连接,并在所有打开的标签页之间共享该连接。无论同时打开多少个标签页,RxDB 都使用广播通道 npm 包中的 LeaderElection 来实现服务器和客户端之间只有一个复制流。你可以在不使用 RxDB 的情况下单独使用该包,以满足任何类型的应用程序需求。
移动应用不会保持连接状态
对于在 Android 和 iOS 等操作系统上运行的移动应用程序来说,保持开放连接(如 WebSockets 和其他连接)是一个巨大的挑战。移动操作系统的设计目的是在一定时间内将应用程序自动移至后台,从而有效地关闭任何打开的连接。这种行为是操作系统资源管理策略的一部分,旨在是节省电池和优化性能。因此,开发人员通常依赖移动推送通知作为从服务器向客户端发送数据的高效可靠方法。推送通知允许服务器在不需要持久连接的情况下通知应用程序有新数据,并触发相应的操作或更新。
代理和防火墙
通过与许多 RxDB 用户的交流,我们发现在企业环境(又称 “工作环境”)中,很难将 WebSocket 服务器集成到基础设施中,因为许多代理和防火墙会阻止非 HTTP 连接。因此,使用 Server-Sent-Events 提供了一种更易于企业集成的方法。此外,长轮询仅使用普通的 HTTP 请求,也可以作为一种选择。
性能比较
要直接比较 WebSockets、服务器发送事件(SSE)、长轮询和 WebTransport 的性能,需要评估各种条件下的延迟、吞吐量、服务器负载和可扩展性等关键方面。
首先让我们来看一下原始数据。在这个使用 Go 语言实现的服务器端测试中,我们可以找到一个良好的性能比较。从中我们可以看到,WebSockets、WebRTC 和 WebTransport 的性能相当:
![WebSockets、服务器推送事件、Long-Polling、WebRTC、WebTransport对比](https://www.nxrte.com/wp-content/themes/justnews/themer/assets/images/lazy.png)
注意
请记住,WebTransport 是基于全新的 HTTP/3 协议的一种较新的技术。在未来(2024 年 3 月之后),可能会有更多的性能优化。此外,WebTransport 还旨在降低功耗,但目前尚未进行相关测试。
我们也来比较延迟、吞吐量和可扩展性:
延迟
- WebSockets:由于其在单个持久连接上的全双工通信,提供了最低的延迟。适用于需要即时数据交换的实时应用。
- 服务器发送事件:也能为服务器到客户端的通信提供低延迟,但在没有额外 HTTP 请求的情况下,无法将信息发送回服务器。
- 长轮询:延迟较高,因为每次数据传输都需要建立新的 HTTP 连接,因此实时更新的效率较低。此外,当客户端正在建立新的连接时,服务器可能会发送事件。在这些情况下,延迟会显著增大。
- WebTransport:承诺提供与 WebSockets 类似的低延迟,并具有利用 HTTP/3 协议实现更高效的多路复用和拥塞控制的额外优势。
吞吐量
- WebSockets:由于其持久连接的特点,具有较高的吞吐量,但由于客户端无法及时处理服务器发送的数据,可能会出现拥塞,导致吞吐量下降。
- 服务器发送事件:对于向许多客户端广播消息,其效率比 WebSockets 更高,从而可能在单向服务器到客户端通信中实现更高的吞吐量。
- 长轮询:由于频繁打开和关闭连接的开销较大,通常会导致较低的吞吐量,并消耗更多的服务器资源。
- WebTransport:预期能够在一个连接中支持高吞吐量的单向和双向流,在需要多个流的场景中表现优于 WebSockets。
可扩展性和服务器负载
- WebSockets:维护大量 WebSocket 连接会会显著增加服务器负载,对于拥有大量用户的应用程序来说,这可能会影响其可扩展性。
- 服务器发送的事件:对于主要需要服务器向客户端推送更新的场景更为适用,因为它比 WebSockets 使用的连接开销更少,因为它使用 “常规” HTTP 请求,而无需像 WebSockets 那样运行协议更新等操作。
- 长轮询:由于频繁建立连接会给服务器带来较高的负载,因此其可扩展性最差,仅作为一种备用机制。
- WebTransport:设计用于高度可扩展,受益于 HTTP/3 在处理连接和流方面的高效率,理论上可以比 WebSockets 和 SSE 减少服务器负载。
建议和用例适用性
在服务器与客户端通信技术的领域中,每种技术都有其独特的优势和适用性。服务器推送事件(Server-Sent Events,简称 SSE)是实现最直接的选择,它利用与传统网页请求相同的 HTTP/S 协议,从而绕过了企业防火墙限制和其他可能出现的技术问题。SSE 很容易与 Node.js 和其他服务器框架集成,因此它是适用于需要频繁进行服务器到客户端更新的应用程序的理想选择,例如新闻提要、股票报价和实时活动流。
另一方面,WebSockets 在需要持续双向通信的场景中表现出色。WebSockets 支持持续交互的能力使其成为浏览器游戏、聊天应用和实时体育更新的首选。
然而,尽管 WebTransport 潜力巨大,但在应用上却面临挑战。它尚未被包括 Node.js 在内的服务器框架广泛支持,且与 Safari 不兼容。此外,WebTransport 对 HTTP/3 的依赖进一步限制了其直接适用性,因为许多 Web 服务器(如 nginx)仅提供实验性的 HTTP/3 支持。尽管 WebTransport 在支持可靠和不可靠数据传输方面具有潜力,但对于大多数用例来说,它目前还不是一个可行的选择。
长轮询 Long-Polling 曾是一种常用技术,但由于其效率低下以及频繁建立新 HTTP 连接的高开销,现在已基本过时。尽管在缺少 WebSockets 或 SSE 支持的环境中,它可以作为一种备用方案,但由于其显著的性能限制,通常不建议使用。
已知问题
对于所有实时流媒体技术来说,都存在一些已知的问题。如果您在它们之上构建任何东西,请务必记住这些问题。
客户端重新连接时,可能会错过一些事件
当客户端正在连接、重新连接或离线时,可能会错过服务器上发生但无法流式传输到客户端的事件。当服务器每次都以流式传输完整内容(如实时更新的股票行情)时,这种漏掉的事件并不重要。但当后端要对部分结果进行流式处理时,就必须考虑遗漏事件。在后端进行修复将导致性能非常糟糕,因为后端必须为每个客户端记住哪些事件已经成功发送。相反,应该在客户端实现此逻辑。
例如,RxDB 复制协议为此使用了两种操作模式。其中一种是检查点迭代模式,在此模式下,使用普通 http 请求来遍历后端数据,直到客户端再次同步。然后,它可以切换到事件观察模式,利用实时流的更新来保持客户端同步。每当客户端断开连接或出现任何错误,复制协议就会很快切换到检查点迭代模式,直到客户端再次同步。这种方法会考虑到遗漏的事件,并确保客户端始终能同步到与服务器完全相同的状态。
公司防火墙可能导致问题
当使用任何一种流式技术时,公司基础架构中存在许多已知问题。代理服务器和防火墙可能会阻塞流量或无意中破坏请求和响应。因此,在该基础架构中实现实时应用时,请务必首先测试该技术是否适合您的需求。
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。