websocket 群聊和单聊实现简单在线客服,前后端分离环境以及遇到的坑

根据菜鸟教程上的解释:

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

需要进行一个简单的在线客服和留言,最简单的办法是ajax定时器轮询,虽然访问量不大,这用方法也简单可靠,但这种方法消耗资源(不主动释放)以及可扩展性不高,所以选择websocket

效果图:前端是vue,后端是ssm,这里展示后端对后端,原理一样,如果是前后端分离环境,复制粘贴即可(如果是,则需要设置跨域) ,后面的jsp和css可能难以理解(不是难,有点乱),抓住重点看就行,如果将我给的css和js全部引入则与效果图一致。

解释:后台代码中会有一些像PageData,和pd.put等等,这些都是我项目的辅助工具类,相当于一个map,而不是websocket的东西,这里是多对一,如果需要多对多改变一下思路即可(jsp中to改为动态)

客服端:如果客服未在线也可以看到其聊天信息,如果不点击沟通或回复,点击自己的话默认是群聊,点击为私聊

用户端:默认客服,可以查看自己的聊天记录,如果客服没在线则可以相应给出提示,这里只是简单版本

步骤:

1.项目使用的是ssm 默认已经将spring的mvc什么的已经引入,首先引入相关jar包,在maven仓库中均可搜到

        <!--websocket-->
        <!-- https://mvnrepository.com/artifact/javax.websocket/javax.websocket-api -->
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>1.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>4.1.4.RELEASE</version>
        </dependency>

2.从外往里配置,需要两个配置文件,webscoket走的是session共享机制,所以需要将所有请求附加session(配好就行,两个文件位置随意,根据自己项目结构定义,只要同包即可)

第一个:

@Component
@WebListener
public class RequestListener implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre)  {
        //将所有request请求都携带上httpSession
        HttpSession session = ((HttpServletRequest) sre.getServletRequest()).getSession();
    }
    public RequestListener() {}

    @Override
    public void requestDestroyed(ServletRequestEvent arg0)  {}
}

第二个:

public class HttpSessionConfigurator extends Configurator {
    @Override
    public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response){
        HttpSession httpSession = (HttpSession)request.getHttpSession();
        config.getUserProperties().put(HttpSession.class.getName(),httpSession);
    }

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

3.这里编写前端代码,css和js都是样式,可以根据需要引入

css:

    <%--jquery-ui  让div可以随意拖动--%>
    <link rel="stylesheet" href=".../static/css/jquery-ui.css"/>
    <link rel="stylesheet" href=".../plugins/amaze/css/amazeui.min.css">
    <link rel="stylesheet" href=".../plugins/amaze/css/admin.css">
    <link rel="stylesheet" href=".../plugins/contextjs/css/context.standalone.css">
    <%--layui--%>
    <link rel="stylesheet" href=".../static/layui/css/layui.css">
    <%--layer--%>
    <link rel="stylesheet" href=".../static/layer/mobile/need/layer.css">


    <link rel="stylesheet" href="http://jqueryui.com/resources/demos/style.css">
    <link rel="stylesheet" href=".../static/font/css/font-awesome.min.css">
    <link id="layuicss-skinlayimcss" rel="stylesheet" href="https://robot.rszhang.top/plugins/layui/css/modules/layim/layim.css?v=3.01Pro" media="all">

js:这里是layui的富文本编辑器,简单好用,就是功能较少

    <%--<script src=".../plugins/jquery/jquery-2.1.4.min.js"></script>--%>
    <script src=".../plugins/amaze/js/amazeui.min.js"></script>
    <script src=".../plugins/amaze/js/app.js"></script>
    <%--<script src=".../statics/plugins/layer/layer.js"></script>--%>
    <script src=".../plugins/laypage/laypage.js"></script>
    <script src=".../plugins/contextjs/js/context.js"></script>

    <%--layui富文本编辑器--%>
    <script type="text/javascript" charset="utf-8" src=".../static/ueditor/ueditor.config.js"></script>
    <script type="text/javascript" charset="utf-8" src=".../static/ueditor/ueditor.all.min.js"> </script>
    <script type="text/javascript" charset="utf-8" src=".../static/ueditor/lang/zh-cn/zh-cn.js"></script>

页面:默认jsp

用户端: to可以动态分配,但也业务需求是 1 V N 所以就设置为死的,ws地址可以设置为服务器地址,但会有很多问题(nginx配置,HttpSession等)

