目录一、WebSocket 到底解决什么问题HTTP 的痛点WebSocket 的解法一句话理解二、核心概念5 分钟搞懂1. 连接过程握手2. WebSocket vs HTTP3. URL 格式三、Spring Boot WebSocket 完整实战做什么项目结构第一步创建项目pom.xml第二步启动类第三步WebSocket 配置告诉 Spring 要用 WebSocket第四步WebSocket 处理器核心业务逻辑第五步REST 接口服务器主动推送消息的入口第六步前端页面index.html第七步配置文件四、启动和测试启动测试五、核心概念速查WebSocket 连接状态readyStateWebSocket 的三种消息类型线程安全六、实际项目中的用法场景1MES 看板实时推送传感器数据场景2生产任务进度实时推送七、生产环境注意事项1. 心保活防止连接断开2. 连接数限制3. 鉴权连接时验证 Token4. 关闭时清理资源八、常见坑和解决方案九、总结适用人群会 Java Spring Boot但没用过 WebSocket 的开发者 目标看完后能独立在 Spring Boot 项目里加 WebSocket 实时推送功能一、WebSocket 到底解决什么问题HTTP 的痛点场景牧原 MES 看板需要实时显示传感器温度 用 HTTP 轮询笨办法 看板页面每隔 2 秒问一次服务器有新数据吗 → 服务器没有 → 看板页面有新数据吗 → 服务器没有 → 看板页面有新数据吗 → 服务器有温度 28.5℃ → 看板页面有新数据吗99% 的时候都是没有 → ...无限循环 问题 ❌ 浪费带宽99% 的请求是无效的 ❌ 浪费服务器资源每秒处理几百个有没有新数据的请求 ❌ 有延迟数据来了最多等 2 秒才能看到WebSocket 的解法用 WebSocket聪明办法 看板页面和服务器建立一条管道 服务器有新数据时直接顺着管道推给看板 → 看板不用问数据来了直接显示 优点 ✅ 实时数据产生后毫秒级推送 ✅ 省资源不用反复建立连接 ✅ 双向客户端也能主动发消息给服务器一句话理解HTTP 你去食堂打饭每次排队点菜拿完走人WebSocket 你和食堂窗口开了根管子厨师炒好菜直接顺着管子滑过来二、核心概念5 分钟搞懂1. 连接过程握手客户端 服务器 | | |--- HTTP 请求带 Upgrade---| ← 我想升级成 WebSocket | | |--- 101 Switching Protocols --| ← 好的升级成功 | | | WebSocket 连接建立 | ← 现在是双向管道了 | | | 随时可以互相发消息 | ← 全双工通信2. WebSocket vs HTTPHTTPWebSocket连接短连接用完就断长连接一直保持通信单向客户端请求→服务器响应双向随时互推数据格式每次都要带 Header冗余只发数据不带 Header精简服务器不能主动找客户端可以主动推消息给客户端3. URL 格式HTTP: http://localhost:8080/api/data WebSocket: ws://localhost:8080/ws/chat HTTPS 的 WebSocket: wss://localhost:8080/ws/chat三、Spring Boot WebSocket 完整实战做什么做一个实时消息推送服务服务器可以随时推消息给所有连接的客户端。场景1MES 看板实时显示传感器数据场景2系统告警实时推送项目结构websocket-demo/ ├── pom.xml ├── src/main/java/com/example/websocketdemo/ │ ├── WebSocketDemoApplication.java # 启动类 │ ├── config/ │ │ └── WebSocketConfig.java # WebSocket 配置 │ ├── controller/ │ │ └── MessageController.java # REST 接口测试用 │ └── handler/ │ └── ChatWebSocketHandler.java # WebSocket 处理器 └── src/main/resources/ ├── application.yml └── static/ └── index.html # 前端页面第一步创建项目pom.xml?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version /parent groupIdcom.example/groupId artifactIdwebsocket-demo/artifactId version1.0.0/version packagingjar/packaging namewebsocket-demo/name descriptionWebSocket 入门演示/description properties java.version17/java.version /properties dependencies !-- Spring Boot Web内嵌 Tomcat -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- ★ 核心Spring Boot WebSocket 支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency !-- Lombok -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project第二步启动类package com.example.websocketdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class WebSocketDemoApplication { public static void main(String[] args) { SpringApplication.run(WebSocketDemoApplication.class, args); System.out.println(); System.out.println( WebSocket 演示项目启动成功); System.out.println( 访问: http://localhost:8080/index.html); System.out.println(); } }第三步WebSocket 配置告诉 Spring 要用 WebSocketpackage com.example.websocketdemo.config; import com.example.websocketdemo.handler.ChatWebSocketHandler; 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; /** * WebSocket 配置类 * * 这个类告诉 Spring * 1. 启用 WebSocket 功能EnableWebSocket * 2. 注册 WebSocket 处理器绑定 URL 路径 */ Configuration EnableWebSocket // ← 开启 WebSocket 功能 public class WebSocketConfig implements WebSocketConfigurer { private final ChatWebSocketHandler chatWebSocketHandler; // Spring 自动注入处理器 public WebSocketConfig(ChatWebSocketHandler chatWebSocketHandler) { this.chatWebSocketHandler chatWebSocketHandler; } Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatWebSocketHandler, /ws/chat) // ← 绑定路径 .setAllowedOrigins(*); // ← 允许所有来源连接开发用 // 生产环境要限制来源比如 // .setAllowedOrigins(https://mes.example.com) } }这一步做了什么当浏览器请求 ws://localhost:8080/ws/chat 时 → Tomcat 把请求交给 ChatWebSocketHandler 处理 → 不是交给 ControllerController 只处理 HTTP 请求第四步WebSocket 处理器核心业务逻辑package com.example.websocketdemo.handler; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.socket.*; import org.springframework.web.socket.handler.TextWebSocketHandler; import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * WebSocket 消息处理器 * * 继承 TextWebSocketHandler → 只处理文本消息 * 如果要处理二进制消息继承 BinaryWebSocketHandler */ Slf4j Component public class ChatWebSocketHandler extends TextWebSocketHandler { /** * 在线用户列表 * key 用户ID用 Session ID 代替 * value WebSocketSession连接对象 * * 用 ConcurrentHashMap 保证线程安全WebSocket 是多线程的 */ private final MapString, WebSocketSession onlineUsers new ConcurrentHashMap(); private static final DateTimeFormatter FORMATTER DateTimeFormatter.ofPattern(HH:mm:ss); // 核心回调方法 /** * ① 连接建立时调用 * 当浏览器 new WebSocket(ws://...) 成功连接后触发 */ Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String sessionId session.getId(); onlineUsers.put(sessionId, session); log.info( 新连接: sessionId{}, 在线人数{}, sessionId, onlineUsers.size()); // 给新连接的客户端发欢迎消息 String welcomeMsg String.format( {\type\:\system\,\content\:\欢迎加入你是第%d位在线用户\,\time\:\%s\}, onlineUsers.size(), LocalDateTime.now().format(FORMATTER) ); session.sendMessage(new TextMessage(welcomeMsg)); // 通知所有人有新用户加入 broadcast({\type\:\system\,\content\:\用户 sessionId.substring(0, 6) 加入了聊天\,\time\:\ LocalDateTime.now().format(FORMATTER) \}, session); } /** * ② 收到消息时调用 * 当浏览器发送 ws.send(xxx) 后触发 */ Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload message.getPayload(); // 获取消息内容 String sessionId session.getId(); log.info( 收到消息: from{}, msg{}, sessionId, payload); // 构建广播消息带发送者信息和时间 String broadcastMsg String.format( {\type\:\chat\,\sender\:\%s\,\content\:\%s\,\time\:\%s\}, sessionId.substring(0, 6), // 用 Session ID 前6位当昵称 payload, LocalDateTime.now().format(FORMATTER) ); // 广播给所有人包括发送者自己 broadcast(broadcastMsg, null); } /** * ③ 连接关闭时调用 * 当浏览器关闭标签页或调用 ws.close() 后触发 */ Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { String sessionId session.getId(); onlineUsers.remove(sessionId); log.info( 连接关闭: sessionId{}, 原因{}, 在线人数{}, sessionId, status, onlineUsers.size()); // 通知其他人 broadcast({\type\:\system\,\content\:\用户 sessionId.substring(0, 6) 离开了\,\time\:\ LocalDateTime.now().format(FORMATTER) \}, null); } /** * ④ 连接出错时调用 */ Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { log.error(❌ 连接错误: sessionId{}, session.getId(), exception); onlineUsers.remove(session.getId()); if (session.isOpen()) { session.close(); } } // 工具方法 /** * 广播消息给所有在线用户 * * param message 要发送的消息 * param excludeSession 要排除的会话null不排除任何人 */ public void broadcast(String message, WebSocketSession excludeSession) { TextMessage textMessage new TextMessage(message); for (WebSocketSession session : onlineUsers.values()) { // 跳过排除的会话 if (excludeSession ! null excludeSession.getId().equals(session.getId())) { continue; } try { if (session.isOpen()) { session.sendMessage(textMessage); } } catch (IOException e) { log.error(广播消息失败: sessionId{}, session.getId(), e); } } } /** * 发送消息给指定用户 */ public void sendToUser(String sessionId, String message) { WebSocketSession session onlineUsers.get(sessionId); if (session ! null session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { log.error(发送消息失败: sessionId{}, sessionId, e); } } } /** * 获取在线用户数 */ public int getOnlineCount() { return onlineUsers.size(); } }第五步REST 接口服务器主动推送消息的入口package com.example.websocketdemo.controller; import com.example.websocketdemo.handler.ChatWebSocketHandler; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * 消息推送 Controller * * 提供 HTTP 接口让外部系统比如 MES 数据采集模块 * 通过 HTTP 请求触发 WebSocket 推送 * * 流程 * 传感器数据 → 数据采集服务 → POST /api/push → WebSocket 广播 → 看板实时显示 */ RestController RequestMapping(/api) RequiredArgsConstructor public class MessageController { private final ChatWebSocketHandler chatWebSocketHandler; private static final DateTimeFormatter FORMATTER DateTimeFormatter.ofPattern(HH:mm:ss); /** * 推送消息给所有在线用户 * * POST /api/push * Body: { message: 温度超过阈值 } * * 测试命令 * curl -X POST http://localhost:8080/api/push \ * -H Content-Type: application/json \ * -d {message:温度超过阈值当前温度 38.5℃} */ PostMapping(/push) public String pushMessage(RequestBody java.util.MapString, String body) { String message body.get(message); if (message null || message.isBlank()) { return 消息不能为空; } // 构建推送消息 String pushMsg String.format( {\type\:\alert\,\content\:\%s\,\time\:\%s\}, message, LocalDateTime.now().format(FORMATTER) ); // 通过 Handler 广播 chatWebSocketHandler.broadcast(pushMsg, null); return 已推送: message; } /** * 模拟传感器数据推送 * * GET /api/sensor-data * * 每次调用生成一条随机温度数据广播给所有客户端 * 测试命令 * curl http://localhost:8080/api/sensor-data */ GetMapping(/sensor-data) public String pushSensorData() { // 模拟传感器数据 double temperature 20 Math.random() * 15; // 20~35℃ String farmId 1001; String sensorId TEMP-001; String sensorMsg String.format( {\type\:\sensor\,\farmId\:\%s\,\sensorId\:\%s\,\temperature\:%.1f,\unit\:\℃\,\time\:\%s\}, farmId, sensorId, temperature, LocalDateTime.now().format(FORMATTER) ); chatWebSocketHandler.broadcast(sensorMsg, null); return String.format(已推送传感器数据: 温度 %.1f℃, temperature); } /** * 获取在线用户数 */ GetMapping(/online) public String getOnlineCount() { return 当前在线: chatWebSocketHandler.getOnlineCount() 人; } }第六步前端页面index.html把文件放在src/main/resources/static/index.htmlSpring Boot 会自动提供静态资源服务。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleWebSocket 实时聊天室/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Microsoft YaHei, sans-serif; background: #1a1a2e; color: #eee; height: 100vh; display: flex; flex-direction: column; } .header { background: #16213e; padding: 15px 20px; text-align: center; font-size: 18px; border-bottom: 2px solid #0f3460; } .header .status { font-size: 13px; color: #aaa; margin-top: 5px; } .header .status.connected { color: #4ecca3; } .header .status.disconnected { color: #e74c3c; } .messages { flex: 1; overflow-y: auto; padding: 15px; } .msg { margin: 8px 0; padding: 10px 15px; border-radius: 12px; max-width: 70%; } .msg.system { background: #2d3436; color: #81ecec; margin: 8px auto; text-align: center; font-size: 13px; border-radius: 20px; max-width: 80%; } .msg.chat { background: #0f3460; } .msg.alert { background: #6c2819; border-left: 4px solid #e74c3c; } .msg.sensor { background: #1b4332; border-left: 4px solid #4ecca3; } .msg .sender { font-size: 12px; color: #4ecca3; margin-bottom: 3px; } .msg .time { font-size: 11px; color: #888; margin-top: 3px; } .input-area { display: flex; padding: 15px; background: #16213e; gap: 10px; } .input-area input { flex: 1; padding: 12px; border: 1px solid #0f3460; border-radius: 8px; background: #1a1a2e; color: #eee; font-size: 15px; outline: none; } .input-area button { padding: 12px 24px; border: none; border-radius: 8px; background: #e94560; color: white; font-size: 15px; cursor: pointer; } .input-area button:hover { background: #c81d4e; } /style /head body div classheader WebSocket 实时聊天室 div classstatus disconnected idstatus未连接/div /div div classmessages idmessages/div div classinput-area input idmsgInput placeholder输入消息按回车发送... / button onclicksendMessage()发送/button /div script // 1. 连接 WebSocket // 地址格式ws://主机:端口/配置的路径 const ws new WebSocket(ws://localhost:8080/ws/chat); // 2. 四个核心事件 // 事件1连接成功 ws.onopen function() { console.log(WebSocket 连接成功); updateStatus(已连接 ✓, true); appendMessage(system, 已连接到服务器); }; // 事件2收到消息服务器推送过来的 ws.onmessage function(event) { console.log(收到消息:, event.data); try { // 解析 JSON 消息 const msg JSON.parse(event.data); // 根据消息类型显示不同样式 if (msg.type system) { appendMessage(system, msg.content); } else if (msg.type chat) { appendMessage(chat, msg.content, msg.sender, msg.time); } else if (msg.type alert) { appendMessage(alert, ⚠️ msg.content, 告警, msg.time); } else if (msg.type sensor) { appendMessage(sensor, ️ ${msg.sensorId}: ${msg.temperature}${msg.unit} (场区: ${msg.farmId}), 传感器, msg.time); } } catch (e) { // 非 JSON 消息直接显示 appendMessage(chat, event.data); } }; // 事件3连接关闭 ws.onclose function(event) { console.log(WebSocket 连接关闭, event); updateStatus(已断开 ✗, false); appendMessage(system, 与服务器断开了连接); // 自动重连3秒后尝试重连 setTimeout(function() { appendMessage(system, 正在尝试重连...); // 实际项目中应该重新创建 WebSocket 连接 }, 3000); }; // 事件4连接出错 ws.onerror function(error) { console.error(WebSocket 出错:, error); updateStatus(连接出错 ✗, false); }; // 3. 发送消息 function sendMessage() { const input document.getElementById(msgInput); const message input.value.trim(); if (!message) return; if (ws.readyState ! WebSocket.OPEN) { appendMessage(system, ❌ 连接未就绪无法发送); return; } // ★ 核心ws.send() 发送消息到服务器 ws.send(message); // 清空输入框 input.value ; } // 回车发送 document.getElementById(msgInput).addEventListener(keydown, function(e) { if (e.key Enter) { sendMessage(); } }); // 4. UI 辅助函数 function appendMessage(type, content, sender, time) { const div document.getElementById(messages); const msgDiv document.createElement(div); msgDiv.className msg type; if (type system) { msgDiv.textContent content; } else { let html ; if (sender) html div classsender sender /div; html div content /div; if (time) html div classtime time /div; msgDiv.innerHTML html; } div.appendChild(msgDiv); // 自动滚动到底部 div.scrollTop div.scrollHeight; } function updateStatus(text, connected) { const el document.getElementById(status); el.textContent text; el.className status (connected ? connected : disconnected); } /script /body /html第七步配置文件# application.yml server: port: 8080 spring: application: name: websocket-demo四、启动和测试启动cd D:\桌面\websocket-demo mvn spring-boot:run或者用 IDEA 直接运行WebSocketDemoApplication.main()测试方式1浏览器测试聊天室打开两个浏览器窗口访问http://localhost:8080/index.html在任意一个窗口发消息两个窗口都会收到。方式2用 curl 模拟服务器推送# 推送告警消息给所有在线用户 curl -X POST http://localhost:8080/api/push ^ -H Content-Type: application/json ^ -d {\message\:\温度超过阈值当前温度 38.5℃\} # 模拟传感器数据推送 curl http://localhost:8080/api/sensor-data # 查看在线人数 curl http://localhost:8080/api/online方式3用 IDEA 的 HTTP Client 测试POST http://localhost:8080/api/push Content-Type: application/json { message: 氨气浓度超标请立即处理 }五、核心概念速查WebSocket 连接状态readyStateWebSocket.CONNECTING 0 // 正在连接 WebSocket.OPEN 1 // 已连接可以收发消息 ← 你只能在这个状态 send() WebSocket.CLOSING 2 // 正在关闭 WebSocket.CLOSED 3 // 已关闭发送消息前必须检查状态// Java 服务端 if (session.isOpen()) { session.sendMessage(new TextMessage(hello)); } // JavaScript 客户端 if (ws.readyState WebSocket.OPEN) { ws.send(hello); }WebSocket 的三种消息类型// 1. 文本消息最常用 session.sendMessage(new TextMessage(hello)); // 2. 二进制消息传文件/图片 session.sendMessage(new BinaryMessage(fileBytes)); // 3. Ping/Pong心跳检测Spring 自动处理 // 你不用手动写Spring WebSocket 默认每 30 秒发一次 Ping线程安全⚠️ WebSocket 是多线程的 多个客户端同时连接 → 多个线程同时调用你的 Handler 所以 ❌ 不能用 ArrayList 存在线用户 → 会并发修改异常 ✅ 用 ConcurrentHashMap → 线程安全 ❌ 不能直接操作 Session 而不判断状态 ✅ 每次操作前 session.isOpen()六、实际项目中的用法场景1MES 看板实时推送传感器数据传感器 → MQTT → 数据采集服务 → 存储到 TDengine ↓ 检测到数据异常 ↓ 调用 WebSocketHandler.broadcast() ↓ 看板实时显示告警 // 在你的 MES 项目中SensorDataCollector 处理完数据后加一行 Service RequiredArgsConstructor public class SensorDataCollector { private final ChatWebSocketHandler wsHandler; // 注入 WebSocket 处理器 public void onMessage(String topic, String payload) { // ... 处理传感器数据 ... if (filterResult.isAbnormal()) { // 异常数据 → 实时推送给看板 String alertMsg String.format( {\type\:\sensor_alert\,\sensorId\:\%s\,\reason\:\%s\,\value\:%.1f}, data.getSensorId(), filterResult.getReason(), data.getValue()); wsHandler.broadcast(alertMsg, null); } } }场景2生产任务进度实时推送Service RequiredArgsConstructor public class ProductionTaskService { private final ChatWebSocketHandler wsHandler; public void updateTaskProgress(Long taskId, int progress) { // 更新数据库 taskMapper.updateProgress(taskId, progress); // 推送给看板 String msg String.format( {\type\:\task_progress\,\taskId\:%d,\progress\:%d}, taskId, progress); wsHandler.broadcast(msg, null); } }七、生产环境注意事项1. 心保活防止连接断开// Spring Boot 已经默认启用了心跳检测Ping/Pong // 在配置类中可以调整 Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatWebSocketHandler, /ws/chat) .setAllowedOrigins(*); // 如果需要自定义可以用注解配置 }2. 连接数限制// 在配置中限制最大连接数 Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatWebSocketHandler, /ws/chat) .setAllowedOrigins(*); } // Tomcat 默认最大连接数可以通过 server.tomcat.max-connections 配置 }3. 鉴权连接时验证 Token// 在 WebSocket 配置中添加拦截器 Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatWebSocketHandler, /ws/chat) .addInterceptors(new HttpSessionHandshakeInterceptor()) // 携带 HTTP Session .setAllowedOrigins(*); } } // 或者用自定义拦截器验证 JWT Component public class JwtWebSocketInterceptor implements HandshakeInterceptor { Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, MapString, Object attributes) { // 从 URL 参数或 Header 中获取 Token // 验证 Token 是否有效 // 无效则 return false拒绝连接 return true; } Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Exception ex) { // 握手完成后可选 } }4. 关闭时清理资源Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { onlineUsers.remove(session.getId()); // 关闭数据库连接、清理缓存等 }八、常见坑和解决方案问题原因解决方案连接后立即断开服务端没有正确处理握手检查EnableWebSocket和 Handler 注册消息收不到readyState 不是 OPEN发送前检查session.isOpen()跨域连不上CORS 限制.setAllowedOrigins(*)连接数爆了没限制连接数Tomcatmax-connections配置服务器重启后客户端没重连客户端没写重连逻辑ws.onclose里 setTimeout 重连消息乱码编码问题确保用TextMessage发送 UTF-8 文本九、总结WebSocket 是全双工长连接协议解决了 HTTP 轮询的延迟和资源浪费问题。在 Spring Boot 中通过EnableWebSocket开启注册WebSocketHandler处理连接、消息、关闭三个核心事件。生产环境要注意线程安全ConcurrentHashMap、心跳保活、连接数限制和鉴权。我们在牧原 MES 项目中用 WebSocket 实现了传感器异常数据的实时推送看板页面毫秒级收到告警。
WebSocket 从零到能跑:Java 开发者版
目录一、WebSocket 到底解决什么问题HTTP 的痛点WebSocket 的解法一句话理解二、核心概念5 分钟搞懂1. 连接过程握手2. WebSocket vs HTTP3. URL 格式三、Spring Boot WebSocket 完整实战做什么项目结构第一步创建项目pom.xml第二步启动类第三步WebSocket 配置告诉 Spring 要用 WebSocket第四步WebSocket 处理器核心业务逻辑第五步REST 接口服务器主动推送消息的入口第六步前端页面index.html第七步配置文件四、启动和测试启动测试五、核心概念速查WebSocket 连接状态readyStateWebSocket 的三种消息类型线程安全六、实际项目中的用法场景1MES 看板实时推送传感器数据场景2生产任务进度实时推送七、生产环境注意事项1. 心保活防止连接断开2. 连接数限制3. 鉴权连接时验证 Token4. 关闭时清理资源八、常见坑和解决方案九、总结适用人群会 Java Spring Boot但没用过 WebSocket 的开发者 目标看完后能独立在 Spring Boot 项目里加 WebSocket 实时推送功能一、WebSocket 到底解决什么问题HTTP 的痛点场景牧原 MES 看板需要实时显示传感器温度 用 HTTP 轮询笨办法 看板页面每隔 2 秒问一次服务器有新数据吗 → 服务器没有 → 看板页面有新数据吗 → 服务器没有 → 看板页面有新数据吗 → 服务器有温度 28.5℃ → 看板页面有新数据吗99% 的时候都是没有 → ...无限循环 问题 ❌ 浪费带宽99% 的请求是无效的 ❌ 浪费服务器资源每秒处理几百个有没有新数据的请求 ❌ 有延迟数据来了最多等 2 秒才能看到WebSocket 的解法用 WebSocket聪明办法 看板页面和服务器建立一条管道 服务器有新数据时直接顺着管道推给看板 → 看板不用问数据来了直接显示 优点 ✅ 实时数据产生后毫秒级推送 ✅ 省资源不用反复建立连接 ✅ 双向客户端也能主动发消息给服务器一句话理解HTTP 你去食堂打饭每次排队点菜拿完走人WebSocket 你和食堂窗口开了根管子厨师炒好菜直接顺着管子滑过来二、核心概念5 分钟搞懂1. 连接过程握手客户端 服务器 | | |--- HTTP 请求带 Upgrade---| ← 我想升级成 WebSocket | | |--- 101 Switching Protocols --| ← 好的升级成功 | | | WebSocket 连接建立 | ← 现在是双向管道了 | | | 随时可以互相发消息 | ← 全双工通信2. WebSocket vs HTTPHTTPWebSocket连接短连接用完就断长连接一直保持通信单向客户端请求→服务器响应双向随时互推数据格式每次都要带 Header冗余只发数据不带 Header精简服务器不能主动找客户端可以主动推消息给客户端3. URL 格式HTTP: http://localhost:8080/api/data WebSocket: ws://localhost:8080/ws/chat HTTPS 的 WebSocket: wss://localhost:8080/ws/chat三、Spring Boot WebSocket 完整实战做什么做一个实时消息推送服务服务器可以随时推消息给所有连接的客户端。场景1MES 看板实时显示传感器数据场景2系统告警实时推送项目结构websocket-demo/ ├── pom.xml ├── src/main/java/com/example/websocketdemo/ │ ├── WebSocketDemoApplication.java # 启动类 │ ├── config/ │ │ └── WebSocketConfig.java # WebSocket 配置 │ ├── controller/ │ │ └── MessageController.java # REST 接口测试用 │ └── handler/ │ └── ChatWebSocketHandler.java # WebSocket 处理器 └── src/main/resources/ ├── application.yml └── static/ └── index.html # 前端页面第一步创建项目pom.xml?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version /parent groupIdcom.example/groupId artifactIdwebsocket-demo/artifactId version1.0.0/version packagingjar/packaging namewebsocket-demo/name descriptionWebSocket 入门演示/description properties java.version17/java.version /properties dependencies !-- Spring Boot Web内嵌 Tomcat -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- ★ 核心Spring Boot WebSocket 支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency !-- Lombok -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project第二步启动类package com.example.websocketdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class WebSocketDemoApplication { public static void main(String[] args) { SpringApplication.run(WebSocketDemoApplication.class, args); System.out.println(); System.out.println( WebSocket 演示项目启动成功); System.out.println( 访问: http://localhost:8080/index.html); System.out.println(); } }第三步WebSocket 配置告诉 Spring 要用 WebSocketpackage com.example.websocketdemo.config; import com.example.websocketdemo.handler.ChatWebSocketHandler; 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; /** * WebSocket 配置类 * * 这个类告诉 Spring * 1. 启用 WebSocket 功能EnableWebSocket * 2. 注册 WebSocket 处理器绑定 URL 路径 */ Configuration EnableWebSocket // ← 开启 WebSocket 功能 public class WebSocketConfig implements WebSocketConfigurer { private final ChatWebSocketHandler chatWebSocketHandler; // Spring 自动注入处理器 public WebSocketConfig(ChatWebSocketHandler chatWebSocketHandler) { this.chatWebSocketHandler chatWebSocketHandler; } Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatWebSocketHandler, /ws/chat) // ← 绑定路径 .setAllowedOrigins(*); // ← 允许所有来源连接开发用 // 生产环境要限制来源比如 // .setAllowedOrigins(https://mes.example.com) } }这一步做了什么当浏览器请求 ws://localhost:8080/ws/chat 时 → Tomcat 把请求交给 ChatWebSocketHandler 处理 → 不是交给 ControllerController 只处理 HTTP 请求第四步WebSocket 处理器核心业务逻辑package com.example.websocketdemo.handler; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.socket.*; import org.springframework.web.socket.handler.TextWebSocketHandler; import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * WebSocket 消息处理器 * * 继承 TextWebSocketHandler → 只处理文本消息 * 如果要处理二进制消息继承 BinaryWebSocketHandler */ Slf4j Component public class ChatWebSocketHandler extends TextWebSocketHandler { /** * 在线用户列表 * key 用户ID用 Session ID 代替 * value WebSocketSession连接对象 * * 用 ConcurrentHashMap 保证线程安全WebSocket 是多线程的 */ private final MapString, WebSocketSession onlineUsers new ConcurrentHashMap(); private static final DateTimeFormatter FORMATTER DateTimeFormatter.ofPattern(HH:mm:ss); // 核心回调方法 /** * ① 连接建立时调用 * 当浏览器 new WebSocket(ws://...) 成功连接后触发 */ Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String sessionId session.getId(); onlineUsers.put(sessionId, session); log.info( 新连接: sessionId{}, 在线人数{}, sessionId, onlineUsers.size()); // 给新连接的客户端发欢迎消息 String welcomeMsg String.format( {\type\:\system\,\content\:\欢迎加入你是第%d位在线用户\,\time\:\%s\}, onlineUsers.size(), LocalDateTime.now().format(FORMATTER) ); session.sendMessage(new TextMessage(welcomeMsg)); // 通知所有人有新用户加入 broadcast({\type\:\system\,\content\:\用户 sessionId.substring(0, 6) 加入了聊天\,\time\:\ LocalDateTime.now().format(FORMATTER) \}, session); } /** * ② 收到消息时调用 * 当浏览器发送 ws.send(xxx) 后触发 */ Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload message.getPayload(); // 获取消息内容 String sessionId session.getId(); log.info( 收到消息: from{}, msg{}, sessionId, payload); // 构建广播消息带发送者信息和时间 String broadcastMsg String.format( {\type\:\chat\,\sender\:\%s\,\content\:\%s\,\time\:\%s\}, sessionId.substring(0, 6), // 用 Session ID 前6位当昵称 payload, LocalDateTime.now().format(FORMATTER) ); // 广播给所有人包括发送者自己 broadcast(broadcastMsg, null); } /** * ③ 连接关闭时调用 * 当浏览器关闭标签页或调用 ws.close() 后触发 */ Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { String sessionId session.getId(); onlineUsers.remove(sessionId); log.info( 连接关闭: sessionId{}, 原因{}, 在线人数{}, sessionId, status, onlineUsers.size()); // 通知其他人 broadcast({\type\:\system\,\content\:\用户 sessionId.substring(0, 6) 离开了\,\time\:\ LocalDateTime.now().format(FORMATTER) \}, null); } /** * ④ 连接出错时调用 */ Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { log.error(❌ 连接错误: sessionId{}, session.getId(), exception); onlineUsers.remove(session.getId()); if (session.isOpen()) { session.close(); } } // 工具方法 /** * 广播消息给所有在线用户 * * param message 要发送的消息 * param excludeSession 要排除的会话null不排除任何人 */ public void broadcast(String message, WebSocketSession excludeSession) { TextMessage textMessage new TextMessage(message); for (WebSocketSession session : onlineUsers.values()) { // 跳过排除的会话 if (excludeSession ! null excludeSession.getId().equals(session.getId())) { continue; } try { if (session.isOpen()) { session.sendMessage(textMessage); } } catch (IOException e) { log.error(广播消息失败: sessionId{}, session.getId(), e); } } } /** * 发送消息给指定用户 */ public void sendToUser(String sessionId, String message) { WebSocketSession session onlineUsers.get(sessionId); if (session ! null session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { log.error(发送消息失败: sessionId{}, sessionId, e); } } } /** * 获取在线用户数 */ public int getOnlineCount() { return onlineUsers.size(); } }第五步REST 接口服务器主动推送消息的入口package com.example.websocketdemo.controller; import com.example.websocketdemo.handler.ChatWebSocketHandler; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * 消息推送 Controller * * 提供 HTTP 接口让外部系统比如 MES 数据采集模块 * 通过 HTTP 请求触发 WebSocket 推送 * * 流程 * 传感器数据 → 数据采集服务 → POST /api/push → WebSocket 广播 → 看板实时显示 */ RestController RequestMapping(/api) RequiredArgsConstructor public class MessageController { private final ChatWebSocketHandler chatWebSocketHandler; private static final DateTimeFormatter FORMATTER DateTimeFormatter.ofPattern(HH:mm:ss); /** * 推送消息给所有在线用户 * * POST /api/push * Body: { message: 温度超过阈值 } * * 测试命令 * curl -X POST http://localhost:8080/api/push \ * -H Content-Type: application/json \ * -d {message:温度超过阈值当前温度 38.5℃} */ PostMapping(/push) public String pushMessage(RequestBody java.util.MapString, String body) { String message body.get(message); if (message null || message.isBlank()) { return 消息不能为空; } // 构建推送消息 String pushMsg String.format( {\type\:\alert\,\content\:\%s\,\time\:\%s\}, message, LocalDateTime.now().format(FORMATTER) ); // 通过 Handler 广播 chatWebSocketHandler.broadcast(pushMsg, null); return 已推送: message; } /** * 模拟传感器数据推送 * * GET /api/sensor-data * * 每次调用生成一条随机温度数据广播给所有客户端 * 测试命令 * curl http://localhost:8080/api/sensor-data */ GetMapping(/sensor-data) public String pushSensorData() { // 模拟传感器数据 double temperature 20 Math.random() * 15; // 20~35℃ String farmId 1001; String sensorId TEMP-001; String sensorMsg String.format( {\type\:\sensor\,\farmId\:\%s\,\sensorId\:\%s\,\temperature\:%.1f,\unit\:\℃\,\time\:\%s\}, farmId, sensorId, temperature, LocalDateTime.now().format(FORMATTER) ); chatWebSocketHandler.broadcast(sensorMsg, null); return String.format(已推送传感器数据: 温度 %.1f℃, temperature); } /** * 获取在线用户数 */ GetMapping(/online) public String getOnlineCount() { return 当前在线: chatWebSocketHandler.getOnlineCount() 人; } }第六步前端页面index.html把文件放在src/main/resources/static/index.htmlSpring Boot 会自动提供静态资源服务。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleWebSocket 实时聊天室/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Microsoft YaHei, sans-serif; background: #1a1a2e; color: #eee; height: 100vh; display: flex; flex-direction: column; } .header { background: #16213e; padding: 15px 20px; text-align: center; font-size: 18px; border-bottom: 2px solid #0f3460; } .header .status { font-size: 13px; color: #aaa; margin-top: 5px; } .header .status.connected { color: #4ecca3; } .header .status.disconnected { color: #e74c3c; } .messages { flex: 1; overflow-y: auto; padding: 15px; } .msg { margin: 8px 0; padding: 10px 15px; border-radius: 12px; max-width: 70%; } .msg.system { background: #2d3436; color: #81ecec; margin: 8px auto; text-align: center; font-size: 13px; border-radius: 20px; max-width: 80%; } .msg.chat { background: #0f3460; } .msg.alert { background: #6c2819; border-left: 4px solid #e74c3c; } .msg.sensor { background: #1b4332; border-left: 4px solid #4ecca3; } .msg .sender { font-size: 12px; color: #4ecca3; margin-bottom: 3px; } .msg .time { font-size: 11px; color: #888; margin-top: 3px; } .input-area { display: flex; padding: 15px; background: #16213e; gap: 10px; } .input-area input { flex: 1; padding: 12px; border: 1px solid #0f3460; border-radius: 8px; background: #1a1a2e; color: #eee; font-size: 15px; outline: none; } .input-area button { padding: 12px 24px; border: none; border-radius: 8px; background: #e94560; color: white; font-size: 15px; cursor: pointer; } .input-area button:hover { background: #c81d4e; } /style /head body div classheader WebSocket 实时聊天室 div classstatus disconnected idstatus未连接/div /div div classmessages idmessages/div div classinput-area input idmsgInput placeholder输入消息按回车发送... / button onclicksendMessage()发送/button /div script // 1. 连接 WebSocket // 地址格式ws://主机:端口/配置的路径 const ws new WebSocket(ws://localhost:8080/ws/chat); // 2. 四个核心事件 // 事件1连接成功 ws.onopen function() { console.log(WebSocket 连接成功); updateStatus(已连接 ✓, true); appendMessage(system, 已连接到服务器); }; // 事件2收到消息服务器推送过来的 ws.onmessage function(event) { console.log(收到消息:, event.data); try { // 解析 JSON 消息 const msg JSON.parse(event.data); // 根据消息类型显示不同样式 if (msg.type system) { appendMessage(system, msg.content); } else if (msg.type chat) { appendMessage(chat, msg.content, msg.sender, msg.time); } else if (msg.type alert) { appendMessage(alert, ⚠️ msg.content, 告警, msg.time); } else if (msg.type sensor) { appendMessage(sensor, ️ ${msg.sensorId}: ${msg.temperature}${msg.unit} (场区: ${msg.farmId}), 传感器, msg.time); } } catch (e) { // 非 JSON 消息直接显示 appendMessage(chat, event.data); } }; // 事件3连接关闭 ws.onclose function(event) { console.log(WebSocket 连接关闭, event); updateStatus(已断开 ✗, false); appendMessage(system, 与服务器断开了连接); // 自动重连3秒后尝试重连 setTimeout(function() { appendMessage(system, 正在尝试重连...); // 实际项目中应该重新创建 WebSocket 连接 }, 3000); }; // 事件4连接出错 ws.onerror function(error) { console.error(WebSocket 出错:, error); updateStatus(连接出错 ✗, false); }; // 3. 发送消息 function sendMessage() { const input document.getElementById(msgInput); const message input.value.trim(); if (!message) return; if (ws.readyState ! WebSocket.OPEN) { appendMessage(system, ❌ 连接未就绪无法发送); return; } // ★ 核心ws.send() 发送消息到服务器 ws.send(message); // 清空输入框 input.value ; } // 回车发送 document.getElementById(msgInput).addEventListener(keydown, function(e) { if (e.key Enter) { sendMessage(); } }); // 4. UI 辅助函数 function appendMessage(type, content, sender, time) { const div document.getElementById(messages); const msgDiv document.createElement(div); msgDiv.className msg type; if (type system) { msgDiv.textContent content; } else { let html ; if (sender) html div classsender sender /div; html div content /div; if (time) html div classtime time /div; msgDiv.innerHTML html; } div.appendChild(msgDiv); // 自动滚动到底部 div.scrollTop div.scrollHeight; } function updateStatus(text, connected) { const el document.getElementById(status); el.textContent text; el.className status (connected ? connected : disconnected); } /script /body /html第七步配置文件# application.yml server: port: 8080 spring: application: name: websocket-demo四、启动和测试启动cd D:\桌面\websocket-demo mvn spring-boot:run或者用 IDEA 直接运行WebSocketDemoApplication.main()测试方式1浏览器测试聊天室打开两个浏览器窗口访问http://localhost:8080/index.html在任意一个窗口发消息两个窗口都会收到。方式2用 curl 模拟服务器推送# 推送告警消息给所有在线用户 curl -X POST http://localhost:8080/api/push ^ -H Content-Type: application/json ^ -d {\message\:\温度超过阈值当前温度 38.5℃\} # 模拟传感器数据推送 curl http://localhost:8080/api/sensor-data # 查看在线人数 curl http://localhost:8080/api/online方式3用 IDEA 的 HTTP Client 测试POST http://localhost:8080/api/push Content-Type: application/json { message: 氨气浓度超标请立即处理 }五、核心概念速查WebSocket 连接状态readyStateWebSocket.CONNECTING 0 // 正在连接 WebSocket.OPEN 1 // 已连接可以收发消息 ← 你只能在这个状态 send() WebSocket.CLOSING 2 // 正在关闭 WebSocket.CLOSED 3 // 已关闭发送消息前必须检查状态// Java 服务端 if (session.isOpen()) { session.sendMessage(new TextMessage(hello)); } // JavaScript 客户端 if (ws.readyState WebSocket.OPEN) { ws.send(hello); }WebSocket 的三种消息类型// 1. 文本消息最常用 session.sendMessage(new TextMessage(hello)); // 2. 二进制消息传文件/图片 session.sendMessage(new BinaryMessage(fileBytes)); // 3. Ping/Pong心跳检测Spring 自动处理 // 你不用手动写Spring WebSocket 默认每 30 秒发一次 Ping线程安全⚠️ WebSocket 是多线程的 多个客户端同时连接 → 多个线程同时调用你的 Handler 所以 ❌ 不能用 ArrayList 存在线用户 → 会并发修改异常 ✅ 用 ConcurrentHashMap → 线程安全 ❌ 不能直接操作 Session 而不判断状态 ✅ 每次操作前 session.isOpen()六、实际项目中的用法场景1MES 看板实时推送传感器数据传感器 → MQTT → 数据采集服务 → 存储到 TDengine ↓ 检测到数据异常 ↓ 调用 WebSocketHandler.broadcast() ↓ 看板实时显示告警 // 在你的 MES 项目中SensorDataCollector 处理完数据后加一行 Service RequiredArgsConstructor public class SensorDataCollector { private final ChatWebSocketHandler wsHandler; // 注入 WebSocket 处理器 public void onMessage(String topic, String payload) { // ... 处理传感器数据 ... if (filterResult.isAbnormal()) { // 异常数据 → 实时推送给看板 String alertMsg String.format( {\type\:\sensor_alert\,\sensorId\:\%s\,\reason\:\%s\,\value\:%.1f}, data.getSensorId(), filterResult.getReason(), data.getValue()); wsHandler.broadcast(alertMsg, null); } } }场景2生产任务进度实时推送Service RequiredArgsConstructor public class ProductionTaskService { private final ChatWebSocketHandler wsHandler; public void updateTaskProgress(Long taskId, int progress) { // 更新数据库 taskMapper.updateProgress(taskId, progress); // 推送给看板 String msg String.format( {\type\:\task_progress\,\taskId\:%d,\progress\:%d}, taskId, progress); wsHandler.broadcast(msg, null); } }七、生产环境注意事项1. 心保活防止连接断开// Spring Boot 已经默认启用了心跳检测Ping/Pong // 在配置类中可以调整 Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatWebSocketHandler, /ws/chat) .setAllowedOrigins(*); // 如果需要自定义可以用注解配置 }2. 连接数限制// 在配置中限制最大连接数 Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatWebSocketHandler, /ws/chat) .setAllowedOrigins(*); } // Tomcat 默认最大连接数可以通过 server.tomcat.max-connections 配置 }3. 鉴权连接时验证 Token// 在 WebSocket 配置中添加拦截器 Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatWebSocketHandler, /ws/chat) .addInterceptors(new HttpSessionHandshakeInterceptor()) // 携带 HTTP Session .setAllowedOrigins(*); } } // 或者用自定义拦截器验证 JWT Component public class JwtWebSocketInterceptor implements HandshakeInterceptor { Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, MapString, Object attributes) { // 从 URL 参数或 Header 中获取 Token // 验证 Token 是否有效 // 无效则 return false拒绝连接 return true; } Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Exception ex) { // 握手完成后可选 } }4. 关闭时清理资源Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { onlineUsers.remove(session.getId()); // 关闭数据库连接、清理缓存等 }八、常见坑和解决方案问题原因解决方案连接后立即断开服务端没有正确处理握手检查EnableWebSocket和 Handler 注册消息收不到readyState 不是 OPEN发送前检查session.isOpen()跨域连不上CORS 限制.setAllowedOrigins(*)连接数爆了没限制连接数Tomcatmax-connections配置服务器重启后客户端没重连客户端没写重连逻辑ws.onclose里 setTimeout 重连消息乱码编码问题确保用TextMessage发送 UTF-8 文本九、总结WebSocket 是全双工长连接协议解决了 HTTP 轮询的延迟和资源浪费问题。在 Spring Boot 中通过EnableWebSocket开启注册WebSocketHandler处理连接、消息、关闭三个核心事件。生产环境要注意线程安全ConcurrentHashMap、心跳保活、连接数限制和鉴权。我们在牧原 MES 项目中用 WebSocket 实现了传感器异常数据的实时推送看板页面毫秒级收到告警。