Springboot整合WebSocket 實(shí)現(xiàn)聊天室功能
前言
WebSocket概述:
在日常的web應(yīng)用開發(fā)中,常見的是前端向后端發(fā)起請(qǐng)求,有些時(shí)候會(huì)涉及到前后端互發(fā)消息,這時(shí)候就用到了WebSocket。
一、WebSocket原理
WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通信的協(xié)議。它通過一個(gè)簡(jiǎn)單的握手過程來建立連接,然后在連接上進(jìn)行雙向數(shù)據(jù)傳輸。與傳統(tǒng)的HTTP請(qǐng)求不同,WebSocket連接一旦建立,就可以在客戶端和服務(wù)器之間保持打開狀態(tài),直到被任何一方關(guān)閉。
核心特點(diǎn)包括:
- 全雙工通信:客戶端和服務(wù)器可以同時(shí)發(fā)送和接收消息。
- 持久連接:一旦建立連接,就可以持續(xù)進(jìn)行數(shù)據(jù)交換,無需像HTTP那樣頻繁地建立新的連接。
- 低延遲:由于連接是持久的,數(shù)據(jù)可以幾乎實(shí)時(shí)地發(fā)送和接收。
- 輕量級(jí)協(xié)議:WebSocket協(xié)議的頭部信息非常簡(jiǎn)單,減少了數(shù)據(jù)傳輸?shù)拈_銷。
二、Spring Boot集成WebSocket
代碼結(jié)構(gòu):
2.1. 引入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
2.2 配置類WebSocketConfig
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; /** * WebSocket配置類。 * 用于啟用Spring WebSocket支持,通過@Bean注解注冊(cè)ServerEndpointExporter, * 從而允許使用@ServerEndpoint注解定義WebSocket端點(diǎn)。 */ @Configuration public class WebSocketConfig { /** * 注冊(cè)ServerEndpointExporter Bean。 * ServerEndpointExporter是Spring提供的一個(gè)工具類, * 它會(huì)掃描并注冊(cè)所有使用@ServerEndpoint注解的類為WebSocket端點(diǎn)。 * * @return ServerEndpointExporter實(shí)例 */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } /** * 通信文本消息和二進(jìn)制緩存區(qū)大小 * 避免報(bào)文過大時(shí),Websocket 1009 錯(cuò)誤 */ @Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); // 文本/二進(jìn)制消息最大緩沖區(qū)(10MB) container.setMaxTextMessageBufferSize(1024 * 1024 * 10); container.setMaxBinaryMessageBufferSize(1024 * 1024 * 10); // 最大會(huì)話空閑超時(shí)時(shí)間(1小時(shí)) container.setMaxSessionIdleTimeout(60 * 60 * 1000L); return container; } }
2.3 WebSocketServer 類
WebSocketServer 類實(shí)現(xiàn)了 WebSocket 服務(wù)端的功能。 負(fù)責(zé)處理 WebSocket 連接的建立、關(guān)閉、消息接收和發(fā)送等操作。
import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Objects; import java.util.concurrent.CopyOnWriteArraySet; /** * WebSocketServer 類實(shí)現(xiàn)了 WebSocket 服務(wù)端的功能。 * 它負(fù)責(zé)處理 WebSocket 連接的建立、關(guān)閉、消息接收和發(fā)送等操作。 */ @Component @Slf4j @ServerEndpoint("/api/websocket/{sid}") public class WebSocketServer { // 靜態(tài)變量,用于記錄當(dāng)前在線連接數(shù) private static int onlineCount = 0; // 存儲(chǔ)所有連接的 WebSocketServer 實(shí)例 @Getter private static final CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>(); // 當(dāng)前連接的會(huì)話對(duì)象 private Session session; // 客戶端唯一標(biāo)識(shí)符 private String sid = ""; /** * 連接建立成功時(shí)調(diào)用的方法。 * * @param session 當(dāng)前連接的會(huì)話對(duì)象 * @param sid 客戶端唯一標(biāo)識(shí)符 */ @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { this.session = session; webSocketSet.add(this); // 將當(dāng)前實(shí)例加入集合 this.sid = sid; addOnlineCount(); // 在線數(shù)加1 try { sendMessage("WebSocket 連接成功"); // 發(fā)送連接成功的消息 log.info("有新窗口開始監(jiān)聽:{},當(dāng)前在線人數(shù)為:{}", sid, getOnlineCount()); } catch (IOException e) { log.error("websocket IO Exception"); } } /** * 連接關(guān)閉時(shí)調(diào)用的方法。 */ @OnClose public void onClose() { webSocketSet.remove(this); // 從集合中移除當(dāng)前實(shí)例 subOnlineCount(); // 在線數(shù)減1 log.info("釋放的sid為:{}", sid); log.info("有一個(gè)連接關(guān)閉!當(dāng)前在線人數(shù)為{}", getOnlineCount()); } /** * 接收到客戶端消息時(shí)調(diào)用的方法。 * * @param message 客戶端發(fā)送的消息 * @param session 當(dāng)前連接的會(huì)話對(duì)象 */ @OnMessage public void onMessage(String message, Session session) { log.info("收到來自窗口{}的信息:{}", sid, message); // 群發(fā)消息 for (WebSocketServer item : webSocketSet) { if (Objects.equals(item.sid, this.sid)) { continue; } sendMessageToClient(item, message); } } /** * 實(shí)現(xiàn)服務(wù)器主動(dòng)推送消息的方法,并統(tǒng)一處理異常。 * * @param client 要推送的客戶端實(shí)例 * @param message 要推送的消息 */ private void sendMessageToClient(WebSocketServer client, String message) { try { client.sendMessage(message); } catch (IOException e) { log.error("向客戶端 {} 發(fā)送消息時(shí)出錯(cuò): {}", client.sid, message, e); } } /** * 群發(fā)自定義消息給指定的客戶端。 * * @param message 要發(fā)送的消息 * @param sid 客戶端唯一標(biāo)識(shí)符,為 null 時(shí)發(fā)送給所有客戶端 * @throws IOException 如果發(fā)送消息時(shí)發(fā)生 I/O 錯(cuò)誤 */ public static void sendInfo(String message, @PathParam("sid") String sid) throws IOException { log.info("推送消息到窗口" + sid + ",推送內(nèi)容:" + message); for (WebSocketServer item : webSocketSet) { try { if (sid == null) { // 如果 sid 為 null,則發(fā)送給所有客戶端 item.sendMessage(message); } else if (item.sid.equals(sid)) { // 如果 sid 匹配,則只發(fā)送給該客戶端 item.sendMessage(message); } } catch (IOException e) { log.error("向客戶端 {} 發(fā)送消息時(shí)出錯(cuò): {}", item.sid, message, e); } } } /** * 發(fā)生錯(cuò)誤時(shí)調(diào)用的方法。 * * @param session 當(dāng)前連接的會(huì)話對(duì)象 * @param error 發(fā)生的錯(cuò)誤 */ @OnError public void onError(Session session, Throwable error) { log.error("發(fā)生錯(cuò)誤"); error.printStackTrace(); } /** * 實(shí)現(xiàn)服務(wù)器主動(dòng)推送消息的方法。 * * @param message 要推送的消息 * @throws IOException 如果發(fā)送消息時(shí)發(fā)生 I/O 錯(cuò)誤 */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 獲取當(dāng)前在線連接數(shù)。 * * @return 當(dāng)前在線連接數(shù) */ public static synchronized int getOnlineCount() { return onlineCount; } /** * 增加在線連接數(shù)。 */ public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } /** * 減少在線連接數(shù)。 */ public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } }
2.4 前端代碼 index.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>WebSocket 聊天室</title> <script src="https://autherp.jd.com/js/jquery.js"></script> <style> .time { font-size: 0.8em; display: block; margin-bottom: 5px; } .user-msg { background-color: #90EE90; padding: 8px; border-radius: 8px; max-width: 70%; display: inline-block; } .system-msg { background-color: #D3D3D3; padding: 8px; border-radius: 8px; max-width: 70%; display: inline-block; font-size: 0.9em; } #message-box { height: 300px; overflow-y: auto; margin-bottom: 10px; border: 1px solid #ccc; padding: 10px; border-radius: 4px; } .message-right { text-align: right; margin: 10px 0; } .message-left { text-align: left; margin: 10px 0; } .message-center { text-align: center; margin: 10px 0; } .container { max-width: 600px; margin: 20px auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .input-group { display: flex; gap: 10px; } .input-group input[type="text"] { flex-grow: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; } .btn-primary, .btn-danger { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; } .btn-primary { background-color: #007BFF; color: white; } .btn-primary:hover { background-color: #0056b3; } .btn-danger { background-color: #DC3545; color: white; } .btn-danger:hover { background-color: #c82333; } /* 添加新樣式讓標(biāo)題和按鈕居中 */ .container h2, .container .btn-danger { text-align: center; display: block; margin-left: auto; margin-right: auto; } /* 為按鈕添加一些外邊距,使其看起來更美觀 */ .container .btn-danger { margin-top: 10px; } </style> </head> <body> <div class="container"> <h2>WebSocket 聊天室</h2> <!-- 添加顯示 sid 的元素 --> <p id="sid-display">當(dāng)前用戶 SID: <span id="sid-value"></span></p> <!-- 消息顯示區(qū)域 --> <div id="message-box"></div> <!-- 輸入框與發(fā)送按鈕 --> <div class="input-group"> <input type="text" id="text" placeholder="請(qǐng)輸入消息..." /> <button class="btn-primary" onclick="send()">發(fā)送</button> </div> <hr/> <!-- 關(guān)閉連接按鈕 --> <button class="btn-danger" onclick="closeWebSocket()">關(guān)閉 WebSocket 連接</button> </div> <script type="text/javascript"> let websocket = ''; // 獲取當(dāng)前頁面 URL 中的 sid 參數(shù)或隨機(jī)生成一個(gè) function getSid() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('sid') || Math.floor(1000 + Math.random() * 9000); // 4位數(shù)字 } const sid = getSid(); const wsUrl = `ws://127.0.0.1:9999/api/websocket/${sid}`; // 頁面加載完成后更新 sid 顯示 window.onload = function() { document.getElementById('sid-value').textContent = sid; }; // 初始化 WebSocket if ('WebSocket' in window) { websocket = new WebSocket(wsUrl); } else { alert('當(dāng)前瀏覽器不支持 WebSocket'); } // 連接成功 websocket.onopen = function () { console.log('WebSocket 連接成功'); }; // 接收消息 websocket.onmessage = function (event) { addMessage(event.data); }; // 錯(cuò)誤處理 websocket.onerror = function () { console.log('WebSocket 連接發(fā)生錯(cuò)誤'); }; // 關(guān)閉連接 websocket.onclose = function () { this.closeWebSocket(); console.log('WebSocket 連接已關(guān)閉'); }; // 頁面關(guān)閉前斷開連接 window.onbeforeunload = function () { this.closeWebSocket(); }; // 發(fā)送消息 function send() { let message = document.getElementById('text').value.trim(); if (!message) return; if (websocket && websocket.readyState === WebSocket.OPEN) { websocket.send(`{"msg":"${message}","sid":"${sid}", "time": "${new Date().toLocaleTimeString()}"}`); addMessage(`{"msg":"${message}","sid":"${sid}", "time": "${new Date().toLocaleTimeString()}"}`); document.getElementById('text').value = ''; } else { addMessage("WebSocket 連接未建立,請(qǐng)稍后再試。"); } } //關(guān)閉WebSocket連接 function closeWebSocket() { if (websocket) { websocket.close(); } } // 添加消息到聊天區(qū) function addMessage(content) { let msgBox = document.getElementById('message-box'); const time = new Date().toLocaleTimeString(); const div = document.createElement('div'); let messageData; let isSystemMessage = false; try { messageData = JSON.parse(content); } catch (e) { isSystemMessage = true; } if (isSystemMessage) { div.className = 'message-center'; div.innerHTML = ` <span class="time">${time}</span> <span class="system-msg"> ${content}</span> `; } else { const isMyMessage = String(messageData.sid) === String(sid); div.className = isMyMessage ? 'message-right' : 'message-left'; div.innerHTML = ` <span class="time">${messageData.time}</span> <span class="${isMyMessage ? 'user-msg' : 'system-msg'}"> ${isMyMessage ? '' : '用戶#' + messageData.sid + ':'} ${messageData.msg} </span> `; } msgBox.appendChild(div); msgBox.scrollTop = msgBox.scrollHeight; } </script> </body> </html>
2.5 Controller訪問首頁
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class TestController { @RequestMapping("/") public String index(){ return "index.html"; } }
打開多個(gè)網(wǎng)頁窗口,訪問ip:端口
到此這篇關(guān)于Springboot整合WebSocket 實(shí)現(xiàn)聊天室功能的文章就介紹到這了,更多相關(guān)Springboot WebSocket聊天室內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Java實(shí)現(xiàn)簡(jiǎn)易的七星彩號(hào)碼生成器
七星彩是中國體育彩票的一種玩法,由中國國家體育總局體育彩票管理中心統(tǒng)一發(fā)行。本文為大家準(zhǔn)備了一個(gè)七星彩號(hào)碼生成器Java工具類,感興趣的可以了解一下2022-08-08mybatis3.3+struts2.3.24+mysql5.1.22開發(fā)環(huán)境搭建圖文教程
這篇文章主要為大家詳細(xì)介紹了mybatis3.3+struts2.3.24+mysql5.1.22開發(fā)環(huán)境搭建圖文教程,感興趣的小伙伴們可以參考一下2016-06-06Java排序算法之歸并排序簡(jiǎn)單實(shí)現(xiàn)
這篇文章主要介紹了Java排序算法之歸并排序簡(jiǎn)單實(shí)現(xiàn),具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12SpringBoot?@GroupSequenceProvider注解實(shí)現(xiàn)bean多屬性聯(lián)合校驗(yàn)的示例代碼
這篇文章主要介紹了SpringBoot?@GroupSequenceProvider注解實(shí)現(xiàn)bean多屬性聯(lián)合校驗(yàn),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08java阻塞隊(duì)列實(shí)現(xiàn)原理及實(shí)例解析
這篇文章主要介紹了java阻塞隊(duì)列實(shí)現(xiàn)原理及實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11Java人民幣小寫轉(zhuǎn)大寫字符串的實(shí)現(xiàn)
這篇文章主要介紹了Java人民幣小寫轉(zhuǎn)大寫字符串的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04springboot通過jar包啟動(dòng)中文日志亂碼問題及解決
這篇文章主要介紹了springboot通過jar包啟動(dòng)中文日志亂碼問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06RxJava的消息發(fā)送和線程切換實(shí)現(xiàn)原理
這篇文章主要介紹了RxJava的消息發(fā)送和線程切換實(shí)現(xiàn)原理,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)爬蟲代碼示例
這篇文章主要介紹了java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)爬蟲代碼示例,還是挺不錯(cuò)的,這里分享給大家,需要的朋友可以參考下。2017-11-11