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

十、延伸阅读

相关推荐
小羊没烦恼!4 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
默_笙4 天前
🍙 给每个请求过安检:FastAPI 是怎么把校验写进类型注解的
python
俊昭喜喜里4 天前
java中的继承和多态的区别
java
小羊没烦恼!4 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
qq_426003964 天前
启动playwright录制codegen生成自动化测试脚本
python·自动化
譕痕4 天前
JSONObject与JSONArray封装数据格式区别
java·json
虎头金猫4 天前
4K 视频总卡在公网带宽?用 N1 + OpenList 把网盘播放链路重新理顺
运维·服务器·网络·python·容器·beautifulsoup·pandas
胡写代码4 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖4 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
长沙三为智能科技4 天前
家政小程序开发从0到上线:五阶段交付流程与验收清单
python