告别等待!后端推送前端数据技术大盘点

733 阅读20分钟

今天想跟大家聊聊一个不咋被提起的话题:后端如何把数据“推”给前端?,作为前端我们要知道这些技术以便和后端联调,作为后端更是不用说,需要了解这些技术,从而选择适合的技术来实现需求。

咱们都知道,传统的 Web 模式是“请求-响应”模型,浏览器不问,服务器不说。这在展示静态页面或者偶尔查个数据的场景下没毛病。但现在是什么时代?实时聊天、在线协作、股价更新、比赛直播、系统监控...哪个等得起用户一遍遍刷新或者前端用 setTimeout / setInterval 去傻傻轮询?

短轮询(Polling)的缺点显而易见:

  1. 延迟高:取决于你轮询的间隔,实时性差。
  2. 浪费资源:大量请求可能只是空轮询,啥新数据也没有,白白消耗服务器和网络资源。
  3. 服务器压力:并发量高的时候,大量无效轮询请求能把服务器压垮。

所以,我们需要更优雅、更高效的方式——后端主动推送。今天,咱们就来深入扒一扒几种主流的实现技术。

1. WebSocket:全双工通信的瑞士军刀

说到推送,WebSocket 绝对是第一个跳进大多数人脑海的。这家伙是真·大佬级别的存在。

image.png

核心理论

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。啥叫全双工?就是客户端和服务器可以同时向对方发送数据,像打电话一样,而不是像对讲机那样一次只能一个人说。

它是怎么做到的?

  1. 握手阶段:看着像 HTTP,其实是“借壳上市”。客户端发起一个特殊的 HTTP 请求(包含 Upgrade: websocket, Connection: Upgrade 等头信息)。
  2. 协议升级:服务器如果同意,返回一个 101 Switching Protocols 状态码,之后这个 TCP 连接就从 HTTP 协议升级成了 WebSocket 协议。
  3. 持续通信:一旦升级成功,这个连接就保持打开状态,双方可以随时互相发送数据,数据格式可以是文本,也可以是二进制。连接关闭前,不需要再进行 HTTP 请求了。

技术特性

  • 真·实时:数据传输延迟极低,因为连接一直在线。
  • 全双工:客户端、服务端都能主动发消息。
  • 性能开销小:握手之后,数据帧的头部信息很小,比 HTTP 请求轻量得多。
  • 事件驱动:基于事件(onopen, onmessage, onerror, onclose)处理,符合现代前端开发习惯。
  • 支持二进制:可以直接传输二进制数据(ArrayBuffer / Blob),对音视频、游戏等场景友好。

