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(目的地, 回调函数)

十、延伸阅读

相关推荐
WALKING_CODE2 小时前
Anaconda安装完成后启动Jupyter报错,解决方法
ide·python·jupyter
一条咸鱼_SaltyFish2 小时前
Spring Cloud Gateway鉴权空指针惊魂:HandlerMethod为null的深度排查
java·开发语言·人工智能·微服务·云原生·架构
i***13242 小时前
SpringCloud实战十三:Gateway之 Spring Cloud Gateway 动态路由
java·spring cloud·gateway
_OP_CHEN2 小时前
【测试理论与实践】(九)Selenium 自动化测试常用函数全攻略:从元素定位到文件上传,覆盖 99% 实战场景
自动化测试·python·测试开发·selenium·测试工具·测试工程师·自动化工具
计算机徐师兄2 小时前
Java基于微信小程序的食堂线上预约点餐系统【附源码、文档说明】
java·微信小程序·食堂线上预约点餐系统小程序·食堂线上预约点餐微信小程序·java食堂线上预约点餐小程序·食堂线上预约点餐小程序·食堂线上预约点餐系统微信小程序
无心水3 小时前
【分布式利器:腾讯TSF】10、TSF故障排查与架构评审实战:Java架构师从救火到防火的生产哲学
java·人工智能·分布式·架构·限流·分布式利器·腾讯tsf
我的xiaodoujiao4 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 38--Allure 测试报告
python·学习·测试工具·pytest
Boilermaker199210 小时前
[Java 并发编程] Synchronized 锁升级
java·开发语言
沈浩(种子思维作者)10 小时前
真的能精准医疗吗?癌症能提前发现吗?
人工智能·python·网络安全·健康医疗·量子计算