<html>
<head>
<title>客户端</title>
</head>    
<style>
        *{
            font-family: 微软雅黑;
        }
        .body{
            width: 818px;
            height: 495px;
            margin-top: 20px;
            /*display: none;*/
            background: #ffffff;
        }
        .title{
            width: 100%;
            height: 45px;
            background: #DC143C;
        }
        .title-left{
            text-align:center;
            width: 640px;
            height: 100%;
            line-height: 45px;
            float:left;
        }
        .title-right{
            width: 176px;
            height: 45px;
            text-align: right;
            line-height: 45px;
            float: left;
        }
        .content{
            width: 100%;
            height: 450px;
        }
        .content-left{
            float: left;
            width:640px;
            height: 100%;
            border: 1px solid #E3E3E3;
        }

        .content-right{
            float: left;
            width:178px;
            height: 100%;
            border-right: 1px solid #E3E3E3;
            border-bottom: 1px solid #E3E3E3;
        }
        .content-left-title{
            width: 100%;
            height: 40px;
            padding: 10px;
        }
        .content-left-body{
            width: 640px;
            height: 260px;
            padding: 5px;
        }
        .content-left-body ul li{
            position: relative;
            font-size: 0;
            margin-bottom: 10px;
            padding-left: 60px;
            min-height: 68px;
        }
        .layim-chat-user {
            position: absolute;
            left: 3px;
        }
        .layim-chat-user img {
            width: 40px;
            height: 40px;
            border-radius: 100%;
        }
        .layim-chat-user cite {
            position: absolute;
            left: 60px;
            top: -2px;
            width: 500px;
            line-height: 24px;
            font-size: 12px;
            white-space: nowrap;
            color: #999;
            text-align: left;
            font-style: normal;
        }
        .layim-chat-user cite i {
            padding-left: 15px;
            font-style: normal;
        }
        .layim-chat-user {
            display: inline-block;
            vertical-align: top;
            font-size: 14px;
        }
        .layim-chat-text {
            position: relative;
            line-height: 22px;
            margin-top: 25px;
            padding: 8px 15px;
            background-color: #e2e2e2;
            border-radius: 3px;
            color: #333;
            word-break: break-all;
            max-width: 462px\9;
        }
        .layim-chat-text, .layim-chat-user {
            display: inline-block;
            vertical-align: top;
            font-size: 14px;
        }
        .layim-chat-text:after {
            content: '';
            position: absolute;
            left: -10px;
            top: 13px;
            width: 0;
            height: 0;
            border-style: solid dashed dashed;
            border-color: #e2e2e2 transparent transparent;
            overflow: hidden;
            border-width: 10px;
        }
        .content-left-body ul .layim-chat-mine {
            text-align: right;
            padding-left: 0;
            padding-right: 60px;
        }
        .layim-chat-mine .layim-chat-user cite {
            left: auto;
            right: 60px;
            text-align: right;
        }
        .layim-chat-mine .layim-chat-user cite i {
            padding-left: 0;
            padding-right: 15px;
        }
        .layim-chat-mine .layim-chat-text {
            margin-left: 0;
            text-align: left;
            background-color: #5FB878;
            color: #fff;
        }
        .layim-chat-mine .layim-chat-text:after {
            left: auto;
            right: -10px;
            border-top-color: #5FB878;
        }
        .layim-chat-mine .layim-chat-user cite {
            left: auto;
            right: 60px;
            text-align: right;
        }

        .content-left-input{
            width: 640px;
            height: 150px;
            border-top: 1px solid #E3E3E3;
            position:relative;
        }
        .input-title{
            width: 640px;
            height: 30px;
            padding: 5px;
            line-height: 15px;
        }
        .input-body{
            width: 640px;
            height: 80px;
        }
        .input-Send{
            width: 640px;
            height: 40px;
            position:absolute; bottom:0;
        }
        .chat-view{
            overflow-x: hidden; overflow-y: auto;
        }
        .chat-view{
            overflow-x: hidden;
        }
        ul,li{
            list-style: none;
        }
        textarea{outline:none;resize:none;}
        .input-core{
            width: 35px;
            height: 25px;
            margin-left: 5px;
            margin-right: 5px;
            float: left;
        }

        /*在线列表css*/
        .one-big{
            width: 177px; height: 50px;
            background: #ffffff;
        }
        .two-big{
            width: 78px;height: 35px; float: left;
        }
        .three-big{
            margin-left: 5px;line-height: 35px;
        }
        .four-big{
            width: 90px;height: 35px; float: right;
        }
        .five-big{
            width: 60px; background: #FF6666; height: 30px;  text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;
        }
        .five-big-one{
            width: 60px; background: #339933; height: 30px;  text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;
        }
        .six-big{
            margin-left: 10px;
        }
        .ur-stitle{
            width: 300px;
            height: 20px;
            border:  1px  solid;
        }
        .layui-layedit {
            border: none;
        }
        /*右上角图标*/
        .picon{
            color: #ffffff; margin-right: 15px; font-size: 18px;
        }
        /*.picon:hover{*/
        /*cursor: pointer;*/
        /*}*/
        .fa{
            cursor: pointer;
        }
    </style>
    <script>
        var layeditIndex;
        var layedit;
        $(function () {
            $( "#draggable" ).draggable();
            $( "#sortable" ).sortable({
                revert: true
            });
            layui.use('layedit', function(){
                layedit = layui.layedit;
                layedit.build('demo1'); //建立编辑器
                layeditIndex = layedit.build('demo1', {
                    height: 65, //设置编辑器高度
                });

            });
        })
    </script>
</head>
<body>
<%--最外层div--%>
<div id="draggable" class="colPer"  style="width: 818px; height: 495px; margin: auto;">
    <%--头部导航条--%>
    <div class="title">
        <%--导航条左半部分--%>
        <div class="title-left">
            <a style="color: #ffffff" id="state" class="stateTitle">正在与客服沟通</a>
        </div><%--导航条左半部分结束--%>
        <div class="title-right">
            <div style="">
                <span class=" picon fa  fa-minus" onclick="narrow()"></span>
                <span class=" picon fa  fa-times-rectangle" onclick="end()"></span>
            </div>
        </div>
    </div><%--头部导航条结束--%>
    <%--主体开始--%>
    <div class="content">
        <%--左边--%>
        <div class="content-left">
            <%--预留功能区域--%>
            <div  class="content-left-title">
                <ul id="" style="width: 100%;">
                    <li class="input-core" id="arr" title="抖一抖">
                        <a class="fa  fa-heartbeat"  style="font-size: 20px;" id="pot"></a>
                    </li>
                    <li class="input-core" title="刷新窗口">
                        <a class="fa fa-refresh" style="font-size: 20px;"></a>
                    </li>
                    <li class="input-core" title="结束会话">
                        <a class="fa  fa-unlink" style="font-size: 20px;"></a>
                    </li>
                    <li class="input-core" title="我的位置">
                        <a class="fa fa-bandcamp" style="font-size: 20px;"></a>
                    </li>
                    <li class="" style=" float: right; margin-right: 5px;" title="清空屏幕">
                        <a class="fa   fa-trash" style="font-size: 20px;" onclick="clearConsole()"></a>
                    </li>
                </ul>
            </div>
            <div class="content-left-body">
                <!-- 聊天区 -->
                <div  class="am-cf admin-main">
                    <div class="admin-content">
                        <div class="am-scrollable-vertical" id="chat-view" style="height:100%; width: 100%;">
                            <ul class="am-comments-list am-comments-list-flip" id="chat">

                            </ul>
                        </div>
                    </div>
                </div>
            </div>
            <div class="content-left-input">
                <%--&lt;%&ndash;输入区域预留功能&ndash;%&gt;--%>
                <%--<div class="input-title">--%>

                <%--</div>--%>
                <div class="input-body">
                    <textarea id="demo1"  style="display: none; border: none;"> </textarea>
                </div>
                <div class="input-Send">
                    <div style="width: 300px; height: 80px; margin-right: 15px; float: right;">
                        <button type="button" class="TableEditor btn btn-default" onclick="sendMessage()" style="float: right; margin-left: 15px; width: 80px; height: 30px; line-height: 15px;background: #DC143C;color: #ffffff">发送</button>
                        <button type="button" class="TableEditor btn btn-default" style="float: right; margin-left: 15px; width: 80px; height: 30px; line-height: 15px">关闭</button>
                    </div>
                </div>
            </div>
        </div>
        <%--右边--%>
        <div  class="content-right">
        </div>
    </div>
