做实时推送时,很多人一开始都会有几个很典型的问题:

  • 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 定时推送流程

如果把整个流程压缩一下,大概就是这样:

  1. 前端和后端建立 WebSocket 连接
  2. 后端保存这条连接
  3. 后端通过定时任务每隔 10 秒取一次数据
  4. 后端把数据通过 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 -> user
  • topic -> sessions

5. 精准推送的核心不是 WebSocket 本身

而是后端维护的映射关系。

搞定🎉~~

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