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>