不足与挑战

  • 协议兼容性:虽然现在主流浏览器都支持,但一些老的网络设备、防火墙、代理服务器可能不认识 WebSocket 协议(或者 ws://, wss://),需要特殊配置或升级。
  • 服务器状态维护:每个 WebSocket 连接都是一个状态,需要服务器维护。当连接数非常大时(比如百万级),对服务器的内存和管理能力是个考验(当然,现代框架和服务器在这方面优化很多了)。
  • 实现相对复杂:相比于简单的 HTTP API,WebSocket 的生命周期管理、心跳保活、断线重连等机制需要开发者自己考虑得更周全(很多库封装了这些,但理解原理还是必要的)。

适用场景

WebSocket 几乎是需要高实时性、频繁双向交互场景的首选:

  • 在线聊天室/即时通讯:最经典的场景。
  • 实时协作编辑:比如 Google Docs, Figma。
  • 在线多人游戏:玩家状态、动作同步。
  • 实时数据仪表盘/监控:股票行情、系统监控、实时地理位置跟踪。
  • 需要服务端主动通知用户的场景:比如订单状态更新、消息提醒。

与 HTPP 的联系

  • HTTP 和 WebSocket 是两种不同的协议,分别适用于不同的场景。
  • HTTP 基于请求-响应模型,适用于传统的 Web 通信。
  • WebSocket 是双向通信协议,适用于实时通信和服务器推送。
  • WebSocket 的建立依赖于 HTTP 协议,但一旦握手成功,通信将使用 WebSocket 协议。

image.png

代码示例 (Node.js + JS):

2. Server-Sent Events (SSE):轻量级单向推送

如果你的场景主要是服务器向客户端单向推送信息,而且不想搞那么复杂的 WebSocket,那么 SSE (Server-Sent Events) 可能是个更好的选择。

image.png

核心理论

SSE 基于 HTTP 协议,本质上是一个长连接的 HTTP 响应。客户端发起一个普通的 HTTP GET 请求,但服务器响应头里会包含 Content-Type: text/event-stream,并且这个连接会一直保持打开状态。服务器随后可以随时通过这个连接,按照特定格式(下面会说)向客户端发送文本消息。

它就是 HTTP,所以天然能穿透防火墙和代理,兼容性好。

技术特性

  • 简单:基于 HTTP,实现和理解都相对简单,前端有标准的 EventSource API,后端也就是维护一个长连接响应流。
  • 轻量:相比 WebSocket,协议开销更小(没有额外的握手和协议升级)。
  • 单向:服务器 -> 客户端。客户端不能通过 SSE 连接向服务器发送数据(还得用普通的 HTTP 请求)。
  • 自动重连EventSource API 标准就包含了断线自动重连机制,省心。
  • 事件类型:可以给不同的消息定义 event 类型,方便前端分类处理。
  • 只支持 UTF-8 文本:不能直接传二进制数据。

不足与挑战

  • 单向通信:最大的限制,只能服务器推给客户端。需要客户端发数据?老老实实用 AJAX/fetch。
  • 浏览器连接数限制:浏览器对同域名下的 HTTP 长连接(包括 SSE)数量有限制(通常是 6 个左右),开多个标签页或者有其他长连接请求时可能会达到上限。不过 HTTP/2 在一定程度上缓解了这个问题,因为多个请求可以复用一个 TCP 连接。
  • 不支持二进制:传输图片、音视频等二进制数据比较麻烦(需要 base64 编码等)。
  • IE/Edge 老版本不支持:虽然现代浏览器都支持了,但如果你需要兼容古董级的 IE,需要 Polyfill。

适用场景

SSE 非常适合只需要从服务器接收实时更新的场景:

  • 目前大火的 ai chat 响应数据的返回,分块返回
  • 新闻 Feed / 股票行情 / 体育比分:服务器持续推送最新信息。
  • 系统通知 / 站内信提醒:服务器有了新通知就推给前端。
  • 日志流展示:实时显示服务器日志。
  • 任务进度更新:比如文件上传、数据处理的进度条。

代码示例 (Node.js + JS):

后端 (Node.js - 原生 http 模块)

const express = require('express');
const app = express();
const port = 3000;

// 模拟 AI 逐块生成响应
function* generateResponse(prompt) {
    const words = prompt.split(' ');
    for (const word of words) {
        yield word + ' '; // 每次生成一个单词
    }
}

// SSE 路由
app.get('/chat', (req, res) => {
    const prompt = req.query.prompt || 'Hello, this is a test';

    // 设置 SSE 响应头
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    // 逐块发送响应
    const generator = generateResponse(prompt);
    const interval = setInterval(() => {
        const { value, done } = generator.next();
        if (done) {
            clearInterval(interval);
            res.write('data: [DONE]\n\n'); // 发送结束标志
            res.end();
        } else {
            res.write(`data: ${value}\n\n`); // 发送数据块
        }
    }, 500); // 每 500ms 发送一个单词
});

// 启动服务器
app.listen(port, () => {
    console.log(`Server is running on http://localhost:${port}`);
});

前端 (HTML + JavaScript - 使用 EventSource)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Chat</title>
</head>
<body>
    <h1>AI Chat</h1>
    <div id="output">等待响应...</div>
    <script>
        // 建立 SSE 连接
        const eventSource = new EventSource('/chat?prompt=你好,这是一个测试');

        // 监听服务器发送的消息
        eventSource.onmessage = (event) => {
            const chunk = event.data;
            const outputDiv = document.getElementById('output');

            if (chunk === '[DONE]') {
                outputDiv.innerHTML += '\n\n响应结束。';
                eventSource.close(); // 关闭连接
            } else {
                outputDiv.innerHTML += chunk; // 追加内容
                outputDiv.scrollTop = outputDiv.scrollHeight; // 自动滚动到底部
            }
        };

        // 处理错误
        eventSource.onerror = () => {
            console.log('连接错误');
            eventSource.close();
        };
    </script>
</body>
</html>

3. Long Polling(长轮询)

这玩意儿算是短轮询(Polling)的优化版,也是在 WebSocket 出现之前,模拟“服务器推”效果的一种常见手段。

b1c885fb4ebaf89dddb3f6a57969f2d0.png

核心理论

  1. 客户端发起请求:和普通轮询一样,客户端向服务器发送一个 HTTP 请求,询问“有新数据吗?”
  2. 服务器hold住连接:关键区别来了!如果服务器没有新数据,它不会立即返回空响应,而是**挂起(hold)**这个连接,等待一段时间(比如 30 秒、1 分钟,或者直到有新数据为止)。
  3. 返回响应
    • 如果在等待期间,有新数据到达了,服务器立即将数据作为响应发送给客户端,然后关闭这个连接。
    • 如果在等待期间,一直没有新数据,直到超时(比如设定的 30 秒到了),服务器会返回一个空响应或特定状态码(表明没新数据),然后关闭连接。
  4. 客户端再请求:客户端收到响应(无论是带数据的还是超时的)后,立即再次发起一个新的长轮询请求,重复步骤 1。

这样看起来就像是服务器在数据到达时“推送”了数据,因为它减少了大量无效的空轮询。

技术特性

  • 兼容性好:纯 HTTP,几乎所有浏览器、网络环境都支持。
  • 实现相对简单:相比 WebSocket,后端逻辑主要是管理挂起连接和超时。
  • 相比普通轮询,实时性提高,无效请求减少

不足与挑战

  • 有延迟:数据从产生到被推送,还是存在延迟(服务器检查数据、网络传输)。虽然比短轮询好,但不如 WebSocket/SSE 实时。
  • 服务器资源消耗:每个客户端连接都需要服务器挂起一个请求,虽然比 WebSocket 轻量(因为它不是持久连接),但在高并发下仍然消耗资源(主要是连接句柄、内存)。请求/响应的头部开销依然存在。
  • 可能的消息丢失或重复:在网络不稳定的情况下,请求发出和响应返回之间可能出现问题,需要客户端和服务器做一些额外的逻辑来保证消息的可靠性(比如带上最后接收消息的 ID)。
  • 超时处理:需要仔细设计超时时间,太短退化成普通轮询,太长则占用资源。

适用场景

  • 对实时性要求不是极致,但又不想频繁轮询的场景
  • 需要兼容非常古老的浏览器或网络环境
  • 作为 WebSocket/SSE 不可用时的降级方案
  • 一些消息队列的 Web 客户端可能采用这种方式。

与短轮询对比

特性轮询(Polling)长轮询(Long Polling)
请求频率固定时间间隔发送请求收到响应后立即发送下一个请求
服务器响应立即响应,无论是否有新数据保持连接打开,直到有新数据或超时
效率低效,频繁发送请求高效,减少不必要的请求
实时性延迟较高实时性较好
服务器资源占用较低较高
实现复杂度简单较复杂
适用场景数据更新频率较低的场景对实时性要求较高的场景

下图可以看出,短轮询存在大量无意义的请求,浪费资源。

image.png

代码示例

后端(Node.js):

const http = require('http');

let messages = [
    { id: 1, message: "Hello, world!" },
    { id: 2, message: "This is a long polling example." },
    { id: 3, message: "Waiting for new messages..." }
];
let messageId = 3; // 初始消息 ID

const server = http.createServer((req, res) => {
    if (req.url === '/getMessage') {
        const lastMessageId = parseInt(req.headers['last-message-id']) || 0;

        // 检查是否有新消息
        const checkForNewMessage = () => {
            const newMessages = messages.filter(msg => msg.id > lastMessageId);
            if (newMessages.length > 0) {
                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify(newMessages));
            } else {
                // 如果没有新消息,等待一段时间再检查
                setTimeout(checkForNewMessage, 1000);
            }
        };

        checkForNewMessage();
    } else {
        res.writeHead(404, { 'Content-Type': 'text/plain' });
        res.end('Not Found');
    }
});

