系统工作台待办实时提醒,取代五分钟刷新一次,判断有没有新的待办,利用 WebSocket 实现

系统工作台待办实时提醒,取代五分钟刷新一次,判断有没有新的待办,利用 WebSocket 实现

一、实现步骤

二、前端代码


三、后端代码





四、效果展示

五、AI 啰嗦(直接忽略)

你好!作为一名 Java 程序员,将传统的"轮询"(Polling)模式升级为基于 WebSocket 的"实时推送"(Push)模式,是提升用户体验和系统性能的最佳实践。

轮询不仅浪费带宽和服务器资源,还会导致消息延迟(最长可达5分钟)。而 WebSocket 建立的是全双工通信,服务器一旦有新待办,可以毫秒级主动推送到前端。

下面我将基于 Spring BootSpring WebSocket 提供一套完整的实现方案。


🛠️ 核心架构思路

  1. 连接建立:前端页面加载时,建立与后端的 WebSocket 长连接。
  2. 用户绑定 :连接建立后,将 WebSocket Session 与当前的 用户ID 进行绑定(通常使用 Map<UserId, Session> 或 Redis)。
  3. 事件触发:当业务层(Service)产生新的待办事项时,触发一个事件。
  4. 消息推送:后端监听该事件,查找对应用户的 Session,主动发送 JSON 消息。
  5. 前端接收 :前端 onmessage 回调收到消息,通过浏览器的通知 API 或 DOM 操作更新界面。

第一步:引入 Maven 依赖

我们需要 spring-boot-starter-websocket

xml 复制代码
<dependencies>
    <!-- Web 基础依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- WebSocket 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <!-- Lombok (可选,简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

第二步:配置 WebSocket

创建一个配置类,开启 WebSocket 支持,并注册处理器。

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    private final TodoWebSocketHandler todoWebSocketHandler;

    // 注入自定义的 Handler
    public WebSocketConfig(TodoWebSocketHandler todoWebSocketHandler) {
        this.todoWebSocketHandler = todoWebSocketHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册处理器的路径,前端连接 ws://host:port/ws/todo
        // setAllowedOrigins("*") 允许跨域,生产环境建议指定具体域名
        registry.addHandler(todoWebSocketHandler, "/ws/todo")
                .setAllowedOrigins("*");
    }
}

第三步:实现 WebSocket Handler (核心逻辑)

这是处理连接、断开和消息转发的核心类。我们需要维护一个 Map 来存储在线用户和他们的会话。

注意 :在生产环境中,如果服务是多实例部署(集群) ,不能只使用本地 ConcurrentHashMap,必须使用 Redis Pub/Sub 来广播消息。这里演示单机版逻辑。

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Component
public class TodoWebSocketHandler extends TextWebSocketHandler {

    // 存储在线用户的 Session,Key 为 userId,Value 为 Session
    // 使用 ConcurrentHashMap 保证线程安全
    private static final Map<String, WebSocketSession> USER_SESSION_MAP = new ConcurrentHashMap<>();

    /**
     * 连接建立成功后
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 获取用户ID,通常从 URL 参数或 HTTP 握手头中获取
        // 例如:ws://localhost:8080/ws/todo?userId=1001
        String userId = (String) session.getAttributes().get("userId");
        
        if (userId != null) {
            USER_SESSION_MAP.put(userId, session);
            log.info("用户 [{}] 建立了 WebSocket 连接,当前在线人数: {}", userId, USER_SESSION_MAP.size());
        }
    }

    /**
     * 接收到客户端消息(通常客户端不需要发什么,主要是心跳检测)
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        log.info("收到客户端消息: {}", message.getPayload());
        // 可以处理心跳包
    }

    /**
     * 连接断开后
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String userId = (String) session.getAttributes().get("userId");
        if (userId != null) {
            USER_SESSION_MAP.remove(userId);
            log.info("用户 [{}] 断开了连接,当前在线人数: {}", userId, USER_SESSION_MAP.size());
        }
    }

    /**
     * 处理异常
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        log.error("WebSocket 传输异常", exception);
    }

    /**
     * 业务层调用此方法,向指定用户推送消息
     */
    public void sendMessageToUser(String userId, String message) {
        WebSocketSession session = USER_SESSION_MAP.get(userId);
        if (session != null && session.isOpen()) {
            try {
                session.sendMessage(new TextMessage(message));
                log.info("成功推送消息给用户 [{}]: {}", userId, message);
            } catch (IOException e) {
                log.error("发送消息失败", e);
            }
        } else {
            log.warn("用户 [{}] 不在线,消息推送失败", userId);
        }
    }
}

第四步:业务触发与拦截器

我们需要在连接建立前把 userId 放入 Session 属性中。这通常通过 HandshakeInterceptor 实现,或者在 Controller 中处理。这里简单演示如何在业务层触发。

假设你有一个 TodoService,当新增待办时:

java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class TodoService {

    private final TodoWebSocketHandler webSocketHandler;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public TodoService(TodoWebSocketHandler webSocketHandler) {
        this.webSocketHandler = webSocketHandler;
    }

    /**
     * 模拟新增待办事项的业务方法
     */
    public void createTodo(String assigneeUserId, String todoTitle) {
        // 1. 保存待办到数据库...
        System.out.println("保存待办: " + todoTitle);

        // 2. 构造推送消息
        Map<String, Object> msg = new HashMap<>();
        msg.put("type", "NEW_TODO");
        msg.put("title", todoTitle);
        msg.put("time", System.currentTimeMillis());

        try {
            // 3. 调用 WebSocket Handler 推送
            String jsonMsg = objectMapper.writeValueAsString(msg);
            webSocketHandler.sendMessageToUser(assigneeUserId, jsonMsg);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

第五步:前端实现 (HTML + JavaScript)

前端不再需要 setInterval,而是监听 WebSocket 消息。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>待办实时提醒</title>
</head>
<body>
    <h1>我的工作台</h1>
    <div id="status">连接状态:未连接</div>
    <ul id="todo-list">
        <!-- 待办列表 -->
    </ul>

    <script>
        // 假设当前登录用户 ID 为 1001
        const userId = '1001';
        // 建立 WebSocket 连接
        const socket = new WebSocket(`ws://localhost:8080/ws/todo?userId=${userId}`);

        // 连接打开
        socket.onopen = function () {
            document.getElementById('status').innerText = '连接状态:实时连接中...';
            document.getElementById('status').style.color = 'green';
            // 请求浏览器通知权限
            if (Notification.permission !== "granted") {
                Notification.requestPermission();
            }
        };

        // 接收消息
        socket.onmessage = function (event) {
            const data = JSON.parse(event.data);
            
            if (data.type === 'NEW_TODO') {
                // 1. 更新页面 DOM
                const list = document.getElementById('todo-list');
                const li = document.createElement('li');
                li.innerText = `[新] ${data.title}`;
                list.prepend(li);

                // 2. 浏览器弹窗提醒 (可选)
                if (Notification.permission === "granted") {
                    new Notification("新待办通知", {
                        body: `你有新的待办任务:${data.title}`,
                        icon: "/path/to/your/icon.png"
                    });
                }
            }
        };

        // 连接关闭
        socket.onclose = function () {
            document.getElementById('status').innerText = '连接状态:已断开,5秒后重试...';
            document.getElementById('status').style.color = 'red';
            // 简单的断线重连机制
            setTimeout(() => {
                window.location.reload();
            }, 5000);
        };

        // 连接错误
        socket.onerror = function (err) {
            console.error('WebSocket 错误', err);
        };
    </script>
</body>
</html>

💡 生产环境的关键点

在实际的企业级开发中,你还需要考虑以下几点:

  1. 集群问题(Redis Pub/Sub)

    • 如果后端部署了多台服务器(例如 Nginx 负载均衡),用户的连接可能连在 Server A,但业务请求打到了 Server B。
    • 解决方案 :Server B 收到业务请求后,不能直接查本地 Map,而是将消息发布到 Redis Channel。所有 Server 订阅该 Channel,Server A 收到 Redis 消息后,发现是发给自己的用户,再通过本地 Session 推送。
  2. 心跳检测

    • 网络波动可能导致连接"假死"。前端需要每隔 30-60 秒发送一个 ping 消息,后端收到后回复 pong,否则后端主动断开连接释放资源。
  3. 安全性

    • 握手认证 :不要在 URL 参数里明文传 userId。应该在建立 WebSocket 连接前,通过 HTTP Header 传递 Token(JWT),在 HandshakeInterceptor 中解析 Token 验证用户身份。
  4. 消息可靠性

    • WebSocket 是"尽力而为"的协议。如果用户断网了,消息就丢了。对于极其重要的待办,建议配合数据库的"未读状态"字段,用户上线后通过 HTTP 接口拉取一次未读列表作为兜底。

通过这套方案,你的系统将彻底告别"5分钟刷新",实现真正的实时响应。

相关推荐
猩猩—点灯3 小时前
部署远程利器-RustDesk
运维·服务器·网络
半壶清水3 小时前
[软考网规考点笔记]-局域网之以太网标准
网络·笔记·网络协议·考试
ringking1233 小时前
Linux 主机通过 Wi-Fi 上网,并将网络通过网口共享给交换机下游设备
linux·服务器·网络
123过去4 小时前
rcracki_mt使用教程
linux·网络·测试工具
钛态5 小时前
Flutter for OpenHarmony:shelf_web_socket 快速构建 WebSocket 服务端,实现端到端实时通信(WebSocket 服务器) 深度解析与鸿蒙适配指南
服务器·前端·websocket·flutter·华为·性能优化·harmonyos
星辰徐哥5 小时前
C++网络编程:TCP服务器与客户端的实现
网络·c++·tcp/ip
初九之潜龙勿用5 小时前
C# 解决“因为算法不同,客户端和服务器无法通信”的问题
服务器·开发语言·网络协议·网络安全·c#
星辰徐哥5 小时前
C语言网络编程:TCP/IP协议栈、套接字、服务器/客户端通信深度解析
c语言·网络·tcp/ip
算法-大模型备案 多米5 小时前
大模型备案实操指南:材料、流程与避坑要点
大数据·网络·人工智能·算法·文心一言