</div><%--最外层结束--%>
</body>
<script type="text/javascript">
    var wsServer = null;
    var ws = null;
    //上传到服务器只需将代码换为服务器地址即可
    //websocket是可以传参数的
    wsServer = "ws://localhost:8080/ChatController/1/${userCode}";
    console.log(wsServer)
    ws = new WebSocket(wsServer); //创建WebSocket对象
    // ws = new SockJS(wsServer); //创建WebSocket对象
    //onopen建立链接
    ws.onopen = function (evt) {
        layer.msg("已经建立连接", { offset: 0});
    };
    //onmessage发送消息
    ws.onmessage = function (evt) {
        analysisMessage(evt.data);  //解析后台传回的消息,并予以展示
    };
    //onerror产生异常
    ws.onerror = function (evt) {
        layer.msg("产生异常", { offset: 0});
    };
    //关闭连接时
    ws.onclose = function (evt) {
        layer.msg("已经关闭连接", { offset: 0});
    };
    /**
     * 发送信息给后台
     */
    function sendMessage(){
        $("#demo1").val(layedit.getContent(layeditIndex));
        if(ws == null){
            layer.msg("连接未开启!",{offset:0, shift: 6});
            return false;
        }
        //获取富文本内容
        var message;
        var mess = layedit.getContent(layeditIndex);
        $("#sendto").text("");
            message = mess;
        //因为业务需求,将客服定为死值,这里可以动态修改to的内容,但必须和你session中的名称一致
        var to = '客服';
        ws.send(JSON.stringify({
            message : {
                type:2,
                content : message,
                //from: 这里是客户的信息,随意指定
                from : '${userid}',
                to : to,      //接收人,如果没有则置空,如果有多个接收人则用,分隔
                time : getDateFull()
            },
            type : "message"
        }));
        layedit.setContent(layeditIndex,"");
    }
    function analysisMessage(message){
        message = JSON.parse(message);
        if(message.type == "message"){      //会话消息
            showChat(message.message);
        }
    }

    /**
     * 展示会话信息
     */
    function showChat(message){
        var to = message.to == null || message.to == ""? "群聊" : message.to;   //获取接收人
        var isSef = '${userid}' == message.from ? "layim-chat-mine" : "";  //如果是自己则显示在右边,他人信息显示在左边
        var html = " <li class="+isSef+">\n" +
            "                            <div class=\"layim-chat-user\">\n" +
            "                                <img src=\"${pageContext.request.contextPath}/static/img/touxiang.jpg\">\n" +
            "                                <cite>\n" +
            "                                    \n" +
            "                                    "+message.time+"<i>&#8195;"+message.from+"</i>\n" +
            "                                </cite>\n" +
            "                            </div>\n" +
            "                            <div class=\"layim-chat-text\">\n" +
            "                               "+message.content+"\n" +
            "                            </div>\n" +
            "                        </li>";
        $("#chat").append(html);
        var chat = $("#chat-view");
        chat.scrollTop(chat[0].scrollHeight);  //让聊天区始终滚动到最下面
    }
   
    /**
     * 清空聊天区
     */
    function clearConsole(){
        $("#chat").html("");
    }

    function appendZero(s){return ("00"+ s).substr((s+"").length);}  //补0函数

    function getDateFull(){
        var date = new Date();
        var currentdate = date.getFullYear() + "-" + appendZero(date.getMonth() + 1) + "-" + appendZero(date.getDate()) + " " + appendZero(date.getHours()) + ":" + appendZero(date.getMinutes()) + ":" + appendZero(date.getSeconds());
        return currentdate;
    }
</script>
</html>

客服端:看着比较乱,但你大多数都不用看,只需要ws.open,ws.close,还有发送信息,展示信息四个方法即可,其他都是辅助

