目录
- 一、项目实现内容
- 二、websocket
- 三、实现过程
- java后端
- vue前端
- 源代码
- WebSocketServer调用spring容器注意事项
- 扩展
一、项目实现内容
- http://localhost:8080/websocket?uid=1
- http://localhost:8080/websocket?uid=2
- http://localhost:8080/websocket?uid=3
二、websocket
websocket api介绍
再看这里,这个是我看介绍比较好的websocket使用
-
websocket方法定义
WebSocket.onclose
用于指定连接关闭后的回调函数。
WebSocket.onerror
用于指定连接失败后的回调函数。
WebSocket.onmessage
用于指定当从服务器接受到信息时的回调函数。
WebSocket.onopen
用于指定连接成功后的回调函数。 -
先是定义websocket的处理逻辑
-
消息流转过程
三、实现过程
前提:这只是一个小demo没用到数据库,只是简单的在后端直接返回准备好的用户,但是逻辑是没有问题的,只要你的用户信息换成查数据库和将发到服务器的消息数据保存一份到数据库就行了。(CURD比较简单,逻辑明白就行)
java后端
@Component
注册到spring容器,交由spring控制@ServerEndpoint("/path")
是和@RequestMapping("/path")
差不多类似的,若是有ws
协议的路上path匹配则交由该对象处理(主要是将目前的类定义成一个websocket服务器端, 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket
服务器端)WebSocketServer
的加载spring容器之前,后面有客户端连接服务器,则将WebSocketServer
的session、uid
替换成客户端对应的存储在Map中记录起来,发送消息还得用到对应的session
- 之后接收到客户端的消息,
onMessage
内可以通过webSocketMap
记录的
WebSocketServer
使用session.getBasicRemote().sendText(message);
发送消息message
@Component
@ServerEndpoint("/wechat/{uid}")
public class WebSocketServer {/*** 记录在线的用户数*/private static AtomicInteger onlineUserNum=new AtomicInteger(0);/*** 存储连接该服务的用户(客户端)对应的WebSocketServer (uid,WebSocketServer)*/private static Map<Integer,WebSocketServer> webSocketMap=new ConcurrentHashMap<>();/*** 与某个客户端的连接会话,需要通过它来给客户端发送数据*/private Session session;/*** 当前连接进行用户的uid*/private int uid;/*** 连接成功后的回调函数* @param uid* @param session*/@OnOpenpublic void onOpen(@PathParam("uid")int uid,Session session){//获取当前的session、uidthis.session=session;this.uid=uid;//存储客户端对应的websocketif (!webSocketMap.containsKey(uid)){//判断这里还应该查一下数据库,但是我这里比较潦草就没做//还未连接过webSocketMap.put(uid,this);//在线人数+1onlineUserNum.incrementAndGet();}else{//已经连接过,记录新的websocketwebSocketMap.replace(uid,this);}System.out.println("用户id:"+uid+"建立连接!");}/*** 连接失败后的回调函数* @param session* @param error*/@OnErrorpublic void onError(Session session, Throwable error) {System.out.println("用户:"+this.uid+"连接失败,原因:"+error.getMessage());error.printStackTrace();}/*** 前提:成功建立连接* 发送过来的消息按如下的机制推送到接收的客户端* @param message* @param session*/@OnMessagepublic void onMessage(String message,Session session){System.out.println(message);if(message.isEmpty()||message==null){//消息不正常,不处理return;}//初始化消息的格式 json->自己定义的消息体Message fromMessage = JSON.parseObject(message,Message.class);if(!webSocketMap.containsKey(fromMessage.getToUid())){System.out.println("要接收的用户不在线,暂存数据库,等该用户上线在获取!");return;}//在线则直接推送数据到接收端客户端WebSocketServer webSocketServer = webSocketMap.get(fromMessage.getToUid());webSocketServer.sendMessage(message);}/*** 推送消息到客户端* @param message*/public void sendMessage(String message) {try {this.session.getBasicRemote().sendText(message);} catch (IOException e) {e.printStackTrace();}}/*** 连接关闭后的回调函数*/@OnClosepublic void onClose(){if (webSocketMap.containsKey(uid)){webSocketMap.remove(uid);//在线人数-1onlineUserNum.decrementAndGet();}}}
vue前端
下面介绍进入页面的逻辑(可以和前面的图多结合理解)
- localhost:8080/?uid=i 获取用户i的信息进入页面(因为没登陆注册就这样用于测试)
- 获取在线用户,除去本身
- 创建WebSocket对象连接服务器(此时服务器记录了和客户端连接时的WebSocketServer)
- 就可以通过全局的WebSocket对象发送消息了
<template><div class="bg"><el-container class="wechat"><el-aside width="35%" style="border-right: 1px solid #fff"><!-- 自己 --><div class="item"><el-avatar:size="46":src="user.avatarUrl"style="float: left; margin-left: 2px"></el-avatar><div class="name">{{ user.nickname}}<el-tag style="margin-left: 5px" type="success">本人</el-tag></div></div><!-- 在线用户 --><divclass="item"v-for="(item1, index) in userlist":key="item1.uid"@click="selectUser(index)"><!-- 新数消息 --><el-badge:value="new_message_num[index]":max="99":hidden="!new_message_num[index] > 0"style="float: left; margin-left: 2px"><el-avatar :size="46" :src="item1.avatarUrl"></el-avatar></el-badge><div class="name">{{ item1.nickname }}</div></div></el-aside><el-main><el-container class="wechat_right"><!-- 右边顶部 --><el-header class="header">{{anotherUser != null && anotherUser.uid > 0? anotherUser.nickname: "未选择聊天对象"}}</el-header><!-- 聊天内容 --><el-main class="showChat"><div v-for="item2 in messageList[index]" :key="item2.msg"><!-- 对方发的 --><div class="leftBox" v-if="item2.FromUid == anotherUser.uid"><span style="font-size: 4px">{{ item2.time }}</span>{{ item2.msg }}</div><div class="myBr" v-if="item2.FromUid == anotherUser.uid"></div><!-- 自己发的 --><div class="rightBox" v-if="item2.FromUid == user.uid"><span style="font-size: 4px">{{ item2.time }}</span>{{ item2.msg }}</div><div class="myBr" v-if="item2.FromUid == user.uid"></div></div></el-main><!-- 输入框 --><el-main class="inputValue"><textarea v-model="inputValue" id="chat" cols="26" rows="5"></textarea><!-- 发送按钮 --><el-buttonv-if="anotherUser != null && anotherUser.uid > 0 && inputValue != ''"type="success"size="mini"roundid="send"@click="senMessage">发送</el-button></el-main></el-container></el-main></el-container></div>
</template><script>
export default {data() {return {//自己user: {},//要私信的人anotherUser: {},//在线的用户userlist: [],//要私信的人在userlist的索引位置index: 0,//消息队列集合 [本人和第一个人之间的消息集合、本人和第二个人之间的消息集合、...]messageList: [],//新消息个数集合new_message_num: [],//将要发送的内容inputValue: "",//websocketwebsocket: null,};},methods: {//获取自己被分配的信息getYourInfo(uid) {let params = new URLSearchParams();this.$axios.post("/user/getYourInfo/" + uid, params).then((res) => {this.user = res.data.data;if (res.data.code == 200) {//获取在线用户this.getUserList();}}).catch((err) => {console.error(err);});},//获取在线用户getUserList() {let params = new URLSearchParams();this.$axios.post("/user/getUserList", params).then((res) => {this.userlist = res.data.data.filter(//去掉自己(user) => user.uid !== this.user.uid);//填充消息数据 messagelist:[[]、[]...] 并且将新消息队列置为0for (let i = 0; i < this.userlist.length; i++) {this.messageList.push([]);this.new_message_num.push(0);}//将当前的客户端和服务端进行连接,并定义接收到消息的处理逻辑this.init(this.user.uid);}).catch((err) => {console.error(err);});},//选择聊天对象selectUser(index) {this.anotherUser = this.userlist[index];this.index = index;//将新消息置为0this.new_message_num[index] = 0;},//将当前的客户端和服务端进行连接,并定义接收到消息的处理逻辑init(uid) {var self = this;if (typeof WebSocket == "undefined") {console.log("您的浏览器不支持WebSocket");return;}//清除之前的记录if (this.websocket != null) {this.websocket.close();this.websocket = null;}//-----------------------连接服务器-----------------------let socketUrl = "ws://localhost:8088/wechat/" + this.user.uid;//开启WebSocket 连接this.websocket = new WebSocket(socketUrl);//指定连接成功后的回调函数this.websocket.onopen = function () {console.log("websocket已打开");};//指定连接失败后的回调函数this.websocket.onerror = function () {console.log("websocket发生了错误");};//指定当从服务器接受到信息时的回调函数this.websocket.onmessage = function (msg) {//消息体例如{"FromUid":1,"ToUid":2,"msg":"你好","time":"00:07:03"} => message对象let data = JSON.parse(msg.data);//添加到对应的消息集合中let index = data.FromUid > uid ? data.FromUid - 2 : data.FromUid - 1;self.messageList[index].push(data);//新消息数+1self.new_message_num[index]++;};//指定连接关闭后的回调函数this.websocket.onclose = function () {console.log("websocket已关闭");};},//发送信息senMessage() {//消息体例如{"FromUid":1,"ToUid":2,"msg":"你好","time":"00:07:03"}let message = {FromUid: this.user.uid,ToUid: this.anotherUser.uid,msg: this.inputValue,time: new Date().toLocaleTimeString(),};//将消息插进消息队列,显示在前端this.messageList[this.index].push(message);//将消息发送至服务器端再转发到对应的用户this.websocket.send(JSON.stringify(message));//清空一下输入框内容this.inputValue = "";},},created() {let uid = this.$route.query.uid;if (uid != undefined) {//获取被分配的用户信息this.getYourInfo(uid);}},
};
</script><style>
/*改变滚动条 */
::-webkit-scrollbar {width: 3px;border-radius: 4px;
}::-webkit-scrollbar-track {background-color: inherit;-webkit-border-radius: 4px;-moz-border-radius: 4px;border-radius: 4px;
}::-webkit-scrollbar-thumb {background-color: #c3c9cd;-webkit-border-radius: 4px;-moz-border-radius: 4px;border-radius: 4px;
}
.bg {background: url("https://s1.ax1x.com/2022/06/12/Xgr9u6.jpg") no-repeat top;background-size: cover;background-attachment: fixed;width: 100%;height: 100%;position: fixed;top: 0;left: 0;right: 0;bottom: 0;
}
.wechat {width: 60%;height: 88%;margin: 3% auto;border-radius: 20px;background-color: rgba(245, 237, 237, 0.3);
}
/*聊天框左侧 */
.item {position: relative;width: 94%;height: 50px;margin-bottom: 3%;border-bottom: 1px solid #fff;
}
.item .name {line-height: 50px;float: left;margin-left: 10px;
}
/*聊天框右侧 */.wechat_right {position: relative;width: 100%;height: 100%;
}
.header {text-align: left;height: 50px !important;
}
.showChat {width: 100%;height: 65%;
}
.inputValue {position: relative;margin: 0;padding: 0;width: 100%;height: 50%;
}
.inputValue #chat {font-size: 18px;width: 96%;height: 94%;border-radius: 20px;resize: none;background-color: rgba(245, 237, 237, 0.3);
}
#send {position: absolute;bottom: 12%;right: 6%;
}
/*展示区 */
.leftBox {float: left;max-width: 60%;padding: 8px;position: relative;font-size: 18px;border-radius: 12px;background-color: rgba(40, 208, 250, 0.76);
}
.rightBox {float: right;max-width: 60%;padding: 8px;font-size: 18px;border-radius: 12px;position: relative;background-color: rgba(101, 240, 21, 0.945);
}
.myBr {float: left;width: 100%;height: 20px;
}
.leftBox > span {left: 3px;width: 120px;position: absolute;top: -16px;
}
.rightBox > span {width: 120px;position: absolute;right: 3px;top: -16px;
}
</style>
源代码
源代码
WebSocketServer调用spring容器注意事项
WebSocket启动的时候优先于spring容器,从而导致在WebSocketServer中调用业务Service会报空指针异常
解决方法,静态初始化并提前加载bean
- 静态初始化
//如需要 MessageService
private static MessageService messageService;
- 提前加载bean
@Configuration
public class WebSocketConfig {/*** 注入ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint*/@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}//通过get方法注入bean@Autowiredprotected void getMessageService(MessageService ms){WebSocketServer.messageService=ms;}
}
扩展
- 群发实现
- 多人聊天实现
- 语音、视屏聊天实现
相信前两个大家如果看明白上面的demo应该能做
- 给消息设置一个状态,后端服务器接收到消息是群发之后,就将消息发送给所有的在线用户,不在线的先存数据库(或者维护一个uid数组,这样更灵活)
- 定义一个群ID,将消息发送至群内的所有人
- 这个我建议自己查查看
使用netty实现了上面一模一样的功能
netty+springboot+vue聊天室(需要了解netty)