zoukankan      html  css  js  c++  java
  • 一套简单的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");
        }
    
    ImsUserController.java

    改成关闭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 + "'}");
        }
    
    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<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);
        }
    
    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 = $("<div class="hz-group-list">" +
                                        "<img class='left' style=' 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错误!");
            }
        });
    }
    
    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<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);
        }
    
    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 = $("<div>" +
                        "<img style=' 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);
                }
            }
        });
    }
    
    socketChart.js
    /**
         * 根据账号或昵称(模糊查询)查询
         */
        @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);
    
    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;

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

    //监听单击系统消息,弹出窗口
    $("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=' 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);
            }
        });
    }
    
    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<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 是线程安全的,也就实现了全局的线程安全。

    版权声明

    作者:huanzi-qch
    若标题中有“转载”字样,则本文版权归原作者所有。若无转载字样,本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利.
  • 相关阅读:
    PHP中无限分类、无限回复评论盖楼的实现方法,thinkphp5.0无限分类实例
    PHP中session详解
    使用thinkPHP做注册程序的实例
    虾米盒子系统开发APP
    angular 使用base64密码加密
    开发中遇到的两种表格文本长度处理,即长文本截断
    树组件使用文件夹图标
    angular实现指定DIV全屏
    JS调用浏览器打印机
    使用blob二进制流的方式下载后台文件
  • 原文地址:https://www.cnblogs.com/Im-Victor/p/15157128.html
Copyright © 2011-2022 走看看