server.listen(3000, () => {
    console.log('Server is listening on port 3000');
});

// 模拟新消息的生成
setInterval(() => {
    messageId++;
    messages.push({ id: messageId, message: `New message at ${new Date().toLocaleTimeString()}` });
}, 5000); // 每 5 秒生成一条新消息

前端:

let lastMessageId = 0;

function fetchMessages() {
    fetch('/getMessage', {
        headers: {
            'Last-Message-Id': lastMessageId
        }
    })
        .then(response => response.json())
        .then(messages => {
            if (messages.length > 0) {
                messages.forEach(msg => {
                    document.getElementById('messages').innerHTML += `<p>${msg.message}</p>`;
                    lastMessageId = msg.id;
                });
            }
            // 继续长轮询
            fetchMessages();
        })
        .catch(error => {
            console.error('Error fetching messages:', error);
            // 继续长轮询
            fetchMessages();
        });
}

// 开始长轮询
fetchMessages();

4. HTTP Streaming

SSE 其实是 HTTP Streaming 的一种标准化应用。但你也可以不用 text/event-stream 格式,直接用 HTTP Streaming 推送任意格式的数据流。

服务器在响应 HTTP 请求时,使用 Transfer-Encoding: chunked。这意味着响应体不是一次性发送完成的,而是分成多个“块”(chunk)发送。服务器保持连接打开,并根据需要随时发送新的数据块给客户端。客户端则持续接收和处理这些数据块。

