WebSocket 全面深度技术文档:原始协议、SockJS 模拟与 STOMP 发布/订阅体系

一、什么是 WebSocket

WebSocket 是一种在单个 TCP 连接上实现全双工、持久化、低延迟双向通信的网络协议,由 IETF 标准化为 RFC 6455(2011 年),是 HTML5 的核心组成部分。

官方定义
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。WebSocket 通信协议在 2011 年由 IETF 定义为 RFC 6455,并由 IANA 注册端口号。

🌐 核心特性

特性说明
全双工客户端与服务器可同时发送和接收数据,无需等待对方响应
持久连接握手成功后连接长期保持,避免 HTTP 的频繁建连开销
低开销数据帧最小仅 2 字节(控制帧),无 HTTP 头部冗余
基于 TCP保证数据可靠、有序传输
协议升级通过 HTTP/1.1 Upgrade 机制从 HTTP 升级而来,兼容代理和防火墙
文本/二进制支持 UTF-8 文本(JSON)和任意二进制数据(图片、音频)

二、WebSocket 的核心作用

WebSocket 的诞生是为了解决传统 HTTP 协议在实时通信中的根本缺陷:

场景HTTP 的问题WebSocket 的解决方案
实时聊天需客户端每秒轮询 → 浪费带宽、延迟高(1~5秒)服务器主动推送 → 延迟 < 50ms,零轮询
股票行情每秒数百次更新 → 服务器压力巨大一条连接推送所有数据 → 资源节省 90%+
在线游戏玩家动作需秒级同步双向实时同步,支持每秒 20+ 次状态更新
协同编辑多人编辑文档,光标位置需实时同步操作事件广播,毫秒级响应
IoT 设备监控设备上报状态 + 服务器下发指令一条连接双向通信,省去双通道
实时通知系统消息、订单状态更新推送而非轮询,用户体验提升

💡 一句话总结作用
WebSocket 让 Web 应用具备了与原生 App 一样的实时交互能力,是构建现代实时 Web 应用的基石。


三、WebSocket 原始交互:底层消息传递机制详解

1. WebSocket 握手流程(HTTP → WebSocket 升级)

✅ 客户端发起升级请求(HTTP)
GET /chat HTTP/1.1
Host: example.com:8080
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
  • Connection: Upgrade:请求协议升级
  • Upgrade: websocket:指定升级为 WebSocket 协议
  • Sec-WebSocket-Key:客户端生成的 16 字节随机值(Base64 编码),用于防伪造
  • Sec-WebSocket-Version: 13:协议版本(RFC 6455)
✅ 服务端响应(101 Switching Protocols)
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

🔐 Sec-WebSocket-Accept 计算方式:

String key = "dGhlIHNhbXBsZSBub25jZQ==";
String magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
String accept = Base64.encodeToString(
    SHA1(key + magic), 
    Base64.NO_WRAP
); // 结果:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

✅ 握手成功后,TCP 连接从 HTTP 协议切换为 WebSocket 协议,后续通信不再使用 HTTP。


2. WebSocket 消息帧格式(二进制协议)

WebSocket 通信使用**二进制帧(Frame)**结构,每个帧包含:

字段长度说明
FIN1 bit是否为最后一帧(单消息可分片)
RSV1, RSV2, RSV33 bits保留位,用于扩展协议(通常为 0)
Opcode4 bits操作码,定义帧类型(见下表)
Mask1 bit是否启用掩码(客户端→服务端必须为 1)
Payload Length7/7+16/7+64 bits负载长度(0~125、126、127)
Masking-Key4 bytes仅当 Mask=1 时存在(用于防缓存攻击)
Payload Data可变实际数据(文本或二进制)
📌 Opcode 常见类型
Opcode名称说明
0x0Continuation分片消息的后续帧
0x1TextUTF-8 文本消息(如 JSON)
0x2Binary二进制数据(如图片、文件)
0x8Close关闭连接
0x9Ping心跳检测(服务端需回复 Pong)
0xAPong心跳响应
✅ 示例:发送一条文本消息 "Hello, WebSocket!"
81 8D 3F 1A 2C 4E 12 05 0B 1F 0C 0B 18 05 0D 1D 0D 1C 18
字段说明
8110000001FIN=1, Opcode=0x1(Text)
8D10001101Mask=1, Payload Length=13(0x0D)
3F 1A 2C 4EMasking-Key客户端生成的 4 字节密钥
12 05 0B 1F 0C 0B 18 05 0D 1D 0D 1C 18Payload(异或后)原始数据 "Hello, WebSocket!" 经掩码异或处理

🔍 解码过程:对每个字节与 Masking-Key[i % 4] 进行 XOR 运算,还原明文。


3. 原始 WebSocket 客户端示例(JavaScript)

<!DOCTYPE html>
<html>
<head>
    <title>原始 WebSocket 示例</title>
</head>
<body>
    <h2>原始 WebSocket 通信</h2>
    <button onclick="connect()">连接</button>
    <button onclick="sendMessage()">发送消息</button>
    <button onclick="disconnect()">断开</button>
    <div id="messages"></div>

    <script>
        let ws = null;

        // 1. 建立连接
        function connect() {
            // 使用 wss:// 保证安全(生产环境必须)
            ws = new WebSocket('ws://localhost:8080/ws');

            // 监听连接打开
            ws.onopen = function(event) {
                console.log('✅ WebSocket 连接已建立');
                appendMessage('连接成功:服务器已就绪');
            };

            // 监听消息接收
            ws.onmessage = function(event) {
                const message = event.data;
                console.log('📥 收到消息:', message);
                appendMessage('收到:' + message);
            };

            // 监听连接关闭
            ws.onclose = function(event) {
                console.log('🔌 WebSocket 连接已关闭,代码:' + event.code);
                appendMessage('连接已断开(代码:' + event.code + ')');
            };

            // 监听错误
            ws.onerror = function(error) {
                console.error('❌ WebSocket 错误:', error);
                appendMessage('连接错误:' + error.message);
            };
        }

        // 2. 发送消息
        function sendMessage() {
            if (ws && ws.readyState === WebSocket.OPEN) {
                const msg = prompt('输入要发送的消息:');
                if (msg) {
                    ws.send(msg); // 发送文本消息(自动编码为 UTF-8)
                    appendMessage('发送:' + msg);
                }
            } else {
                alert('请先连接服务器!');
            }
        }

        // 3. 断开连接
        function disconnect() {
            if (ws) {
                ws.close(); // 发送 Close 帧(Opcode=0x8)
                ws = null;
            }
        }

        // 4. 显示消息到页面
        function appendMessage(text) {
            const div = document.createElement('div');
            div.textContent = text;
            div.style.margin = '5px 0';
            div.style.padding = '8px';
            div.style.border = '1px solid #ddd';
            div.style.borderRadius = '4px';
            document.getElementById('messages').appendChild(div);
        }
    </script>
</body>
</html>

4. 原始 WebSocket 服务端示例(Java + Spring Boot)

package com.example.websocketdemo;

import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * 原始 WebSocket 服务端端点
 * 
 * @ServerEndpoint("/ws"):注册 WebSocket 端点路径
 * 每个连接创建一个独立实例(@ServerEndpoint 是线程不安全的,需注意共享资源)
 */
@Component
@ServerEndpoint("/ws")
public class RawWebSocketEndpoint {

    // 存储所有活跃连接(线程安全)
    private static final CopyOnWriteArraySet<RawWebSocketEndpoint> clients = new CopyOnWriteArraySet<>();

