什么是WebSockets、服务器发送事件、长轮询、WebRTC、WebTransport?

对于现代实时网络应用程序来说,从服务器向客户端发送事件的能力是不可或缺的。多年来,人们根据这种需要开发了多种方法,每种方法都有自己的优点和缺点。

  • 最初,长轮询是唯一可用的方法。
  • 随后,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 => {
           
/**
             * 在正常情况下,当连接超时或客户端离线时,可能会出现错误。
             * 出现错误时,我们会在延迟一段时间后重新开始轮询。
             */

            setTimeout(longPoll, 10000);
        });
}
longPoll();
// Initiate the long polling

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

什么是 WebSockets?
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 的基本原理很容易使用,但在生产中却显得相当复杂。
套接字可能会失去连接,因此必须相应地重新创建。尤其是检测连接是否仍然可用,可能非常棘手。
大多数情况下,你需要添加一个 "乒乓心跳 "来确保打开的连接没有关闭。
这种复杂性正是大多数人在 WebSockets 上使用 Socket.IO 等库的原因,这些库可以处理所有这些情况,甚至在需要时提供长时间轮询的后备功能。

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

你可以把服务器-发送-事件看作是一个单一的 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);
};

与 WebSockets 不同的是,EventSource 会在连接丢失时自动重新连接。

在服务器端,您的脚本必须将 Content-Type 标头设置为文本/事件流,并根据 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,旨在实现网络客户端和服务器之间高效、低延迟的通信。它利用 HTTP/3 QUIC 协议实现了多种数据传输功能,如通过多个流以可靠或不可靠的方式发送数据,甚至允许不按顺序发送数据。

这使得 WebTransport 成为需要高性能网络的应用(如实时游戏、实时流媒体和协作平台)的强大工具。

不过,值得注意的是,WebTransport 目前只是一个工作草案,尚未得到广泛采用。截至目前(2024 年 3 月),WebTransport 仍处于工作草案阶段,尚未得到广泛支持。

您还不能在 Safari 浏览器中使用 WebTransport,Node.js 中也没有本机支持。这限制了它在不同平台和环境中的可用性。

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

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

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

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

局限性
1、双向发送数据
只有 WebSockets 和 WebTransport 允许双向发送数据,因此您可以通过同一连接接收服务器数据和发送客户端数据。

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

服务器发送事件(Server-Sent-Events)不支持向服务器发送任何附加数据。你只能执行初始请求,而且即使在初始请求中,你也不能使用本地事件源 API 在 http-body 中发送类似于 POST 的数据。取而代之的是,你必须将所有数据放在 url 参数中,这被认为是一种不利于安全的做法,因为凭据可能会泄露到服务器日志、代理和缓存中。

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

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

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

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

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

延迟:

  • WebSockets:由于其通过单个持久连接进行全双工通信,因此提供最低的延迟。非常适合即时数据交换至关重要的实时应用程序。
  • 服务器发送的事件:还为服务器到客户端的通信提供低延迟,但如果没有额外的 HTTP 请求,则无法本机将消息发送回服务器。
  • 长轮询:由于每次数据传输都依赖于建立新的 HTTP 连接,因此会产生较高的延迟,从而降低实时更新的效率。当客户端仍在打开新连接的过程中时,服务器也可能想要发送事件。在这些情况下,延迟会明显变大。
  • WebTransport:承诺提供类似于 WebSocket 的低延迟,并具有利用 HTTP/3 协议实现更高效的多路复用和拥塞控制的额外优势。

吞吐量:

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

可扩展性和服务器负载

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

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

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

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

长轮询Long-Polling 曾是一种常用技术,但由于其效率低下,而且重复建立新 HTTP 连接的开销很大,现在已基本过时。虽然在不支持 WebSockets 或 SSE 的环境中,它可以作为一种备用技术,但由于其性能受到很大限制,一般不建议使用。

已知问题
所有实时流媒体技术都存在已知问题。当你在它们的基础上构建任何东西时,请记住这些问题。

客户端重新连接时可能会错过事件
当客户端正在连接、重新连接或离线时,可能会错过服务器上发生但无法流式传输到客户端的事件。当服务器每次都在流式传输完整内容(如实时更新的股票行情)时,这种漏掉的事件并不重要。但当后端要对部分结果进行流式处理时,就必须考虑漏报事件。如果在后端解决这个问题,规模会非常大,因为后端必须记住每个客户端已经成功发送了哪些事件。相反,这应该通过客户端逻辑来实现。


公司防火墙可能导致问题
在使用任何流媒体技术时,公司基础设施都会出现许多已知的问题。代理和防火墙可能会阻止流量或无意中破坏请求和响应。无论何时在这样的基础设施中实施实时应用程序,请确保首先测试技术本身是否适合您。