例如视频的下载播放:

image.png

特点

  • 分块传输
    • 媒体文件被分割成多个小片段(chunks),每个片段可以独立传输和播放。
    • 服务器将这些片段按顺序发送给客户端。
  • 渐进式加载
    • 客户端在接收到第一个片段后即可开始播放,同时继续下载后续片段。
    • 这种方式避免了用户等待整个文件下载完成。
  • 动态调整
    • 根据网络带宽和设备性能,客户端可以动态选择不同质量的片段(如高清、标清等),以提供最佳播放体验。

与 SSE 的区别

  • 格式:SSE 有固定的 event, data, id, retry 格式,并以 \n\n 分隔。通用 HTTP Streaming 没有规定格式,可以是 JSON、XML、纯文本,甚至二进制(需要小心处理),由应用自行约定。
  • 客户端 API:SSE 有标准的 EventSource API。通用 HTTP Streaming 需要用 fetchXMLHttpRequest,并处理 response.body (ReadableStream) 来逐步读取数据,相对更底层。
  • 自动重连/事件类型EventSource 自带这些功能。通用 Streaming 需要自己实现。

适用场景

  • 需要推送非文本或特定格式的数据流
  • 服务端已经有现成的流式接口,不想为了 SSE 改造
  • 下载大文件时,边下载边处理 (虽然这不算严格意义的“推送”,但技术原理相似)。

基于 HTTP Streaming 实现的技术:

  • HLS (HTTP Live Streaming)
    • 由 Apple 开发,广泛用于 iOS 和 macOS 设备。
    • 将媒体内容分割成 .ts 文件(MPEG-2 Transport Stream),并通过 .m3u8 播放列表文件进行索引。
  • DASH (Dynamic Adaptive Streaming over HTTP)
    • 由 MPEG 标准化,支持多种设备和平台。
    • 将媒体内容分割成 .mp4 或 .webm 文件,并通过 .mpd 文件(Media Presentation Description)进行索引。
  • MSS (Microsoft Smooth Streaming)
    • 由 Microsoft 开发,主要用于 Windows 设备和 Silverlight 平台。
    • 使用 .ism 和 .ismv 文件格式进行传输。

