WebSocket 两种实现方式对比与入门
一、概述
本文档是我在开发项目时写的, 主要是介绍了 原生 WebSocket 和 STOMP+SockJS 的区别
介绍的比较意识流,有些地方读者可以忽略,重点关注它们的区别和设计的技术知识点
项目中存在两种 WebSocket 实现方式:
| 方式 | 文件 | 状态 | 说明 |
|---|---|---|---|
| 原生 WebSocket | RawWebSocketHandler.java |
已弃用 | 你之前写的方式 |
| STOMP + SockJS | WebSocketConfig.java + WebSocketService.java |
正在使用 | Spring 推荐方式 |
二、原生 WebSocket 写法回顾
2.1 核心代码结构
java
// 继承 TextWebSocketHandler
public class RawWebSocketHandler extends TextWebSocketHandler {
// 手动管理连接
private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
// 手动管理订阅关系
private static final Map<Long, Map<String, WebSocketSession>> meetingSubscriptions = new ConcurrentHashMap<>();
// 连接建立时
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// 1. 从 URL 解析 token
// 2. 验证用户身份
// 3. 存储 session 到 Map
}
// 收到消息时
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
// 1. 解析 JSON
// 2. 根据 type 字段判断操作类型
// 3. 手动处理订阅/取消订阅
}
// 连接关闭时
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
// 1. 从 Map 移除 session
// 2. 清理订阅关系
}
// 广播消息
public void broadcast(Long meetingId, Object payload) {
// 1. 获取订阅该会议的所有 session
// 2. 遍历发送消息
}
}
2.2 配置类
java
@Configuration
@EnableWebSocket
public class RawWebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(rawWebSocketHandler, "/ws-raw")
.setAllowedOriginPatterns("*");
}
}
2.3 前端连接
javascript
// 原生 WebSocket
const ws = new WebSocket('ws://localhost:12120/api/ws-raw?token=xxx');
ws.onopen = () => {
// 手动发送订阅消息
ws.send(JSON.stringify({ type: 'SUBSCRIBE', meetingId: 123 }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 处理消息
};
2.4 原生方式的痛点
| 痛点 | 说明 |
|---|---|
| 手动管理连接 | 需要自己维护 ConcurrentHashMap |
| 手动管理订阅 | 需要自己实现订阅/取消订阅逻辑 |
| 自定义协议 | 需要自己定义 { type: "SUBSCRIBE" } 等消息格式 |
| 兼容性问题 | 部分浏览器/网络环境不支持原生 WebSocket |
| 心跳机制 | 需要自己实现心跳保活 |
| 重连机制 | 需要前端自己实现断线重连 |
三、STOMP + SockJS 写法(推荐)
3.1 什么是 STOMP?
STOMP(Simple Text Oriented Messaging Protocol)是一种简单的文本消息协议,类似于 HTTP,但专为消息传递设计。
STOMP 帧结构:
┌─────────────────────┐
│ COMMAND │ ← 命令:CONNECT, SUBSCRIBE, SEND, etc.
│ header1:value1 │ ← 头信息
│ header2:value2 │
│ │
│ Body │ ← 消息体
└─────────────────────┘
常用命令:
CONNECT- 建立连接SUBSCRIBE- 订阅目的地UNSUBSCRIBE- 取消订阅SEND- 发送消息DISCONNECT- 断开连接
3.2 什么是 SockJS?
SockJS 是一个浏览器端的 JavaScript 库,提供跨浏览器的 WebSocket 兼容层。
浏览器支持 WebSocket?
├── 是 → 使用原生 WebSocket
└── 否 → 自动降级为其他方式
├── xhr-streaming
├── xhr-polling
└── iframe-htmlfile
3.3 后端配置(超简单)
java
@Configuration
@EnableWebSocketMessageBroker // 一个注解开启一切
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 启用简单消息代理,前缀为 /topic
registry.enableSimpleBroker("/topic");
// 客户端发送消息的前缀
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册端点,客户端通过 /ws 连接
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS(); // 启用 SockJS 回退
}
}
就这么多! 不需要:
- 不需要手动管理连接
- 不需要手动管理订阅
- 不需要自定义协议
- Spring 帮你全部搞定
3.4 后端发送消息(超简单)
java
@Service
@RequiredArgsConstructor
public class WebSocketService {
// Spring 提供的消息模板,注入即用
private final SimpMessagingTemplate messagingTemplate;
// 推送消息到会议房间
public void pushMessage(Long meetingId, Message message) {
// 构建目的地
String destination = "/topic/meeting/" + meetingId;
// 构建消息体
Map<String, Object> payload = new HashMap<>();
payload.put("type", "NEW_MESSAGE");
payload.put("data", message);
// 一行代码发送!
messagingTemplate.convertAndSend(destination, payload);
}
}
3.5 前端连接(使用 STOMP.js + SockJS)
javascript
import SockJS from 'sockjs-client/dist/sockjs.min.js';
import { Client } from '@stomp/stompjs';
// 创建 STOMP 客户端
const stompClient = new Client({
// 使用 SockJS 作为传输层
webSocketFactory: () => new SockJS('http://localhost:12120/api/ws'),
// 自动重连
reconnectDelay: 5000,
// 心跳间隔
heartbeatIncoming: 10000,
heartbeatOutgoing: 10000,
});
// 连接成功回调
stompClient.onConnect = () => {
console.log('WebSocket 已连接');
// 订阅会议频道(一行代码!)
stompClient.subscribe('/topic/meeting/123', (message) => {
const data = JSON.parse(message.body);
console.log('收到消息:', data);
});
};
// 连接错误回调
stompClient.onStompError = (frame) => {
console.error('STOMP 错误:', frame);
};
// 激活连接
stompClient.activate();
四、两种方式对比
| 维度 | 原生 WebSocket | STOMP + SockJS |
|---|---|---|
| 代码量 | 200+ 行 | 60 行 |
| 连接管理 | 手动 Map 维护 | Spring 自动 |
| 订阅管理 | 手动实现 | 内置支持 |
| 消息协议 | 自定义 JSON | STOMP 标准 |
| 心跳机制 | 手动实现 | 内置支持 |
| 断线重连 | 手动实现 | 库自带 |
| 浏览器兼容 | 差(需 polyfill) | 好(SockJS 降级) |
| 调试难度 | 较难 | 简单(有标准帧) |
| 扩展性 | 差 | 可切换外部代理 |
五、STOMP 核心概念
5.1 目的地(Destination)
类似于 HTTP 的 URL,但用于消息路由。
/topic/meeting/123 ← 会议 123 的频道
/topic/system/notice ← 系统通知频道
/user/queue/private ← 用户私有队列
前缀含义:
/topic- 广播模式,所有订阅者都能收到/queue- 点对点模式,只有一个消费者收到/user- 用户私有,发送给特定用户
5.2 消息代理(Message Broker)
┌───────────────┐
│ Message Broker│
│ (SimpleBroker)│
└───────┬───────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
/topic/meeting/1 /topic/meeting/2 /topic/system
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ 用户A │ │ 用户C │ │ 全体用户 │
│ 用户B │ │ 用户D │ │ │
└─────────┘ └─────────┘ └─────────┘
本项目使用 SimpleBroker(内存代理),适合小规模应用。
如需扩展,可替换为 RabbitMQ、Redis 等外部代理。
5.3 消息流向
发送消息:
┌──────────┐ REST API ┌──────────┐ STOMP ┌──────────┐
│ 管理员 │ ──────────▶ │ 后端 │ ─────────▶ │ 成员 │
│ (发送者) │ POST请求 │ (推送) │ /topic │ (接收者) │
└──────────┘ └──────────┘ └──────────┘
本项目没有用 STOMP 发送,而是用 REST API 发送 + STOMP 推送。
这样更适合做权限控制和消息持久化。
六、快速入门实战
6.1 场景:新增一个系统公告频道
需求:向所有在线用户推送系统公告
Step 1:后端添加推送方法
java
// WebSocketService.java
public void pushGlobalNotice(String content) {
String destination = "/topic/system/notice";
Map<String, Object> payload = new HashMap<>();
payload.put("type", "GLOBAL_NOTICE");
payload.put("content", content);
payload.put("time", LocalDateTime.now());
messagingTemplate.convertAndSend(destination, payload);
}
Step 2:后端调用(例如在 Controller 中)
java
@PostMapping("/notice")
public Result<Void> sendNotice(@RequestBody String content) {
webSocketService.pushGlobalNotice(content);
return Result.success();
}
Step 3:前端订阅
javascript
stompClient.subscribe('/topic/system/notice', (message) => {
const data = JSON.parse(message.body);
// 显示公告
showNotification(data.content);
});
完成! 三步搞定一个新频道。
6.2 场景:向特定用户发送私信
Step 1:后端推送
java
public void pushToUser(Long userId, Object message) {
// 使用 convertAndSendToUser 方法
messagingTemplate.convertAndSendToUser(
userId.toString(), // 用户标识
"/queue/private", // 目的地
message // 消息体
);
}
Step 2:前端订阅
javascript
// 注意:实际目的地会变成 /user/queue/private
stompClient.subscribe('/user/queue/private', (message) => {
const data = JSON.parse(message.body);
// 处理私信
});
七、本项目的 WebSocket 架构
┌─────────────────────────────────────────────────────────────────┐
│ 前端 (H5/PC) │
├─────────────────────────────────────────────────────────────────┤
│ SockJS + STOMP.js │
│ ├── 连接: /api/ws │
│ ├── 订阅: /topic/meeting/{meetingId} │
│ └── 接收: { type: "NEW_MESSAGE", data: {...} } │
└──────────────────────────┬──────────────────────────────────────┘
│ WebSocket (STOMP over SockJS)
▼
┌─────────────────────────────────────────────────────────────────┐
│ 后端 (Spring Boot) │
├─────────────────────────────────────────────────────────────────┤
│ WebSocketConfig.java │
│ ├── 端点: /ws │
│ ├── 消息代理: SimpleBroker │
│ └── 目的地前缀: /topic │
├─────────────────────────────────────────────────────────────────┤
│ WebSocketService.java │
│ ├── pushMessage() → 推送新消息 │
│ ├── pushMessageRecall() → 推送撤回通知 │
│ ├── pushMessageEdit() → 推送编辑通知 │
│ ├── pushMemberKicked() → 推送踢人通知 │
│ └── pushMeetingStatusChange() → 推送状态变更 │
└─────────────────────────────────────────────────────────────────┘
八、常见问题
Q1:为什么不用 @MessageMapping 接收前端消息?
本项目中,消息发送走的是 REST API(POST /api/message),而不是 WebSocket。
原因:
- REST API 更容易做权限校验(Sa-Token 拦截器)
- REST API 更容易做参数校验
- 消息需要持久化到数据库
- WebSocket 只负责推送,不负责接收
Q2:SimpleBroker 和外部代理的区别?
| 特性 | SimpleBroker | RabbitMQ/Redis |
|---|---|---|
| 部署 | 内嵌,无需额外服务 | 需要额外部署 |
| 性能 | 足够(1000连接) | 更高(10万连接) |
| 持久化 | 无 | 有 |
| 集群 | 不支持 | 支持 |
| 适用场景 | 小型项目 | 大型分布式 |
Q3:如何调试 WebSocket?
- Chrome 开发者工具:Network → WS 标签页
- 查看 STOMP 帧:可以看到 CONNECT、SUBSCRIBE 等命令
- 后端日志 :开启
logging.level.org.springframework.messaging=DEBUG
九、总结
| 之前的写法 | 现在的写法 |
|---|---|
继承 TextWebSocketHandler |
配置 WebSocketMessageBrokerConfigurer |
手动维护 ConcurrentHashMap |
Spring 自动管理 |
| 自定义 JSON 协议 | STOMP 标准协议 |
| 手动遍历发送 | messagingTemplate.convertAndSend() |
| 代码量 200+ 行 | 代码量 60 行 |
核心要点:
- 后端只需配置 + 注入
SimpMessagingTemplate - 前端用 SockJS + STOMP.js 连接
- 发消息 = 调用
convertAndSend(目的地, 消息体) - 收消息 = 前端
subscribe(目的地, 回调函数)