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 配置类
@Configuration
@EnableWebSocket // 开启websocket
public class WebSocketConfig implements WebSocketConfigurer {
// 注册websocket处理器
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(), "/ws")
.setAllowedOrigins("*"); // 允许跨域
}
}
编写处理器
// 自定义websocket处理器
public class MyWebSocketHandler extends TextWebSocketHandler {
// 保存所有连接的session
private static final CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<>();
// 当有新的连接时,将session添加到sessions中
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.add(session);
System.out.println("新连接:" + session.getId());
}
// 处理消息
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String msg = message.getPayload();
System.out.println("收到消息:" + msg);
// 回复客户端
session.sendMessage(new TextMessage("服务器收到:" + msg));
}
// 连接关闭时
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session);
System.out.println("连接关闭:" + session.getId());
}
// 向所有连接推送消息
public static void broadcast(String message) throws Exception {
for (WebSocketSession session : sessions) {
if (session.isOpen()) {
session.sendMessage(new TextMessage(message));
}
}
}
}
创建控制类
@RestController
@RequestMapping("/api/test")
public class TestController {
@GetMapping("/push")
public String pushMessage(@RequestParam String msg) throws Exception {
MyWebSocketHandler.broadcast("服务端推送:" + msg);
return "ok";
}
}
2 、 vue
创建工具类
let ws = null;
let reconnectTimer = null;
let url = "ws://localhost:8081/ws";
function createWebSocket(onMessage) {
if (ws) return; // 防止重复创建
// 创建 WebSocket 连接
ws = new WebSocket(url);
ws.onopen = () => {
console.log("WebSocket 已连接");
};
// 接收服务器消息
ws.onmessage = (event) => {
if (onMessage) onMessage(event.data);
};
ws.onclose = () => {
console.log(" WebSocket 已关闭,尝试重连...");
ws = null;
reconnect(); // 自动重连
};
ws.onerror = (err) => {
console.error("WebSocket 出错:", err);
ws.close();
};
}
// 自动重连
function reconnect() {
if (reconnectTimer) return;
reconnectTimer = setTimeout(() => {
createWebSocket();
reconnectTimer = null;
}, 3000);
}
// 发送消息
function sendMsg(msg) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(msg);
} else {
console.warn("WebSocket 未连接,消息未发送");
}
}
// 关闭 WebSocket 连接
function closeWebSocket() {
if (ws) {
ws.close();
ws = null;
}
}
export default {
createWebSocket,
sendMsg,
closeWebSocket
};
写一个简单的页面来调用
<template>
<div>
<h2>WebSocket 测试</h2>
<p>收到消息:{{ message }}</p>
<input v-model="input" />
<button @click="send">发送</button>
</div>
</template>
<script>
import wsService from "@/utils/websocket";
export default {
data() {
return {
message: "",
input: ""
};
},
mounted() {
wsService.createWebSocket((msg) => {
this.message = msg;
});
},
beforeUnmount() {
wsService.closeWebSocket();
},
methods: {
send() {
wsService.sendMsg(this.input);
this.input = "";
}
}
};
</script>
注意:运行后, WebSocket 服务地址为:
ws://localhost:8081/ws
我们可以测试一下,启动vue、springboot项目:
可以看到前后端控制台都打印出了websocket连接成功的日志。


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

三、websocket 高级封装
在一般的项目中通常会:
- 使用 Pinia/Vuex 管理 WebSocket 状态(是否在线、消息队列等);
- 支持 心跳检测;
- 支持 断线重连 + 消息缓存;
- 区分不同业务消息类型(如 chat, notice, system)。
这里我们为了直接支持
a.自动重连
b.心跳检测
c.区分业务消息类型(如 chat, notice, system)
d.统一管理连接状态
来修改一下websocket的工具类封装:
let ws = null;
let heartbeatTimer = null;
let reconnectTimer = null;
let reconnectAttempts = 0;
const HEARTBEAT_INTERVAL = 5000; // 心跳间隔 5s
const MAX_RECONNECT_ATTEMPTS = 10;
const WS_URL = "ws://localhost:8081/ws"; // 后端地址
// === 初始化 WebSocket 连接 ===
function createWebSocket(onMessageCallback) {
if (ws && ws.readyState === WebSocket.OPEN) return;
ws = new WebSocket(WS_URL);
ws.onopen = () => {
console.log("WebSocket 连接成功");
reconnectAttempts = 0;
startHeartbeat();
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleMessage(data, onMessageCallback);
} catch (e) {
console.warn("收到非JSON消息:", event.data);
}
};
ws.onclose = () => {
console.warn("WebSocket 已关闭,尝试重连...");
stopHeartbeat();
reconnect();
};
ws.onerror = (err) => {
console.error("WebSocket 出错:", err);
ws.close();
};
}
// === 心跳机制 ===
function startHeartbeat() {
stopHeartbeat();
heartbeatTimer = setInterval(() => {
send({ type: "ping" });
}, HEARTBEAT_INTERVAL);
}
function stopHeartbeat() {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
}
// === 重连机制 ===
function reconnect() {
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.error("重连次数过多,停止尝试");
return;
}
reconnectAttempts++;
const delay = reconnectAttempts * 2000; // 递增延迟
console.log(`${delay / 1000}s 后重连第 ${reconnectAttempts} 次...`);
reconnectTimer = setTimeout(() => {
createWebSocket();
}, delay);
}
// === 消息分发 ===
function handleMessage(data, callback) {
if (!data.type) {
console.warn("未知消息类型:", data);
return;
}
switch (data.type) {
case "chat":
console.log("聊天消息:", data.content);
break;
case "notice":
console.log("通知:", data.content);
break;
case "system":
console.log("系统消息:", data.content);
break;
case "pong":
console.log("心跳响应");
break;
default:
console.log("其他类型消息:", data);
}
if (callback) callback(data);
}
// === 发送消息 ===
function send(msg) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.warn("WebSocket 未连接,发送失败:", msg);
return;
}
ws.send(JSON.stringify(msg));
}
// === 主动关闭 ===
function close() {
stopHeartbeat();
if (ws) {
ws.close();
ws = null;
}
}
export default {
createWebSocket,
send,
close
};
创建一个新的页面:
<template>
<div class="p-4">
<h2>💬 WebSocket 聊天示例</h2>
<p>收到的消息:{{ lastMessage }}</p>
<div class="mt-4">
<input v-model="message" placeholder="输入消息..." />
<select v-model="type">
<option value="chat">聊天</option>
<option value="notice">通知</option>
<option value="system">系统</option>
</select>
<button @click="sendMsg">发送</button>
</div>
</div>
</template>
<script>
import wsService from "@/utils/websocket";
export default {
data() {
return {
message: "",
type: "chat",
lastMessage: ""
};
},
mounted() {
wsService.createWebSocket((msg) => {
this.lastMessage = `${msg.type}: ${msg.content}`;
});
},
beforeUnmount() {
wsService.close();
},
methods: {
sendMsg() {
wsService.send({
type: this.type,
content: this.message,
from: "前端用户"
});
this.message = "";
}
}
};
</script>
修改后端的处理数据方式:
// 处理消息
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String msg = message.getPayload();
System.out.println("收到消息:" + msg);
/**
*websocket高级封装
*区分不同业务消息类型
*支持 心跳检测
*/
// 解析前端JOSN
JSONObject json = new JSONObject(msg);
String type = json.optString("type");
// 构造回复数据
JSONObject data = new JSONObject();
data.put("type", type);
data.put("content","服务器已收到:"+ json.optString("content"));
//模拟心跳回应
if ("ping".equals(type)) {
data.put("type", "pong");
}
// 回复客户端
session.sendMessage(new TextMessage(json.toString()));
}
发送后可以看到:


