【超详细】一文搞懂 WebSocket 定时推送!

张开发
2026/4/9 4:55:09 15 分钟阅读

分享文章

【超详细】一文搞懂 WebSocket 定时推送!
做实时推送时很多人一开始都会有几个很典型的问题WebSocket 到底是怎么做到每隔 10 秒给前端发一次数据的如果某个前端页面不需要数据了后端怎么知道后端又是怎么判断“该给谁发什么”的一个用户开了多个页面后端怎么区分WebSocket 和普通接口轮询到底差在哪这些问题如果一开始没理顺后面越做越乱。这篇文章就不讲太多大而空的概念直接结合实际开发场景把这条链路捋清楚。一、首先WebSocket 和普通接口轮询有什么区别很多人第一次接触 WebSocket会把它和“前端每隔几秒调一次接口”混在一起。实际上这两种方式完全不是一个思路。1. 普通接口轮询轮询的意思是前端定时去问后端有新数据吗有新数据吗有新数据吗比如前端每 10 秒请求一次setInterval(() { fetch(/api/data) .then(res res.json()) .then(data { console.log(data); }); }, 10000);这种方式的特点是主动方是前端没有长连接每次都要重新发起一次 HTTP 请求2. WebSocketWebSocket 是另一种思路。前端先和后端建立一条长连接后面不需要每次都重新发 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 CopyOnWriteArraySetSession 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 - userA2. 再区分“这个连接要什么数据”前端再发一条订阅消息ws.send(JSON.stringify({ type: subscribe, topic: orderData }));后端收到后记录orderData - [session1]这样一来后端就同时知道了两件事这个连接属于谁这个连接订阅了什么七、后端通常会维护哪些映射关系最常见的就是下面两种MapSession, String sessionUserMap; MapString, SetSession 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 MapSession, String sessionUserMap new ConcurrentHashMap(); private static final MapString, SetSession 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(); SetSession 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 (SetSession sessions : topicSessionMap.values()) { sessions.remove(session); } } public static void sendToTopic(String topic, String message) { SetSession 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。所以很多时候你不能简单写成MapString, Session userSessionMap;更合理的是MapString, SetSession userSessionMap;也就是说一个用户可以对应多条连接。这样你想给某个用户发消息时就可以把这个用户下的所有连接都遍历一遍。十、做这类推送时最容易混淆的三个点1. 不是前端每 10 秒调一次 WebSocket前端通常只在一开始建立一次连接。后面如果是“定时推送”触发动作是在后端。2. 不是 WebSocket 自己每 10 秒查数据WebSocket 只是通道。“每 10 秒”一般来自后端定时任务。3. 后端不是天然知道该给谁发什么是因为你自己维护了连接是谁连接订阅了什么哪个主题应该发给哪些连接所以最终能精准推送。十一、用一句话把整件事讲明白如果让我用最短的话概括这套机制那就是前端先建立 WebSocket 连接再告诉后端“我是谁、我要什么”后端把这些关系记下来之后通过定时任务获取数据并按订阅关系把数据推给对应的前端。十二、总结把这篇文章浓缩一下其实就这几点1. WebSocket 和轮询不是一回事轮询是前端不断发请求WebSocket 是先建长连接后端可以主动推送。2. WebSocket 本身不负责“每隔 10 秒”这个能力通常是后端定时任务实现的。3. 后端通过Session管理连接连接建立时保存连接关闭时移除。4. 后端要自己维护身份和订阅关系比如session - usertopic - sessions5. 精准推送的核心不是 WebSocket 本身而是后端维护的映射关系。结语如果你刚开始接触 WebSocket建议先把这三个问题吃透连接怎么建立订阅关系怎么维护数据最终怎么推到正确的前端这三步明白了后面再去看动态调度、Nacos 配置刷新、RefreshScope原理就不会觉得乱。下篇预告这一篇主要讲清楚了WebSocket 如何做定时推送后端如何知道谁订阅了什么为什么能做到精准发送下一篇再继续讲另外两个高频问题Scheduled(fixedRate 10000)为什么不适合直接做热更新RefreshScope到底是怎么实现热刷新的

更多文章