一套简单的web即时通讯——第二版
发布日期:2021-05-20 17:28:27 浏览次数:21 分类:博客文章

本文共 18637 字,大约阅读时间需要 62 分钟。

  前言

  接上一版,这一版的页面与功能都有所优化,具体如下:

  1、优化登录拦截

  2、登录后获取所有好友并区分显示在线、离线好友,好友上线、下线都有标记

  3、将前后端交互的值改成用户id、显示值改成昵称nickName

  4、聊天消息存储,点击好友聊天,先追加聊天记录

  5、登录后获取所有未读消息并以小圆点的形式展示

  6、搜索好友、添加好友

 

  优化细节

    1、登录拦截由之前的通过路径中获取账号,判断WebSocketServer.loginList中是否存在key改成登录的时候设置cookie,登录拦截从cookie中取值

  登录、登出的时候设置、删除cookie,

/**     * 登录     */    @PostMapping("login")    public Result
login(ImsUserVo userVo, HttpServletResponse response) { //加密后再去对比密文 userVo.setPassword(MD5Util.getMD5(userVo.getPassword())); Result
> 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"); }
ImsUserController.java

  改成关闭websocket时不做操作,仅减减socket连接数

/**     * 连接关闭调用的方法     */    @OnClose    public void onClose(Session session) {        //下线用户名        String logoutUserName = "";        //从webSocketMap删除下线用户        for (Entry
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 + "'}"); }
WebSocketServer.java

  在登录拦截器中从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");        }
LoginFilter.java

  

  2、登录之后的用户列表不再是显示websocket连接的用户,而是登录用户的好友,同时要区分显示好友的在线与离线,所以新增一个获取在线好友的接口

/**     * 获取在线好友     */    @PostMapping("getOnlineList")    private Result
> getOnlineList(ImsFriendVo imsFriendVo) { return imsFriendService.getOnlineList(imsFriendVo); } /** * 获取在线好友 */ @Override public Result
> getOnlineList(ImsFriendVo imsFriendVo) { //好友列表 List
friendList = list(imsFriendVo).getData(); //在线好友列表 ArrayList
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); }
ImsFriend
//连接成功建立的回调方法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 = $("
" + "
" + "
" + friend.nickName + "
[离线]" + "
0
" + "
"); $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错误!"); } });}
socketChart.js

 

  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();        }    }
WebSocketServer.java

  前端点击好友时,获取聊天记录关键代码

//读取聊天记录    $.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                    });                }            }        }    });
socketChart.js
/**     * 获取A-B的聊天记录     */    @RequestMapping("getChattingRecords")    public Result
> getChattingRecords(ImsFriendMessageVo imsFriendMessageVo){ return imsFriendMessageService.getChattingRecords(imsFriendMessageVo); } @Override public Result
> getChattingRecords(ImsFriendMessageVo imsFriendMessageVo) { //A对B的聊天记录 List
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); }
ImsFriendMessage

 

 

  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");                        }                    }                });
socketChart.js

 

  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 = $("
" + "
" + "
" + user.nickName + "(" + user.userName + ")" + "
" + "
" + "
"); $userDiv[0].user = user; $("#friendList").append($userDiv); } } });}
socketChart.js
/**     * 根据账号或昵称(模糊查询)查询     */    @PostMapping("findUserByUserNameOrNickName")    public Result
> findUserByUserNameOrNickName(ImsUserVo userVo) { return imsUserService.findUserByUserNameOrNickName(userVo); } @Override public Result
> 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
findUserByUserNameOrNickName(@Param("userName") String userName,@Param("nickName") String nickName);
ImsUser

   添加好友

  首先要修改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;
View Code

  在工具栏新加一个系统消息,贴出对应关键代码

//监听单击系统消息,弹出窗口$("body").on("click", "#sysNotification", function () {    //此处为单击事件要执行的代码    if ($(".sysNotification").length <= 0) {        tip.dialog({            title: "系统消息",            class: "sysNotification",            content: "
", 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 = $("
" + "
" + "
" + user.nickName + "(" + user.userName + ") 申请添加好友
" + "
" + "
" + "
"); $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); } });}
socketChart.js

  在线、离线提示出来小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
> getOnlineList(ImsFriendVo imsFriendVo) { imsFriendVo.setIsAgree(1); //好友列表 List
friendList = list(imsFriendVo).getData(); //在线好友列表 ArrayList
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 是线程安全的,也就实现了全局的线程安全。

上一篇:WebJar的打包和使用
下一篇:一套简单的web即时通讯——第一版

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2025年05月09日 09时12分23秒