? ? ? 最近公司在做一個婚戀app,需要增加一個功能,實現多人實時在線聊天。基于WebSocket在Springboot中的使用,前端使用vue開發。
一:后端?
1. 引入 websocket 的?maven?依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. 進行 config 配置 ServerEndpointExporter 確保【后續在使用 @ServerEndpoint 】時候能被 SpringBoot 自動檢測并注冊
?
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration // 這個類為配置類,Spring 將掃描這個類中定義的 Beans
public class WebSocketConfig {/*** serverEndpointExporter 方法的作用是將 ServerEndpointExporter 注冊為一個 Bean,* 這個 Bean 負責自動檢測帶有 @ServerEndpoint 注解的類,并將它們注冊為 WebSocket 服務器端點,* 這樣,這些端點就可以接收和處理 WebSocket 請求**/@Bean // 這個方法返回的對象應該被注冊為一個 Bean 在 Spring 應用上下文中public ServerEndpointExporter serverEndpointExporter() {// 創建并返回 ServerEndpointExporter 的實例,其中ServerEndpointExporter 是用來處理 WebSocket 連接的關鍵組件return new ServerEndpointExporter();}}
3.后端注冊webSocket服務
? ?后端:廣播給所有客戶端?,在@OnMessage 將單一廣播,切換為群體廣播
? ?存儲所有用戶會話userId
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
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.concurrent.ConcurrentHashMap;//為 ConcurrentHashMap<String,WebSocketServer> 加入一個會話userId
@ServerEndpoint("/chatWebSocket/{userId}")
@Component
@Slf4j
public class WebSocketServer {/*** [關于@OnOpen、@OnMessage、@OnClose、@OnError 中 Session session 的用意]** Session session: 主要用于代表一個單獨的 WebSocket 連接會話.每當一個 WebSocket 客戶端與服務器端點建立連接時,都會創建一個新的 Session 實例* 標識連接:每個 Session 對象都有一個唯一的 ID,可以用來識別和跟蹤每個單獨的連接。 ——> 可以使用 session.getId() 方法來獲取這個 ID.對于日志記錄、跟蹤用戶會話等方面非常有用。* 管理連接:可以通過 Session 對象來管理對應的 WebSocket 連接,例如發送消息給客戶端、關閉連接等 ——> session.getBasicRemote().sendText(message) 同步地發送文本消息,* 或者使用 session.getAsyncRemote().sendText(message) 異步地發送.可以調用 session.close() 來關閉 WebSocket 連接。* 獲取連接信息:Session 對象提供了方法來獲取連接的詳細信息,比如連接的 URI、用戶屬性等。 ——> 可以使用 session.getRequestURI() 獲取請求的 URI* **///存儲所有用戶會話//ConcurrentHashMap<String,WebSocketServer> 中String 鍵(String類型)通常是用戶ID或其他唯一標識符。允許服務器通過這個唯一標識符快速定位到對應的 WebSocketServer 實例,從而進行消息發送、接收或其他與特定客戶端相關的操作//ConcurrentHashMap<String,WebSocketServer> 中為什么寫 WebSocketServer 而不是其他,因為 WebSocketServer 作為一個實例,用于存儲每個客戶端連接。//所以在接下來@Onopen等使用中,當使用 ConcurrentHashMap<String,WebSocketServer> 時候,就不能單獨使用 session, 需要添加一個諸如 userId 這樣的會話來作為鍵。private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>();private Session session;private String userId="";//建立連接時@OnOpen//獲取會話userId//@PathParam: 是Java JAX-RS API(Java API for RESTful Web Services)的一部分,用于WebSocket和RESTful Web服務. 在WebSocket服務器端,@PathParam 注解用于提取客戶端連接URL中的參數值。public void onOpen(Session session, @PathParam("userId") String userId){this.session = session; //當前WebSocket連接的 Session 對象存儲在 WebSocketServer 實例 【這樣做是為了在后續的通信過程中(例如在處理消息、關閉連接時),您可以使用 this.session 來引用當前連接的 Session 對象。】this.userId = userId; //存儲前端傳來的 userId;webSocketMap.put(userId,this); //WebSocketServer 實例與用戶userId關聯,并將這個關聯存儲在 webSocketMap 中。【其中this: 指的是當前的 WebSocketServer 實例】log.info("會話id:" + session.getId() + "對應的會話用戶:" + userId + "【進行鏈接】");log.info("【websocket消息】有新的連接, 總數:{}", webSocketMap.size());System.out.println("會話id:" + session.getId() + " 對應的會話用戶:" + userId + " 【進行鏈接】");System.out.println("【websocket消息】有新的連接, 總數: "+webSocketMap.size());}//接收客戶端消息@OnMessagepublic void onMessage(String message,Session session) throws IOException {//當從客戶端接收到消息時調用log.info("會話id"+ session.getId() +"對應的會話用戶:" + userId + "的消息:" + message);System.out.println("會話id: "+ session.getId() +" 對應的會話用戶:" + userId + " 的消息: " + message);//修改 onMessage 方法來實現廣播: 當服務器接收到消息時,不是只發送給消息的發送者,而是廣播給所有連接的客戶端。 ——> (實現群聊)//判斷message傳來的消息不為空時,才能在頁面上進行顯示if(message != null && !message.isEmpty()){JSONObject obj = new JSONObject();obj.put("userId", userId);obj.put("message", message);// 封裝成 JSON (Java對象轉換成JSON格式的字符串。)String json = new ObjectMapper().writeValueAsString(obj);for(WebSocketServer client :webSocketMap.values()){client.session.getBasicRemote().sendText(json);}}}//鏈接關閉時@OnClosepublic void onClose(Session session){//關閉瀏覽器時清除存儲在 webSocketMap 中的會話對象。webSocketMap.remove(userId);log.info("會話id:" + session.getId() + "對應的會話用戶:" + userId + "【退出鏈接】");log.info("【websocket消息】有新的連接, 總數:{}", webSocketMap.size());System.out.println("會話id:" + session.getId() + " 對應的會話用戶:" + userId + " 【退出鏈接】");System.out.println("【websocket消息】有新的連接, 總數: "+ webSocketMap.size());}//鏈接出錯時@OnErrorpublic void onError(Session session,Throwable throwable){//錯誤提示log.error("出錯原因 " + throwable.getMessage());System.out.println("出錯原因 " + throwable.getMessage());//拋出異常throwable.printStackTrace();}
}
如果不是群發,一對一對話 單一廣播, onMessage方法如下:
//接收客戶端消息@OnMessagepublic void onMessage(String message,Session session) throws IOException {//當從客戶端接收到消息時調用log.info("會話id:" + session.getId() + ": 的消息" + message);session.getBasicRemote().sendText("回應" + "[" + message + "]");
}
?
二:前端
在?Vue?中使用 WebSocket 并不需要引入專門的庫或框架,因為 WebSocket 是一個瀏覽器內置的 API,可以直接在任何現代瀏覽器中使用。但是,你可能需要編寫一些代碼來適當地處理?WebSocket 連接、消息的發送與接收、錯誤處理以及連接的關閉。
前端: (前端整體沒有發生太大變化,只加了一個userId用于向后端傳輸)
<template><div class="iChat"><div class="container"><div class="content"><div class="item item-center"><span>今天 10:08</span></div><div class="item" v-for="(item, index) in receivedMessage" :key="index" :class="{'item-right':isCurrentUser(item),'item-left':!isCurrentUser(item)}"><!-- 右結構 --><div v-if="isCurrentUser(item)" style="display: flex"><div class="bubble" :class="{'bubble-right':isCurrentUser(item),'bubble-left':!isCurrentUser(item)}">{{item.message}}</div><div class="avatar"><imgsrc="http://192.168.0.134/img/20250701114048Tclu5k.png"/>{{item.userId}}</div></div><!-- 左結構 --><div v-else style="display: flex"><div class="avatar">{{item.userId}}<imgsrc="http://192.168.0.134/img/202507031603386lQ4ft.png"/></div><div class="bubble" :class="{'bubble-right':isCurrentUser(item),'bubble-left':!isCurrentUser(item)}">{{item.message}}</div></div></div></div><div class="input-area"><!-- 文本框 --><textarea v-model="message" id="textarea"></textarea><div class="button-area"><button id="send-btn" @click="sendMessage()">發 送</button></div></div></div></div>
</template><script>
export default {data() {return {ws:null,message:'',receivedMessage:[],currentUserId:"用戶1" + Math.floor(Math.random() * 1000)};},mounted() {this.initWebSocket()},methods: {//建立webSocket連接initWebSocket(){//定義用戶的,并加入到下述鏈接中,且記不要少了/const userId = this.currentUserId;//鏈接接口this.ws = new WebSocket('ws://localhost:9000/jsonflow/chatWebSocket/' + userId)console.log('ws://localhost:9000/jsonflow/chatWebSocket/' + userId);//打開事件this.ws.onopen = function(){console.log("websocket已打開");}//消息事件this.ws.onmessage = (event) => {//接到后端傳來數據 - 并對其解析this.receivedMessage.push(JSON.parse(event.data));console.log(this.receivedMessage)}//關閉事件this.ws.onclose = function() {console.log("websocket已關閉");};//錯誤事件this.ws.onerror = function() {console.log("websocket發生了錯誤");};},//發送消息到服務器sendMessage(){this.ws.send(this.message);this.message = '';},//判斷是否是當前用戶(boolean值)isCurrentUser(item){return item.userId == this.currentUserId}},
};
</script>
<style lang="scss" scoped>
.container{height: 666px;border-radius: 4px;border: 0.5px solid #e0e0e0;background-color: #f5f5f5;display: flex;flex-flow: column;overflow: hidden;
}
.content{width: calc(100% - 40px);padding: 20px;overflow-y: scroll;flex: 1;
}
.content:hover::-webkit-scrollbar-thumb{background:rgba(0,0,0,0.1);
}
.bubble{max-width: 400px;padding: 10px;border-radius: 5px;position: relative;color: #000;word-wrap:break-word;word-break:normal;
}
.item-left .bubble{margin-left: 15px;background-color: #fff;
}
.item-left .bubble:before{content: "";position: absolute;width: 0;height: 0;border-left: 10px solid transparent;border-top: 10px solid transparent;border-right: 10px solid #fff;border-bottom: 10px solid transparent;left: -20px;
}
.item-right .bubble{margin-right: 15px;background-color: #9eea6a;
}
.item-right .bubble:before{content: "";position: absolute;width: 0;height: 0;border-left: 10px solid #9eea6a;border-top: 10px solid transparent;border-right: 10px solid transparent;border-bottom: 10px solid transparent;right: -20px;
}
.item{margin-top: 15px;display: flex;width: 100%;
}
.item.item-right{justify-content: flex-end;
}
.item.item-center{justify-content: center;
}
.item.item-center span{font-size: 12px;padding: 2px 4px;color: #fff;background-color: #dadada;border-radius: 3px;-moz-user-select:none; /*火狐*/-webkit-user-select:none; /*webkit瀏覽器*/-ms-user-select:none; /*IE10*/-khtml-user-select:none; /*早期瀏覽器*/user-select:none;
}.avatar img{width: 42px;height: 42px;border-radius: 50%;
}
.input-area{border-top:0.5px solid #e0e0e0;height: 150px;display: flex;flex-flow: column;background-color: #fff;
}
textarea{flex: 1;padding: 5px;font-size: 14px;border: none;cursor: pointer;overflow-y: auto;overflow-x: hidden;outline:none;resize:none;
}
.button-area{display: flex;height: 40px;margin-right: 10px;line-height: 40px;padding: 5px;justify-content: flex-end;
}
.button-area button{width: 80px;border: none;outline: none;border-radius: 4px;float: right;cursor: pointer;
}/* 設置滾動條的樣式 */
::-webkit-scrollbar {width:10px;
}
/* 滾動槽 */
::-webkit-scrollbar-track {-webkit-box-shadow:inset006pxrgba(0,0,0,0.3);border-radius:8px;
}
/* 滾動條滑塊 */
::-webkit-scrollbar-thumb {border-radius:10px;background:rgba(0,0,0,0);-webkit-box-shadow:inset006pxrgba(0,0,0,0.5);
}
</style>
三:最終效果
?頁面效果:
?
?
后端日志:
四:后續:
? ? ? ?代碼中用戶頭像我是用nginx代理的,寫死了一個頭像。后期前端可以根據系統當前登錄人取他的頭像,選中頭像后,與某個人對話。總之基本對話功能都實現了,歡迎白嫖黨一鍵三連,哈哈哈哈哈哈~~~~~~~~~~~