WebSocket实现多人实时在线聊天
最近公司在做一个婚恋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代理的,写死了一个头像。后期前端可以根据系统当前登录人取他的头像,选中头像后,与某个人对话。总之基本对话功能都实现了,欢迎白嫖党一键三连,哈哈哈哈哈哈~~~~~~~~~~~