5. HTTP/2 Server Push

HTTP/2 带来了一个叫 Server Push 的特性,听起来好像也是服务器推送?但此“推”非彼“推”。

核心理论

HTTP/2 Server Push 允许服务器在客户端请求一个资源(比如 HTML)时,主动将它认为客户端接下来会需要的其他资源(比如 CSS、JS、图片)一并推送给客户端。目的是减少请求的 RTT (Round Trip Time),提前把资源准备好,加快页面加载速度。

注意关键词:资源。它主要设计用来推送静态资源,而不是像 WebSocket 或 SSE 那样推送动态数据流

推送过程大致是:

  1. 客户端请求 /index.html
  2. 服务器判断这个页面需要 /style.css/script.js
  3. 服务器在发送 /index.html 的响应的同时,主动发起两个 "PUSH_PROMISE" 帧,告诉客户端:“我马上要给你推 /style.css/script.js 了,你先别自己去请求了。”
  4. 随后服务器就把这两个文件的内容推给客户端缓存。

技术特性

  • 性能优化:减少了客户端发起请求的等待时间,理论上能提升首屏加载速度。
  • 基于 HTTP/2:需要客户端和服务器都支持 HTTP/2。
  • 与请求关联:推送是与客户端的某个初始请求相关联的。

不足与挑战

  • 缓存问题(致命伤):服务器怎么知道客户端是否已经缓存了某个资源?如果推了客户端已有的资源,就浪费了带宽。这个问题很难完美解决(虽然有 Cache-Digest 等提案,但实现复杂,效果不一)。这是导致它实用性大打折扣的主要原因。
  • 实现复杂:服务器需要有逻辑去判断哪些资源应该被推送,这可能需要复杂的配置或应用层逻辑。
  • 浏览器支持和策略变化:虽然浏览器实现了 H/2 Push,但由于缓存等问题带来的实际效益不确定,甚至有时会起反作用(推了不需要或已缓存的资源),Chrome 已经宣布计划移除对 HTTP/2 Server Push 的支持。这基本上给它的未来判了死缓。
  • 不是用来推动态数据的:再次强调,它不适合用来做实时消息、通知等动态数据的推送。你想用它搞个聊天室?那真是找错人了。

适用场景

理论上,适用于首次访问网站时,将关键的 CSS、JS 和 Logo 等伴随 HTML 一起推送。但鉴于上述的挑战和浏览器支持的变化,目前已经不推荐在新项目中使用 HTTP/2 Server Push 了

更多请参考:HTTP/2 服务器推送(Server Push)教程

6. WebTransport (新一代实验性)

这是个更新的技术,基于 QUIC (HTTP/3 的基础),旨在提供比 WebSocket 更现代、更灵活的传输层。可以看作是 WebSocket + WebRTC Data Channels 的结合与进化。

核心理论

WebTransport 利用 QUIC 协议的特性,提供:

  • 多个流 (Streams):在一个连接上可以并发、独立地传输多个数据流,避免了 WebSocket 的队头阻塞问题(一个大消息或慢消息会阻塞后面所有消息)。
  • 可靠和不可靠传输:可以选择像 TCP 一样可靠有序的流 (Datagrams),也可以选择像 UDP 一样尽力而为、可能乱序或丢失的不可靠数据报 (Datagrams),对实时游戏、音视频等场景更友好。
  • 单向和双向流
  • 更快的连接建立:基于 QUIC 的 0-RTT 或 1-RTT 连接。

适用场景

  • 对延迟极其敏感的应用:在线游戏、云游戏流送、实时音视频传输。
  • 需要在一个连接上高效传输多个独立数据流的场景
  • 希望利用不可靠传输优化性能的场景