四、广播(全员发)和私聊(发给指定userId )功能
那么问题来了,广播和私聊功能是如何在websocket中实现的?
这里,我们可通过记录连接成功的用户(也就是在线的用户)来进行,在前端拼接websocket url时,可以加上用户唯一的/id 来分辨不同的用户。。
如:
ws://localhost:8080/ws/1
这时在后端截取id来获取在线用户。
后端修改config
@Configuration
@EnableWebSocket // 开启websocket
public class WebSocketConfig implements WebSocketConfigurer {
// 注册websocket处理器
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(), "/ws/{userId}")
.setAllowedOrigins("*"); // 允许跨域
}
}
public class MyWebSocketHandler extends TextWebSocketHandler {
// 保存所有连接:userId -> session
private static final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
// 当有新的连接时,将session添加到sessions中
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// 获取用户ID ws://localhost:8081/ws/1
String path = session.getUri().getPath();
System.out.println("新连接:" + path);
String userId = path.substring(path.lastIndexOf("/") + 1);
sessions.put(userId, session);
System.out.println("新连接:" + userId + " (sessionId=" + session.getId() + ")");
}
// 处理消息
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String msg = message.getPayload();
System.out.println("收到消息:" + msg);
// 默认逻辑:回复收到的内容
// JSONObject json = new JSONObject(msg);
// String content = json.optString("content");
//
// JSONObject reply = new JSONObject();
// reply.put("content", "服务器已收到:" + content);
// reply.put("type", "system");
// session.sendMessage(new TextMessage(reply.toString()));
}
// 连接关闭时
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.entrySet().removeIf(entry -> entry.getValue().equals(session));
System.out.println("连接关闭:" + session.getId());
}
// 专用方法分发
/**
* 广播
*/
public static void broadcast(String from, String message,String type) throws Exception {
JSONObject data = new JSONObject();
data.put("from", from);
data.put("type", type);
data.put("content", message);
// 发送给所有连接
for (WebSocketSession session : sessions.values()) {
if (session.isOpen()) {
session.sendMessage(new TextMessage(data.toString()));
}
}
System.out.println("广播消息:" + message);
}
/**
* 私聊
*/
public static void sendToUser(String from, String to, String message, String type) throws Exception {
WebSocketSession target = sessions.get(to);
if (target != null && target.isOpen()) {
JSONObject data = new JSONObject();
data.put("from", from);
data.put("type", type);
data.put("to", to);
data.put("content", message);
// 发送给指定连接
target.sendMessage(new TextMessage(data.toString()));
System.out.println("私聊消息:" + from + " → " + to + ":" + message);
} else {
System.out.println("用户 " + to + " 不在线或不存在");
}
}
}
控制器区分不同业务:
@RestController
@RequestMapping("/api/test")
public class TestController {
// @GetMapping("/push")
// public String pushMessage(@RequestParam String msg) throws Exception {
// MyWebSocketHandler.broadcast("服务端推送:" + msg);
// return "ok";
// }
// 广播
@PostMapping("/broadcast")
public void broadcast(@RequestBody Map<String, String> body) throws Exception {
MyWebSocketHandler.broadcast(body.get("from"), body.get("content"), body.get("type"));
}
// 私聊
@PostMapping("/private")
public void privateMsg(@RequestBody Map<String, String> body) throws Exception {
MyWebSocketHandler.sendToUser(body.get("from"), body.get("to"), body.get("content"), body.get("type"));
}
}
前端封装websocket 时,获取id并拼接:
创建广播和私聊方法:
// WebSocket 工具模块
let ws = null; // WebSocket 实例
let heartbeatTimer = null; // 心跳定时器
let reconnectTimer = null; // 重连定时器
let reconnectAttempts = 0; // 重连尝试次数
const HEARTBEAT_INTERVAL = 10000; // 心跳间隔 10s
const MAX_RECONNECT_ATTEMPTS = 10;
const WS_URL = "ws://localhost:8081/ws"; // 后端地址
// === 初始化 WebSocket 连接 ===
function createWebSocket(onMessageCallback, uid) {
return new Promise((resolve, reject) => {
if (!uid) {
console.error("缺少用户ID,WebSocket无法建立连接");
reject("缺少用户ID");
return;
}
// 如果已存在连接则直接返回
if (ws && ws.readyState === WebSocket.OPEN) {
console.log("WebSocket 已连接");
resolve();
return;
}
// 拼装 URL
const id = `${WS_URL}/${uid}`;
ws = new WebSocket(id);
ws.onopen = () => {
console.log(`用户 ${uid} WebSocket 连接成功`);
reconnectAttempts = 0;
startHeartbeat();
resolve(); // 通知外部连接成功
};
ws.onmessage = (event) => {
console.log("收到消息:", event.data);
try {
const data = JSON.parse(event.data);
handleMessage(data, onMessageCallback);
} catch (e) {
console.warn("收到非JSON消息:", event.data);
}
};
ws.onclose = () => {
console.warn("WebSocket 已关闭,尝试重连...");
stopHeartbeat();
reconnect(() => createWebSocket(onMessageCallback, uid));
};
ws.onerror = (err) => {
console.error("WebSocket 出错:", err);
ws.close();
reject(err); // 通知外部连接失败
};
});
}
// === 心跳机制 ===
function startHeartbeat() {
stopHeartbeat();
heartbeatTimer = setInterval(() => {
send({ type: "ping" });
}, HEARTBEAT_INTERVAL);
}
// === 停止心跳 ===
function stopHeartbeat() {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
}
// === 重连机制 ===
function reconnect(reconnectAction) {
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.error("重连次数过多,停止尝试");
return;
}
reconnectAttempts++;
const delay = reconnectAttempts * 2000; // 递增延迟
console.log(`${delay / 1000}s 后重连第 ${reconnectAttempts} 次...`);
reconnectTimer = setTimeout(() => {
if (typeof reconnectAction === "function") {
reconnectAction();
}
}, delay);
}
// === 消息分发 ===
function handleMessage(data, callback) {
if (!data.type) {
console.warn("未知消息类型:", data);
return;
}
switch (data.type) {
case "chat":
console.log("聊天消息:", data.content);
break;
case "notice":
console.log("通知:", data.content);
break;
case "system":
console.log("系统消息:", data.content);
break;
case "private":
console.log("私聊消息:", data);
break;
case "broadcast":
console.log("广播消息:", data);
break;
case "ping":
console.log("心跳响应");
break;
default:
console.log("其他类型消息:", data);
}
// 回调交给 Vue 层处理
if (callback) callback(data);
}
// === 发送消息 ===
function send(msg) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.warn("WebSocket 未连接,发送失败:", msg);
return;
}
ws.send(JSON.stringify(msg));
}
// === 广播 ===
function broadcast(from, msg) {
send({ type: "broadcast", from, content: msg });
}
// === 私聊 ===
function privateChat(from, to, msg) {
send({ type: "private", from, to, content: msg });
}
// === 主动关闭 ===
function close() {
stopHeartbeat();
if (ws) {
ws.close();
ws = null;
}
}
// === 导出 ===
export default {
createWebSocket,
send,
close,
broadcast,
privateChat
};
创建聊天/接收页面:
<template>
<div class="p-4">
<h2 class="text-xl font-bold mb-4">💬 WebSocket 聊天示例</h2>
<div class="mb-4">
<p>当前用户ID:<strong>{{ from }}</strong></p>
<p>WebSocket状态:<span>{{ wsConnected ? "✅ 已连接" : "❌ 未连接" }}</span></p>
</div>
<!-- 消息显示区 -->
<div class="border rounded p-4 h-64 overflow-y-auto bg-gray-50 mb-4">
<div
v-for="(msg, index) in messages"
:key="index"
:class="[
'mb-2 p-2 rounded',
msg.type === 'broadcast'
? 'bg-green-100'
: msg.from === from
? 'bg-blue-100 text-right'
: 'bg-white'
]"
>
<p class="text-sm">
<strong>{{ msg.from === from ? "我" : msg.from }}</strong>:
{{ msg.content }}
</p>
</div>
</div>
<!-- 私聊输入区 -->
<div class="space-y-2 mb-4">
<h3 class="font-semibold">📨 私聊消息</h3>
<input
v-model="to"
placeholder="输入对方用户ID"
class="border p-2 w-full"
/>
<input
v-model="privateMsg"
placeholder="输入要发送的私聊内容"
class="border p-2 w-full"
/>
<button
@click="sendPrivate"
class="bg-blue-500 text-white px-4 py-1 rounded"
>
发送私聊
</button>
</div>
<!-- 广播输入区 -->
<div class="space-y-2">
<h3 class="font-semibold">📢 广播消息</h3>
<input
v-model="broadcastMsg"
placeholder="输入要广播的内容"
class="border p-2 w-full"
/>
<button
@click="sendBroadcast"
class="bg-green-500 text-white px-4 py-1 rounded"
>
发送广播
</button>
</div>
</div>
</template>
<script>
import wsService from "@/utils/websocket";
import axios from "axios";
export default {
data() {
return {
from: "1", // 当前用户ID(测试用)
to: "",
privateMsg: "",
broadcastMsg: "",
messages: [],
wsConnected: false
};
},
mounted() {
// 创建 WebSocket 连接(如 ws://localhost:8081/ws/1)
wsService.createWebSocket(
// 接收消息时的回调
(msg) => {
this.messages.push(msg);
},
// 当前用户 ID
this.from,
)
.then(() => {
this.wsConnected = true;
})
},
beforeUnmount() {
wsService.close();
},
methods: {
// 发送私聊消息
async sendPrivate() {
if (!this.to || !this.privateMsg) {
alert("请填写对方ID和消息内容");
return;
}
await axios.post("/api/api/test/private", {
from: this.from,
to: this.to,
type: "chat",
content: this.privateMsg
});
this.privateMsg = "";
},
// 发送广播消息
async sendBroadcast() {
if (!this.broadcastMsg) {
alert("请输入广播内容");
return;
}
await axios.post("/api/api/test/broadcast", {
from: this.from,
type: "chat",
content: this.broadcastMsg
});
this.broadcastMsg = "";
}
}
};
</script>
<style scoped>
input {
border-radius: 6px;
}
</style>
效果:

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

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

用户1广播信息:

