前言
最近,公司有一个AI项目,要做一个文档问答的AI产品。前端部分呢,还是「友好借鉴」ChatGPT。别问为什么,问就是要站在巨人的肩膀上进行「带有中国特色」的创新。而后端是接入我们团队的模型,我咨询过模型团队,也是基于开源模型做参数的微调,这个魔幻的世界真让人欲罢不能。这就是大概的业务背景。
针对前端部分,其实没啥可聊的,就是接入模型返回的数据然后进行展示处理。大家以为这就是一个简单到令人发指的功能时。有一个点却映入眼帘,如何才能实现类似ChatGPT结果展示效果(逐步输出结果,类似打字效果)。也就是在结果返回的时候,如何做打字效果。
此外,还有一个大背景就是,由于需求是可能要上传多个文件,并且模型那边的操作可能对文档解析有一定的难度。所以,在客户端发起请求时,可能投喂给模型的物料有点多,返回的结果的时间也会很长。也就是如果处理不当的话,在结果没返回之前或者一股脑把结果处理完再返回的话,前端会有一段很长的等待时间。
从上面的需求点和解决方案,我们不难看出,其实结果的展示(打字效果)不是一个难点,我们可以借助简单的库或者手搓一个打字效果都是可以的,而是数据的获取制约我们应用响应。
我们又可以按照数据的发起方是谁(客户端/服务端)
- 基于最原始的数据获取方式,客户端发起请求,服务端接入模型数据并返回,然后前端一股脑把所以结果都接入。
- 数据的发起方是服务端,然后在有合适的数据时,就将其发布给客户端,前端接收到数据后就进行结果的显示。此处我们可以按照流式将数据返回
所以,这又引起了另外一个问题,前后端数据交互我们应该采用何种方式。其实针对,后端主动发起数据的方式我们有很多方案
- 长轮询(Long-Polling)
- WebSockets
- 服务器发送事件(Server-Sent Events,SSE)
- WebRTC
- WebTransport
那我们到底用哪种方式亦或者说它们都是个啥,都有啥优缺点。所以,今天我们来用一篇文章来讲讲它们直接的区别和联系。
好了,天不早了,干点正事哇。
我们能所学到的知识点
- 长轮询(Long-Polling)
- WebSockets
- 服务器发送事件(SSE)
- WebTransport
- WebRTC
- 技术的限制
- 性能比较
- 适用场景
1. 长轮询(Long-Polling)
长轮询可以在浏览器上通过 HTTP 启用一种服务器-客户端消息传递方法。该技术通过普通的 XHR 请求模拟了服务器推送通信。与传统的轮询不同,其中客户端会在「固定的时间间隔内重复向服务器请求数据」,长轮询建立了一条连接到服务器的连接,该连接保持打开状态,直到有新数据可用为止。一旦服务器有了新信息,就会将响应发送给客户端,并关闭连接。
在接收到服务器的响应后,客户端立即发起新的请求,这个过程会重复进行。这种方法允许「更即时地更新数据,并减少不必要的网络流量和服务器负载」。然而,它仍然可能引入通信延迟,并且不如其他实时技术(如 WebSockets)高效。
function longPoll() {
fetch('http://front789.com/poll')
.then(response => response.json())
.then(data => {
console.log("接收到的数据:", data);
longPoll(); // 立即发起新的长轮询请求
})
.catch(error => {
/**
* 在正常情况下可能会出现错误,
* 当连接超时或客户端离线时。
* 出现错误时,我们会在一段延迟后重新启动轮询。
*/
setTimeout(longPoll, 10000);
});
}
longPoll(); // 初始化长轮询
长轮询解决了在网络平台上构建双向应用程序的问题,也就是我们经常用的模式- 「客户端发出请求,服务器响应」。这是通过颠覆请求-响应模型来实现的:
- 客户端向服务器发送 GET 请求:与传统的 HTTP 请求不同,我们可以将其视为开放式的。它不是请求特定的响应,而是在准备好时请求任何响应。
- 请求时间设置:HTTP 超时可以使用 Keep-Alive 头进行调整。
长轮询利用此功能,通过设置非常长或无限期的超时时间,使请求保持打开状态,即使服务器没有立即响应。
- 服务器响应:当服务器有要发送的内容时,它会使用响应关闭连接。
返回的数据可以是新的聊天消息、体育比分或突发新闻等。
客户端发送新的 GET 请求,循环重新开始。
图片
2. WebSockets
WebSockets[1] 是一种实时技术,可通过持久的单套接字(socket)连接在客户端和服务器之间实现「双向全双工通信」。WebSockets 相对于传统的 HTTP,代表了一个重大进步,因为一旦建立连接,双方就可以「独立发送数据」,这使其非常适合需要低延迟和高频更新的场景。
WebSocket 技术由两个核心构建块组成:
- WebSocket协议:WebSocket是建立在TCP协议之上的一种「应用层协议」。该协议旨在允许客户端和服务器「实时通信」,从而在 Web 应用程序中实现高效且响应迅速的数据传输。
- WebSocket API:WebSocket API 是一个编程接口,用于创建 WebSocket 连接并管理 Web 应用程序中客户端和服务器之间的数据交换。几乎所有现代浏览器都支持 WebSocket API
图片
如何工作的
概括地说,使用 WebSockets 涉及三个主要步骤:
- 打开 WebSocket 连接
建立 WebSocket 连接的过程称为握手,由客户端和服务器之间的 HTTP 请求/响应交换组成。
- 通过 WebSockets 传输数据
成功打开握手后,客户端和服务器可以通过持久 WebSocket 连接交换消息(帧)。WebSocket 消息可能包含字符串(纯文本)或二进制数据。
关闭 WebSocket 连接。
一旦持久的 WebSocket 连接达到其目的,它就可以终止;
客户端和服务器都可以通过发送关闭消息来启动关闭握手。
图片
// 创建 `WebSocket` 连接
const socket = new WebSocket("ws://localhost:7899");
// 打开链接,并发送信息
socket.addEventListener("open", (event) => {
socket.send("Hello Front789!");
});
// 监听来自服务端的数据
socket.addEventListener("message", (event) => {
console.log("来自服务端的数据", event.data);
});
// 关闭链接
socket.onclose = function(e) {
console.log("关闭链接", e);
};
虽然 WebSocket API 的基础用法很容易,但在生产环境中却相当复杂。一个 socket 可能会断开连接,必须相应地重新创建。特别是检测连接是否仍然可用或不可用可能会非常棘手。通常,我们会添加一个 ping-and-pong[2] 心跳以确保打开的连接不会关闭。我们可以借助类似像 Socket.IO[3] 这样的库来处理重连的情况,需要时提供了以「长轮询」为回退方案。
想了解更多关于WebSocket可以参考The WebSocket API and protocol explained[4]
3. 服务器发送事件(SSE)
服务器发送事件(Server-Sent Events,SSE)提供了一种标准方法,通过 HTTP 将服务器数据推送到客户端。与 WebSockets 不同,SSE 专门设计用于「服务器到客户端的单向通信」,使其非常适用于实时信息的更新或者那些在不向服务器发送数据的情况下实时更新客户端的情况。
我们可以将服务器发送事件视为单个 HTTP 请求,其中后端不会立即发送整个主体,而是保持连接打开,并通过每次发送事件时发送单个行来逐步传输答复。
图片
SSE是一个由两个组件组成的标准:
- 浏览器中的 EventSource 接口,允许客户端订阅事件:它提供了一种通过抽象较低级别的连接和消息处理来订阅事件流的便捷方法。
- 事件流协议:描述服务器发送的事件必须遵循的标准纯文本格式,以便 EventSource 客户端理解和传播它们
在浏览器的客户端上,我们可以使用服务器端生成事件脚本的 URL 初始化一个 EventSource[5] 实例。
// 连接到服务器端事件流
const evtSource = new EventSource("https://front789.com/events");
// 处理通用消息事件
evtSource.onmessage = event => {
if(event.data.trim() !== 'undefined'){
const newData = event.data;
// 数据追加
setResponse((prevResponse) => prevResponse.concat(newData));
} else{
// 当从服务端接收到值为`undefined`的数据时,关闭链接
setTempPrompt('');
eventSource.close();
}
};
与 WebSockets 不同,EventSource 在连接丢失时会自动重新连接。
在服务器端,我们的脚本必须将 Content-Type 标头设置为 text/event-stream,并根据 SSE 规范[6]格式化每条消息。这包括指定事件类型、数据有效负载和可选字段,如事件 ID。
以下是使用Node.js Express处理SSE的示例:
import express from 'express';
const app = express();
const PORT = process.env.PORT || 7890;
const headers = {
'Content-Type': 'text/event-stream',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache'
}
app.get('/events', (req, res) => {
res.writeHead(200, headers);
const sendEvent = (data) => {
// 所有数据都必须以'data:'开头
const formattedData = `data: ${JSON.stringify(data)}\n\n`;
res.write(formattedData);
};
// 每两秒发送一个事件
const intervalId = setInterval(() => {
const message = {
time: new Date().toTimeString(),
message: '服务端产生的数据',
};
sendEvent(message);
}, 2000);
// 关闭轮询
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
文章最开始,我们不是说想实现实时响应后端返回并且逐字显示聊天机器人回复,我们其实就可以使用SSE的方案。而ChatGPT也是使用这个机制实现的。
4. WebTransport
WebTransport[7] 是一个专为 Web 客户端和服务器之间进行高效、低延迟通信而设计的前沿 API。它利用了 HTTP/3 QUIC 协议[8],可以实现以可靠和不可靠的方式实现多个流的数据传输功能,甚至允许数据无序发送。这使得 WebTransport 成为需要高性能网络的应用程序的强大工具,如实时游戏、直播和协作平台。但是,值得注意的是,WebTransport 目前是一个工作草案,尚未被广泛采用。
图片
截至目前(2024 年 5 月),WebTransport 仍处于工作草案阶段[9],并没有得到广泛支持。
图片
目前还不能在 Safari 浏览器中使用 WebTransport,而且 Node.js 也没有原生支持。这限制了其在不同平台和环境中的可用性。
5. WebRTC
网页实时通信(Web Real-time Communication,WebRTC)[10]是一个增强网页浏览模式。它允许浏览器通过安全访问输入设备(如网络摄像头和麦克风),以「点对点的方式直接与其他浏览器交换实时媒体数据」。
WebRTC 既是 API 又是协议。
- WebRTC 协议是一组规则,供两个 WebRTC 代理协商双向安全实时通信。
- WebRTC API 允许开发人员使用 WebRTC 协议。WebRTC API 仅针对 JavaScript。
传统的网页架构是基于客户端-服务器模型,客户端发送HTTP请求到服务器并获得包含所请求信息的响应。与此相对,WebRTC允许N个实体之间交换数据。在这种交换中,实体彼此直接通信,而无需中间服务器。
WebRTC内置于HTML 5,因此我们不需要第三方软件或插件即可使用它,我们可以通过WebRTC API在浏览器中访问它。它支持浏览器之间的音频、视频和数据流交换的点对点连接。WebRTC 设计用于通过 NAT 和防火墙工作,利用诸如 ICE、STUN 和 TURN 等协议来建立对等之间的连接。
虽然 WebRTC 是为客户端-客户端交互设计的,但也可以利用它进行服务器-客户端通信,其中「服务器只是模拟成一个客户端」。这种方法只适用于特定的用例,问题在于,要使 WebRTC 正常工作,我们仍然需要一个服务器,这个服务器会再次通过 WebSockets、SSE 或 WebTransport 运行。这就背离了使用 WebRTC 作为这些技术的替代方案的初衷。
6. 技术的限制
双向发送数据
只有 WebSockets 和 WebTransport 是「双向全双工通信」,这样我们就可以在同一个连接上接收服务器数据并发送客户端数据。
虽然理论上使用长轮询也是可能的,但并不建议,因为向现有的长轮询连接发送“新”数据实际上还是需要额外的 HTTP 请求。因此,我们可以通过额外的 HTTP 请求直接将数据从客户端发送到服务器,而不会中断长轮询连接。
SSE不支持向服务器发送任何附加数据。我们只能进行初始请求,即使在原生的 EventSource API 中,默认情况下也无法在 HTTP 主体中发送类似 POST 的数据。相反,我们必须将所有数据放在 URL 参数中,这被认为是一种不安全的做法,因为凭据可能会泄漏到服务器日志、代理和缓存中。
每个域的 6 个请求限制
大多数现代浏览器允许「每个域最多六个连接」这限制了服务器-客户端消息传递方法的可用性。这六个连接的限制甚至在浏览器选项卡之间共享,因此当我们在多个选项卡中打开相同的页面时,它们必须彼此共享六个连接池。
虽然这个策略可以防止D-DOS 攻击,但当多个连接是为了处理合法的通信时,它可能会造成很大的问题。为了解决这个限制,我们必须使用 HTTP/2 或 HTTP/3,其中浏览器为每个域只会打开一个连接,然后使用「多路复用」来通过单个连接传输所有数据。虽然这样可以给我们几乎无限量的并行连接,但有一个 SETTINGS_MAX_CONCURRENT_STREAMS[11] 设置,它限制了实际的连接数量。对于大多数配置,默认值为 100 个并发流。
在移动应用程序中不保持连接
在 Android 和 iOS 等操作系统上运行的移动应用程序中,保持打开连接(例如 WebSockets 和其他连接)会带来很大的挑战。移动操作系统被设计为「在一段时间的不活动后自动将应用程序移至后台,从而有效关闭任何打开的连接」。这种行为是操作系统资源管理策略的一部分,旨在节省电池并优化性能。因此,我们通常依赖于移动推送通知作为一种高效可靠的方法,以将数据从服务器发送到客户端。推送通知允许服务器提醒应用程序有新数据到达,促使执行某个操作或更新,而无需保持持续的打开连接。
7. 性能比较
对于一些我们平时可能会用到的技术例如WebSockets、SSE、长轮询和 WebTransport 我们可以从延迟、吞吐量、服务器负载和在不同条件下的可伸缩性的角度来比较。
延迟
- WebSockets:由于其通过单个持久连接进行全双工通信,提供了最低的延迟。适用于实时应用程序,其中立即数据交换至关重要。
- SSE:也提供了低延迟的服务器到客户端通信,但不能直接发送消息回服务器,需要额外的 HTTP 请求。
- 长轮询:由于依赖于为每个数据传输「建立新的 HTTP 连接」,因此产生较高的延迟,使其对实时更新不太有效。此外,当服务器希望在客户端仍在打开新连接的过程中发送事件时,可能会出现延迟显著较大的情况。
- WebTransport:承诺提供类似于 WebSockets 的低延迟,同时利用 HTTP/3 协议进行更高效的多路复用和拥塞控制。
吞吐量
- WebSockets:由于其持久连接,能够实现高吞吐量,但当客户端无法处理数据时,吞吐量可能会受到反压的影响,反压[12]是指客户端无法处理服务器发送的数据速度。
- SSE:对于向客户端广播消息而言,效率高于 WebSockets,开销较小,因此在单向的服务器到客户端通信中可能会实现更高的吞吐量。
- 长轮询:由于频繁打开和关闭连接的开销较大,通常提供较低的吞吐量,这会「消耗更多的服务器资源」。
- WebTransport:支持单个连接内的双向和单向数据流的高吞吐量,性能优于需要多个流的场景下的 WebSockets。
可伸缩性和服务器负载
- WebSockets:维护大量 WebSocket 连接可能会显著增加服务器负载,可能影响具有许多用户的应用程序的可伸缩性。
- SSE:对于主要需要来自服务器到客户端的更新的场景,更具可伸缩性,因为与 WebSockets 相比,它使用的连接开销更小,因为它使用的是常规的 HTTP 请求,而不是像 WebSockets 那样需要运行协议更新的请求。
- 长轮询:由于频繁建立连接产生的高服务器负载,所以是最不可伸缩的,通常仅适用于作为「后备机制」。
- WebTransport:设计为高度可伸缩,受益于 HTTP/3 在处理连接和流时的高效性,与 WebSockets 和 SSE 相比,可能减少服务器负载。
8. 适用场景
在服务器-客户端通信技术的领域中,每种技术都有其独特的优势和适用用例。SSE是最简单的实现选项,利用与传统 Web 请求相同的 HTTP/S 协议,因此可以规避企业防火墙限制和其他可能出现的技术问题。它们很容易集成到 Node.js 和其他服务器框架中,因此非常适合需要频繁服务器到客户端更新的应用程序,如新闻源、股票行情和实时事件流。
另一方面,WebSockets 在需要持续的双向通信的场景中表现出色。它们支持连续互动的能力,使其成为浏览器游戏、聊天应用程序和实时体育更新的首选。
然而,WebTransport 虽然潜力巨大,但面临着采用挑战。它在包括 Node.js 在内的服务器框架中得到的支持不广泛,并且与 Safari 不兼容。此外,它对 HTTP/3 的依赖进一步限制了其即时适用性,因为许多 Web 服务器(如 nginx)只有实验性的 HTTP/3 支持。虽然在支持可靠和不可靠数据传输的未来应用程序中有所希望,但在大多数用例中,WebTransport 还不是一个可行的选择。
长轮询曾经是一种常见的技术,但由于其效率低下和频繁建立新的 HTTP 连接的高开销,现在已经大大过时。虽然它可以作为没有对 WebSockets 或 SSE 进行支持的环境的后备方案,但由于存在显著的性能限制,通常不建议使用。