本文将基于 SpringBoot
+ Vue
+ WebSocket
实现一个简单的在线聊天功能
页面如下:
在线体验地址: http://www.zhengqingya.com:8101
SpringBoot
+ Vue
+ WebSocket
实现在线聊天 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
@Slf4j @Component @ServerEndpoint("/groupChat/{sid}/{userId}") public class WebSocketServerController { /** * 房间号 -> 组成员信息 */ private static ConcurrentHashMap<String, List<Session>> groupMemberInfoMap = new ConcurrentHashMap<>(); /** * 房间号 -> 在线人数 */ private static ConcurrentHashMap<String, Set<Integer>> onlineUserMap = new ConcurrentHashMap<>(); /** * 收到消息调用的方法,群成员发送消息 * * @param sid:房间号 * @param userId:用户id * @param message:发送消息 */ @OnMessage public void onMessage(@PathParam("sid") String sid, @PathParam("userId") Integer userId, String message) { List<Session> sessionList = groupMemberInfoMap.get(sid); Set<Integer> onlineUserList = onlineUserMap.get(sid); // 先一个群组内的成员发送消息 sessionList.forEach(item -> { try { // json字符串转对象 MsgVO msg = JSONObject.parseObject(message, MsgVO.class); msg.setCount(onlineUserList.size()); // json对象转字符串 String text = JSONObject.toJSONString(msg); item.getBasicRemote().sendText(text); } catch (IOException e) { e.printStackTrace(); } }); } /** * 建立连接调用的方法,群成员加入 * * @param session * @param sid */ @OnOpen public void onOpen(Session session, @PathParam("sid") String sid, @PathParam("userId") Integer userId) { List<Session> sessionList = groupMemberInfoMap.computeIfAbsent(sid, k -> new ArrayList<>()); Set<Integer> onlineUserList = onlineUserMap.computeIfAbsent(sid, k -> new HashSet<>()); onlineUserList.add(userId); sessionList.add(session); // 发送上线通知 sendInfo(sid, userId, onlineUserList.size(), "上线了~"); } public void sendInfo(String sid, Integer userId, Integer onlineSum, String info) { // 获取该连接用户信息 User currentUser = ApplicationContextUtil.getApplicationContext().getBean(UserMapper.class).selectById(userId); // 发送通知 MsgVO msg = new MsgVO(); msg.setCount(onlineSum); msg.setUserId(userId); msg.setAvatar(currentUser.getAvatar()); msg.setMsg(currentUser.getNickName() + info); // json对象转字符串 String text = JSONObject.toJSONString(msg); onMessage(sid, userId, text); } /** * 关闭连接调用的方法,群成员退出 * * @param session * @param sid */ @OnClose public void onClose(Session session, @PathParam("sid") String sid, @PathParam("userId") Integer userId) { List<Session> sessionList = groupMemberInfoMap.get(sid); sessionList.remove(session); Set<Integer> onlineUserList = onlineUserMap.get(sid); onlineUserList.remove(userId); // 发送离线通知 sendInfo(sid, userId, onlineUserList.size(), "下线了~"); } /** * 传输消息错误调用的方法 * * @param error */ @OnError public void OnError(Throwable error) { log.info("Connection error"); } }
@Data @ApiModel(description = "websocket消息内容") public class MsgVO { @ApiModelProperty(value = "用户id") private Integer userId; @ApiModelProperty(value = "用户名") private String username; @ApiModelProperty(value = "用户头像") private String avatar; @ApiModelProperty(value = "消息") private String msg; @ApiModelProperty(value = "在线人数") private int count; }
温馨小提示:当用户登录成功之后,可以发起websocket连接,存在store中...
下面只是单页面的简单实现
<template> <div class="chat-box"> <header>聊天室 (在线:{{count}}人)</header> <div class="msg-box" ref="msg-box"> <div v-for="(i,index) in list" :key="index" class="msg" :style="i.userId == userId?'flex-direction:row-reverse':''" > <div class="user-head"> <img :src="i.avatar" height="30" width="30" :title="i.username"> </div> <div class="user-msg"> <span :style="i.userId == userId?' float: right;':''" :class="i.userId == userId?'right':'left'">{{i.content}}</span> </div> </div> </div> <div class="input-box"> <input type="text" ref="sendMsg" v-model="contentText" @keyup.enter="sendText()" /> <div class="btn" :class="{['btn-active']:contentText}" @click="sendText()">发送</div> </div> </div> </template> <script> export default { data() { return { ws: null, count: 0, userId: this.$store.getters.id, // 当前用户ID username: this.$store.getters.name, // 当前用户昵称 avatar: this.$store.getters.avatar, // 当前用户头像 list: [], // 聊天记录的数组 contentText: "" // input输入的值 }; }, mounted() { this.initWebSocket(); }, destroyed() { // 离开页面时关闭websocket连接 this.ws.onclose(undefined); }, methods: { // 发送聊天信息 sendText() { let _this = this; _this.$refs["sendMsg"].focus(); if (!_this.contentText) { return; } let params = { userId: _this.userId, username: _this.username, avatar: _this.avatar, msg: _this.contentText, count: _this.count }; _this.ws.send(JSON.stringify(params)); //调用WebSocket send()发送信息的方法 _this.contentText = ""; setTimeout(() => { _this.scrollBottm(); }, 500); }, // 进入页面创建websocket连接 initWebSocket() { let _this = this; // 判断页面有没有存在websocket连接 if (window.WebSocket) { var serverHot = window.location.hostname; let sip = '房间号' // 填写本地IP地址 此处的 :9101端口号 要与后端配置的一致! var url = 'ws://' + serverHot + ':9101' + '/groupChat/' + sip + '/' + this.userId; // `ws://127.0.0.1/9101/groupChat/10086/聊天室` let ws = new WebSocket(url); _this.ws = ws; ws.onopen = function(e) { console.log("服务器连接成功: " + url); }; ws.onclose = function(e) { console.log("服务器连接关闭: " + url); }; ws.onerror = function() { console.log("服务器连接出错: " + url); }; ws.onmessage = function(e) { //接收服务器返回的数据 let resData = JSON.parse(e.data) _this.count = resData.count; _this.list = [ ..._this.list, { userId: resData.userId, username: resData.username, avatar: resData.avatar, content: resData.msg } ]; }; } }, // 滚动条到底部 scrollBottm() { let el = this.$refs["msg-box"]; el.scrollTop = el.scrollHeight; } } }; </script> <style lang="scss" scoped> .chat-box { margin: 0 auto; background: #fafafa; position: absolute; height: 100%; width: 100%; max-width: 700px; header { position: fixed; width: 100%; height: 3rem; background: #409eff; max-width: 700px; display: flex; justify-content: center; align-items: center; font-weight: bold; color: white; font-size: 1rem; } .msg-box { position: absolute; height: calc(100% - 6.5rem); width: 100%; margin-top: 3rem; overflow-y: scroll; .msg { width: 95%; min-height: 2.5rem; margin: 1rem 0.5rem; position: relative; display: flex; justify-content: flex-start !important; .user-head { min-width: 2.5rem; width: 20%; width: 2.5rem; height: 2.5rem; border-radius: 50%; background: #f1f1f1; display: flex; justify-content: center; align-items: center; .head { width: 1.2rem; height: 1.2rem; } // position: absolute; } .user-msg { width: 80%; // position: absolute; word-break: break-all; position: relative; z-index: 5; span { display: inline-block; padding: 0.5rem 0.7rem; border-radius: 0.5rem; margin-top: 0.2rem; font-size: 0.88rem; } .left { background: white; animation: toLeft 0.5s ease both 1; } .right { background: #53a8ff; color: white; animation: toright 0.5s ease both 1; } @keyframes toLeft { 0% { opacity: 0; transform: translateX(-10px); } 100% { opacity: 1; transform: translateX(0px); } } @keyframes toright { 0% { opacity: 0; transform: translateX(10px); } 100% { opacity: 1; transform: translateX(0px); } } } } } .input-box { padding: 0 0.5rem; position: absolute; bottom: 0; width: 100%; height: 3.5rem; background: #fafafa; box-shadow: 0 0 5px #ccc; display: flex; justify-content: space-between; align-items: center; input { height: 2.3rem; display: inline-block; width: 100%; padding: 0.5rem; border: none; border-radius: 0.2rem; font-size: 0.88rem; } .btn { height: 2.3rem; min-width: 4rem; background: #e0e0e0; padding: 0.5rem; font-size: 0.88rem; color: white; text-align: center; border-radius: 0.2rem; margin-left: 0.5rem; transition: 0.5s; } .btn-active { background: #409eff; } } } </style>
https://gitee.com/zhengqingya/xiao-xiao-su