WebSocket详解及Spring Boot+Java高级实战用法示例
一、为什么需要 WebSocket?
传统的 Web 应用基于 HTTP 协议,是请求-响应模式:
- 客户端发请求 → 服务端处理 → 返回结果 → 连接关闭
- 如果服务端有新数据(如聊天消息、订单状态变更),无法主动推给客户端
为实现“实时通信”,早期常用 轮询(Polling) 或 长轮询(Long Polling):
- 轮询:每隔几秒发一次请求 → 浪费带宽、延迟高
- 长轮询:请求挂起直到有数据 → 仍基于 HTTP,连接频繁建立/断开
✅ WebSocket 的优势:
- 全双工通信:客户端和服务端可随时互相发送数据
- 单 TCP 连接:握手后复用连接,低开销
- 低延迟、高效率:适合实时场景(聊天、股票、游戏、监控)
二、WebSocket 协议简析
握手阶段:客户端通过 HTTP 发起 Upgrade 请求
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13
服务端响应:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
连接建立:后续通信使用 WebSocket 帧(Frame),不再是 HTTP
🔑 关键:一次握手,长期通信
三、Spring Boot 整合 WebSocket(基础版)
场景:简易在线聊天室
1. 添加依赖(pom.xml)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>2. 启用 WebSocket 支持
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册 STOMP 端点,允许跨域
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS(); // 兼容不支持 WebSocket 的浏览器(降级到 xhr-streaming)
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客户端订阅前缀
registry.enableSimpleBroker("/topic", "/queue"); // 服务端推送前缀
registry.setApplicationDestinationPrefixes("/app"); // 客户端发送消息前缀
}
}
💡 使用 STOMP(Simple Text Oriented Messaging Protocol)作为子协议,简化消息路由。
3. 编写控制器(处理客户端消息)
@Controller
public class ChatController {
@MessageMapping("/chat.sendMessage") // 客户端发送到 /app/chat.sendMessage
@SendTo("/topic/public") // 广播给所有订阅 /topic/public 的客户端
public ChatMessage sendMessage(ChatMessage chatMessage) {
return chatMessage;
}
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(@Payload ChatMessage chatMessage,
SimpMessageHeaderAccessor headerAccessor) {
// 将用户名加入会话
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
chatMessage.setContent("加入聊天室");
return chatMessage;
}
}
4. 定义消息实体
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
private String content;
private String sender;
private MessageType type; // enum: CHAT, JOIN, LEAVE
public enum MessageType {
CHAT, JOIN, LEAVE
}
}
5. 前端连接(HTML + JS)
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
<script>
const stompClient = Stomp.over(new SockJS('/ws'));
stompClient.connect({}, () => {
console.log('连接成功');
// 订阅公共频道
stompClient.subscribe('/topic/public', (payload) => {
const msg = JSON.parse(payload.body);
document.getElementById('chat').innerHTML +=
`<div><b>${msg.sender}</b>: ${msg.content}</div>`;
});
// 发送消息
function sendMessage() {
const content = document.getElementById('message').value;
stompClient.send("/app/chat.sendMessage", {}, JSON.stringify({
content: content,
sender: '张三',
type: 'CHAT'
}));
}
});
</script>
✅ 效果:用户加入、发消息,所有在线用户实时看到。
四、高级实战用法
场景1:私聊(点对点通信)
❌ 反例:用 /topic 广播实现私聊 → 信息泄露!
✅ 正确做法:使用 用户专属队列
// 获取当前登录用户(需集成 Spring Security)
private String getCurrentUser(SimpMessageHeaderAccessor headerAccessor) {
// 从 token / session 中解析
return (String) headerAccessor.getSessionAttributes().get("username");
}
@MessageMapping("/chat.private")
public void sendPrivateMessage(@Payload ChatMessage message,
SimpMessageHeaderAccessor headerAccessor) {
String currentUser = getCurrentUser(headerAccessor);
String recipient = message.getRecipient(); // 假设消息中包含接收者
// 构造用户专属队列路径
String destination = "/queue/messages/" + recipient;
// 使用 convertAndSendToUser 发送(自动加 /user 前缀)
messagingTemplate.convertAndSendToUser(recipient, "/queue/messages", message);
}
⚠️ 注意:前端需订阅
/user/queue/messages(STOMP 会自动映射到用户会话)
// 前端订阅私信
stompClient.subscribe('/user/queue/messages', (payload) => {
const msg = JSON.parse(payload.body);
// 显示私聊消息
});
场景2:连接管理 & 在线人数统计
@Component
public class WebSocketEventListener {
private static final Set<String> onlineUsers = ConcurrentHashMap.newKeySet();
@EventListener
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
// 用户上线(需结合认证)
System.out.println("用户连接: " + event.getMessage().getHeaders());
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String username = (String) headerAccessor.getSessionAttributes().get("username");
if (username != null) {
onlineUsers.remove(username);
// 广播在线人数
messagingTemplate.convertAndSend("/topic/online", onlineUsers.size());
}
}
}
可用于“好友在线状态”、“客服排队人数”等场景。
场景3:消息持久化 & 离线消息
当用户离线时,消息不能丢!
方案:
- 发送前检查用户是否在线(维护
Map<userId, sessionId>) - 若不在线,将消息存入数据库(
offline_messages表) - 用户上线时,拉取离线消息并推送
public void sendPrivateMessage(ChatMessage message) {
if (isUserOnline(message.getRecipient())) {
messagingTemplate.convertAndSendToUser(...);
} else {
offlineMessageService.save(message); // 存 DB
}
}
// 用户上线时
@EventListener
public void onUserOnline(SessionConnectedEvent event) {
String userId = getCurrentUserId(event);
List<ChatMessage> offlineMsgs = offlineMessageService.getAndDelete(userId);
offlineMsgs.forEach(msg ->
messagingTemplate.convertAndSendToUser(userId, "/queue/offline", msg)
);
}
五、注意事项 & 常见坑点
| 问题 | 解决方案 |
|---|---|
| 跨域问题 | setAllowedOriginPatterns("*")(生产环境应限制域名) |
| 集群部署 | 单机内存存储会话 → 多节点无法互通 → 需用 Redis + WebSocket 消息广播 |
| 连接断开重连 | 前端监听 onclose,自动重连 + 重新订阅 |
| 消息丢失 | 关键消息需 ACK 机制 + 重试(WebSocket 本身不保证可靠) |
| 安全认证 | 握手时校验 Token(通过 HandshakeInterceptor) |
示例:Token 认证拦截器
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
String token = servletRequest.getServletRequest().getParameter("token");
if (isValidToken(token)) {
attributes.put("userId", parseUserId(token));
return true;
}
}
return false; // 拒绝连接
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {}
}
注册到配置中:
registry.addEndpoint("/ws")
.addInterceptors(new HttpHandshakeInterceptor())
.setAllowedOriginPatterns("*");
六、替代方案对比
| 技术 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| WebSocket | 真正双向、低延迟 | 需要保持长连接 | 聊天、实时协作、游戏 |
| SSE (Server-Sent Events) | 简单、基于 HTTP | 仅服务端→客户端 | 实时通知、日志流 |
| MQTT over WebSocket | 轻量、支持 QoS | 需要额外 Broker | IoT 设备通信 |
| gRPC-Web | 高性能、强类型 | 浏览器支持有限 | 内部微服务调用 |
七、总结
WebSocket 是构建实时 Web 应用的核心技术,关键要点:
- 使用 STOMP + Spring Message Broker 简化开发
- 私聊用
/user/queue,广播用/topic - 连接状态管理 实现在线人数、离线消息
- 集群部署需 Redis 广播(如
spring-boot-starter-redis+RedisMessageListener) - 安全第一:握手时校验身份,避免未授权连接
到此这篇关于WebSocket详解及Spring Boot+Java高级实战用法示例的文章就介绍到这了,更多相关Spring Boot WebSocket详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Spring Data JPA系列JpaSpecificationExecutor用法详解
这篇文章主要为大家介绍了Spring Data JPA系列JpaSpecificationExecutor用法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2022-09-09


最新评论