一、什么是 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)**结构,每个帧包含:
| 字段 | 长度 | 说明 |
|---|---|---|
| FIN | 1 bit | 是否为最后一帧(单消息可分片) |
| RSV1, RSV2, RSV3 | 3 bits | 保留位,用于扩展协议(通常为 0) |
| Opcode | 4 bits | 操作码,定义帧类型(见下表) |
| Mask | 1 bit | 是否启用掩码(客户端→服务端必须为 1) |
| Payload Length | 7/7+16/7+64 bits | 负载长度(0~125、126、127) |
| Masking-Key | 4 bytes | 仅当 Mask=1 时存在(用于防缓存攻击) |
| Payload Data | 可变 | 实际数据(文本或二进制) |
📌 Opcode 常见类型
| Opcode | 名称 | 说明 |
|---|---|---|
0x0 | Continuation | 分片消息的后续帧 |
0x1 | Text | UTF-8 文本消息(如 JSON) |
0x2 | Binary | 二进制数据(如图片、文件) |
0x8 | Close | 关闭连接 |
0x9 | Ping | 心跳检测(服务端需回复 Pong) |
0xA | Pong | 心跳响应 |
✅ 示例:发送一条文本消息 "Hello, WebSocket!"
81 8D 3F 1A 2C 4E 12 05 0B 1F 0C 0B 18 05 0D 1D 0D 1C 18
| 字段 | 值 | 说明 |
|---|---|---|
81 | 10000001 | FIN=1, Opcode=0x1(Text) |
8D | 10001101 | Mask=1, Payload Length=13(0x0D) |
3F 1A 2C 4E | Masking-Key | 客户端生成的 4 字节密钥 |
12 05 0B 1F 0C 0B 18 05 0D 1D 0D 1C 18 | Payload(异或后) | 原始数据 "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 的对比
| 特性 | WebSocket | SockJS |
|---|---|---|
| 协议 | 原生 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等命令- 每条消息有 Headers 和 Body
- 与 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 的对比
| 维度 | 原始 WebSocket | STOMP over WebSocket |
|---|---|---|
| 协议 | 二进制帧 | 文本命令(可读) |
| 消息语义 | 自定义 JSON | 标准化(SEND/SUBSCRIBE) |
| 发布/订阅 | 需手动实现频道 | 原生支持(/topic, /queue) |
| 用户认证 | 手动传递 Token | 支持 login/passcode 头 |
| 消息确认 | 无 | 支持 receipt |
| 客户端库 | 原生 WebSocket | stompjs(官方库) |
| 适用场景 | 高性能流 | 企业级应用(聊天、通知) |
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 收到消息 │
└───────────────────────┘
✅ 关键优势:
- 解耦:客户端只关心订阅/发送主题,不关心其他客户端
- 可扩展:增加新客户端无需修改服务端逻辑
- 标准化:使用
destination、subscription、id等标准头
六、完整技术栈对比总结
| 技术 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 原始 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 时通过 login 和 passcode 头传递 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 做产品。”
1495

被折叠的 条评论
为什么被折叠?