<html>
<head>
    <title>客服端</title>
    <style>
        *{
            font-family: 微软雅黑;
        }
        .body{
            width: 818px;
            height: 495px;
            margin-top: 20px;
            /*display: none;*/
            background: #ffffff;
        }
        .title{
            width: 100%;
            height: 45px;
            background: #DC143C;
        }
        .title-left{
            text-align:center;
            width: 640px;
            height: 100%;
            line-height: 45px;
            float:left;
        }
        .title-right{
            width: 176px;
            height: 45px;
            text-align: right;
            line-height: 45px;
            float: left;
        }
        .content{
            width: 100%;
            height: 450px;
        }
        .content-left{
            float: left;
            width:640px;
            height: 100%;
            border: 1px solid #E3E3E3;
        }

        .content-right{
            float: left;
            width:340px;
            height: 100%;
            border-right: 1px solid #E3E3E3;
            border-bottom: 1px solid #E3E3E3;
        }
        .content-left-title{
            width: 100%;
            height: 40px;
            padding: 10px;
        }
        .content-left-body{
            width: 640px;
            height: 260px;
            padding: 5px;
        }
        .content-left-body ul li{
            position: relative;
            font-size: 0;
            margin-bottom: 10px;
            padding-left: 60px;
            min-height: 68px;
        }
        .layim-chat-user {
            position: absolute;
            left: 3px;
        }
        .layim-chat-user img {
            width: 40px;
            height: 40px;
            border-radius: 100%;
        }
        .layim-chat-user cite {
            position: absolute;
            left: 60px;
            top: -2px;
            width: 500px;
            line-height: 24px;
            font-size: 12px;
            white-space: nowrap;
            color: #999;
            text-align: left;
            font-style: normal;
        }
        .layim-chat-user cite i {
            padding-left: 15px;
            font-style: normal;
        }
        .layim-chat-user {
            display: inline-block;
            vertical-align: top;
            font-size: 14px;
        }
        .layim-chat-text {
            position: relative;
            line-height: 22px;
            margin-top: 25px;
            padding: 8px 15px;
            background-color: #e2e2e2;
            border-radius: 3px;
            color: #333;
            word-break: break-all;
            max-width: 462px\9;
        }
        .layim-chat-text, .layim-chat-user {
            display: inline-block;
            vertical-align: top;
            font-size: 14px;
        }
        .layim-chat-text:after {
            content: '';
            position: absolute;
            left: -10px;
            top: 13px;
            width: 0;
            height: 0;
            border-style: solid dashed dashed;
            border-color: #e2e2e2 transparent transparent;
            overflow: hidden;
            border-width: 10px;
        }
        .content-left-body ul .layim-chat-mine {
            text-align: right;
            padding-left: 0;
            padding-right: 60px;
        }
        .layim-chat-mine .layim-chat-user cite {
            left: auto;
            right: 60px;
            text-align: right;
        }
        .layim-chat-mine .layim-chat-user cite i {
            padding-left: 0;
            padding-right: 15px;
        }
        .layim-chat-mine .layim-chat-text {
            margin-left: 0;
            text-align: left;
            background-color: #5FB878;
            color: #fff;
        }
        .layim-chat-mine .layim-chat-text:after {
            left: auto;
            right: -10px;
            border-top-color: #5FB878;
        }
        .layim-chat-mine .layim-chat-user cite {
            left: auto;
            right: 60px;
            text-align: right;
        }

        .content-left-input{
            width: 640px;
            height: 150px;
            border-top: 1px solid #E3E3E3;
            position:relative;
        }
        .input-title{
            width: 640px;
            height: 30px;
            padding: 5px;
            line-height: 15px;
        }
        .input-body{
            width: 640px;
            height: 80px;
        }
        .input-Send{
            width: 640px;
            height: 40px;
            position:absolute; bottom:0;
        }
        .chat-view{
            overflow-x: hidden; overflow-y: auto;
        }
        .chat-view{
            overflow-x: hidden;
        }
        ul,li{
            list-style: none;
        }
        textarea{outline:none;resize:none;}
        .input-core{
            width: 35px;
            height: 25px;
            margin-left: 5px;
            margin-right: 5px;
            float: left;
        }

        /*在线列表css*/
        .one-big{
            width: 339px; height: 50px;
            background: #ffffff;
        }
        .two-big{
            width: 120px;height: 20px; float: left; margin-top: 5px;
        }
        .three-big{
            margin-left: 5px;line-height: 20px;
        }
        .four-big{
            width: 180px;height: 35px; float: right;
        }
        .five-big{
            width: 60px; background: #FF6666; height: 30px;  text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;float: right; margin-right: 3px;
        }
        .five-big-green{
            width: 60px; background: #339933; height: 30px;  text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;float: right;
        }
        /*鼠标移动显示关闭按钮*/
        .one-big:hover .five-big-close {
            display: block;
        }
        .five-big-close{
            width: 60px; background: #ff6700; height: 30px;  text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;float: right;margin-left: 3px;display: none;
        }
        .five-big-one{
            width: 60px; background: #339933; height: 30px;  text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;
        }
        .six-big{
            margin-left: 10px;
            margin-top: 12px;
        }
        .ur-stitle{
            width: 300px;
            height: 20px;
            border:  1px  solid;
        }
        .layui-layedit {
            border: none;
        }
        /*右上角图标*/
        .picon{
            color: #ffffff; margin-right: 15px; font-size: 18px;
        }
        /*.picon:hover{*/
        /*cursor: pointer;*/
        /*}*/
        .fa{
            cursor: pointer;
        }
        .newAddTrip{
            display:inline;
        }
        .mag-top{
            margin-top: 5px;
        }
    </style>
    <script>
        var layeditIndex;
        var layedit;
        $(function () {
            $( "#draggable" ).draggable();
            $( "#sortable" ).sortable({
                revert: true
            });
            $( "#sortableNotMess" ).sortable({
                revert: true
            });

            layui.use('layedit', function(){
                layedit = layui.layedit;
                layedit.build('demo1'); //建立编辑器
                layeditIndex = layedit.build('demo1', {
                    height: 65, //设置编辑器高度
                });
            });
        })
    </script>
