一套简单的web即时通讯——第二版
前言
接上一版,这一版的页面与功能都有所优化,具体如下:
1、优化登录拦截
2、登录后获取所有好友并区分显示在线、离线好友,好友上线、下线都有标记
3、将前后端交互的值改成用户id、显示值改成昵称nickName
4、聊天消息存储,点击好友聊天,先追加聊天记录
5、登录后获取所有未读消息并以小圆点的形式展示
6、搜索好友、添加好友
优化细节
1、登录拦截由之前的通过路径中获取账号,判断WebSocketServer.loginList中是否存在key改成登录的时候设置cookie,登录拦截从cookie中取值
登录、登出的时候设置、删除cookie,
/** * 登录 */ @PostMapping("login") public Result<ImsUserVo> login(ImsUserVo userVo, HttpServletResponse response) { //加密后再去对比密文 userVo.setPassword(MD5Util.getMD5(userVo.getPassword())); Result<List<ImsUserVo>> result = list(userVo); if (result.isFlag() && result.getData().size() > 0) { ImsUserVo imsUserVo = result.getData().get(0); //置空隐私信息 imsUserVo.setPassword(null); //add WebSocketServer.loginList WebSocketServer.loginList.put(imsUserVo.getUserName(), imsUserVo); //设置cookie Cookie cookie = new Cookie("imsLoginToken", imsUserVo.getUserName()); cookie.setMaxAge(60 * 30); //设置域 // cookie.setDomain("huanzi.cn"); //设置访问路径 cookie.setPath("/"); response.addCookie(cookie); return Result.of(imsUserVo); } else { return Result.of(null, false, "账号或密码错误!"); } } /** * 登出 */ @RequestMapping("logout/{username}") public ModelAndView loginOut(HttpServletResponse response, @PathVariable String username) { new WebSocketServer().deleteUserByUsername(username,response); return new ModelAndView("login.html"); }
改成关闭websocket时不做操作,仅减减socket连接数
/** * 连接关闭调用的方法 */ @OnClose public void onClose(Session session) { //下线用户名 String logoutUserName = ""; //从webSocketMap删除下线用户 for (Entry<String, Session> entry : sessionMap.entrySet()) { if (entry.getValue() == session) { sessionMap.remove(entry.getKey()); logoutUserName = entry.getKey(); break; } } deleteUserByUsername(logoutUserName,null); } /** 用户下线 */ public void deleteUserByUsername(String username, HttpServletResponse response){ //在线人数减减 WebSocketServer.onlineCount--; if(WebSocketServer.onlineCount <= 0){ WebSocketServer.onlineCount = 0; } if(StringUtils.isEmpty(response)){ return; } //用户集合delete WebSocketServer.loginList.remove(username); //删除cookie 思路就是替换原来的cookie,并设置它的生存时间为0 //设置cookie Cookie cookie = new Cookie("imsLoginToken", username); cookie.setMaxAge(0); //设置域 // cookie.setDomain("huanzi.cn"); //设置访问路径 cookie.setPath("/"); response.addCookie(cookie); //通知除了自己之外的所有人 sendOnlineCount(username, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}"); }
在登录拦截器中从cookie取用户账户
//其实存的是用户账号 String imsLoginToken = ""; Cookie[] cookies = request.getCookies(); if (null != cookies) { for (Cookie cookie : cookies) { if ("imsLoginToken".equals(cookie.getName())) { imsLoginToken = cookie.getValue(); } } } if(WebSocketServer.loginList.containsKey(imsLoginToken)){ //正常处理请求 filterChain.doFilter(servletRequest, servletResponse); }else{ //重定向登录页面 response.sendRedirect("/imsUser/loginPage.html"); }
2、登录之后的用户列表不再是显示websocket连接的用户,而是登录用户的好友,同时要区分显示好友的在线与离线,所以新增一个获取在线好友的接口
/** * 获取在线好友 */ @PostMapping("getOnlineList") private Result<List<ImsUserVo>> getOnlineList(ImsFriendVo imsFriendVo) { return imsFriendService.getOnlineList(imsFriendVo); } /** * 获取在线好友 */ @Override public Result<List<ImsUserVo>> getOnlineList(ImsFriendVo imsFriendVo) { //好友列表 List<ImsFriendVo> friendList = list(imsFriendVo).getData(); //在线好友列表 ArrayList<ImsUserVo> onlineFriendList = new ArrayList<>(); //遍历friendList for(ImsFriendVo imsFriendVo1 : friendList){ ImsUserVo imsUserVo = imsFriendVo1.getUser(); if (!StringUtils.isEmpty(WebSocketServer.getSessionMap().get(imsUserVo.getId().toString()))) { onlineFriendList.add(imsUserVo); } } return Result.of(onlineFriendList); }
//连接成功建立的回调方法 websocket.onopen = function () { //获取好友列表 // $.post(ctx + "/imsFriend/list",{userId: username},function (data) { // console.log(data) // }); $.ajax({ type: 'post', url: ctx + "/imsFriend/list", contentType: 'application/x-www-form-urlencoded; charset=UTF-8', dataType: 'json', data: {userId: user.id}, success: function (data) { if (data.flag) { //列表 let friends = data.data; for (let i = 0; i < friends.length; i++) { let friend = friends[i].user; let $friendGroupList = $("<div class=\"hz-group-list\">" + "<img class='left' style='width: 23px;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" + "<span class='hz-group-list-username'>" + friend.nickName + "</span><span id=\"" + friend.id + "-status\" style='color: #9c0c0c;;'>[离线]</span>" + "<div id=\"hz-badge-" + friend.id + "\" class='hz-badge'>0</div>" + "</div>"); $friendGroupList.user = friend; $("#hz-group-body").append($friendGroupList); } //好友人数 $("#friendCount").text(friends.length); getOnlineList(user.id); } }, error: function (xhr, status, error) { console.log("ajax错误!"); } }); }; /** * 获取在线好友 */ function getOnlineList(userId){ $.ajax({ type: 'post', url: ctx + "/imsFriend/getOnlineList", contentType: 'application/x-www-form-urlencoded; charset=UTF-8', dataType: 'json', data: {userId: userId}, success: function (data) { if (data.flag) { //列表 let onlineFriends = data.data; for (let i = 0; i < onlineFriends.length; i++) { let friend = onlineFriends[i]; $("#" + friend.id + "-status").text("[在线]"); $("#" + friend.id + "-status").css("color", "#497b0f"); } //好友人数 $("#onlineCount").text(onlineFriends.length); } }, error: function (xhr, status, error) { console.log("ajax错误!"); } }); }
3、将之前前后端传递用户账户username改成用户id,同时,显示的是nickName昵称,改动的地方比较多,我就不贴代码了
4、消息存储
后端存储关键代码
/** * 服务器接收到客户端消息时调用的方法 */ @OnMessage public void onMessage(String message, Session session) { try { //JSON字符串转 HashMap HashMap hashMap = new ObjectMapper().readValue(message, HashMap.class); //消息类型 String type = (String) hashMap.get("type"); //来源用户 Map srcUser = (Map) hashMap.get("srcUser"); //目标用户 Map tarUser = (Map) hashMap.get("tarUser"); //如果点击的是自己,那就是群聊 if (srcUser.get("userId").equals(tarUser.get("userId"))) { //群聊 groupChat(session,hashMap); } else { //私聊 privateChat(session, tarUser, hashMap); } //后期要做消息持久化 ImsFriendMessageVo imsFriendMessageVo = new ImsFriendMessageVo(); imsFriendMessageVo.setToUserId((Integer) tarUser.get("userId")); imsFriendMessageVo.setFromUserId((Integer) srcUser.get("userId")); //聊天内容 imsFriendMessageVo.setContent(hashMap.get("message").toString()); try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); imsFriendMessageVo.setCreatedTime(simpleDateFormat.parse(hashMap.get("date").toString())); imsFriendMessageVo.setUpdataTime(simpleDateFormat.parse(hashMap.get("date").toString())); } catch (ParseException e) { e.printStackTrace(); } imsFriendMessageService.save(imsFriendMessageVo); } catch (IOException e) { e.printStackTrace(); } }
前端点击好友时,获取聊天记录关键代码
//读取聊天记录 $.post(ctx + "/imsFriendMessage/getChattingRecords", { fromUserId: userId, toUserId: toUserId }, function (data) { if (data.flag) { for (let i = 0; i < data.data.length; i++) { let msgObj = data.data[i]; //当聊天窗口与msgUserName的人相同,文字在左边(对方/其他人),否则在右边(自己) if (msgObj.fromUserId === userId) { //追加聊天数据 setMessageInnerHTML({ id: msgObj.id, isRead: msgObj.isRead, toUserId: msgObj.toUserId, fromUserId: msgObj.fromUserId, message: msgObj.content, date: msgObj.createdTime }); } else { //追加聊天数据 setMessageInnerHTML({ id: msgObj.id, isRead: msgObj.isRead, toUserId: msgObj.fromUserId, message: msgObj.content, date: msgObj.createdTime }); } } } });
/** * 获取A-B的聊天记录 */ @RequestMapping("getChattingRecords") public Result<List<ImsFriendMessageVo>> getChattingRecords(ImsFriendMessageVo imsFriendMessageVo){ return imsFriendMessageService.getChattingRecords(imsFriendMessageVo); } @Override public Result<List<ImsFriendMessageVo>> getChattingRecords(ImsFriendMessageVo imsFriendMessageVo) { //A对B的聊天记录 List<ImsFriendMessageVo> allList = new ArrayList<>(super.list(imsFriendMessageVo).getData()); Integer fromUserId = imsFriendMessageVo.getFromUserId(); imsFriendMessageVo.setFromUserId(imsFriendMessageVo.getToUserId()); imsFriendMessageVo.setToUserId(fromUserId); //B对A的聊天记录 allList.addAll(super.list(imsFriendMessageVo).getData()); //默认按时间排序 allList.sort(Comparator.comparingLong(vo -> vo.getCreatedTime().getTime())); return Result.of(allList); }
5、登录后获取所有未读消息并以小圆点的形式展示
登录成功后获取与好友的未读消息关键代码,在获取好友列表之后调用
//获取未读消息 $.post(ctx + "/imsFriendMessage/list",{toUserId:userId,isRead:0},function(data){ if(data.flag){ let friends = {}; //将fromUser合并 for (let i = 0; i < data.data.length; i++) { let fromUser = data.data[i]; if(!friends[fromUser.fromUserId]){ friends[fromUser.fromUserId] = {}; friends[fromUser.fromUserId].count = 1; }else{ friends[fromUser.fromUserId].count = friends[fromUser.fromUserId].count + 1; } } for (let key in friends) { let fromUser = friends[key]; //小圆点++ $("#hz-badge-" + key).text(fromUser.count); $("#hz-badge-" + key).css("opacity", "1"); } } });
6、搜索好友、添加好友
可按照账号、昵称进行搜索,其中账号是等值查询,昵称是模糊查询
关键代码
//搜索好友 function findUserByUserNameOrNickName() { let userNameOrNickName = $("#userNameOrNickName").val(); if (!userNameOrNickName) { tip.msg("账号/昵称不能为空"); return; } $.post(ctx + "/imsUser/findUserByUserNameOrNickName", { userName: userNameOrNickName, nickName: userNameOrNickName, }, function (data) { if (data.flag) { $("#friendList").empty(); for (let i = 0; i < data.data.length; i++) { let user = data.data[i]; let $userDiv = $("<div>" + "<img style='width: 23px;margin: 0 5px 0 0;' src='" + user.avatar + "'/>" + "<span>" + user.nickName + "(" + user.userName + ")</span>" + "<button onclick='tipUserInfo($(this).parent()[0].user)'>用户详情</button>" + "<button onclick=''>加好友</button>" + "</div>"); $userDiv[0].user = user; $("#friendList").append($userDiv); } } }); }
/** * 根据账号或昵称(模糊查询)查询 */ @PostMapping("findUserByUserNameOrNickName") public Result<List<ImsUserVo>> findUserByUserNameOrNickName(ImsUserVo userVo) { return imsUserService.findUserByUserNameOrNickName(userVo); } @Override public Result<List<ImsUserVo>> findUserByUserNameOrNickName(ImsUserVo userVo) { return Result.of(CopyUtil.copyList(imsUserRepository.findUserByUserNameOrNickName(userVo.getUserName(), userVo.getNickName()), ImsUserVo.class)); } @Query(value = "select * from ims_user where user_name = :userName or nick_name like %:nickName%",nativeQuery = true) List<ImsUser> findUserByUserNameOrNickName(@Param("userName") String userName,@Param("nickName") String nickName);
添加好友
首先要修改ims_friend结构,SQL如下,添加了一个字段is_agree,是否已经同意好友申请 0已申请但未同意 1同意 -1拒绝,之前查询好友列表的post请求则需要新增参数isAgree=1
/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50528 Source Host : localhost:3306 Source Schema : test Target Server Type : MySQL Target Server Version : 50528 File Encoding : 65001 Date: 14/05/2019 17:25:35 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for ims_friend -- ---------------------------- DROP TABLE IF EXISTS `ims_friend`; CREATE TABLE `ims_friend` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `user_id` int(11) NULL DEFAULT NULL COMMENT '用户id', `friend_id` int(11) NULL DEFAULT NULL COMMENT '好友id', `friend_type` int(11) NULL DEFAULT NULL COMMENT '好友分组id', `friend_remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '好友备注', `is_agree` int(1) NULL DEFAULT NULL COMMENT '是否已经同意好友申请 0已申请但未同意 1同意 -1拒绝', `created_time` datetime NULL DEFAULT NULL COMMENT '创建时间', `updata_time` datetime NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '好友表' ROW_FORMAT = Compact; SET FOREIGN_KEY_CHECKS = 1;
在工具栏新加一个系统消息,贴出对应关键代码
//监听单击系统消息,弹出窗口 $("body").on("click", "#sysNotification", function () { //此处为单击事件要执行的代码 if ($(".sysNotification").length <= 0) { tip.dialog({ title: "系统消息", class: "sysNotification", content: "<div></div>", shade: 0 }); } else { $(".sysNotification").click(); } $("#sysNotification").find(".hz-badge").css("opacity",0); $("#sysNotification").find(".hz-badge").text(0); //已拒绝 //申请好友 $.post(ctx + "/imsFriend/list", { friendId: userId, isAgree: 0, }, function (data) { if (data.flag) { for (let i = 0; i < data.data.length; i++) { let user = data.data[i].user; let $userDiv = $("<div>" + "<img style='width: 23px;margin: 0 5px 0 0;' src='" + user.avatar + "'/>" + "<span>" + user.nickName + "(" + user.userName + ")</span> 申请添加好友<br/>" + "<button onclick='tipUserInfo($(this).parent()[0].user)'>用户详情</button>" + "<button onclick='agreeAddFriend(" + data.data[i].id + ")'>同意</button>" + "</div>"); $userDiv[0].user = user; $(".sysNotification .tip-content").append($userDiv); } } }); }); //申请添加好友 function applyToAddFriend(friendUserId) { let nowTime = commonUtil.getNowTime(); $.post(ctx + "/imsFriend/save", { userId: userId, friendId: friendUserId, friendType: 1, friendRemark: "", isAgree: 0, createdTime: nowTime, updataTime: nowTime, }, function (data) { if (data.flag) { tip.msg({text:"已为你递交好友申请,对方同意好即可成为好友!",time:3000}); } }); } //同意好友添加 function agreeAddFriend(id){ let nowTime = commonUtil.getNowTime(); $.post(ctx + "/imsFriend/save", { id:id, isAgree: 1, updataTime: nowTime, }, function (data) { if (data.flag) { $.post(ctx + "/imsFriend/save", { userId: data.data.friendId, friendId: data.data.userId, friendType: 1, friendRemark: "", isAgree: 1, createdTime: nowTime, updataTime: nowTime, }, function (data) { if (data.flag) { tip.msg({text:"你们已经是好友了,可以开始聊天!",time:2000}); } }); } }); } //获取我的申请好友,并做小圆点提示 function getApplyFriend(userId){ $.post(ctx + "/imsFriend/list", { friendId: userId, isAgree: 0, }, function (data) { if (data.flag && data.data.length > 0) { $("#sysNotification").find(".hz-badge").css("opacity",1); $("#sysNotification").find(".hz-badge").text(data.data.length); } }); }
在线、离线提示出来小bug...
2019-05-17更新
问题找到了,是因为我们将关联的好友对象属性名改成了
@OneToOne @JoinColumn(name = "friendId",referencedColumnName = "id", insertable = false, updatable = false) @NotFound(action= NotFoundAction.IGNORE) private ImsUser friendUser;//好友
但在获取在线好友那里还是,getUser();,导致数据错乱,bug修改:改成getFriendUser();即可
/** * 获取在线好友 */ @Override public Result<List<ImsUserVo>> getOnlineList(ImsFriendVo imsFriendVo) { imsFriendVo.setIsAgree(1); //好友列表 List<ImsFriendVo> friendList = list(imsFriendVo).getData(); //在线好友列表 ArrayList<ImsUserVo> onlineFriendList = new ArrayList<>(); //遍历friendList for(ImsFriendVo imsFriendVo1 : friendList){ ImsUserVo imsUserVo = imsFriendVo1.getUser(); if (!StringUtils.isEmpty(WebSocketServer.getSessionMap().get(imsUserVo.getId().toString()))) { onlineFriendList.add(imsUserVo); } } return Result.of(onlineFriendList); }
后记
第二版暂时记录到这,第三版持续更新中...
2019-06-18补充:HashMap不支持并发操作,线程不安全,ConcurrentHashMap支持并发操作线程安全,因此,我们应该用后者,而不是前者,今天在这里补充一下,就不再其他地方做补充说明了
PS:ConcurrentHashMap是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
版权声明
捐献、打赏
支付宝
微信