1.概念
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。
HTTP协议和WebSocket协议对比:
-
HTTP是短连接
-
WebSocket是长连接
-
HTTP通信是单向的,基于请求响应模式
-
WebSocket支持双向通信
-
HTTP和WebSocket底层都是TCP连接
-
WebSocket缺点:
服务器长期维护长连接需要一定的成本 各个浏览器支持程度不一 WebSocket 是长连接,受网络限制比较大,需要处理好重连
结论:WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用
WebSocket的使用场景:视频弹幕,网页聊天,股票基金报价实时更新, 体育实况更新
2.示例
2.1 基础配置
导入Maven坐标
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dependency>
/*** WebSocket配置类,用于注册WebSocket的Bean* @Author GuihaoLv*/ @Configuration public class WebSocketConfig {/*** 创建并返回一个 ServerEndpointExporter 实例。** ServerEndpointExporter 是 Spring 提供的一个工具类,用于自动注册使用了 @ServerEndpoint 注解的 WebSocket 端点。* 这样可以避免手动注册每个 WebSocket 端点。** @return ServerEndpointExporter 实例*/@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}}
WebSocket服务器端组件:
import org.springframework.stereotype.Component; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet;/** * WebSocket服务器端组件 * @Author GuihaoLv */ @Component //标记这个类是一个WebSocket端点,可以接收来自客户端的WebSocket连接请求。/ws/{sid}表示WebSocket的URL路径模式,其中{sid}是路径参数,代表会话ID(Session ID)。 @ServerEndpoint("/ws/{userId}") public class WebSocketServer {//这里声明了一个静态的Map,用来存储每个连接的Session对象,键是sid,值是对应的Session。通过这种方式,服务器可以追踪每一个连接的客户端。private static Map<String, Session> sessionMap = new ConcurrentHashMap();//一个线程安全的集合,用来存储所有活动的WebSocketServer实例。public static CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();//@OnOpen:当一个新的WebSocket连接成功建立时,此方法会被调用。@OnOpenpublic void onOpen(Session session, @PathParam("userId") String userId) {System.out.println("客户端:" + userId + "建立连接");webSockets.add(this);sessionMap.put(userId, session);//把新的Session对象存入sessionMap中。}//每当从客户端接收到消息时,该方法就会触发。这里只是简单地打印出收到的消息。@OnMessagepublic void onMessage(String message, @PathParam("userId") String userId) {System.out.println("收到来自客户端:" + userId + "的信息:" + message);}//当一个WebSocket连接关闭时,此方法会被调用。它会从sessionMap中移除相应的Session对象,并输出一条日志信息。@OnClosepublic void onClose(@PathParam("userId") String userId) {System.out.println("连接断开:" + userId);webSockets.remove(this);sessionMap.remove(userId);}//遍历所有现存的Session对象,并尝试向每个客户端发送文本消息。如果发送过程中遇到任何异常,就捕获异常并打印堆栈跟踪信息。public void sendToAllClient(String message) {Collection<Session> sessions = sessionMap.values();for (Session session : sessions) {try {session.getBasicRemote().sendText(message);} catch (Exception e) {e.printStackTrace();}}}/*** 发生错误时调用*/@OnErrorpublic void onError(Session session, Throwable error) {System.err.println("WebSocket 发生错误: " + error.getMessage());error.printStackTrace();} }
前端工具类:
const url = "ws://127.0.0.1:8080/ws/{userId}"; // 注意:这里需要替换 {userId} 为实际的用户ID //定义了 WebSocket 工具类的结构和方法签名。 interface Socket {websocket: WebSocket | null; // WebSocket 连接实例init: (userId: string) => void; // 需要传入 userId 初始化连接send: (data: object) => void; // 发送数据的方法onMessage: (callback: (msg) => void) => void; // 消息监听回调onClose: (callback: () => void) => void; // 关闭回调onError: (callback: (error: Event) => void) => void; // 错误回调onMessageCallback: ((msg) => void) | null; // 存储消息回调onCloseCallback: (() => void) | null; // 存储关闭回调onErrorCallback: ((error: Event) => void) | null; // 存储错误回调 }const socket: Socket = {websocket: null,//用来存储事件回调函数。onMessageCallback: null,onCloseCallback: null,onErrorCallback: null,init: (userId: string) => {const fullUrl = url.replace("{userId}", userId); // 动态替换 userIdif (socket.websocket) return; // 避免重复连接socket.websocket = new WebSocket(fullUrl);/*** 当收到消息时调用该回调函数。*/socket.websocket.onopen = () => {console.log("WebSocket 连接成功");};/*** 当 WebSocket 连接关闭时调用该回调函数。* @param e*/socket.websocket.onclose = (e) => {console.log("WebSocket 连接关闭", e);socket.websocket = null; // 连接关闭后重置 WebSocketif (socket.onCloseCallback) {socket.onCloseCallback();}};/*** 当 WebSocket 发生错误时调用该回调函数。* @param e*/socket.websocket.onerror = (e) => {console.error("WebSocket 错误", e);if (socket.onErrorCallback) {socket.onErrorCallback(e);}};//收到 WebSocket 消息 时执行:// event.data 是接收到的字符串,先 JSON.parse() 解析成对象。// 调用 onMessageCallback 回调,如果外部监听了消息。socket.websocket.onmessage = (event) => {try {const message = JSON.parse(event.data);console.log("📩 收到 WebSocket 消息:", message);if (socket.onMessageCallback) {socket.onMessageCallback(message);}} catch (error) {console.error("解析消息失败:", error);}};},//消息发送 (send 方法)send: (data: object) => {if (socket.websocket && socket.websocket.readyState === WebSocket.OPEN) {socket.websocket.send(JSON.stringify(data));} else {console.log("WebSocket 未连接,尝试重连");setTimeout(() => socket.send(data), 1000); // 尝试重新发送消息}},//监听消息,onMessageCallback 存储消息回调函数,收到消息时触发。onMessage: (callback: (msg) => void) => {socket.onMessageCallback = callback;},//监听关闭,onCloseCallback 存储关闭回调函数,连接关闭时触发。onClose: (callback: () => void) => {socket.onCloseCallback = callback;},//监听错误,onErrorCallback 存储错误回调函数,连接错误时触发。onError: (callback: (error: Event) => void) => {socket.onErrorCallback = callback;}, };export default socket;
2.2 客户端与服务端交互示例
服务端接收消息的方法:
@OnMessage public void onMessage(String message, @PathParam("userId") String userId) {System.out.println("收到来自客户端:" + userId + "的信息:" + message); }
客户端测试页面:
<script setup lang="ts"> import socket from '@/utils/webSocket0.ts'; import {ref} from "vue"; // 引入 WebSocket 工具类const userId = ref('');function connect() {if (!userId.value) {alert('Please enter a User ID');return;}//初始化 WebSocket 连接,传入用户ID作为参数。socket.init(userId.value);socket.onMessage((message) => {console.log('收到消息:', message);appendMessage(`收到消息: ${JSON.stringify(message)}`);});socket.onClose(() => {console.log('WebSocket 连接已关闭');appendMessage('WebSocket 连接已关闭');});socket.onError((error) => {console.error('WebSocket 错误:', error);appendMessage('WebSocket 错误');}); }//造一条类型为 'chat' 的消息,内容为 'Hello, Server!'。 //使用 socket.send(message); 发送消息到服务器。 //通过 appendMessage 函数在页面上显示已发送的消息内容。 function sendMessage() {const message = { type: 'chat', content: 'Hello, Server!' };socket.send(message);appendMessage(`发送消息: ${JSON.stringify(message)}`); }//检查 socket.websocket 是否存在(即 WebSocket 连接是否已经建立)。 //如果存在,则调用 close() 方法关闭连接。 function disconnect() {if (socket.websocket) {socket.websocket.close();} }//获取页面上 ID 为 messages 的 div 元素。 //创建一个新的 div 元素,设置其文本内容为传入的消息,并将其追加到 messagesDiv 中 function appendMessage(message: string) {const messagesDiv = document.getElementById('messages');if (messagesDiv) {const messageElement = document.createElement('div');messageElement.textContent = message;messagesDiv.appendChild(messageElement);} } </script><template><div><h1>WebSocket Client Simulation</h1><input v-model="userId" type="text" placeholder="Enter User ID"/><button @click="connect">Connect</button><button @click="sendMessage">Send Message</button><button @click="disconnect">Disconnect</button><div id="messages"></div></div> </template><style scoped> /* 样式可以根据需要进行调整 */ input {margin-right: 10px; }button {margin-right: 10px; }#messages {margin-top: 20px;border: 1px solid #ccc;padding: 10px;width: 300px;height: 200px;overflow-y: scroll; } </style>
页面原型:
连接客户端:
客户端向服务端发送消息:
断开连接:
2.3 客户端与客户端的交互监听
服务端接收消息处理:
@OnMessage public void onMessage(String message, @PathParam("userId") String userId) {System.out.println("收到来自客户端:" + userId + "的信息:" + message);try {// 使用 FastJSON 解析 JSON 字符串JSONObject jsonMessage = JSON.parseObject(message);String toUserId = jsonMessage.getString("toUserId");// 获取目标用户的会话Session targetSession = sessionMap.get(toUserId);if (targetSession != null && targetSession.isOpen()) {System.out.println("正在向用户:" + toUserId + "发送消息");// 将消息转发给目标用户targetSession.getBasicRemote().sendText(jsonMessage.toJSONString());} else {System.out.println("无法找到目标用户或连接已关闭:" + toUserId);}} catch (Exception e) {e.printStackTrace();} }
用户A:
<template><div><h1>用户A</h1><input v-model="message" placeholder="输入消息" /><button @click="sendMessage">发送消息</button><div id="messages"><div v-for="(msg, index) in messages" :key="index">{{ msg }}</div></div></div> </template><script setup lang="ts"> import { ref } from 'vue'; import socket from '@/utils/webSocket0.ts'; // 引入 WebSocket 工具类const userId = 'userA'; // 用户A的ID const message = ref(''); const messages = ref<string[]>([]);socket.init(userId);socket.onMessage((msg) => {console.log('收到消息:', msg);appendMessage(`收到消息: ${JSON.stringify(msg)}`); });socket.onError((error) => {console.error('WebSocket 错误:', error);appendMessage('WebSocket 错误'); });function sendMessage() {const msgContent = { type: 'chat', content: message.value, toUserId: 'userB' };socket.send(msgContent);appendMessage(`发送消息: ${JSON.stringify(msgContent)}`);message.value = ''; // 清空输入框 }function appendMessage(messageText: string) {messages.value.push(messageText); } </script><style scoped> /* 样式可以根据需要进行调整 */ input {margin-right: 10px; }button {margin-right: 10px; }#messages {margin-top: 20px;border: 1px solid #ccc;padding: 10px;width: 300px;height: 200px;overflow-y: scroll; } </style>
用户B:
<template><div><h1>用户B</h1><div id="messages"><div v-for="(msg, index) in messages" :key="index">{{ msg }}</div></div></div> </template><script setup lang="ts"> import { ref, onMounted } from 'vue'; import socket from '@/utils/webSocket0.ts'; // 引入 WebSocket 工具类const userId = 'userB'; // 用户B的ID const messages = ref<string[]>([]);// 初始化 WebSocket 连接 function initSocket() {socket.init(userId);socket.onMessage((msg) => {if (msg.toUserId === userId) {console.log('收到消息:', msg);appendMessage(`收到消息: ${JSON.stringify(msg)}`);}});socket.onError((error) => {console.error('WebSocket 错误:', error);appendMessage('WebSocket 错误');}); }// 将新消息添加到消息列表中 function appendMessage(messageText: string) {messages.value.push(messageText); }// 在组件挂载时初始化 WebSocket onMounted(() => {initSocket(); }); </script><style scoped> /* 样式可以根据需要进行调整 */ #messages {margin-top: 20px;border: 1px solid #ccc;padding: 10px;width: 300px;height: 200px;overflow-y: scroll; } </style>
用户A发消息给用户B:
2.4: 一个实际的消息推送场景:
服务端消息推送处理:
//如果是回复评论 blogCommentsSaveDto.setAnswerUserId(userMapper.getUserByUsername(blogCommentsSaveDto.getAnswerUserName())); //将回复推送给接收者 webSocketServer.sendToClient(blogCommentsSaveDto.getAnswerUserId().toString(),getUser().getId().toString(),blogCommentsSaveDto.getContent());
/*** 向指定客户端发送文本消息* @param userId 客户端的会话ID* @param message 要发送的消息*/ public void sendToClient(String userId, String fromId,String message) {try {// 创建标准消息结构JSONObject messageJson = new JSONObject();messageJson.put("content", message); // 原始内容放在content字段messageJson.put("timestamp", System.currentTimeMillis());messageJson.put("status", "success");messageJson.put("from",fromId);Session session = sessionMap.get(userId);if (session != null && session.isOpen()) {// 发送序列化的JSON字符串session.getBasicRemote().sendText(messageJson.toJSONString());}} catch (Exception e) {e.printStackTrace();} }
客户端与服务端建立连接并且实时监听消息:
// 新增导入 import { onUnmounted } from 'vue' import socket from '@/utils/webSocket0.ts' // 假设socket工具类路径 import { useUserStore } from '@/stores/userStore.ts'// 新增消息通知状态 const notifications = ref<string[]>([]) const userStore = useUserStore()// 初始化WebSocket const initWebSocket = () => {if (!userStore.userInfo?.userId) {console.error('用户未登录,无法建立WebSocket连接')return}// 初始化连接socket.init(userStore.userInfo.userId.toString())// 消息监听socket.onMessage((msg) => {console.log('收到新回复通知:', msg)notifications.value.push(msg.content)// 自动刷新评论(带1秒延迟避免请求冲突)setTimeout(loadComments, 1000)})// 错误处理socket.onError((err) => {console.error('WebSocket错误:', err)}) }// 组件挂载时 onMounted(() => {loadComments()initWebSocket() })
<h1>评论区</h1> <!-- 在顶部添加通知栏 --> <div v-if="notifications.length" class="notifications"><divv-for="(msg, index) in notifications":key="index"class="notification-item">🆕 您收到新回复:{{ msg }}</div> </div>