</head>
<body>
<%--最外层div--%>
<div id="draggable" class="colPer"  style="width: 980px; height: 495px; margin: auto;">
        <%--头部导航条--%>
        <div class="title">
            <%--导航条左半部分--%>
            <div class="title-left">
                <a style="color: #ffffff" id="state" class="stateTitle">群聊</a>
            </div><%--导航条左半部分结束--%>
            <%--<div class="title-right">--%>
                <%--<div style="">--%>
                    <%--<span class=" picon fa  fa-minus" onclick="narrow()"></span>--%>
                    <%--<span class=" picon fa  fa-times-rectangle" onclick="end()"></span>--%>
                <%--</div>--%>
            <%--</div>--%>
        </div><%--头部导航条结束--%>
        <%--主体开始--%>
        <div class="content">
            <%--左边--%>
            <div class="content-left">
                <%--预留功能区域--%>
                <div  class="content-left-title">
                    <ul id="" style="width: 100%;">
                        <li class="input-core" id="arr" title="检查链接">
                            <a class="fa  fa-heartbeat"  style="font-size: 20px;" id="pot" onclick="checkConnection()"></a>
                        </li>
                        <li class="input-core" title="重新连接">
                            <a class="fa fa-refresh" style="font-size: 20px;" onclick="getConnection()"></a>
                        </li>
                        <li class="input-core" title="结束会话">
                            <a class="fa  fa-unlink" style="font-size: 20px;" onclick="closeConnection()"></a>
                        </li>
                        <li class="input-core" title="我的位置">
                            <a class="fa fa-bandcamp" style="font-size: 20px;"></a>
                        </li>
                        <li class="" style=" float: right; margin-right: 5px;" title="清空屏幕">
                            <a class="fa   fa-trash" style="font-size: 20px;" onclick="clearConsole()"></a>
                        </li>
                    </ul>
                </div>
                <div class="content-left-body">
                    <!-- 聊天区 -->
                    <div  class="am-cf admin-main">
                        <div class="admin-content">
                            <div class="am-scrollable-vertical" id="chat-view" style="height:100%; width: 100%;">
                                <ul class="am-comments-list am-comments-list-flip" id="chat">

                                </ul>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="content-left-input">
                    <%--&lt;%&ndash;输入区域预留功能&ndash;%&gt;--%>
                    <%--<div class="input-title">--%>

                    <%--</div>--%>
                    <div class="input-body">
                        <textarea id="demo1"  style="display: none; border: none;"> </textarea>
                    </div>
                    <div class="input-Send">
                        <div style="width: 300px; height: 80px; margin-right: 15px; float: right;">
                            <button type="button" class="TableEditor btn btn-default" onclick="sendMessage()" style="float: right; margin-left: 15px; width: 80px; height: 30px; line-height: 15px;background: #DC143C;color: #ffffff">发送</button>
                            <button type="button" class="TableEditor btn btn-default" onclick="javascript:location.reload();" style="float: right; margin-left: 15px; width: 80px; height: 30px; line-height: 15px">刷新</button>
                        </div>
                    </div>
                </div>
            </div>
            <%--右边--%>
            <div  class="content-right">
                <div>
                    <%--在线列表--%>
                    <div style="text-align: center; width: 100%;height: 40px;line-height: 40px; background: #E3E3E3;">
                        <p style="color:#000000;font-size: 14px;">在线列表</p>
                    </div>
                    <%--在线人员--%>
                    <div class=" am-panel-default">
                        <p>当前在线人数<span id="onlinenum"></span></p>
                        <ul class="am-list am-list-static am-list-striped" id="sortable">
                            <li class="one-big">
                                <div class="two-big">
                                    <span class="three-big">@</span>
                                </div>
                                <div class="four-big">
                                    <button type="button" class="TableEditor btn btn-default  five-big"  onclick="group()">全体成员</button>

                                    <div class="newAddTrip">                                      <span class="layui-badge">6</span></div>
                                </div>
                            </li>
                        </ul>
                    </div>
                        <%--未查看消息--%>
                        <div class=" am-panel-default">
                            <p>未查看消息<span id="notMess"></span></p>
                            <ul class="am-list am-list-static am-list-striped" id="sortableNotMess">
                            </ul>
                        </div>
                </div>
            </div>
        </div>
