WebSockets、服务器发送事件、长轮询、WebRTC、WebTransport

597 阅读17分钟
  1. 概述

对于现代实时 Web 应用程序,从服务器向客户端发送事件的能力是必不可少的。这种必要性导致多年来开发了几种方法,每种方法都有自己的优点和缺点。

最初,长轮询 是唯一可用的选项。后来,WebSockets取代了它,为双向通信提供了更强大的解决方案。继 WebSockets 之后,服务器发送事件 (SSE)为从服务器到客户端的单向通信提供了一种更简单的方法。展望未来,WebTransport协议有望通过提供更高效、更灵活、更可扩展的方法进一步彻底改变这一格局。对于小众用例,WebRTC也可能适用于服务器-客户端事件。

什么是长轮询?

长轮询是第一个启用服务器-客户端消息传递方法的“黑客技术”,该方法可通过 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

在客户端实现长轮询非常简单,如上面的代码所示。然而在后端,要确保客户端收到所有事件,并且在客户端当前重新连接时不会错过更新,可能会遇到很多困难。

 什么是 WebSocket?

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

// 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心跳以确保打开的连接不会关闭。这种复杂性就是为什么大多数人使用 WebSocket 上的库(如Socket.IO)的原因,它可以处理所有这些情况,甚至在需要时提供长轮询的回退。

什么是服务器发送事件?

服务器发送事件 (SSE) 提供了一种通过 HTTP 将服务器更新推送到客户端的标准方法。与 WebSocket 不同,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 得到广泛支持,它的 API 使用起来也非常复杂,人们很可能会在 WebTransport 之上构建库,而不是直接在应用程序的源代码中使用它。

 什么是 WebRTC?

WebRTC(Web 实时通信)是一个开源项目和 API 标准,它可以直接在 Web 浏览器和移动应用程序中实现实时通信 (RTC) 功能,而无需复杂的服务器基础架构或安装其他插件。它支持点对点连接,用于在浏览器之间流式传输音频、视频和数据交换。WebRTC 旨在通过 NAT 和防火墙工作,利用 ICE、STUN 和 TURN 等协议在对等端之间建立连接。

虽然 WebRTC 是为客户端与客户端交互而设计的,但它也可以用于服务器与客户端通信,其中服务器只是模拟客户端。这种方法只适用于小众用例,这就是为什么在下文中 WebRTC 将被忽略的原因。

