webSocket快速入门
一、WebSocket 是什么?
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。
简单说,它能让客户端(浏览器)和服务器之间实时双向通信。
对比传统 HTTP
| 特性 | HTTP | WebSocket |
|---|---|---|
| 连接方式 | 请求-响应 | 持久连接 |
| 通信方向 | 客户端 → 服务端 | 双向 |
| 实时性 | 差,需要轮询 | 极好 |
| 传输协议 | HTTP/1.1 | ws:// 或 wss:// |
| 应用场景 | 普通网页请求 | 实时聊天、通知、监控数据推送等 |
二、Vue + Spring Boot 实现 WebSocket (基础)
1 、 springboot
依赖
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>编写 WebSocket 配置类
1@Configuration 2@EnableWebSocket // 开启websocket 3public class WebSocketConfig implements WebSocketConfigurer { 4// 注册websocket处理器 5 @Override 6 public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 7 registry.addHandler(new MyWebSocketHandler(), "/ws") 8 .setAllowedOrigins("*"); // 允许跨域 9 } 10}
编写处理器
1// 自定义websocket处理器 2public class MyWebSocketHandler extends TextWebSocketHandler { 3 // 保存所有连接的session 4 private static final CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<>(); 5 // 当有新的连接时,将session添加到sessions中 6 @Override 7 public void afterConnectionEstablished(WebSocketSession session) { 8 sessions.add(session); 9 System.out.println("新连接:" + session.getId()); 10 } 11 // 处理消息 12 @Override 13 public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 14 String msg = message.getPayload(); 15 System.out.println("收到消息:" + msg); 16 // 回复客户端 17 session.sendMessage(new TextMessage("服务器收到:" + msg)); 18 } 19 // 连接关闭时 20 @Override 21 public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { 22 sessions.remove(session); 23 System.out.println("连接关闭:" + session.getId()); 24 } 25 26 // 向所有连接推送消息 27 public static void broadcast(String message) throws Exception { 28 for (WebSocketSession session : sessions) { 29 if (session.isOpen()) { 30 session.sendMessage(new TextMessage(message)); 31 } 32 } 33 } 34}
创建控制类
1@RestController 2@RequestMapping("/api/test") 3public class TestController { 4 5 @GetMapping("/push") 6 public String pushMessage(@RequestParam String msg) throws Exception { 7 MyWebSocketHandler.broadcast("服务端推送:" + msg); 8 return "ok"; 9 } 10}
2 、 vue
创建工具类
1let ws = null; 2let reconnectTimer = null; 3let url = "ws://localhost:8081/ws"; 4 5function createWebSocket(onMessage) { 6 if (ws) return; // 防止重复创建 7 // 创建 WebSocket 连接 8 ws = new WebSocket(url); 9 10 ws.onopen = () => { 11 console.log("WebSocket 已连接"); 12 }; 13 // 接收服务器消息 14 ws.onmessage = (event) => { 15 if (onMessage) onMessage(event.data); 16 }; 17 18 ws.onclose = () => { 19 console.log(" WebSocket 已关闭,尝试重连..."); 20 ws = null; 21 reconnect(); // 自动重连 22 }; 23 24 ws.onerror = (err) => { 25 console.error("WebSocket 出错:", err); 26 ws.close(); 27 }; 28} 29// 自动重连 30function reconnect() { 31 if (reconnectTimer) return; 32 reconnectTimer = setTimeout(() => { 33 createWebSocket(); 34 reconnectTimer = null; 35 }, 3000); 36} 37// 发送消息 38function sendMsg(msg) { 39 if (ws && ws.readyState === WebSocket.OPEN) { 40 ws.send(msg); 41 } else { 42 console.warn("WebSocket 未连接,消息未发送"); 43 } 44} 45// 关闭 WebSocket 连接 46function closeWebSocket() { 47 if (ws) { 48 ws.close(); 49 ws = null; 50 } 51} 52 53export default { 54 createWebSocket, 55 sendMsg, 56 closeWebSocket 57};
写一个简单的页面来调用
1<template> 2 <div> 3 <h2>WebSocket 测试</h2> 4 <p>收到消息:{{ message }}</p> 5 <input v-model="input" /> 6 <button @click="send">发送</button> 7 </div> 8</template> 9 10<script> 11import wsService from "@/utils/websocket"; 12 13export default { 14 data() { 15 return { 16 message: "", 17 input: "" 18 }; 19 }, 20 mounted() { 21 wsService.createWebSocket((msg) => { 22 this.message = msg; 23 }); 24 }, 25 beforeUnmount() { 26 wsService.closeWebSocket(); 27 }, 28 methods: { 29 send() { 30 wsService.sendMsg(this.input); 31 this.input = ""; 32 } 33 } 34}; 35</script>
注意:运行后, WebSocket 服务地址为:
ws://localhost:8081/ws
我们可以测试一下,启动vue、springboot项目:
可以看到前后端控制台都打印出了websocket连接成功的日志。


发送消息后后端也能及时的收到消息:

三、websocket 高级封装
在一般的项目中通常会:
- 使用 Pinia/Vuex 管理 WebSocket 状态(是否在线、消息队列等);
- 支持 心跳检测;
- 支持 断线重连 + 消息缓存;
- 区分不同业务消息类型(如 chat, notice, system)。
这里我们为了直接支持
a.自动重连
b.心跳检测
c.区分业务消息类型(如 chat, notice, system)
d.统一管理连接状态
来修改一下websocket的工具类封装:
1let ws = null; 2let heartbeatTimer = null; 3let reconnectTimer = null; 4let reconnectAttempts = 0; 5 6const HEARTBEAT_INTERVAL = 5000; // 心跳间隔 5s 7const MAX_RECONNECT_ATTEMPTS = 10; 8const WS_URL = "ws://localhost:8081/ws"; // 后端地址 9 10// === 初始化 WebSocket 连接 === 11function createWebSocket(onMessageCallback) { 12 if (ws && ws.readyState === WebSocket.OPEN) return; 13 14 ws = new WebSocket(WS_URL); 15 16 ws.onopen = () => { 17 console.log("WebSocket 连接成功"); 18 reconnectAttempts = 0; 19 startHeartbeat(); 20 }; 21 22 ws.onmessage = (event) => { 23 try { 24 const data = JSON.parse(event.data); 25 handleMessage(data, onMessageCallback); 26 } catch (e) { 27 console.warn("收到非JSON消息:", event.data); 28 } 29 }; 30 31 ws.onclose = () => { 32 console.warn("WebSocket 已关闭,尝试重连..."); 33 stopHeartbeat(); 34 reconnect(); 35 }; 36 37 ws.onerror = (err) => { 38 console.error("WebSocket 出错:", err); 39 ws.close(); 40 }; 41} 42 43// === 心跳机制 === 44function startHeartbeat() { 45 stopHeartbeat(); 46 heartbeatTimer = setInterval(() => { 47 send({ type: "ping" }); 48 }, HEARTBEAT_INTERVAL); 49} 50 51function stopHeartbeat() { 52 if (heartbeatTimer) { 53 clearInterval(heartbeatTimer); 54 heartbeatTimer = null; 55 } 56} 57 58// === 重连机制 === 59function reconnect() { 60 if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { 61 console.error("重连次数过多,停止尝试"); 62 return; 63 } 64 65 reconnectAttempts++; 66 const delay = reconnectAttempts * 2000; // 递增延迟 67 console.log([`${delay / 1000}s 后重连第 ${reconnectAttempts} 次...`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.s.md)); 68 69 reconnectTimer = setTimeout(() => { 70 createWebSocket(); 71 }, delay); 72} 73 74// === 消息分发 === 75function handleMessage(data, callback) { 76 if (!data.type) { 77 console.warn("未知消息类型:", data); 78 return; 79 } 80 81 switch (data.type) { 82 case "chat": 83 console.log("聊天消息:", data.content); 84 break; 85 case "notice": 86 console.log("通知:", data.content); 87 break; 88 case "system": 89 console.log("系统消息:", data.content); 90 break; 91 case "pong": 92 console.log("心跳响应"); 93 break; 94 default: 95 console.log("其他类型消息:", data); 96 } 97 98 if (callback) callback(data); 99} 100 101// === 发送消息 === 102function send(msg) { 103 if (!ws || ws.readyState !== WebSocket.OPEN) { 104 console.warn("WebSocket 未连接,发送失败:", msg); 105 return; 106 } 107 ws.send(JSON.stringify(msg)); 108} 109 110// === 主动关闭 === 111function close() { 112 stopHeartbeat(); 113 if (ws) { 114 ws.close(); 115 ws = null; 116 } 117} 118 119export default { 120 createWebSocket, 121 send, 122 close 123};
创建一个新的页面:
1<template> 2 <div class="p-4"> 3 <h2>💬 WebSocket 聊天示例</h2> 4 <p>收到的消息:{{ lastMessage }}</p> 5 6 <div class="mt-4"> 7 <input v-model="message" placeholder="输入消息..." /> 8 <select v-model="type"> 9 <option value="chat">聊天</option> 10 <option value="notice">通知</option> 11 <option value="system">系统</option> 12 </select> 13 <button @click="sendMsg">发送</button> 14 </div> 15 </div> 16</template> 17 18<script> 19import wsService from "@/utils/websocket"; 20 21export default { 22 data() { 23 return { 24 message: "", 25 type: "chat", 26 lastMessage: "" 27 }; 28 }, 29 mounted() { 30 wsService.createWebSocket((msg) => { 31 this.lastMessage = [`${msg.type}: ${msg.content}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.type.md); 32 }); 33 }, 34 beforeUnmount() { 35 wsService.close(); 36 }, 37 methods: { 38 sendMsg() { 39 wsService.send({ 40 type: this.type, 41 content: this.message, 42 from: "前端用户" 43 }); 44 this.message = ""; 45 } 46 } 47}; 48</script>
修改后端的处理数据方式:
1 // 处理消息 2 @Override 3 public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 4 String msg = message.getPayload(); 5 System.out.println("收到消息:" + msg); 6/** 7 *websocket高级封装 8 *区分不同业务消息类型 9 *支持 心跳检测 10 */ 11 // 解析前端JOSN 12 JSONObject json = new JSONObject(msg); 13 String type = json.optString("type"); 14 // 构造回复数据 15 JSONObject data = new JSONObject(); 16 data.put("type", type); 17 data.put("content","服务器已收到:"+ json.optString("content")); 18 19 //模拟心跳回应 20 if ("ping".equals(type)) { 21 data.put("type", "pong"); 22 } 23 24 25 // 回复客户端 26 session.sendMessage(new TextMessage(json.toString())); 27 }
发送后可以看到:


四、广播(全员发)和私聊(发给指定userId )功能
那么问题来了,广播和私聊功能是如何在websocket中实现的?
这里,我们可通过记录连接成功的用户(也就是在线的用户)来进行,在前端拼接websocket url时,可以加上用户唯一的/id 来分辨不同的用户。。
如:
ws://localhost:8080/ws/1
这时在后端截取id来获取在线用户。
后端修改config
1@Configuration 2@EnableWebSocket // 开启websocket 3public class WebSocketConfig implements WebSocketConfigurer { 4// 注册websocket处理器 5 @Override 6 public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 7 registry.addHandler(new MyWebSocketHandler(), "/ws/{userId}") 8 .setAllowedOrigins("*"); // 允许跨域 9 } 10} 11 12 13public class MyWebSocketHandler extends TextWebSocketHandler { 14 15 // 保存所有连接:userId -> session 16 private static final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>(); 17 // 当有新的连接时,将session添加到sessions中 18 @Override 19 public void afterConnectionEstablished(WebSocketSession session) { 20 // 获取用户ID ws://localhost:8081/ws/1 21 String path = session.getUri().getPath(); 22 System.out.println("新连接:" + path); 23 String userId = path.substring(path.lastIndexOf("/") + 1); 24 sessions.put(userId, session); 25 System.out.println("新连接:" + userId + " (sessionId=" + session.getId() + ")"); 26 } 27 // 处理消息 28 @Override 29 public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 30 String msg = message.getPayload(); 31 System.out.println("收到消息:" + msg); 32 33 // 默认逻辑:回复收到的内容 34// JSONObject json = new JSONObject(msg); 35// String content = json.optString("content"); 36// 37// JSONObject reply = new JSONObject(); 38// reply.put("content", "服务器已收到:" + content); 39// reply.put("type", "system"); 40// session.sendMessage(new TextMessage(reply.toString())); 41 } 42 // 连接关闭时 43 @Override 44 public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { 45 sessions.entrySet().removeIf(entry -> entry.getValue().equals(session)); 46 System.out.println("连接关闭:" + session.getId()); 47 } 48 49 50 // 专用方法分发 51 52 53 /** 54 * 广播 55 */ 56 public static void broadcast(String from, String message,String type) throws Exception { 57 JSONObject data = new JSONObject(); 58 data.put("from", from); 59 data.put("type", type); 60 data.put("content", message); 61 // 发送给所有连接 62 for (WebSocketSession session : sessions.values()) { 63 if (session.isOpen()) { 64 session.sendMessage(new TextMessage(data.toString())); 65 } 66 } 67 System.out.println("广播消息:" + message); 68 } 69 70 /** 71 * 私聊 72 */ 73 public static void sendToUser(String from, String to, String message, String type) throws Exception { 74 WebSocketSession target = sessions.get(to); 75 if (target != null && target.isOpen()) { 76 JSONObject data = new JSONObject(); 77 data.put("from", from); 78 data.put("type", type); 79 data.put("to", to); 80 data.put("content", message); 81 // 发送给指定连接 82 target.sendMessage(new TextMessage(data.toString())); 83 System.out.println("私聊消息:" + from + " → " + to + ":" + message); 84 } else { 85 System.out.println("用户 " + to + " 不在线或不存在"); 86 } 87 } 88}
控制器区分不同业务:
1@RestController 2@RequestMapping("/api/test") 3public class TestController { 4 5// @GetMapping("/push") 6// public String pushMessage(@RequestParam String msg) throws Exception { 7// MyWebSocketHandler.broadcast("服务端推送:" + msg); 8// return "ok"; 9// } 10 11 12 // 广播 13 @PostMapping("/broadcast") 14 public void broadcast(@RequestBody Map<String, String> body) throws Exception { 15 MyWebSocketHandler.broadcast(body.get("from"), body.get("content"), body.get("type")); 16 } 17 // 私聊 18 @PostMapping("/private") 19 public void privateMsg(@RequestBody Map<String, String> body) throws Exception { 20 MyWebSocketHandler.sendToUser(body.get("from"), body.get("to"), body.get("content"), body.get("type")); 21 } 22}
前端封装websocket 时,获取id并拼接:
创建广播和私聊方法:
1// WebSocket 工具模块 2let ws = null; // WebSocket 实例 3let heartbeatTimer = null; // 心跳定时器 4let reconnectTimer = null; // 重连定时器 5let reconnectAttempts = 0; // 重连尝试次数 6 7const HEARTBEAT_INTERVAL = 10000; // 心跳间隔 10s 8const MAX_RECONNECT_ATTEMPTS = 10; 9const WS_URL = "ws://localhost:8081/ws"; // 后端地址 10 11// === 初始化 WebSocket 连接 === 12function createWebSocket(onMessageCallback, uid) { 13 return new Promise((resolve, reject) => { 14 if (!uid) { 15 console.error("缺少用户ID,WebSocket无法建立连接"); 16 reject("缺少用户ID"); 17 return; 18 } 19 20 // 如果已存在连接则直接返回 21 if (ws && ws.readyState === WebSocket.OPEN) { 22 console.log("WebSocket 已连接"); 23 resolve(); 24 return; 25 } 26 27 // 拼装 URL 28 const id = `${WS_URL}/${uid}`; 29 ws = new WebSocket(id); 30 31 ws.onopen = () => { 32 console.log(`用户 ${uid} WebSocket 连接成功`); 33 reconnectAttempts = 0; 34 startHeartbeat(); 35 resolve(); // 通知外部连接成功 36 }; 37 38 ws.onmessage = (event) => { 39 console.log("收到消息:", event.data); 40 try { 41 const data = JSON.parse(event.data); 42 handleMessage(data, onMessageCallback); 43 } catch (e) { 44 console.warn("收到非JSON消息:", event.data); 45 } 46 }; 47 48 ws.onclose = () => { 49 console.warn("WebSocket 已关闭,尝试重连..."); 50 stopHeartbeat(); 51 reconnect(() => createWebSocket(onMessageCallback, uid)); 52 }; 53 54 ws.onerror = (err) => { 55 console.error("WebSocket 出错:", err); 56 ws.close(); 57 reject(err); // 通知外部连接失败 58 }; 59 }); 60} 61 62// === 心跳机制 === 63function startHeartbeat() { 64 stopHeartbeat(); 65 heartbeatTimer = setInterval(() => { 66 send({ type: "ping" }); 67 }, HEARTBEAT_INTERVAL); 68} 69 70// === 停止心跳 === 71function stopHeartbeat() { 72 if (heartbeatTimer) { 73 clearInterval(heartbeatTimer); 74 heartbeatTimer = null; 75 } 76} 77 78// === 重连机制 === 79function reconnect(reconnectAction) { 80 if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { 81 console.error("重连次数过多,停止尝试"); 82 return; 83 } 84 85 reconnectAttempts++; 86 const delay = reconnectAttempts * 2000; // 递增延迟 87 console.log([`${delay / 1000}s 后重连第 ${reconnectAttempts} 次...`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.s.md)); 88 89 reconnectTimer = setTimeout(() => { 90 if (typeof reconnectAction === "function") { 91 reconnectAction(); 92 } 93 }, delay); 94} 95 96// === 消息分发 === 97function handleMessage(data, callback) { 98 if (!data.type) { 99 console.warn("未知消息类型:", data); 100 return; 101 } 102 103 switch (data.type) { 104 case "chat": 105 console.log("聊天消息:", data.content); 106 break; 107 case "notice": 108 console.log("通知:", data.content); 109 break; 110 case "system": 111 console.log("系统消息:", data.content); 112 break; 113 case "private": 114 console.log("私聊消息:", data); 115 break; 116 case "broadcast": 117 console.log("广播消息:", data); 118 break; 119 case "ping": 120 console.log("心跳响应"); 121 break; 122 default: 123 console.log("其他类型消息:", data); 124 } 125 126 // 回调交给 Vue 层处理 127 if (callback) callback(data); 128} 129 130// === 发送消息 === 131function send(msg) { 132 if (!ws || ws.readyState !== WebSocket.OPEN) { 133 console.warn("WebSocket 未连接,发送失败:", msg); 134 return; 135 } 136 ws.send(JSON.stringify(msg)); 137} 138 139// === 广播 === 140function broadcast(from, msg) { 141 send({ type: "broadcast", from, content: msg }); 142} 143 144// === 私聊 === 145function privateChat(from, to, msg) { 146 send({ type: "private", from, to, content: msg }); 147} 148 149// === 主动关闭 === 150function close() { 151 stopHeartbeat(); 152 if (ws) { 153 ws.close(); 154 ws = null; 155 } 156} 157 158// === 导出 === 159export default { 160 createWebSocket, 161 send, 162 close, 163 broadcast, 164 privateChat 165};
创建聊天/接收页面:
1<template> 2 <div class="p-4"> 3 <h2 class="text-xl font-bold mb-4">💬 WebSocket 聊天示例</h2> 4 5 <div class="mb-4"> 6 <p>当前用户ID:<strong>{{ from }}</strong></p> 7 <p>WebSocket状态:<span>{{ wsConnected ? "✅ 已连接" : "❌ 未连接" }}</span></p> 8 </div> 9 10 <!-- 消息显示区 --> 11 <div class="border rounded p-4 h-64 overflow-y-auto bg-gray-50 mb-4"> 12 <div 13 v-for="(msg, index) in messages" 14 :key="index" 15 :class="[ 16 'mb-2 p-2 rounded', 17 msg.type === 'broadcast' 18 ? 'bg-green-100' 19 : msg.from === from 20 ? 'bg-blue-100 text-right' 21 : 'bg-white' 22 ]" 23 > 24 <p class="text-sm"> 25 <strong>{{ msg.from === from ? "我" : msg.from }}</strong>: 26 {{ msg.content }} 27 </p> 28 </div> 29 </div> 30 31 <!-- 私聊输入区 --> 32 <div class="space-y-2 mb-4"> 33 <h3 class="font-semibold">📨 私聊消息</h3> 34 <input 35 v-model="to" 36 placeholder="输入对方用户ID" 37 class="border p-2 w-full" 38 /> 39 <input 40 v-model="privateMsg" 41 placeholder="输入要发送的私聊内容" 42 class="border p-2 w-full" 43 /> 44 <button 45 @click="sendPrivate" 46 class="bg-blue-500 text-white px-4 py-1 rounded" 47 > 48 发送私聊 49 </button> 50 </div> 51 52 <!-- 广播输入区 --> 53 <div class="space-y-2"> 54 <h3 class="font-semibold">📢 广播消息</h3> 55 <input 56 v-model="broadcastMsg" 57 placeholder="输入要广播的内容" 58 class="border p-2 w-full" 59 /> 60 <button 61 @click="sendBroadcast" 62 class="bg-green-500 text-white px-4 py-1 rounded" 63 > 64 发送广播 65 </button> 66 </div> 67 </div> 68</template> 69 70<script> 71 import wsService from "@/utils/websocket"; 72 import axios from "axios"; 73 74 export default { 75 data() { 76 return { 77 from: "1", // 当前用户ID(测试用) 78 to: "", 79 privateMsg: "", 80 broadcastMsg: "", 81 messages: [], 82 wsConnected: false 83 }; 84 }, 85 mounted() { 86 // 创建 WebSocket 连接(如 ws://localhost:8081/ws/1) 87 wsService.createWebSocket( 88 // 接收消息时的回调 89 (msg) => { 90 this.messages.push(msg); 91 }, 92 // 当前用户 ID 93 this.from, 94 ) 95 .then(() => { 96 this.wsConnected = true; 97 }) 98 }, 99 100 beforeUnmount() { 101 wsService.close(); 102 }, 103 methods: { 104 // 发送私聊消息 105 async sendPrivate() { 106 if (!this.to || !this.privateMsg) { 107 alert("请填写对方ID和消息内容"); 108 return; 109 } 110 await axios.post("/api/api/test/private", { 111 from: this.from, 112 to: this.to, 113 type: "chat", 114 content: this.privateMsg 115 }); 116 this.privateMsg = ""; 117 }, 118 119 // 发送广播消息 120 async sendBroadcast() { 121 if (!this.broadcastMsg) { 122 alert("请输入广播内容"); 123 return; 124 } 125 await axios.post("/api/api/test/broadcast", { 126 from: this.from, 127 type: "chat", 128 content: this.broadcastMsg 129 }); 130 this.broadcastMsg = ""; 131 } 132 } 133 }; 134</script> 135 136<style scoped> 137 input { 138 border-radius: 6px; 139 } 140</style>
效果:

这时我们可以用几个页面模拟几个用户:
写两个页面,修改from
1data() { 2 return { 3 from: "2", // 当前用户ID(测试用) 4 to: "", 5 privateMsg: "", 6 broadcastMsg: "", 7 messages: [], 8 wsConnected: false 9 }; 10},
打开页面后,查看后端日志:发现有两个设备在线:

用户2给用户1发送信息:

用户1广播信息:


《webSocket快速入门》 是转载文章,点击查看原文。