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

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

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

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

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

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

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

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

核心理论

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 的联系

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

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

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

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

核心理论

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 模块)

javascript 复制代码
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)

html 复制代码
<!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 出现之前,模拟"服务器推"效果的一种常见手段。

核心理论

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

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

技术特性

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

不足与挑战

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

适用场景

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

与短轮询对比

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

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

代码示例

后端(Node.js):

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 秒生成一条新消息

前端:

js 复制代码
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)发送。服务器保持连接打开,并根据需要随时发送新的数据块给客户端。客户端则持续接收和处理这些数据块。

例如视频的下载播放:

特点

  • 分块传输
    • 媒体文件被分割成多个小片段(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 (离线/后台通知)

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

  • 核心理论 :它允许网站(在获得用户授权后)通过浏览器或操作系统的推送服务 (如 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 对比图,可根据需要决定使用哪一种。

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

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

参考阅读

相关推荐
小黑屋的黑小子7 分钟前
【数据结构】反射、枚举以及lambda表达式
数据结构·面试·枚举·lambda表达式·反射机制
程序猿chen9 分钟前
JVM考古现场(十九):量子封神·用鸿蒙编译器重铸天道法则
java·jvm·git·后端·程序人生·java-ee·restful
JiangJiang17 分钟前
🚀 Vue人看React useRef:它不只是替代 ref
javascript·react.js·面试
1024小神22 分钟前
在GitHub action中使用添加项目中配置文件的值为环境变量
前端·javascript
Chandler2426 分钟前
Go:接口
开发语言·后端·golang
ErizJ28 分钟前
Golang|Channel 相关用法理解
开发语言·后端·golang
automan0228 分钟前
golang 在windows 系统的交叉编译
开发语言·后端·golang
Pandaconda29 分钟前
【新人系列】Golang 入门(十三):结构体 - 下
后端·golang·go·方法·结构体·后端开发·值传递
齐尹秦30 分钟前
CSS 列表样式学习笔记
前端
Mnxj34 分钟前
渐变边框设计
前端