</div><%--最外层结束--%>
</body>
<style>
</style>
<script type="text/javascript">
    //私聊按钮
    var privateChatButton;
    //小绿点
    var smallGreenDot;
    //新增提示
    var newAddTrip;
    //群聊 or 单聊
    var messWhat = "群聊";
    //要关闭的客户名称
    var closeUser;
    //点击为群聊
    function group(){
        $(".stateTitle").text("群聊");
        messWhat="群聊";

    }
    $(function () {
        context.init({preventDoubleContext: false});
        context.settings({compress: true});
        context.attach('#chat-view', [
            {header: '操作菜单',},
            {text: '清理', action: clearConsole},
            {divider: true},
            {
                text: '选项', subMenu: [
                    {header: '连接选项'},
                    {text: '检查', action: checkConnection},
                    {text: '连接', action: getConnection},
                    {text: '断开', action: closeConnection}
                ]
            },
            {
                text: '销毁菜单', action: function (e) {
                    e.preventDefault();
                    context.destroy('#chat-view');
                }
            }
        ]);
    });
    if("${message}"){
        layer.msg('${message}', {
            offset: 0
        });
    }
    if("${error}"){
        layer.msg('${error}', {
            offset: 0,
            shift: 6
        });
    }


    var wsServer = null;
    var ws = null;
    wsServer = "ws://localhost:8080/ChatController/0/${userCode}";
    console.log(wsServer);
    ws = new WebSocket(wsServer); //创建WebSocket对象
    // ws = new SockJS(wsServer); //创建WebSocket对象
    //onopen建立链接
    ws.onopen = function (evt) {
        layer.msg("已经建立连接", { offset: 0});
    };
    //onmessage发送消息
    ws.onmessage = function (evt) {
        analysisMessage(evt.data);  //解析后台传回的消息,并予以展示
    };
    //onerror产生异常
    ws.onerror = function (evt) {
        layer.msg("产生异常", { offset: 0});
    };
    //关闭连接时
    ws.onclose = function (evt) {
        layer.msg("已经关闭连接", { offset: 0});
    };

    /**
     * 连接
     */
    function getConnection(){
        if(ws == null){
            ws = new WebSocket(wsServer); //创建WebSocket对象
            ws.onopen = function (evt) {
                layer.msg("成功建立连接!", { offset: 0});
            };
            ws.onmessage = function (evt) {
                analysisMessage(evt.data);  //解析后台传回的消息,并予以展示
            };
            ws.onerror = function (evt) {
                layer.msg("产生异常", { offset: 0});
            };
            ws.onclose = function (evt) {
                layer.msg("已经关闭连接", { offset: 0});
            };
        }else{
            layer.msg("连接已存在!", { offset: 0, shift: 6 });
        }
    }

    /**
     * 关闭连接
     */
    function closeConnection(){
        if(ws != null){
            ws.close();
            ws = null;
            $("#sortable").html("");    //清空在线列表
            layer.msg("已经关闭连接", { offset: 0});
        }else{
            layer.msg("未开启连接", { offset: 0, shift: 6 });
        }
    }

    /**
     * 检查连接
     */
    function checkConnection(){
        if(ws != null){
            layer.msg(ws.readyState == 0? "连接异常":"连接正常", { offset: 0});
        }else{
            layer.msg("连接未开启!", { offset: 0, shift: 6 });
        }
    }

    /**
     * 发送信息给后台
     */
    function sendMessage(){
        $("#demo1").val(layedit.getContent(layeditIndex));
        if(ws == null){
            layer.msg("连接未开启!",{offset:0, shift: 6});
            return false;
        }
        //获取富文本内容
        var message;
        var mess = layedit.getContent(layeditIndex);
        $("#sendto").text("");
        if(messWhat=="群聊"){
            message = mess;
        }
        if(messWhat=="单聊"){
            message = mess;
        }
        var to = messWhat == "群聊"? "": $(".stateTitle").text();
        ws.send(JSON.stringify({
            message : {
                type:1,
                content : message,
                from : '${userid}',
                to : to,      //接收人,如果没有则置空,如果有多个接收人则用,分隔
                time : getDateFull()
            },
            type : "message"
        }));
        layedit.setContent(layeditIndex,"");
    }
    function analysisMessage(message){
        message = JSON.parse(message);
        if(message.type == "message"){      //会话消息
            showChat(message.message);
        }
        if(message.type == "notice"){       //提示消息
            showNotice(message.message);
        }
        if(message.list != null && message.list != undefined){      //在线列表
            showOnline(message.list,message.notMess);
        }
    }

    /**
     * 展示提示信息
     */
    function showNotice(notice){
        $("#chat").append("<div class='pert'><p class=\"am-text-success\" style=\"text-align:center\"><span class=\"am-icon-bell\"></span> "+notice+"</p></div>");
        var chat = $("#chat-view");
        // $(".pert").fadeOut(3000);
        chat.scrollTop(chat[0].scrollHeight);   //让聊天区始终滚动到最下面
        $("#onlinenum").text($("#sortable li").length-1);
    }
    //在线列表
    var mychats = [];
    //消息列表
    var userMessage = [];
    /**
     * 展示会话信息
     */
    function showChat(message){

        var thisIndex = message.from;
        var thisTitle = $(".stateTitle").text();
        var userid = '${userid}';
        if(thisIndex!=thisTitle && userid!=message.from){
            if(mychats.length==0){
                mychats[0] = thisIndex;
            }else{
                if(mychats.indexOf(thisIndex)<0) {
                    mychats[mychats.length] = thisIndex;
                }
            }
        }else{
            var isSef = '${userid}' == message.from ? "layim-chat-mine" : "";  //如果是自己则显示在右边,他人信息显示在左边
            var html = " <li class="+isSef+">\n" +
                "                            <div class=\"layim-chat-user\">\n" +
                "                                <img src=\"${pageContext.request.contextPath}/static/img/touxiang.jpg\">\n" +
                "                                <cite>\n" +
                "                                    \n" +
                "                                    "+message.time+"<i>&#8195;"+message.from+"</i>\n" +
                "                                </cite>\n" +
                "                            </div>\n" +
                "                            <div class=\"layim-chat-text\">\n" +
                "                               "+message.content+"\n" +
                "                            </div>\n" +
                "                        </li>";
            $("#chat").append(html);
            var chat = $("#chat-view");
            chat.scrollTop(chat[0].scrollHeight);  //让聊天区始终滚动到最下面
        }
        var to = message.to == null || message.to == ""? "群聊" : message.to;   //获取接收人
    }
    /**
     * 展示在线列表
     */
    function showOnline(list,notMess){
        console.log(notMess)
        //在线列表
        if(list.length>0){
            shenfen = "沟通";
            smallGreenDot="";
            $("#sortable").html("");    //清空在线列表
            $.each(list, function(index, item){
                privateChatButton="<button type=\"button\" class=\"TableEditor btn btn-default  five-big\"  onclick=\"addChat('"+item+"')\">"+shenfen+"</button>";
                var li=
                    "                            <li class=\"one-big\">\n" +
                    "                                <div class=\"two-big\">\n" +
                    "                                    <span class=\"three-big\">"+item+"</span>\n" +
                    "                                </div>\n" +
                    "                                <div class=\"four-big\">\n" +
                    ""+privateChatButton+""+
                    ""+smallGreenDot+""+
                    "                                    <div class=\"newAddTrip\"><span class=\"layui-badge-dot layui-bg-green six-big\"></span>\n</div>\n" +
                    "                                </div>\n" +
                    "                            </li>";
                $("#sortable").append(li);
            })
        }else{
            var li=
                "                            <li class=\"one-big\">\n" +
                "                                <div class=\"two-big\">\n" +
                "                                    <span class=\"three-big\">无</span>\n" +
                "                                </div>\n" +
                "                                <div class=\"four-big\">\n" +
                "                                    <div class=\"newAddTrip\"></div>\n" +
                "                                </div>\n" +
                "                            </li>";
            $("#sortable").append(li);
        }
        if(notMess.length>0){
            $("#sortableNotMess").html("");    //清空未读消息列表
            var title = $(".stateTitle").html();
            var offline = "回复";
            $.each(notMess, function(index, item1){
             if(title==item1){
                 return true;
             }else{
                 var arbutton="<button type=\"button\" class=\"TableEditor btn btn-default  five-big-green\"  onclick=\"addChat('"+item1.userA+"')\">"+offline+"</button>";
                 var closeButton = "<button type=\"button\" class=\"TableEditor btn btn-default  five-big-close\" onclick=\"closeButton('"+item1.userA+"')\">关闭</button>";
                 // privateChatButton="<button type=\"button\" class=\"TableEditor btn btn-default  five-big\"  onclick=\"addChat('"+item+"')\">"+shenfen+"</button>";
                 var li=
                     "                            <li class=\"one-big\">\n" +
                     "                                <div class=\"two-big\">\n" +
                     "                                    <span class=\"three-big endClose \">"+item1.userA+"</span>\n" +
                     "                                </div>\n" +
                     "                                <div class=\"four-big\">\n" +
                     ""+arbutton+""+
                     ""+closeButton+""+
                     ""+smallGreenDot+""+
                     "                                    <div class=\"newAddTrip\"><span class=\"layui-badge mag-top\">"+item1.counts+"</span></div>\n" +
                     "                                </div>\n" +
                     "                            </li>";
                 $("#sortableNotMess").append(li);
             }
            })
        }else{
            $("#sortableNotMess").html("");    //清空未读消息列表
            var li=
                "                            <li class=\"one-big\">\n" +
                "                                <div class=\"two-big\">\n" +
                "                                    <span class=\"three-big\">无</span>\n" +
                "                                </div>\n" +
                "                                <div class=\"four-big\">\n" +
                "                                    <div class=\"newAddTrip\"></div>\n" +
                "                                </div>\n" +
                "                            </li>";
            $("#sortableNotMess").append(li);
        }
        $("#onlinenum").text($("#sortable li").length);     //获取在线人数
    }
    /**
     * 添加接收人
     */
    function addChat(user){
        $(".stateTitle").text("");
        var urr;
        var sendto = $(".stateTitle");
        messWhat="单聊";
        var receive = sendto.text() == "群聊" ? "" : sendto.text();
        // $(".state").text(user);
        clearConsole();
        if(receive.indexOf(user) == -1){    //排除重复
            urr = receive+user;
            sendto.text(urr);
        }
        for(var i=0;i<mychats.length;i++){
            if(mychats[i]==urr){
                var index = mychats.indexOf(mychats[i]);
                mychats.splice(index);
                break;
            }
        }
    }
    $(function () {
        // $(".five-big-closa").click(function(){
        //     alert("a")
        //     $(this).closest('li').remove();
        // })
        $(document).on("click", ".five-big-close", function () {
            <%--更改信息状态--%>
            var arr = confirm("确认已查看信息,并删除吗?");
            if(arr){
//这里去请求你的后台,删除聊天记录(如果你的聊天记录需要保存)
                $(this).closest('li').remove();
            }else{
            }
        });
    })
    /**
     * 清空聊天区
     */
    function clearConsole(){
        $("#chat").html("");
    }

    function appendZero(s){return ("00"+ s).substr((s+"").length);}  //补0函数

    function getDateFull(){
        var date = new Date();
        var currentdate = date.getFullYear() + "-" + appendZero(date.getMonth() + 1) + "-" + appendZero(date.getDate()) + " " + appendZero(date.getHours()) + ":" + appendZero(date.getMinutes()) + ":" + appendZero(date.getSeconds());
        return currentdate;
    }