    // 当连接建立时调用
    @OnOpen
    public void onOpen(Session session) {
        clients.add(this);
        System.out.println("✅ 新客户端连接,当前在线人数:" + clients.size());
        try {
            // 主动发送欢迎消息
            session.getBasicRemote().sendText("🎉 欢迎连接原始 WebSocket 服务!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 当收到消息时调用
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("📥 收到消息:" + message);

        // 广播给所有客户端(群发)
        for (RawWebSocketEndpoint client : clients) {
            try {
                client.session.getBasicRemote().sendText("广播:" + message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // 当连接关闭时调用
    @OnClose
    public void onClose() {
        clients.remove(this);
        System.out.println("🔌 客户端断开,当前在线人数:" + clients.size());
    }

    // 当发生错误时调用
    @OnError
    public void onError(Session session, Throwable error) {
        System.err.println("❌ WebSocket 错误:" + error.getMessage());
        error.printStackTrace();
    }

    // 注入当前 Session(用于发送消息)
    private Session session;

    @OnOpen
    public void onOpen(Session session) {
        this.session = session; // 保存 session 实例
        clients.add(this);
        // ... 其他逻辑
    }
}

⚠️ 原始 WebSocket 的缺点

  • 无标准消息格式(需自行设计 JSON 协议)
  • 无发布/订阅模型(需手动实现频道管理)
  • 无心跳机制(需手动实现 Ping/Pong)
  • 无用户认证(需在握手阶段传递 Token)
  • 无重连机制(需前端自行实现)

四、通过 SockJS 进行 WebSocket 模拟(降级兼容)

1. 为什么需要 SockJS?

尽管 WebSocket 是现代标准,但在以下场景中可能无法使用

场景问题
企业防火墙拦截 Upgrade
老旧浏览器IE9、Android 4.4 以下不支持
代理服务器不支持 WebSocket 协议升级
CDN/负载均衡不支持长连接

💡 SockJS 的使命
在不支持 WebSocket 的环境中,自动降级为兼容性更好的通信方式,但对外提供完全一致的 JavaScript API

2. SockJS 支持的降级传输方式(优先级从高到低)

传输方式说明兼容性
WebSocket原生协议(首选)现代浏览器
xhr-streaming使用 XMLHttpRequest 长连接IE9+
xhr-polling使用 XMLHttpRequest 轮询IE8+
jsonp-polling使用 JSONP 跨域轮询IE6+(仅用于跨域)

✅ SockJS 客户端会自动选择最优可用通道,开发者无需关心。

3. SockJS 与 WebSocket 的对比

特性WebSocketSockJS
协议原生 WebSocket模拟 WebSocket API
延迟极低(<10ms)略高(轮询 1~5s)
带宽极低较高(每次请求有 HTTP 头)
兼容性现代浏览器IE6+、老旧设备
服务端依赖需 WebSocket 服务需 SockJS 服务端库
API 一致性new WebSocket()new SockJS(url) → 完全相同接口

4. SockJS 客户端示例(兼容所有浏览器)

<!DOCTYPE html>
<html>
<head>
    <title>SockJS 模拟 WebSocket</title>
    <!-- 引入 SockJS 客户端库 -->
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
</head>
<body>
    <h2>SockJS 模拟 WebSocket(兼容 IE8+)</h2>
    <button onclick="connect()">连接</button>
    <button onclick="sendMessage()">发送</button>
    <button onclick="disconnect()">断开</button>
    <div id="messages"></div>

    <script>
        let sock = null;

        function connect() {
            // 使用 SockJS 替代 WebSocket
            // SockJS 会自动选择最佳传输方式(WebSocket → xhr-streaming → xhr-polling)
            sock = new SockJS('http://localhost:8080/sockjs');

            sock.onopen = function() {
                console.log('✅ SockJS 连接成功(传输方式:' + sock.protocol + ')');
                appendMessage('连接成功(传输:' + sock.protocol + ')');
            };

            sock.onmessage = function(event) {
                console.log('📥 收到:', event.data);
                appendMessage('收到:' + event.data);
            };

            sock.onclose = function() {
                console.log('🔌 SockJS 连接已关闭');
                appendMessage('连接已断开');
            };

            sock.onerror = function(error) {
                console.error('❌ SockJS 错误:', error);
                appendMessage('连接错误:' + error.message);
            };
        }

        function sendMessage() {
            if (sock && sock.readyState === SockJS.OPEN) {
                const msg = prompt('输入消息:');
                if (msg) {
                    sock.send(msg); // 与 WebSocket API 完全一致
                    appendMessage('发送:' + msg);
                }
            } else {
                alert('请先连接!');
            }
        }

        function disconnect() {
            if (sock) {
                sock.close();
                sock = null;
            }
        }

        function appendMessage(text) {
            const div = document.createElement('div');
            div.textContent = text;
            div.style.margin = '5px 0';
            div.style.padding = '8px';
            div.style.border = '1px solid #ccc';
            div.style.borderRadius = '4px';
            document.getElementById('messages').appendChild(div);
        }
    </script>
</body>
</html>

5. SockJS 服务端配置(Spring Boot)

package com.example.websocketdemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket // 启用 WebSocket 支持
public class SockJSConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册 SockJS 端点
        // 1. 使用 .addHandler() 注册处理器
        // 2. 使用 .withSockJS() 启用 SockJS 降级
        registry.addHandler(new RawWebSocketHandler(), "/sockjs")
                .setAllowedOriginPatterns("*") // 允许所有来源(开发用)
                .withSockJS(); // 启用 SockJS 兼容模式
    }
}

⚠️ 注意:RawWebSocketHandler 必须实现 WebSocketHandler 接口,而非 @ServerEndpoint

package com.example.websocketdemo.handler;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

@Component
public class RawWebSocketHandler extends TextWebSocketHandler {

    private static final CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
        System.out.println("✅ SockJS 客户端连接成功,当前在线:" + sessions.size());
        session.sendMessage(new TextMessage("🎉 欢迎使用 SockJS 服务!"));
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        System.out.println("📥 收到消息:" + message.getPayload());

        // 广播给所有连接
        for (WebSocketSession s : sessions) {
            if (s.isOpen()) {
                s.sendMessage(new TextMessage("广播:" + message.getPayload()));
            }
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        sessions.remove(session);
        System.out.println("🔌 客户端断开,当前在线:" + sessions.size());
    }
}

SockJS 的优势

  • API 与 WebSocket 完全一致,前端代码无需修改即可切换
  • 自动降级,确保老旧浏览器可用
  • 企业级兼容,金融、政务系统首选方案

五、通过 STOMP 作为 WebSocket 的子协议实现发布/订阅消息传递

1. 什么是 STOMP?

STOMP(Simple Text Oriented Messaging Protocol)是一种轻量级、基于文本的消息协议,最初为消息中间件(如 ActiveMQ、RabbitMQ)设计。它在 WebSocket 之上运行,提供发布/订阅(Pub/Sub)点对点 消息模型。

STOMP 协议特点

  • 基于文本(可读性强)
  • 支持 SEND, SUBSCRIBE, UNSUBSCRIBE, CONNECT, DISCONNECT 等命令
  • 每条消息有 HeadersBody
  • 与 WebSocket 结合后,成为企业级实时通信标准

2. STOMP 消息格式示例

✅ 客户端发送消息(SEND)
SEND
destination:/app/chat
content-type:application/json
content-length:28

{"from":"张三","content":"你好!"}
✅ 服务器推送消息(MESSAGE)
MESSAGE
destination:/topic/public
subscription:sub-0
message-id:0
content-type:application/json
content-length:45

{"from":"系统","content":"张三:你好!","timestamp":"14:30:05"}
✅ 客户端订阅主题(SUBSCRIBE)
SUBSCRIBE
id:sub-0
destination:/topic/public
✅ 客户端断开(DISCONNECT)
DISCONNECT
receipt:123

💡 每条 STOMP 消息由 命令 + 头部 + 空行 + 负载 组成,以 \n 分隔。

3. STOMP 与原始 WebSocket 的对比

维度原始 WebSocketSTOMP over WebSocket
协议二进制帧文本命令(可读)
消息语义自定义 JSON标准化(SEND/SUBSCRIBE)
发布/订阅需手动实现频道原生支持(/topic, /queue)
用户认证手动传递 Token支持 login/passcode
消息确认支持 receipt
客户端库原生 WebSocketstompjs(官方库)
适用场景高性能流企业级应用(聊天、通知)

4. STOMP 服务端配置(Spring Boot)

package com.example.websocketdemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker // 启用 STOMP 消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 注册 STOMP 端点(客户端连接入口)
     * 
     * 为什么需要端点?
     *   - 客户端通过此 URL 建立 WebSocket(或 SockJS)连接
     *   - 服务端监听该路径,处理协议升级
     * 
     * withSockJS():启用 SockJS 兼容降级(推荐生产使用)
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/chat")
                .setAllowedOriginPatterns("*")
                .withSockJS(); // 启用 SockJS 降级
    }

    /**
     * 配置消息代理(Message Broker)
     * 
     * enableSimpleBroker:使用内存代理(适用于单机部署)
     *   - /topic:广播主题(所有订阅者收到)
     *   - /queue:点对点队列(仅一个消费者收到)
     * 
     * setApplicationDestinationPrefixes:客户端发送消息的前缀
     *   - 客户端发送 /app/chat → 映射到 @MessageMapping("/chat")
     * 
     * setUserDestinationPrefix:用户私有消息前缀
     *   - 用于向特定用户推送:/user/{username}/queue/messages
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 客户端可订阅的主题前缀(广播/队列)
        registry.enableSimpleBroker("/topic", "/queue");

        // 客户端发送消息的前缀(必须以 /app 开头)
        registry.setApplicationDestinationPrefixes("/app");

        // 用户私有消息前缀(用于 @SendToUser)
        registry.setUserDestinationPrefix("/user");
    }
}

5. STOMP 消息控制器(@MessageMapping)

package com.example.websocketdemo.controller;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.handler.annotation.SendToUser;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * STOMP 消息控制器
 * 
 * @MessageMapping("/chat"):接收客户端发送到 /app/chat 的消息
 * @SendTo("/topic/public"):广播到 /topic/public 主题
 * @SendToUser:向特定用户发送私有消息(需登录)
 */
@Controller
public class ChatController {

    /**
     * 接收客户端发送的聊天消息并广播
     * 
     * 客户端发送:/app/chat → 此方法接收
     * 返回值自动序列化为 JSON → 广播到 /topic/public
     */
    @MessageMapping("/chat")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(ChatMessage message) {
        message.setTimestamp(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
        System.out.println("📥 收到消息:" + message);
        return message; // 自动发送到 /topic/public
    }

    /**
     * 当客户端订阅 /app/hello 时,自动推送欢迎消息
     * 与 @MessageMapping 不同:这是“订阅触发”,非“发送触发”
     */
    @SubscribeMapping("/app/hello")
    public ChatMessage handleHello() {
        ChatMessage msg = new ChatMessage();
        msg.setFrom("系统");
        msg.setContent("🎉 欢迎使用 STOMP 聊天室!");
        msg.setTimestamp(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
        return msg;
    }

    /**
     * 用户登录后,向其私有队列发送通知
     * 需要客户端在 CONNECT 时提供 login 头(如用户名)
     */
    @MessageMapping("/user.login")
    @SendToUser("/queue/notifications")
    public ChatMessage handleUserLogin(ChatMessage message, SimpMessageHeaderAccessor headerAccessor) {
        String username = message.getFrom();
        ChatMessage response = new ChatMessage();
        response.setFrom("系统");
        response.setContent(username + " 已上线!");
        response.setTimestamp(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
        return response;
    }
}

6. STOMP 客户端示例(使用 stompjs)

<!DOCTYPE html>
<html>
<head>
    <title>STOMP over WebSocket 聊天室</title>
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
</head>
<body>
    <h2>STOMP 聊天室(支持发布/订阅)</h2>
    <input type="text" id="username" placeholder="输入用户名" />
    <button onclick="connect()">连接</button>
    <button onclick="disconnect()">断开</button>
    <input type="text" id="message-input" placeholder="输入消息..." />
    <button onclick="sendMessage()">发送</button>
    <div id="messages"></div>

    <script>
        let stompClient = null;
        let username = null;

        // 连接 STOMP 服务
        function connect() {
            username = document.getElementById('username').value.trim() || '游客' + Math.floor(Math.random() * 100);

            // 创建 SockJS 连接(兼容所有浏览器)
            const socket = new SockJS('http://localhost:8080/chat');

            // 创建 STOMP 客户端
            stompClient = Stomp.over(socket);

            // 连接(可选:携带登录信息)
            stompClient.connect({}, function (frame) {
                console.log('✅ STOMP 连接成功:' + frame);

                // 1. 订阅公共聊天频道(广播)
                stompClient.subscribe('/topic/public', function (message) {
                    const msg = JSON.parse(message.body);
                    appendMessage(msg.from, msg.content, msg.timestamp, 'other');
                });

                // 2. 订阅用户私有通知(仅自己收到)
                stompClient.subscribe('/user/queue/notifications', function (message) {
                    const msg = JSON.parse(message.body);
                    appendMessage(msg.from, msg.content, msg.timestamp, 'system');
                });

                // 3. 订阅欢迎消息(@SubscribeMapping)
                stompClient.subscribe('/app/hello', function (message) {
                    const msg = JSON.parse(message.body);
                    appendMessage(msg.from, msg.content, msg.timestamp, 'system');
                });

                // 4. 发送登录消息(触发 @MessageMapping("/user.login"))
                stompClient.send("/app/user.login", {}, JSON.stringify({from: username}));

                document.getElementById('message-input').placeholder = '你好,' + username + ',输入消息...';
            }, function (error) {
                console.error('❌ 连接失败:', error);
                appendMessage('系统', '连接失败,请检查服务端是否启动', '', 'system');
            });
        }

        // 发送消息
        function sendMessage() {
            const input = document.getElementById('message-input');
            const content = input.value.trim();
            if (!content || !stompClient || !stompClient.connected) return;

            // 发送消息到 /app/chat(映射到 @MessageMapping("/chat"))
            stompClient.send("/app/chat", {}, JSON.stringify({
                from: username,
                content: content,
                timestamp: ""
            }));

            // 显示自己发送的消息
            appendMessage(username, content, new Date().toLocaleTimeString(), 'me');
            input.value = '';
        }

        // 断开连接
        function disconnect() {
            if (stompClient && stompClient.connected) {
                stompClient.disconnect(function () {
                    console.log('👋 已断开 STOMP 连接');
                });
                stompClient = null;
            }
        }

        // 显示消息到页面
        function appendMessage(from, content, timestamp, type) {
            const div = document.createElement('div');
            div.style.margin = '5px 0';
            div.style.padding = '8px';
            div.style.borderRadius = '6px';
            div.style.maxWidth = '70%';

            if (type === 'me') {
                div.style.backgroundColor = '#e3f2fd';
                div.style.marginLeft = 'auto';
            } else if (type === 'system') {
                div.style.backgroundColor = '#f5f5f5';
                div.style.color = '#666';
                div.style.fontStyle = 'italic';
            } else {
                div.style.backgroundColor = '#f0f0f0';
            }

            div.innerHTML = `<strong>${from}</strong> (${timestamp})<br>${content}`;
            document.getElementById('messages').appendChild(div);
            document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
        }

        // 回车发送
        document.getElementById('message-input').addEventListener('keypress', function (e) {
            if (e.key === 'Enter') sendMessage();
        });

        // 页面加载后自动连接
        window.onload = function () {
            connect();
        };
    </script>
</body>
</html>

7. STOMP 消息模型图解

┌─────────────────┐     ┌───────────────────────┐     ┌───────────────────┐
│   客户端 A       │     │      服务端           │     │   客户端 B        │
│                 │     │                       │     │                   │
│ SUBSCRIBE /topic/public │◄───┐  (STOMP Broker)  │───►│ SUBSCRIBE /topic/public │
│                 │     │                       │     │                   │
│ SEND /app/chat  │───► │ @MessageMapping("/chat") │◄───│ SEND /app/chat    │
│ {"from":"A"}    │     │  → 广播到 /topic/public │     │ {"from":"B"}      │
└─────────────────┘     └─────────┬─────────────┘     └───────────────────┘
                                  │
                                  ▼
                        ┌───────────────────────┐
                        │   /topic/public 消息    │
                        │   {"from":"A", ...}   │
                        └─────────┬─────────────┘
                                  │
                                  ▼
                        ┌───────────────────────┐
                        │   客户端 A 收到消息     │
                        │   客户端 B 收到消息     │
                        └───────────────────────┘

关键优势

  • 解耦:客户端只关心订阅/发送主题,不关心其他客户端
  • 可扩展:增加新客户端无需修改服务端逻辑
  • 标准化:使用 destinationsubscriptionid 等标准头

六、完整技术栈对比总结

技术适用场景优点缺点
原始 WebSocket高性能流(游戏、视频)极低延迟、极小开销无标准协议、需自定义、难调试
SockJS企业级兼容(IE8+)100% API 兼容、自动降级带宽较高、延迟略高
STOMP over WebSocket企业级实时应用(聊天、通知)标准化、发布/订阅、易维护、可调试比原始 WebSocket 多一层封装
SockJS + STOMP生产环境首选兼容性 + 标准化 = 最佳实践配置稍复杂

推荐组合
SockJS(兼容) + STOMP(语义) + Spring Boot(后端)
是构建生产级、跨平台、企业级实时 Web 应用的黄金组合。


七、生产环境最佳实践

实践说明
始终使用 WSS生产环境必须使用 wss://(WebSocket Secure)
启用 SockJS即使你不需要 IE 支持,也建议启用,提高网络穿透成功率
使用 STOMP避免自定义 JSON 协议,STOMP 已是行业标准
设置心跳configureMessageBroker 中配置 setHeartbeat 防止连接被防火墙断开
用户认证connect 时通过 loginpasscode 头传递 JWT Token
消息持久化重要消息(如订单通知)应存入数据库,防止断线丢失
集群部署使用 Redis 作为 STOMP 消息代理(enableStompBrokerRelay)实现跨节点广播
监控连接数使用 SimpMessagingTemplate + @EventListener 监听连接事件

示例:配置心跳(防止连接被代理关闭)

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
    registry.enableSimpleBroker("/topic", "/queue")
            .setHeartbeatValue(new long[]{10000, 10000}); // 客户端和服务端每10秒互相发送 Ping/Pong
    registry.setApplicationDestinationPrefixes("/app");
    registry.setUserDestinationPrefix("/user");
}

八、总结:WebSocket 生态全景图

┌────────────────────────────────────────────────────────────────────┐
│                         WebSocket 生态系统                           │
├────────────────────────────────────────────────────────────────────┤
│  1. 原始 WebSocket (RFC 6455) → 二进制帧,高性能,难维护               │
│  2. SockJS → 模拟 WebSocket API,兼容 IE6+,自动降级                 │
│  3. STOMP → 文本协议,发布/订阅,标准化消息模型                      │
│  4. SockJS + STOMP → 生产环境黄金组合(兼容+标准)                   │
│  5. Spring Boot Starter WebSocket → 自动配置,开箱即用                │
│  6. Redis + STOMP → 集群部署,跨节点广播                             │
└────────────────────────────────────────────────────────────────────┘

最终建议

  • 学习路径:原始 WebSocket → SockJS → STOMP
  • 开发建议永远使用 SockJS + STOMP 组合
  • 面试重点:理解握手过程、STOMP 消息格式、发布/订阅模型
  • 企业标准:STOMP over SockJS 是银行、政务、医疗系统首选方案

附录:完整项目结构(Spring Boot + SockJS + STOMP)

src/
├── main/
│   ├── java/
│   │   └── com/example/websocketdemo/
│   │       ├── WebSocketDemoApplication.java
│   │       ├── config/WebSocketConfig.java          // STOMP + SockJS 配置
│   │       ├── controller/ChatController.java       // @MessageMapping 控制器
│   │       └── model/ChatMessage.java               // 消息实体
│   └── resources/
│       └── static/
│           ├── chat.html                            // STOMP + SockJS 客户端
│           └── raw-websocket.html                   // 原始 WebSocket 客户端
│           └── sockjs-only.html                     // 仅 SockJS 客户端

GitHub 模板推荐
https://github.com/spring-projects/spring-framework/tree/main/spring-websocket/src/test/java/org/springframework/web/socket


📌 结语

WebSocket 不是“可选技术”,而是现代 Web 实时交互的基础设施

  • 原始 WebSocket 是“引擎”,
  • SockJS 是“变速箱”,
  • STOMP 是“导航系统”。

掌握三者关系,你就能在任何网络环境下,构建出稳定、兼容、可扩展的实时应用。
从微信聊天到金融交易,从物联网到元宇宙,WebSocket 正在重新定义 Web 的边界。

💡 记住
“用原始 WebSocket 做原型,用 SockJS + STOMP 做产品。”

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值