WebSocket 两种实现方式对比与入门

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。

原因:

  1. REST API 更容易做权限校验(Sa-Token 拦截器)
  2. REST API 更容易做参数校验
  3. 消息需要持久化到数据库
  4. WebSocket 只负责推送,不负责接收

Q2:SimpleBroker 和外部代理的区别?

特性 SimpleBroker RabbitMQ/Redis
部署 内嵌,无需额外服务 需要额外部署
性能 足够(1000连接) 更高(10万连接)
持久化
集群 不支持 支持
适用场景 小型项目 大型分布式

Q3:如何调试 WebSocket?

  1. Chrome 开发者工具:Network → WS 标签页
  2. 查看 STOMP 帧:可以看到 CONNECT、SUBSCRIBE 等命令
  3. 后端日志 :开启 logging.level.org.springframework.messaging=DEBUG

九、总结

之前的写法 现在的写法
继承 TextWebSocketHandler 配置 WebSocketMessageBrokerConfigurer
手动维护 ConcurrentHashMap Spring 自动管理
自定义 JSON 协议 STOMP 标准协议
手动遍历发送 messagingTemplate.convertAndSend()
代码量 200+ 行 代码量 60 行

核心要点

  1. 后端只需配置 + 注入 SimpMessagingTemplate
  2. 前端用 SockJS + STOMP.js 连接
  3. 发消息 = 调用 convertAndSend(目的地, 消息体)
  4. 收消息 = 前端 subscribe(目的地, 回调函数)

十、延伸阅读

相关推荐
全栈老石11 分钟前
Python 异步生存手册:给被 JS async/await 宠坏的全栈工程师
后端·python
大模型玩家七七14 分钟前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习
梨落秋霜19 分钟前
Python入门篇【模块/包】
python
默默前行的虫虫21 分钟前
解决EMQX WebSocket连接不稳定及优化WS配置提升稳定性?
websocket
CodeToGym1 小时前
【Java 办公自动化】Apache POI 入门:手把手教你实现 Excel 导入与导出
java·apache·excel
凡人叶枫1 小时前
C++中智能指针详解(Linux实战版)| 彻底解决内存泄漏,新手也能吃透
java·linux·c语言·开发语言·c++·嵌入式开发
JMchen1231 小时前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
那就回到过去1 小时前
MPLS多协议标签交换
网络·网络协议·hcip·mpls·ensp
阔皮大师1 小时前
INote轻量文本编辑器
java·javascript·python·c#
小法师爱分享1 小时前
StickyNotes,简单便签超实用
java·python