问题是,要使 WebRTC 正常工作,无论如何您都需要一个信令服务器,而该服务器又会通过 websockets、SSE 或 WebTransport 运行。这违背了使用 WebRTC 替代这些技术的目的。

  1. 技术的局限性

    双向发送数据

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

    虽然理论上使用长轮询也可以实现这一点,但不建议这样做,因为向现有长轮询连接发送“新”数据无论如何都需要执行额外的 http 请求。因此,您可以直接通过额外的 http 请求从客户端向服务器发送数据,而无需中断长轮询连接,而不必这样做。

    Server-Sent-Events不支持向服务器发送任何其他数据。您只能发出初始请求,而且即使如此,您也无法默认使用原生EventSource API在 http-body 中发送 POST 类数据。相反,您必须将所有数据放在 url 参数中,这被认为是一种不好的安全做法,因为凭据可能会泄露到服务器日志、代理和缓存中。为了解决这个问题,例如, RxDB使用eventsource polyfill而不是原生的EventSource API。这个库增加了额外的功能,比如发送自定义 http 标头。此外,还有来自微软的这个库,它允许发送正文数据并使用POST请求而不是GET

    每个域名限制

    大多数现代浏览器允许每个域有六个连接(),这限制了所有稳定的服务器到客户端消息传递方法的可用性。六个连接的限制甚至在浏览器选项卡之间共享,因此当您在多个选项卡中打开同一页面时,它们必须彼此共享六个连接池。此限制是 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 个并发流。

    理论上,浏览器也可以增加连接限制,至少对于像 EventSource 这样的特定 API 来说是这样,但这些问题已被chromiumfirefox标记为“无法修复” 。

    降低浏览器应用中的连接数量

    构建浏览器应用程序时,您必须假设用户不仅会使用该应用程序一次,还会在多个浏览器选项卡中同时使用该应用程序。默认情况下,您可能会为每个选项卡打开一个服务器流连接,而这通常根本没有必要。相反,无论打开了多少个选项卡,您都只需打开一个连接并在选项卡之间共享它。RxDB 使用来自广播通道 npm 包的 LeaderElection 来实现这一点,以便在服务器客户端之间只有一个复制流。您可以将该包独立(无需 RxDB)用于任何类型的应用程序。

    移动应用程序

    对于在 Android 和 iOS 等操作系统上运行的移动应用程序而言,维护开放连接(例如用于 WebSockets 和其他协议的连接)是一项重大挑战。移动操作系统旨在在应用程序处于不活动状态一段时间后自动将其移至后台,从而有效地关闭所有开放连接。此行为是操作系统资源管理策略的一部分,旨在节省电池电量并优化性能。因此,开发人员通常依赖移动推送通知作为一种高效可靠的方法,将数据从服务器发送到客户端。推送通知允许服务器向应用程序发出新数据的警报,提示其执行操作或更新,而无需持续开放连接。

    代理和防火墙

    通过咨询许多RxDB用户,我们发现在企业环境(即“工作环境”)中,通常很难将 WebSocket 服务器实现到基础架构中,因为许多代理和防火墙会阻止非 HTTP 连接。因此,使用 Server-Sent-Events 可以更轻松地实现企业集成。此外,长轮询仅使用普通 HTTP 请求,可能是一种选择。

  2. 性能比较

    比较 WebSockets、服务器发送事件 (SSE)、长轮询和 WebTransport 的性能直接涉及评估各种条件下的延迟、吞吐量、服务器负载和可扩展性等关键方面。

    首先让我们看看原始数据。在这个 repo中可以找到一个很好的性能比较,它测试了Go Lang服务器实现中的消息时间。在这里我们可以看到 WebSockets、WebRTC 和 WebTransport 的性能是相当的:

    WebSocket WebRTC WebTransport 性能

    笔记

    请记住,WebTransport 是一项相当新的技术,它基于同样新的 HTTP/3 协议。未来(2024 年 3 月之后)可能会进行更多性能优化。此外,WebTransport 还经过优化以减少功耗,但尚未测试该指标。

    让我们比较一下延迟、吞吐量和可扩展性:

    延迟
    • WebSockets:通过单一持久连接进行全双工通信,可提供最低的延迟。非常适合需要即时数据交换的实时应用程序。
    • 服务器发送事件:也为服务器到客户端的通信提供了低延迟,但如果没有额外的 HTTP 请求,则无法将消息本地发送回服务器。
    • 长轮询:由于它依赖于为每次数据传输建立新的 HTTP 连接,因此会导致更高的延迟,从而降低实时更新的效率。此外,当客户端仍在打开新连接时,服务器可能会想要发送事件。在这些情况下,延迟会明显增大。
    • WebTransport:承诺提供与 WebSockets 类似的低延迟,同时利用 HTTP/3 协议实现更高效的多路复用和拥塞控制。
    吞吐量
    • WebSockets:由于其具有持久连接,因此具有高吞吐量,但是吞吐量可能受到影响,即客户端处理数据的速度不能像服务器发送数据的速度那样快。
    • 服务器发送事件:能够以比 WebSocket 更少的开销向许多客户端广播消息,从而可能提高单向服务器到客户端通信的吞吐量。
    • 长轮询:通常提供较低的吞吐量,因为频繁打开和关闭连接的开销会消耗更多的服务器资源。
    • WebTransport:预计在单个连接内支持单向和双向流的高吞吐量,在需要多个流的场景中表现优于 WebSockets。
    可扩展性和服务器负载
    • WebSockets:维护大量 WebSocket 连接会显著增加服务器负载,可能会影响具有许多用户的应用程序的可扩展性。
    • 服务器发送事件:对于主要需要从服务器到客户端更新的场景更具可扩展性,因为它使用比 WebSockets 更少的连接开销,因为它使用“正常”HTTP 请求,而不需要使用 WebSockets 运行的协议更新等。
    • 长轮询:由于频繁建立连接会导致服务器负载过高,因此可扩展性最差,仅适合作为后备机制。
    • WebTransport:设计为高度可扩展,受益于 HTTP/3 处理连接和流的效率,与 WebSockets 和 SSE 相比,可以潜在地减少服务器负载。
  3. 建议和用例适用性

    在服务器-客户端通信技术领域,每种技术都有其独特的优势和用例适用性。服务器发送事件(SSE) 是最直接的实现选项,它利用与传统 Web 请求相同的 HTTP/S 协议,从而绕过企业防火墙限制和其他协议可能出现的其他技术问题。它们很容易集成到 Node.js 和其他服务器框架中,使其成为需要频繁进行服务器到客户端更新的应用程序的理想选择,例如新闻提要、股票行情和实时事件流。

    另一方面,WebSocket在需要持续双向通信的场景中表现出色。它们支持持续交互的能力使其成为浏览器游戏、聊天应用程序和体育直播更新的首选。

    然而,尽管WebTransport潜力巨大,但它仍面临采用方面的挑战。它并未得到包括 Node.js 在内的服务器框架的广泛支持,并且与Safari缺乏兼容性。此外,它对 HTTP/3 的依赖进一步限制了它的直接适用性,因为许多 WebServer(如 nginx)仅具有实验性的HTTP/3 支持。尽管 WebTransport 支持可靠和不可靠的数据传输,有望成为未来应用的理想选择,但对于大多数用例而言,它还不是一个可行的选择。

    长轮询曾经是一种常用技术,但由于其效率低下以及反复建立新的 HTTP 连接的高开销,现在已经基本过时了。虽然它可以在缺乏对 WebSockets 或 SSE 支持的环境中作为后备方案,但由于其性能限制严重,因此通常不建议使用它。

  4. 已知问题

    对于所有实时流式传输技术,都存在一些已知问题。当你在这些技术上构建任何东西时,请记住这些问题。

    客户端在重新连接

    当客户端正在连接、重新连接或离线时,它可能会错过服务器上发生但无法传输到客户端的事件。当服务器每次都传输完整内容时,这些错过的事件并不重要,例如实时更新股票行情。但是当后端传输部分结果时,您必须考虑错过的事件。在后端修复这个问题会非常糟糕,因为后端必须记住每个客户端已经成功发送了哪些事件。相反,这应该用客户端逻辑来实现。

    例如,RxDB 复制协议为此使用了两种操作模式。一种是检查点迭代模式,其中使用正常的 http 请求迭代后端数据,直到客户端再次同步。然后它可以切换到事件观察模式,其中使用来自实时流的更新来保持客户端同步。每当客户端断开连接或出现任何错误时,复制都会立即切换到检查点迭代模式,直到客户端再次同步。此方法考虑了遗漏的事件并确保客户端始终可以同步到服务器的完全相同的状态。

    公司防火墙可能会造成问题

    使用任何流媒体技术时,公司基础设施都存在许多已知问题。代理和防火墙可能会阻止流量或无意中中断请求和响应。每当您在这样的基础设施中实现实时应用时,请务必先测试该技术本身是否适合