</script>
</html>

3.后台:核心的部分

//userType 0为服务器端 1为客户端 userCode为用户id
@ServerEndpoint(value = "/ChatController/{userType}/{userCode}", configurator = HttpSessionConfigurator.class)
public class ChatController extends BaseController {

    //坑点:自动注入是找不到service的,需要手动在spring.xml中注入,然后引入
    private OnlineService onlineService = (OnlineService) ContextLoader.getCurrentWebApplicationContext().getBean("OnlineService");
    private ChatService chatService = (ChatService) ContextLoader.getCurrentWebApplicationContext().getBean("ChatService");
    private static int onlineCount = 0; //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static CopyOnWriteArraySet<ChatController> webSocketSet = new CopyOnWriteArraySet<ChatController>();
    private Session session;    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private String userid;      //用户名
    private HttpSession httpSession;    //request的session

    private static List list = new ArrayList<>();   //在线列表,记录用户名称

    private static List byone = new ArrayList<>();  //状态为 1的用户
    private static Map routetab = new HashMap<>();  //用户名和websocket的session绑定的路由表
    private static List notMess = new ArrayList();       //定义list 当有客户端传过来消息时 查询数据库,查出所有未查看的消息列表


    /**
     * 连接建立成功调用的方法
     * @param session  可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void onOpen(@PathParam(value = "userType")String userType,@PathParam(value = "userCode") String userCode, Session session, EndpointConfig config){
        System.out.println("输出session"+session);
        PageData pd = new PageData();
        this.session = session;
        webSocketSet.add(this);     //加入set中
        this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        this.userid=(String) httpSession.getAttribute("userid");    //获取当前用户
        userid=userCode;
//        boolean isExist = list.contains(userid);
//        System.out.println("输出boolean:"+isExist);
        list.add(userid);           //将用户名加入在线列表
        addOnlineCount(userid);           //在线数加1;
        routetab.put(userid, session);
        //如果此用户是
        try {
            //查询所有客服暂未查看的信息
            List<Chat> list = chatService.selChatNotMess(pd);
            //如果有没看的,赋值给notMess
            if(list.size()!=0){
                notMess.clear();
                for(Chat chat1:list){
                    notMess.add(chat1);
                }
            }else{
                notMess.clear();
            }
            System.out.println("输出userCode:"+userCode);
            if(userType.equals("1")){
                System.out.println("是用户登录");
            }
            //如果是客服登录,将其状态改为已登录
            if(userType.equals("0")){
                System.out.println("是客服上线");
                pd.put("onlineStatus",1);
                pd.put("onlineName",userCode);
                int is = onlineService.updOnlineStatusTwo(pd);
                System.out.println("客服上线:"+is);

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        String message = getMessage("[" + userid + "]加入,当前在线人数"+getOnlineCount()+"位", "notice",  list,notMess);
        broadcast(message);     //广播
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(@PathParam(value = "userType")String userType,@PathParam(value = "userCode") String userCode){
        webSocketSet.remove(this);  //从set中删除
        subOnlineCount(userid);           //在线数减1
        list.remove(userid);        //从在线列表移除这个用户
        routetab.remove(userid);
        String message = getMessage("[" + userid +"]离开了聊天,当前在线人数"+getOnlineCount()+"位", "notice", list,notMess);
        PageData pd = new PageData();

        //如果是客服下线,则将其状态改为已下线
        if(userType.equals("0")){
            System.out.println("是客服下线");
            pd.put("onlineStatus",0);
            pd.put("onlineName",userCode);
            try {
                int is = onlineService.updOnlineStatusTwo(pd);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //如果是
        if(userType.equals("1")){
            notMess.remove(userCode);
        }
        broadcast(message);         //广播
    }

    /**
     * 接收客户端的message,判断是否有接收人而选择进行广播还是指定发送
     * @param _message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String _message) {

        JSONObject chat = JSON.parseObject(_message);
        JSONObject message = JSON.parseObject(chat.get("message").toString());
        if(message.get("to") == null || message.get("to").equals("")){      //如果to为空,则广播;如果不为空,则对指定的用户发送消息
            broadcast(_message);
        }else{
            try {
                String [] userlist = message.get("to").toString().split(",");

                OrderNumUtil orderNumUtil = new OrderNumUtil();
                PageData pd = new PageData();
                //编号
                pd.put("seNum",orderNumUtil.OrderNum());
                //消息类型 1 客服发送 2 用户发送
//            pd.put("messageType",message.get("type"));
                int type =(Integer) message.get("type");
                if(type==1){
                    //关系用户A
                    pd.put("userA",userlist[0]);
                    //关系用户B
                    pd.put("userB",message.get("from"));

                }
                if(type==2){
                    //关系用户A
                    pd.put("userA",message.get("from"));
                    //关系用户B
                    pd.put("userB",userlist[0]);
                    pd.put("onlineName",message.get("from"));
                }
                pd.put("messageType",message.get("type"));
                //发送者者id
                pd.put("userId",message.get("from"));
                //接受者id
                pd.put("friendId",userlist[0]);
                //消息内容
                pd.put("messageContent",message.get("content"));
                //消息种类
                pd.put("messageKind",1);
                //消息发送时间
                pd.put("sendTime",message.get("time"));
                //消息状态
                pd.put("status",1);


                int is = chatService.insChat(pd);
                singleSend(_message, (Session) routetab.get(message.get("from"))); //发送给自己,这个别忘了
                for(String user : userlist){
                    if(!user.equals(message.get("from"))){
                        singleSend(_message, (Session) routetab.get(user));     //分别发送给每个指定用户
                    }
                }
                //查询所有客服暂未查看的信息
                List<Chat> list = chatService.selChatNotMess(pd);
                //如果有没看的,赋值给notMess
                if(list.size()!=0){
                    notMess.clear();
                    for(Chat chat1:list){
                        notMess.add(chat1);
                    }
                }else{
                    notMess.clear();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            String messages = getMessage("用户:[ "+ message.get("from")+" ]发送了一条信息,请注意查看", "notice", list,notMess);
            broadcast(messages);         //广播
        }
    }

    /**
     * 发生错误时调用
     * @param error
     */
    @OnError
    public void onError(Throwable error){
        error.printStackTrace();
    }

