后端服务端主动推送消息的常见方式
# Web 后端服务端主动推送消息的常见方式
# 1. 引言
在传统的 Web 通信模式中,客户端(如浏览器、移动应用)需要向服务器发起请求,服务器才能返回数据。这种模式通常被称为 请求-响应(Request-Response) 模型,即:
- 客户端请求数据:用户操作(如打开网页、点击按钮)会触发 HTTP 请求,发送到服务器。
- 服务器处理请求:服务器接收请求后,查询数据库、执行逻辑运算,并返回响应数据。
- 客户端接收并渲染数据:浏览器或应用解析服务器返回的数据,并更新界面。
这种模式虽然简单直观,但它有一个核心问题:服务器无法主动通知客户端新数据的到来。如果客户端想要获取最新数据,就必须主动发送请求,这在某些实时性要求较高的场景下并不高效例如:
- 即时通讯(IM):如微信、Slack、WhatsApp 等应用,需要服务器主动推送新消息,否则用户需要频繁刷新才能看到新消息。
- 股票、加密货币行情:金融市场的价格波动快,客户端需要尽可能快地收到最新报价。
- 系统通知:如社交媒体的消息提醒(微博、推特)、邮件系统的新邮件通知等。
- 多人协作应用:如在线文档协作(Google Docs)、多人游戏,服务器需要向所有客户端推送最新状态更新。
- 直播、弹幕系统:视频直播、弹幕等场景下,服务器需要向成千上万的观众推送最新内容。
由于传统 HTTP 请求模式无法满足这些需求,因此出现了一系列优化方案,如短轮询、长轮询、WebSocket、Server-Sent Events(SSE)、MQTT 等,这些技术允许服务器主动向客户端推送消息,减少延迟并提升用户体验。
# 2. 轮询(Polling)
轮询(Polling)是一种早期解决服务器推送需求的方式,它的核心思想是:客户端不断向服务器发送请求,检查是否有新数据可用。
轮询方法的主要优点是实现简单,可以直接使用标准的 HTTP 请求来完成。但是,它也有较大的缺点,例如 资源消耗高、延迟不可控。
# 3.1 短轮询(Short Polling)
工作原理
短轮询的实现方式如下:
- 客户端定期(如每 5 秒)向服务器发送 HTTP 请求,询问是否有新数据。
- 服务器检查是否有新数据,并返回相应的响应:
- 有新数据:返回最新的数据。
- 没有新数据:返回空响应(或重复返回旧数据)。
- 客户端收到响应后,等待一段时间后再次发送请求。
示例代码(JavaScript Fetch API)
function shortPolling() {
fetch('/api/messages')
.then(response => response.json())
.then(data => console.log('Received data:', data))
.catch(error => console.error('Error:', error));
setTimeout(shortPolling, 5000); // 每 5 秒轮询一次
}
shortPolling();
2
3
4
5
6
7
8
9
优缺点分析
✅ 优点:
- 实现简单,直接基于 HTTP 请求,无需额外协议支持。
- 兼容性好,适用于几乎所有浏览器和后端框架。
❌ 缺点:
- 资源浪费:即使没有新数据,客户端仍然会不断发起请求,占用带宽和服务器资源。
- 延迟不可控:如果轮询间隔太长,可能会错过实时更新;如果间隔太短,会增加服务器负载。
- 不适用于高并发场景:大规模用户同时轮询可能会导致服务器崩溃。
# 3.2 长轮询(Long Polling)
短轮询的问题在于 即使没有新数据,客户端也会不断请求服务器,导致大量无效请求。长轮询(Long Polling)是一种改进方案,它的核心思路是:让服务器保持连接,直到有新数据时再返回响应。
工作原理
客户端发送 HTTP 请求给服务器,询问是否有新数据。
服务器不立即返回响应,而是保持连接,直到有新数据可用或超时:
- 有新数据:服务器立即返回数据,客户端收到数据后再次发起新请求。
- 没有新数据:服务器保持连接,直到超时后返回空响应,客户端再发起新请求。
示例代码(JavaScript Fetch API + Express 后端)
前端:
function longPolling() {
fetch('/api/messages')
.then(response => response.json())
.then(data => {
console.log('Received data:', data);
longPolling(); // 继续下一次请求
})
.catch(error => {
console.error('Error:', error);
setTimeout(longPolling, 5000); // 发生错误时稍等后重试
});
}
longPolling();
2
3
4
5
6
7
8
9
10
11
12
13
14
后端(Node.js + Express):
const express = require('express');
const app = express();
app.get('/api/messages', (req, res) => {
// 模拟新消息
setTimeout(() => {
res.json({ message: "New message from server!" });
}, Math.random() * 10000); // 服务器等待 0~10 秒后返回数据
});
app.listen(3000, () => console.log('Server running on port 3000'));
2
3
4
5
6
7
8
9
10
11
优缺点分析
✅ 优点:
- 减少无效请求,只有在有新数据时才返回,避免了短轮询的资源浪费。
- 相对较低的服务器负载,减少了 HTTP 请求的次数。
❌ 缺点:
- 仍然有一定的延迟,因为每次请求仍然是独立的,数据更新的时间受请求间隔影响。
- 服务器需要维持连接,如果并发量大,服务器需要处理大量长时间挂起的请求,可能导致连接池耗尽。
- 不适用于 Web 浏览器以外的长连接场景,如物联网设备的低功耗通信。
# 3.3 轮询方式的适用场景
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
短轮询 | 低访问量的应用,如定期检查新邮件 | 简单易实现,兼容性好 | 资源消耗大,延迟不可控 |
长轮询 | 实时性要求较高但用户数量有限的应用,如客服聊天 | 减少无效请求,提高实时性 | 服务器需维持大量挂起请求,影响性能 |
随着应用对实时性的要求越来越高,轮询方式逐渐暴露出性能问题。因此,开发者开始寻找更高效的解决方案,如 WebSocket、Server-Sent Events(SSE)等,这些技术可以提供更低延迟、低资源消耗的双向通信能力,我们将在接下来的章节中详细介绍。
# 3. WebSocket:全双工通信
# 3.1 WebSocket 介绍
WebSocket 是一种 全双工(Full-Duplex)通信协议,允许客户端与服务器之间 建立持久连接,并在连接存活期间实现 实时的双向数据传输。
在传统 HTTP 轮询方案中,客户端需要不断发送请求获取新数据,而 WebSocket 通过 一次握手,建立持久连接,之后客户端和服务器可以随时主动发送消息,无需额外的请求开销,从而极大提高了通信效率。
WebSocket 工作流程
- HTTP Upgrade 进行协议升级
- WebSocket 连接的建立始于标准的 HTTP 请求,客户端发送
Upgrade
头 请求服务器升级协议: - 服务器同意后,连接切换为 WebSocket,后续通信不再依赖 HTTP。
- WebSocket 连接的建立始于标准的 HTTP 请求,客户端发送
- 建立连接后,客户端和服务器可双向通信
- 连接建立后,服务器和客户端可以随时主动发送和接收消息,不再需要客户端轮询服务器。
- 连接保持,支持低延迟通信
- WebSocket 连接在 不关闭的情况下会一直保持,只有当客户端或服务器主动关闭,或者遇到网络问题时才会断开。
示例代码
前端 WebSocket 客户端(JavaScript):
const socket = new WebSocket('ws://localhost:8080');
// 监听连接打开
socket.onopen = function () {
console.log('WebSocket 连接已建立');
socket.send('Hello Server!'); // 发送消息给服务器
};
// 监听服务器消息
socket.onmessage = function (event) {
console.log('收到服务器消息:', event.data);
};
// 监听连接关闭
socket.onclose = function () {
console.log('WebSocket 连接已关闭');
};
// 监听错误
socket.onerror = function (error) {
console.log('WebSocket 发生错误:', error);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
后端 WebSocket 服务器(Node.js + ws 库):
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
server.on('connection', (socket) => {
console.log('客户端连接成功');
socket.on('message', (message) => {
console.log('收到客户端消息:', message);
socket.send('服务器响应: ' + message); // 服务器主动推送消息
});
socket.on('close', () => {
console.log('客户端断开连接');
});
});
console.log('WebSocket 服务器运行在 ws://localhost:8080');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 3.2 适用场景
WebSocket 适用于 高实时性、低延迟、双向通信需求的场景,例如:
✅ 实时聊天应用
- 微信、WhatsApp、Slack 等即时通讯应用需要服务器在新消息到达时立即推送给客户端。
✅ 多人协作编辑
- Google Docs、在线白板等多人协作应用,需要所有用户共享编辑状态。
✅ 在线游戏
- 需要低延迟同步玩家状态(如 FPS、多人 RPG)。
✅ 实时监控
- 物联网(IoT)设备、服务器日志监控等需要实时数据推送。
✅ 金融交易
- 交易所行情、股票价格变动等,WebSocket 可快速推送最新价格。
# 3.3 WebSocket 存在的问题
尽管 WebSocket 提供了高效的实时通信,但它仍然存在一些挑战和需要注意的地方。
1️⃣ 连接数管理
- WebSocket 连接会一直保持,因此服务器需要管理大量的并发连接,特别是在高并发场景下,如 WebSocket 直播。
- 服务器通常需要 连接池、负载均衡 来管理 WebSocket 连接,例如使用 Nginx + WebSocket 代理。
2️⃣ 网络状态变化处理
- 如果用户的网络断开(如手机切换 Wi-Fi 到 4G),WebSocket
# 4. Server-Sent Events(SSE)
# 4.1 SSE 介绍
Server-Sent Events(SSE) 是一种基于 HTTP 协议的单向推送技术,允许 服务器主动向客户端推送数据。
与 WebSocket 不同,SSE 仅支持服务器向客户端发送数据,但它比 WebSocket 更加轻量级,并且直接支持 浏览器的 EventSource
API。
比如当你在使用 ChatGPT 时,当你询问它问题时,你会看到它会逐字地将回答显示出来,实际上这是 ChatGPT 将先计算出的数据主动的“推送”给你,采用 SSE 技术边计算边返回,避免接口等待时间过长而直接关闭页面。
# 4.2 SSE连接过程
它的连接过程主要包括以下步骤:
客户端发起请求
客户端(通常是浏览器)通过 JavaScript 使用EventSource
API 向服务器的 SSE 端点发送一个 HTTP GET 请求。
示例代码:const source = new EventSource('/events');
1这里
/events
是服务器提供的事件流端点的 URL。设置请求头
客户端会自动在请求中设置特定的头信息,以表明它期望接收 SSE 数据流:Accept: text/event-stream
:告诉服务器客户端期望接收事件流。Connection: keep-alive
:指示保持长连接。Cache-Control: no-cache
:避免缓存干扰。
服务器响应并建立连接
服务器接收到请求后,返回一个 HTTP 响应,状态码通常为200 OK
,并设置响应头:Content-Type: text/event-stream
:表明返回的是 SSE 事件流。Connection: keep-alive
:保持连接开放。Transfer-Encoding: chunked
(可选):支持分块传输。
连接建立后,服务器不会立即关闭,而是保持开放状态,随时推送数据。
服务器推送事件
服务器通过已建立的连接向客户端发送数据,数据格式遵循 SSE 协议:- 数据以纯文本(UTF-8 编码)发送。
- 每个事件由若干字段组成(如
event
、data
、id
等),以换行符分隔,事件之间用空行(两个换行符\n\n
)分隔。
示例事件:
event: message data: 这是一条实时消息 id: 1
1
2
3
4客户端通过
EventSource
的监听器接收这些事件,例如:source.onmessage = function(event) { console.log(event.data); // 输出:这是一条实时消息 };
1
2
3连接保持与重连机制
- 如果连接意外断开(例如网络中断),
EventSource
会自动尝试重连,默认间隔通常为 2-3 秒(可通过服务器发送retry: 毫秒数
自定义)。 - 客户端会携带上一次接收到的
Last-Event-ID
(如果有)以恢复数据流。
- 如果连接意外断开(例如网络中断),
关闭连接
- 客户端可通过调用
source.close()
主动关闭连接。 - 服务器也可通过关闭响应流或返回非
text/event-stream
的内容类型来终止连接。
- 客户端可通过调用
注意事项
- 单向通信:SSE 是服务器到客户端的单向推送,客户端无法通过同一连接向服务器发送数据。
- 浏览器限制:在 HTTP/1.1 下,每个域名最多支持 6 个并发 SSE 连接;HTTP/2 可协商更多连接。
- 数据格式:仅支持 UTF-8 文本,不支持二进制数据。
# 4.3 Java后端实现方式
# 4.3.1 SseEmitter(Spring MVC)
SseEmitter
属于 Spring MVC,适用于 Servlet 线程模型(同步、阻塞)。- 通过
SseEmitter.send()
发送事件,由 Servlet 线程池 维护连接。 - 适用于 非响应式(Blocking) 应用程序,希望在传统的 Spring MVC 方式下实现 SSE。
- 底层依赖:基于 Servlet API,适合 Tomcat、Jetty 等 Servlet 容器。
示例:
@RestController
public class SseController {
@GetMapping("/sse")
public SseEmitter stream() {
SseEmitter emitter = new SseEmitter();
Executors.newSingleThreadExecutor().execute(() -> {
try {
for (int i = 0; i < 5; i++) {
emitter.send("Message " + i);
Thread.sleep(1000);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
特点:
SseEmitter
适合 同步(阻塞)模型,由 Spring MVC 线程池管理。- 不适用于高并发,因为每个连接都会占用一个服务器线程。
# 4.3.2 Flux(Project Reactor - WebFlux)
Flux
是 Spring WebFlux 响应式编程的一部分,基于 Reactive Streams 规范,适用于 异步非阻塞 处理。- 通过
Flux.interval()
或Flux.generate()
创建事件流,由 Reactor 线程模型 处理。 - 适用于 高并发、低延迟 的 SSE 应用场景。
- 底层依赖:基于 Netty、Undertow 等异步 Web 服务器,不依赖 Servlet API。
示例:
@RestController
public class ReactiveSseController {
@GetMapping(value = "/flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamFlux() {
return Flux.interval(Duration.ofSeconds(1))
.map(i -> "Message " + i)
.take(5);
}
}
2
3
4
5
6
7
8
9
10
特点:
Flux
采用 异步(非阻塞) 模型,更适合 WebFlux(Netty)。- 适用于 高吞吐、并发量大 的 SSE 应用场景。
- 响应式流在 WebFlux 运行时进行优化,不占用固定线程资源。
# 4.3.3 核心区别
特性 | SseEmitter (Spring MVC) | Flux (Spring WebFlux) |
---|---|---|
运行模式 | 阻塞(Blocking) | 非阻塞(Non-Blocking) |
编程模型 | 基于 Servlet 线程池 | Reactive Streams 响应式 |
底层容器 | Servlet(Tomcat、Jetty) | Netty(默认)、Undertow |
并发能力 | 低(一个连接占用一个线程) | 高(Reactor 线程池管理) |
适用场景 | 传统 Spring MVC + SSE | 高吞吐 SSE,Reactive 应用 |
线程管理 | 服务器线程池维护连接 | Reactor 事件驱动 |