目录
[一、WebSocket 到底解决什么问题?](#一、WebSocket 到底解决什么问题?)
[HTTP 的痛点](#HTTP 的痛点)
[WebSocket 的解法](#WebSocket 的解法)
[二、核心概念(5 分钟搞懂)](#二、核心概念(5 分钟搞懂))
[1. 连接过程(握手)](#1. 连接过程(握手))
[2. WebSocket vs HTTP](#2. WebSocket vs HTTP)
[3. URL 格式](#3. URL 格式)
[三、Spring Boot + WebSocket 完整实战](#三、Spring Boot + WebSocket 完整实战)
[第三步:WebSocket 配置(告诉 Spring 要用 WebSocket)](#第三步:WebSocket 配置(告诉 Spring 要用 WebSocket))
[第四步:WebSocket 处理器(核心业务逻辑)](#第四步:WebSocket 处理器(核心业务逻辑))
[第五步:REST 接口(服务器主动推送消息的入口)](#第五步:REST 接口(服务器主动推送消息的入口))
[WebSocket 连接状态(readyState)](#WebSocket 连接状态(readyState))
[WebSocket 的三种消息类型](#WebSocket 的三种消息类型)
[场景1:MES 看板实时推送传感器数据](#场景1:MES 看板实时推送传感器数据)
[1. 心保活(防止连接断开)](#1. 心保活(防止连接断开))
[2. 连接数限制](#2. 连接数限制)
[3. 鉴权(连接时验证 Token)](#3. 鉴权(连接时验证 Token))
[4. 关闭时清理资源](#4. 关闭时清理资源)
适用人群:会 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 HTTP
| HTTP | WebSocket | |
|---|---|---|
| 连接 | 短连接,用完就断 | 长连接,一直保持 |
| 通信 | 单向(客户端请求→服务器响应) | 双向(随时互推) |
| 数据格式 | 每次都要带 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 完整实战
做什么
做一个实时消息推送服务:服务器可以随时推消息给所有连接的客户端。
-
场景1:MES 看板实时显示传感器数据
-
场景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)
java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<groupId>com.example</groupId>
<artifactId>websocket-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>websocket-demo</name>
<description>WebSocket 入门演示</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web(内嵌 Tomcat) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ★ 核心:Spring Boot 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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
第二步:启动类
java
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 要用 WebSocket)
java
package 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 处理
→ 不是交给 Controller(Controller 只处理 HTTP 请求)
第四步:WebSocket 处理器(核心业务逻辑)
java
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 Map<String, 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 接口(服务器主动推送消息的入口)
java
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.Map<String, 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.html,Spring Boot 会自动提供静态资源服务。
java
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebSocket 实时聊天室</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 class="header">
💬 WebSocket 实时聊天室
<div class="status disconnected" id="status">未连接</div>
</div>
<div class="messages" id="messages"></div>
<div class="input-area">
<input id="msgInput" placeholder="输入消息,按回车发送..." />
<button onclick="sendMessage()">发送</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 class="sender">' + sender + '</div>';
html += '<div>' + content + '</div>';
if (time) html += '<div class="time">' + 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>
第七步:配置文件
XML
# 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 连接状态(readyState)
WebSocket.CONNECTING = 0 // 正在连接 WebSocket.OPEN = 1 // 已连接,可以收发消息 ← 你只能在这个状态 send() WebSocket.CLOSING = 2 // 正在关闭 WebSocket.CLOSED = 3 // 已关闭
发送消息前必须检查状态:
java
// Java 服务端
if (session.isOpen()) {
session.sendMessage(new TextMessage("hello"));
}
// JavaScript 客户端
if (ws.readyState === WebSocket.OPEN) {
ws.send("hello");
}
WebSocket 的三种消息类型
java
// 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()
六、实际项目中的用法
场景1:MES 看板实时推送传感器数据
java
传感器 → 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:生产任务进度实时推送
java
@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. 心保活(防止连接断开)
java
// Spring Boot 已经默认启用了心跳检测(Ping/Pong)
// 在配置类中可以调整:
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatWebSocketHandler, "/ws/chat")
.setAllowedOrigins("*");
// 如果需要自定义,可以用注解配置
}
2. 连接数限制
java
// 在配置中限制最大连接数
@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)
java
// 在 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, Map<String, Object> attributes) {
// 从 URL 参数或 Header 中获取 Token
// 验证 Token 是否有效
// 无效则 return false,拒绝连接
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler handler, Exception ex) {
// 握手完成后(可选)
}
}
4. 关闭时清理资源
java
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
onlineUsers.remove(session.getId());
// 关闭数据库连接、清理缓存等
}
八、常见坑和解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 连接后立即断开 | 服务端没有正确处理握手 | 检查 @EnableWebSocket 和 Handler 注册 |
| 消息收不到 | readyState 不是 OPEN | 发送前检查 session.isOpen() |
| 跨域连不上 | CORS 限制 | .setAllowedOrigins("*") |
| 连接数爆了 | 没限制连接数 | Tomcat max-connections 配置 |
| 服务器重启后客户端没重连 | 客户端没写重连逻辑 | ws.onclose 里 setTimeout 重连 |
| 消息乱码 | 编码问题 | 确保用 TextMessage 发送 UTF-8 文本 |
九、总结
WebSocket 是全双工长连接协议,解决了 HTTP 轮询的延迟和资源浪费问题。在 Spring Boot 中,通过
@EnableWebSocket开启,注册WebSocketHandler处理连接、消息、关闭三个核心事件。生产环境要注意线程安全(ConcurrentHashMap)、心跳保活、连接数限制和鉴权。我们在牧原 MES 项目中用 WebSocket 实现了传感器异常数据的实时推送,看板页面毫秒级收到告警。