对WebSocket做一点简单的理解
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 实例 */ @Bean public 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连接成功建立时,此方法会被调用。 @OnOpen public void onOpen(Session session, @PathParam("userId") String userId) { System.out.println("客户端:" + userId + "建立连接"); webSockets.add(this); sessionMap.put(userId, session);//把新的Session对象存入sessionMap中。 } //每当从客户端接收到消息时,该方法就会触发。这里只是简单地打印出收到的消息。 @OnMessage public void onMessage(String message, @PathParam("userId") String userId) { System.out.println("收到来自客户端:" + userId + "的信息:" + message); } //当一个WebSocket连接关闭时,此方法会被调用。它会从sessionMap中移除相应的Session对象,并输出一条日志信息。 @OnClose public 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(); } } } /** * 发生错误时调用 */ @OnError public 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); // 动态替换 userId if (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; // 连接关闭后重置 WebSocket if (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"> <div v-for="(msg, index) in notifications" :key="index" class="notification-item" > 🆕 您收到新回复:{{ msg }} </div> </div>