介绍:接下来我会把学习阶段学到的框架等知识点进行整合,每一次整合是在前一章的基础上进行的,所以后面的整合不会重复放前面的代码。每次的demo我放在结尾,本次是接着上一章的内容延续的,只增加新增的或者修改的代码。
上一章是初步整合websocket之后实现了一个进度条的小demo,这次使用websocket实现聊天室功能,实现多个用户在线聊天以及私聊。
首先我先展示一下效果图(随便弄的,界面比较丑陋):
我再展示一下我的目录结构(前端我用的HbuilderX):
前端就一个html文件,引用的线上的jquery和bootstrap,所以我没有写样式文件。
后端相比上一章没有新增文件。只是进行了WebSocket文件的改造,websocket的依赖不需要导了,上一章已经导入了。
第一步:改造WebSocket
package com.swagger.demo.config; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.*; import java.util.concurrent.CopyOnWriteArraySet; /** * @ClassName WebSocketConfig * @Description TODO * @Author zrc * @Date 11:01 * @Version 1.0 **/ //注册成组件 @Component //定义websocket服务器端,它的功能主要是将目前的类定义成一个websocket服务器端。注解的值将被用于监听用户连接的终端访问URL地址 @ServerEndpoint("/websocket/{username}") //如果不想每次都写private final Logger logger = LoggerFactory.getLogger(当前类名.class); 可以用注解@Slf4j;可以直接调用log.info @Slf4j public class WebSocket { //实例一个session,这个session是websocket的session private Session session; //存放当前用户名 private String userName; //存放需要接受消息的用户名 private String toUserName; //存放在线的用户数量 private static Integer userNumber = 0; //存放websocket的集合(本次demo不会用到,聊天室的demo会用到) private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>(); //前端请求时一个websocket时 @OnOpen public void onOpen(Session session,@PathParam("username") String username) throws IOException { this.session = session; //将当前对象放入webSocketSet webSocketSet.add(this); //增加在线人数 userNumber++; //保存当前用户名 this.userName = username; //获得所有的用户 Set<String> userLists = new TreeSet<>(); for (WebSocket webSocket : webSocketSet) { userLists.add(webSocket.userName); } //将所有信息包装好传到客户端(给所有用户) Map<String, Object> map1 = new HashMap(); // 把所有用户列表 map1.put("onlineUsers", userLists); //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息 map1.put("messageType", 1); // 返回用户名 map1.put("username", username); // 返回在线人数 map1.put("number", this.userNumber); //发送给所有用户谁上线了,并让他们更新自己的用户菜单 sendMessageAll(JSON.toJSONString(map1),this.userName); log.info("【websocket消息】有新的连接, 总数:{}", this.userNumber); // 更新在线人数(给所有人) Map<String, Object> map2 = new HashMap(); //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息 map2.put("messageType", 3); //把所有用户放入map2 map2.put("onlineUsers", userLists); //返回在线人数 map2.put("number", this.userNumber); // 消息发送指定人(所有的在线用户信息) sendMessageAll(JSON.toJSONString(map2),this.userName); } //前端关闭时一个websocket时 @OnClose public void onClose() throws IOException { //从集合中移除当前对象 webSocketSet.remove(this); //在线用户数减少 userNumber--; Map<String, Object> map1 = new HashMap(); //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息 map1.put("messageType", 2); //所有在线用户 map1.put("onlineUsers", this.webSocketSet); //下线用户的用户名 map1.put("username", this.userName); //返回在线人数 map1.put("number", userNumber); //发送信息,所有人,通知谁下线了 sendMessageAll(JSON.toJSONString(map1),this.userName); log.info("【websocket消息】连接断开, 总数:{}", webSocketSet.size()); } //前端向后端发送消息 @OnMessage public void onMessage(String message) throws IOException { log.info("【websocket消息】收到客户端发来的消息:{}", message); //将前端传来的数据进行转型 com.alibaba.fastjson.JSONObject jsonObject = JSON.parseObject(message); //获取所有数据 String textMessage = jsonObject.getString("message"); String username = jsonObject.getString("username"); String type = jsonObject.getString("type"); String tousername = jsonObject.getString("tousername"); //群发 if(type.equals("群发")){ Map<String, Object> map3 = new HashMap(); map3.put("messageType", 4); //所有在线用户 map3.put("onlineUsers", this.webSocketSet); //发送消息的用户名 map3.put("username", username); //返回在线人数 map3.put("number", userNumber); //发送的消息 map3.put("textMessage", textMessage); //发送信息,所有人,通知谁下线了 sendMessageAll(JSON.toJSONString(map3),this.userName); } //私发 else{ //发送给对应的私聊用户 Map<String, Object> map3 = new HashMap(); map3.put("messageType", 4); //所有在线用户 map3.put("onlineUsers", this.webSocketSet); //发送消息的用户名 map3.put("username", username); //返回在线人数 map3.put("number", userNumber); //发送的消息 map3.put("textMessage", textMessage); //发送信息,所有人,通知谁下线了 sendMessageTo(JSON.toJSONString(map3),tousername); //发送给自己 Map<String, Object> map4 = new HashMap(); map4.put("messageType", 4); //所有在线用户 map4.put("onlineUsers", this.webSocketSet); //发送消息的用户名 map4.put("username", username); //返回在线人数 map4.put("number", userNumber); //发送的消息 map4.put("textMessage", textMessage); //发送信息,所有人,通知谁下线了 sendMessageTo(JSON.toJSONString(map3),username); } } /** * 消息发送所有人 */ public void sendMessageAll(String message, String FromUserName) throws IOException { for (WebSocket webSocket: webSocketSet) { //消息发送所有人(同步)getAsyncRemote webSocket.session.getBasicRemote().sendText(message); } } /** * 消息发送指定人 */ public void sendMessageTo(String message, String ToUserName) throws IOException { //遍历所有用户 for (WebSocket webSocket : webSocketSet) { if (webSocket.userName.equals(ToUserName)) { //消息发送指定人 webSocket.session.getBasicRemote().sendText(message); log.info("【发送消息】:", this.userName+"向"+ToUserName+"发送消息:"+message); break; } } } }
下面,我逐步介绍:
1、定义全局变量
首先定义几个全局变量:
- username用于保存当前的websocket连接传过来的用户名,作为websocket的唯一标识,后面私聊时通过这个唯一标识发送消息给对应的websocket连接。
- userNumber用于保存更新在线用户数,每次新增或断开一个websocket连接之后,更新该在线用户数。
- webSocketSet用于保存在线的所有websocket对象,是个websocket对象的集合,使用Set集合,保证不会出现重复的对象,后面私发或群发时通过遍历该对象,将消息发送给对应的对象。
2、新增两个发送消息的方法
sendMessageAll是将消息发送给全部websocket对象,从上面可以看到,遍历websocket集合的所有对象,调用websocket的session里面的getBasicRemote的sendText方法发送传入的message消息。
sendMessageTo是将消息发送给指定的websocket对象,从上面可以看到,遍历websocket集合的所有对象,当用户名满足传入的接受用户时,调用websocket的session里面的getBasicRemote的sendText方法发送传入的message消息。
3、改造onOpen
@PathParam("username") String username用来接受前端传入的用户名,注意在ServerEndpoint注解参数改一下,加上用户名的传输。
在onopen里面增加全局的在线人数以及websocket对象集合,保存当前用户名。
遍历webSocketSet集合,获取出来所有在线用户。
将所有前端需要的信息包装到map,调用sendMessageAll方法,通知所有在线用户,某某用户上线了。
此处为了前端方便处理,调用两次sendMessageAll方法,type如上图所示,分别两次sendMessageAll方法处理上线通知和用户列表更新通知。
4、改造onClose
该方法是连接关闭时触发,所以将当前websocket对象从websocket的集合从移除并减少在线用户数,将前端需要的数据包装好后调用sendMessageAll方法,通知前端某某用户下线了。
5、改造onMessage
onMessage方法是接受前端传来数据时触发。
通过JSON.parseObject方法解析前端传过来的数据。
获取message里面的键值对数据
判断是群发还是私发然后调用不同的方法。
此处私发时需要发送给对应的websocket对象还需要发送给自己。
后端的改造到此结束,下面介绍前端的代码。
第二步:创建前端html文件
<!DOCTYPE HTML> <html> <head> <title>聊天室</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script> </head> <body style="padding-left: 200px;padding-right: 200px;"> <div class="alert alert-primary" style="margin-bottom: -20px;text-align: center;" role="alert">聊天室</div> <div class="jumbotron" style="height: 500px;width: 1135px;"> <!-- 用户列表 --> <div class="jumbotron" style="height: 500px;width: 200px;float: right;background-color: green;margin-top: -65px;margin-right: -30px;"> <div class="alert alert-primary" role="alert" style="width: 200px;margin-top: -43px;margin-left: -32px;">在线用户列表:</div> <ul class="list-group" id="userlist" style="width: 200px;margin-top: -18px;margin-left: -32px;"> </ul> </div> <div id="content" style="height: 500px;width: 900px;background-color: white;margin-top: -45px;margin-left: -20px;"> </div> </div> <div class="form-group"> <input type="text" style="width: 90%;;float: left;" class="form-control" id="message" placeholder="请输入内容"> <button type="button" style="width: 10%;float: left;" class="fasong">发送</button> <button type="button" style="width: 10%;float: left;" class="qingping">清屏</button> </div> </body> <script type="text/javascript"> //私发全局消息 var tomessage = ""; //私发用户名称 var tousername = ""; //清屏 $(".qingping").click(function(){ $("#content").html(``); }) //生成一个随机用户名 var username = "用户"+Math.random()*100; //定义一个websocket var websocket = null; //判断当前浏览器是否支持WebSocket(固定写法) if('WebSocket' in window){ websocket = new WebSocket("ws://localhost:8090/websocket/"+username); }else{ alert('浏览器不支持websocket') } //连接发生错误的回调方法 websocket.onerror = function(){ console.log("发生错误"); }; //连接成功建立的回调方法 websocket.onopen = function(event){ console.log("建立连接"+"event"); } //接收到消息的回调方法 websocket.onmessage = function(event){ var data = JSON.parse(event.data); console.log(JSON.parse(event.data)) //更新content的内容(上线) if(data.messageType=="1"){ // console.log("成功") $("#content").append(`<span style="width: 100%;height: 30px;line-height: 30px;font-size: 14px;">`+data.username+"上线了"+`</span><br>`); } //更新content的内容(下线) if(data.messageType=="2"){ // console.log("成功") $("#content").append(`<span style="width: 100%;height: 30px;line-height: 30px;font-size: 14px;">`+data.username+"下线了"+`</span><br>`); } //更新content的内容(更新用户列表) if(data.messageType=="3"){ //先清空 $("#userlist").html(``); var list = data.onlineUsers; console.log(list) for(var i=0;i<list.length;i++){ $("#userlist").append(`<li style="cursor:pointer;" class="list-group-item" onclick="friend(this)" values="`+list[i]+`">`+list[i]+`</li>`); } } //更新content的内容(更新用户群发消息) if(data.messageType=="4"){ // console.log(data); $("#content").append(`<span style="width: 100%;height: 30px;line-height: 30px;font-size: 14px;">`+data.username+`说:`+data.textMessage+`</span><br>`); } } //选择用户 function friend(e){ console.log(e); $("#message").val("@ "+e.innerHTML+" "); var data = e.innerHTML; console.log(data); tousername = data; } //连接关闭的回调方法 websocket.onclose = function(){ console.log("关闭连接"); } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function(){ alert("已关闭连接"); websocket.close(); } //发送按钮 $(".fasong").click(function(){ console.log("发送消息") var message = $("#message").val(); //判断是群发还是私发 console.log(message); //获取发送对象 tousername = message.split(' ')[1]; //获取发送消息 tomessage = message.split(' ')[2]; console.log(tomessage); console.log(tousername); if(message.indexOf("@")!=-1){ //私发 console.log("私发") var param = {}; param['username'] = username; param['message'] = tomessage; param['type'] = '私发'; param['tousername'] = tousername; } else{ //群发 console.log("群发") var param = {}; param['username'] = username; param['message'] = message; param['type'] = '群发'; param['tousername'] = ''; } //发送消息到后端 websocket.send(JSON.stringify(param)); }) </script> </html>
下面介绍各部分代码:
1、html代码
这部分代码没什么说的,我为了方便直接使用的bootstrap框架 。
2、js代码
为了简便,又为了每个websocket的用户名唯一,我直接使用的随机数,在新建websocket实例里面,新增一个username参数的传递。
接收到消息的回调方法,先使用JSON.parse()方法解析一下数据格式,再通过判断传来的数据类型type,分别进行处理。
新增一个用户列表点击功能,点击时,将输入框显示如下,便于私聊:
发送按钮,获取要发送的数据,将数据包装好,调用websocket的send方法发送到后端,后端通过OnMessage注解的方法进行处理。
完毕!
第三步:演示
此处我跑了四个websocket实例,如下:
可见,通知上线实现正常,接下来关闭实例4,如下:
下线通知正常,下面展示发送消息
可以看到群发成功,点击用户列表的某一用户,展示私发。
可以看到,私发只有选择的那个人可以收到消息。
到此,整合完毕。
本期整合到此完毕,接下来会继续更新加强整合,尽情期待。
客户端访问地址:http://localhost:8090/swagger-ui.html或者http://localhost:8090/doc.html
修改(2021/10/9)
上面的代码忘记实现用户下线时更新用户列表
修改一下onClose注解注解的onClose方法,如下:
//前端关闭时一个websocket时 @OnClose public void onClose() throws IOException { //从集合中移除当前对象 webSocketSet.remove(this); //在线用户数减少 userNumber--; //通知下线 Map<String, Object> map1 = new HashMap(); //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息 map1.put("messageType", 2); //所有在线用户 map1.put("onlineUsers", this.webSocketSet); //下线用户的用户名 map1.put("username", this.userName); //返回在线人数 map1.put("number", userNumber); //发送信息,所有人,通知谁下线了 sendMessageAll(JSON.toJSONString(map1),this.userName); //通知修改用户列表 // 更新在线人数(给所有人) Map<String, Object> map2 = new HashMap(); //获得所有的用户 Set<String> userLists = new TreeSet<>(); for (WebSocket webSocket : webSocketSet) { userLists.add(webSocket.userName); } //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息 map2.put("messageType", 3); //把所有用户放入map2 map2.put("onlineUsers", userLists); //返回在线人数 map2.put("number", this.userNumber); // 消息发送指定人(所有的在线用户信息) sendMessageAll(JSON.toJSONString(map2),this.userName); log.info("【websocket消息】连接断开, 总数:{}", webSocketSet.size()); }
新增部分代码如下: