最近公司在做一个婚恋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());
}
//接收客户端消息
@OnMessage
public 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);
}
}
}
//链接关闭时
@OnClose
public 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());
}
//链接出错时
@OnError
public void onError(Session session,Throwable throwable){
//错误提示
log.error("出错原因 " + throwable.getMessage());
System.out.println("出错原因 " + throwable.getMessage());
//抛出异常
throwable.printStackTrace();
}
}
如果不是群发,一对一对话 单一广播, onMessage方法如下:
//接收客户端消息
@OnMessage
public 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">
<img
src="http://192.168.0.134/img/20250701114048Tclu5k.png"
/>
{{item.userId}}
</div>
</div>
<!-- 左结构 -->
<div v-else style="display: flex">
<div class="avatar">
{{item.userId}}
<img
src="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代理的,写死了一个头像。后期前端可以根据系统当前登录人取他的头像,选中头像后,与某个人对话。总之基本对话功能都实现了,欢迎白嫖党一键三连,哈哈哈哈哈哈~~~~~~~~~~~