【超详细】一文搞懂 WebSocket 定时推送!
做实时推送时,很多人一开始都会有几个很典型的问题:
- WebSocket 到底是怎么做到每隔 10 秒给前端发一次数据的?
- 如果某个前端页面不需要数据了,后端怎么知道?
- 后端又是怎么判断“该给谁发什么”的?
- 一个用户开了多个页面,后端怎么区分?
- WebSocket 和普通接口轮询到底差在哪?
这些问题如果一开始没理顺,后面越做越乱。
这篇文章就不讲太多大而空的概念,直接结合实际开发场景,把这条链路捋清楚。
一、首先,WebSocket 和普通接口轮询有什么区别
很多人第一次接触 WebSocket,会把它和“前端每隔几秒调一次接口”混在一起。
实际上,这两种方式完全不是一个思路。
1. 普通接口轮询
轮询的意思是,前端定时去问后端:
有新数据吗?
有新数据吗?
有新数据吗?
比如前端每 10 秒请求一次:
setInterval(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
console.log(data);
});
}, 10000);
这种方式的特点是:
- 主动方是前端
- 没有长连接
- 每次都要重新发起一次 HTTP 请求
2. WebSocket
WebSocket 是另一种思路。
前端先和后端建立一条长连接,后面不需要每次都重新发 HTTP 请求了。
后端一旦有数据,就可以主动通过这条连接发给前端。
前端代码通常像这样:
const ws = new WebSocket("ws://localhost:8080/ws");
ws.onmessage = (event) => {
console.log("收到后端消息:", event.data);
};
这种方式的特点是:
- 主动方可以是后端
- 前后端之间有一条持续存在的连接
- 更适合消息推送、实时状态、监控面板这类场景
二、WebSocket 每隔 10 秒推送一次数据,到底是谁在“每隔 10 秒”
很多人一开始会误以为:
WebSocket 自己就带定时查询能力。
其实不是。
WebSocket 只负责“通信通道”,不负责“多久执行一次业务逻辑”。
真正实现“每隔 10 秒推送一次”的,一般是后端的定时任务。
也就是说,这个过程本质上是两部分组合:
- WebSocket,负责推消息
- 定时任务,负责每隔 10 秒触发一次推送
一句话概括:
WebSocket 负责发,定时任务负责定时。
三、一个最简单的 WebSocket 定时推送流程
如果把整个流程压缩一下,大概就是这样:
- 前端和后端建立 WebSocket 连接
- 后端保存这条连接
- 后端通过定时任务每隔 10 秒取一次数据
- 后端把数据通过 WebSocket 发给前端
流程图可以简单理解成这样:
前端建立 WebSocket 连接
↓
后端拿到 Session 并保存
↓
定时任务每10秒触发一次
↓
查询接口 / 调用 service / 查数据库
↓
通过 Session 把数据推给前端
四、后端代码一般怎么写
1. WebSocket 服务端先把连接保存起来
在 Java 里,WebSocket 连接建立后,后端通常会拿到一个 Session 对象。
这个 Session 可以理解成“当前这个前端客户端和后端之间的通道”。
示例代码:
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/ws/data")
public class DataWebSocketServer {
private static CopyOnWriteArraySet<Session> sessions = new CopyOnWriteArraySet<>();
@OnOpen
public void onOpen(Session session) {
sessions.add(session);
System.out.println("客户端连接成功");
}
@OnClose
public void onClose(Session session) {
sessions.remove(session);
System.out.println("客户端断开连接");
}
public static void broadcast(String message) {
for (Session session : sessions) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
这里最重要的动作就两个:
@OnOpen时把连接放进去@OnClose时把连接移除
2. 定时任务每 10 秒执行一次
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class DataPushTask {
@Scheduled(fixedRate = 10000)
public void pushData() {
String result = "这是后端每10秒获取到的新数据";
DataWebSocketServer.broadcast(result);
}
}
再配合启动类:
@EnableScheduling
@SpringBootApplication
public class Application {
}
这样就完成了一个最基础的“后端每 10 秒给前端推一次数据”。
五、如果某个前端不需要数据了,后端怎么知道
这是做 WebSocket 时特别容易问到的问题。
本质上有两种情况。
情况 1 ➡️ 前端整个连接都不要了
比如:
- 页面关闭
- 页面刷新
- 用户离开这个页面
- 前端主动关闭连接
前端通常会执行:
ws.close();
一旦连接关闭,后端会触发:
@OnClose
public void onClose(Session session) {
sessions.remove(session);
}
这时候后端就知道:
这个前端已经不要数据了。
情况 2 ➡️ 前端还在线,但只是不想接收某类数据
比如一个页面同时订阅了:
- 订单数据
- 告警数据
- 设备状态
后来用户把“订单数据”模块关掉了,但页面整体还在。
这时候就不能直接 close(),否则别的数据也收不到了。
更常见的做法是前端发一条消息告诉后端:
ws.send(JSON.stringify({
type: "unsubscribe",
topic: "orderData"
}));
后端收到后,把这个连接从 orderData 的订阅列表里删掉。
也就是说:
- 连接还在
- 只是订阅关系变了
六、后端怎么知道哪个前端想要什么数据
这个问题比“定时推送”本身更关键。
因为 WebSocket 只是提供一条连接,它不会自动知道:
- 这个连接是谁
- 它需要什么数据
- 该给它发什么
这些关系都要在后端自己维护。
1. 先区分“连接是谁”
后端一般会给每个连接绑定一个身份信息,比如:
- 用户 ID
- 设备 ID
- 页面 ID
- 浏览器标签页 ID
最简单的做法,是前端连接成功后先发一条注册消息:
ws.send(JSON.stringify({
type: "register",
userId: "userA"
}));
后端收到后就能记住:
session1 -> userA
2. 再区分“这个连接要什么数据”
前端再发一条订阅消息:
ws.send(JSON.stringify({
type: "subscribe",
topic: "orderData"
}));
后端收到后记录:
orderData -> [session1]
这样一来,后端就同时知道了两件事:
- 这个连接属于谁
- 这个连接订阅了什么
七、后端通常会维护哪些映射关系
最常见的就是下面两种:
Map<Session, String> sessionUserMap;
Map<String, Set<Session>> topicSessionMap;
第一张表:连接和用户的关系
session1 -> userA
session2 -> userB
它解决的是:
“这个连接是谁?”
第二张表:主题和连接的关系
orderData -> [session1]
alarmData -> [session2]
它解决的是:
“哪些连接订阅了这个主题?”
这样后端推送时就很清楚了
比如现在后端拿到了最新的订单数据:
topic = orderData
data = 最新订单数:25
那么它只需要去查:
orderData -> [session1]
然后把消息只发给 session1 就行。
这就是“精准推送”的基本原理。
八、一个更完整的订阅式 WebSocket 示例
下面给一个更接近真实项目的最小实现。
WebSocket 服务端
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/ws")
public class MyWebSocketServer {
private static final Map<Session, String> sessionUserMap = new ConcurrentHashMap<>();
private static final Map<String, Set<Session>> topicSessionMap = new ConcurrentHashMap<>();
private static final ObjectMapper objectMapper = new ObjectMapper();
@OnOpen
public void onOpen(Session session) {
System.out.println("连接建立: " + session.getId());
}
@OnMessage
public void onMessage(String message, Session session) {
try {
JsonNode jsonNode = objectMapper.readTree(message);
String type = jsonNode.get("type").asText();
if ("register".equals(type)) {
String userId = jsonNode.get("userId").asText();
sessionUserMap.put(session, userId);
}
if ("subscribe".equals(type)) {
String topic = jsonNode.get("topic").asText();
topicSessionMap.putIfAbsent(topic, new CopyOnWriteArraySet<>());
topicSessionMap.get(topic).add(session);
}
if ("unsubscribe".equals(type)) {
String topic = jsonNode.get("topic").asText();
Set<Session> sessions = topicSessionMap.get(topic);
if (sessions != null) {
sessions.remove(session);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@OnClose
public void onClose(Session session) {
sessionUserMap.remove(session);
for (Set<Session> sessions : topicSessionMap.values()) {
sessions.remove(session);
}
}
public static void sendToTopic(String topic, String message) {
Set<Session> sessions = topicSessionMap.get(topic);
if (sessions == null) {
return;
}
for (Session session : sessions) {
try {
if (session.isOpen()) {
session.getBasicRemote().sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
定时任务推送订单数据
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class DataPushTask {
@Scheduled(fixedRate = 10000)
public void pushOrderData() {
String data = "{\"topic\":\"orderData\",\"value\":\"最新订单数:25\"}";
MyWebSocketServer.sendToTopic("orderData", data);
}
}
前端示例
const ws = new WebSocket("ws://localhost:8080/ws");
ws.onopen = () => {
ws.send(JSON.stringify({
type: "register",
userId: "userA"
}));
ws.send(JSON.stringify({
type: "subscribe",
topic: "orderData"
}));
};
ws.onmessage = (event) => {
console.log("收到后端消息:", event.data);
};
ws.onclose = () => {
console.log("连接关闭");
};
九、如果一个用户开了多个页面怎么办
这是实际项目里经常遇到的情况。
比如同一个用户:
- 在电脑上开了一个页面
- 又在另一个浏览器标签页开了一个页面
- 或者手机端也连了一次
这时候同一个用户可能对应多个 Session。
所以很多时候你不能简单写成:
Map<String, Session> userSessionMap;
更合理的是:
Map<String, Set<Session>> userSessionMap;
也就是说:
一个用户,可以对应多条连接。
这样你想给某个用户发消息时,就可以把这个用户下的所有连接都遍历一遍。
十、做这类推送时最容易混淆的三个点
1. 不是前端每 10 秒调一次 WebSocket
前端通常只在一开始建立一次连接。
后面如果是“定时推送”,触发动作是在后端。
2. 不是 WebSocket 自己每 10 秒查数据
WebSocket 只是通道。
“每 10 秒”一般来自后端定时任务。
3. 后端不是天然知道该给谁发什么
是因为你自己维护了:
- 连接是谁
- 连接订阅了什么
- 哪个主题应该发给哪些连接
所以最终能精准推送。
十一、用一句话把整件事讲明白
如果让我用最短的话概括这套机制,那就是:
前端先建立 WebSocket 连接,再告诉后端“我是谁、我要什么”,后端把这些关系记下来,之后通过定时任务获取数据,并按订阅关系把数据推给对应的前端。
十二、总结
把这篇文章浓缩一下,其实就这几点:
1. WebSocket 和轮询不是一回事
轮询是前端不断发请求,WebSocket 是先建长连接,后端可以主动推送。
2. WebSocket 本身不负责“每隔 10 秒”
这个能力通常是后端定时任务实现的。
3. 后端通过 Session 管理连接
连接建立时保存,连接关闭时移除。
4. 后端要自己维护身份和订阅关系
比如:
session -> usertopic -> sessions
5. 精准推送的核心不是 WebSocket 本身
而是后端维护的映射关系。
搞定🎉~~
更多推荐




所有评论(0)