7. Web Push API (离线/后台通知)

这个严格来说,跟上面讨论的应用内实时数据推送目的不同,但它确实是“后端推送前端”的一种技术,所以提一下以示区别。

image.png

  • 核心理论:它允许网站(在获得用户授权后)通过浏览器或操作系统的推送服务(如 Google 的 FCM, Apple 的 APNS)向用户发送通知 (Notifications),即使用户当前没有打开该网站的标签页,甚至浏览器没有运行(Service Worker 在后台接收)。
  • 工作流程:后端需要调用 Push Service 的 API,Push Service 再把通知推给对应的设备/浏览器。前端需要 Service Worker 来接收和显示通知。
  • 关键点:它不是用来在网页打开时实时更新页面内容的!它是用来发送操作系统级别的通知的。
  • 适用场景:新闻更新提醒、社交媒体新消息通知、日历事件提醒、PWA 的离线消息推送等。

8. 第三方推送服务 (PaaS/SaaS)

别忘了还有“钞能力”和“站在巨人肩膀上”的选择。市面上有很多成熟的第三方实时消息推送服务。

  • 例子:Pusher, Ably, PubNub, Google Firebase Realtime Database / Firestore (通过 SDK 实现实时同步), AWS AppSync (GraphQL Subscriptions, 通常底层是 WebSocket), Socket.IO (虽然是库,但其托管服务也可算此类)...
  • 核心思想:你不用自己搭 WebSocket/SSE 服务器、处理连接管理、扩容、心跳、重连等麻烦事。直接用他们的 SDK 或者 API,把消息发给他们的平台,由他们负责把消息可靠地推送到你的前端客户端。
  • 优点:开发速度快,省心省力,通常提供跨平台 SDK,功能丰富(如在线状态、消息历史、权限控制等),高可用和弹性伸缩有保障。
  • 缺点:成本(通常按连接数、消息量等收费),有厂商锁定风险,数据隐私和合规性考量,可能不如自建方案灵活或性能极致可控。
  • 适用场景:快速开发、中小项目、需要跨平台推送、不想自己维护复杂后端实时架构、预算允许。

技术选型思考:到底用哪个?

对于大部分需要实时数据更新和后端推送前端的场景,WebSocketSSE 是目前最成熟、最实用的选择。

  • 如果只是简单的通知、信息流类(例如ai chat)推送,SSE 的简单性很有吸引力。
  • 如果交互复杂,需要客户端也频繁发数据,WebSocket 更强大。

下图是短轮询、长轮询、SSE、WebSocket 对比图,可根据需要决定使用哪一种。

1708006766709.jpg

到底该怎么选?总结一下:

技术适用场景特点
WebSocket需要实时、双向通信的场景(如聊天、游戏、协作)双向通信,低延迟,适合实时交互
SSE服务器向客户端单向推送消息,且对兼容性、简单性要求高(如新闻、通知、状态更新、AI Chat)单向通信,基于 HTTP,简单易用,兼容性好
HTTP Streaming更灵活的流式传输,适合自定义数据格式的场景,例如视频播放、直播、视频会议更灵活,也更麻烦。
长轮询 (Long Polling)兼容性好,模拟推送,适合兼容或降级场景古老但兼容性好,有延迟,资源消耗较高
第三方服务快速开发,省心省力有成本和锁定风险,适合快速实现功能
Web Push API发送系统级通知,非应用内数据流用于推送系统通知,与实时数据流无关
WebTransport对延迟极其敏感,需要超低延迟和可靠传输(如云游戏、金融高频交易的 Web 端)基于 QUIC/HTTP/3,结合 WebSocket 的灵活性和 UDP 的低延迟,目前还在发展和标准化阶段,浏览器支持有限
HTTP/2 Push优化静态资源加载速度曾经用于优化资源加载,但现在不推荐,建议使用预加载、预连接、CDN 和良好的缓存策略替代

参考阅读