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

十、延伸阅读

相关推荐
智购科技无人售货机工厂店几秒前
2026自动售货机大屏交互设计原则:从信息层级到视觉动线的工程实践~YH
运维·python·单片机·嵌入式硬件·自动化·交互
ZGG0034 分钟前
MySQL 索引详解:B+ 树、聚簇索引与最左前缀
java·数据库·mysql
quantdash_cc5 分钟前
量化数据源怎么选:稳定性、实时性和数据完整性到底该如何权衡?
开发语言·python·数据分析·量化交易·股票数据·quantdash
萧瑟余晖13 分钟前
Java深入解析篇五十四之对象模型详解
java·开发语言
hfywmsj27 分钟前
广州餐饮铺位招租数据分析:选址权重模型与租金评估实操
人工智能·python·数据分析·广州餐饮铺位招租
张文是假的啊32 分钟前
Java | record | Controller逻辑
java·开发语言
cui_ruicheng35 分钟前
FastAPI 应用开发(一):FastAPI、路由参数与交互式 API 文档
python·fastapi
勿忘,瞬间38 分钟前
Mybatis高阶
java·数据库·mybatis
Multipath7121 小时前
乾元通基于原生UDP时隙感知的多链路聚合方案
网络·网络协议·5g·安全·udp·智能路由器·实时音视频
小柯南敲键盘1 小时前
跨境电商翻译工具,AI批量图片及视频字幕翻译
大数据·人工智能·python·音视频