    /**
     * 广播消息
     * @param message
     */
    public void broadcast(String message){
        for(ChatController chat: webSocketSet){
            try {
                chat.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
                continue;
            }
        }
    }

    /**
     * 对特定用户发送消息
     * @param message
     * @param session
     */
    public void singleSend(String message, Session session){
        try {
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 组装返回给前台的消息
     * @param message   交互信息
     * @param type      信息类型
     * @param list      在线列表
     * @param notMess   客服未读消息列表
     * @return
     */
    public String getMessage(String message, String type, List list,List notMess){
        JSONObject member = new JSONObject();
        member.put("message", message);
        member.put("type", type);
        member.put("list", list);
        member.put("notMess",notMess);
        return member.toString();
    }

    public  int getOnlineCount() {
        return onlineCount;
    }

    //在线人数+1
    public  void addOnlineCount(String userid) {
        ChatController.onlineCount++;
    }

    //在线人数-1
    public  void subOnlineCount(String userid) {
        ChatController.onlineCount--;
    }

到这里就差不多了,注意点如下

    1.jsp中 ${userid} 是我后台获取的登录用户的session;

    2.由于我的聊天记录需保存到数据库中,里面的一些PageData,pd.put等等都是辅助类(相当于Map),一些list都是获取数据库数据和查询客服未查看的记录而写的,可以花点时间跳着看

    3.websocket是可以向后台传递参数的,只需在地址后面加    .../参数1/参数2即可 后台接参可以看代码

    4.前端代码看着很费劲,但大多数没用,可以结合他人代码观看。。。

4.下面是数据库聊天记录保存的设计

简单明了

5.如果你的项目搭的nginx 服务上的,还需加上两行配置(在你的nginx.conf 中),这两句的意思是当有websoket请求过来时,自动将http请求升级为socket请求(百度很多,还是贴一下吧)

总结坑点:

    1.如果你是内嵌式的tomcat7以上则不需要引入java-websocket包,因为自带,如果不是内嵌式则不影响

    2.websocket中 service是扫描不到的,需要手动注入bean,然后手动引入(如后台代码中所示)

    3.spring-socket 4.15版本以后不支持session跨域,这个一定要注意,尽量别用4.15以后版本,要不然报的错你自己都稀里糊涂,如果你的项目不涉及其他项目端访问,只是本项目访问则不需注意。

    4.需要加入心跳检测,因为close可以触发,但检测不到断网,所以就成了两头黑(单机版聊天)

猜你喜欢

转载自blog.csdn.net/qq_38169344/article/details/86489255