WebSocket实现客服在线实时聊天(单聊模式)

时间:2024-02-23 16:52:46

本篇文章讲述如何将WebSocket群聊模式改造为单聊模式,并详细讲解该功能的实现过程。

Demo是一个商城小项目,将单聊应用在了前台用户与后台客服的在线聊天,此模式为单聊模式,代码都是在我之前写的websocket群聊代码的基础之上改写的,可参考之前的文章

一,思路梳理

1,首先思考群聊的实现方式。

每当一个用户使用websocket建立连接时,都会存放一个连接对象(在connectMap集合存放,键为sessionId,值为该连接对象),每次当用户发送一条数据时,都会遍历connectMap中所有的连接对象,然后进行广播发送消息,接收消息也是,谁的消息都收,来之不拒,所以此为群聊。

2,再来举一反三,思考单聊的实现方式。

单聊的话,要改变下面几个地方。

1,存放connectMap连接对象时,值要为userId(唯一标识)。

2,使用websocket建立连接时要把自己的userId(发送人)和对方的userId(接收人)传到后台,因为发消息时,根据userId得到连接对象,并使用连接对象发消息到对方的窗口和自己的窗口。

3,在js获取消息的地方要判断该消息的发送人userId是否和初始化时的接收人userId一致,如果一致的话则将消息添加到自己的窗口进行显示,如果不一致则不显示。

有一个地方需要提一下,比如要给A用户发送消息,就要根据A的userId得到连接对象,然后就会把消息发送给A用户。

二,代码分析

1,首先是用户聊天框的js代码(html忽略)

此为用户点击在线客服按钮进入聊天框页面就执行的js代码。

说明:下面接收人id写死了为超级管理员,我这里省事情了,因为超级管理员的账号(也就是唯一标识)就是超级管理员这五个字,当真正开发时就要动态获取唯一标识了!

<script type="text/javascript">
	//1,获取连接,new WebSocket()
	//获取到路径url的参数(用户的手机号,这里将手机号作为了用户的唯一标识)
    var accounta=window.location.search;
    var account=accounta.slice(9);
    //发送人id
    var sb=account;
	//将发送人id参数传给服务端
	var wsUrl="ws://127.0.0.1:8082/charRoomServer";
	var allUrl=wsUrl+"/"+sb;
	//接收人id
	var jsrid=\'超级管理员\';
	//客户端与服务端建立连接,建立连接以后,它会发出一个ws.open事件
	var ws=new WebSocket(allUrl);
	//连接成功后就将接收人id发送给服务端
	ws.onopen=function(){
		ws.send(jsrid);
	}
	//客户端收到服务端发送的消息
	ws.onmessage=function(message){
	    var shenfen=JSON.stringify(message.data).toString();
	    var shenfens=shenfen.slice(shenfen.lastIndexOf("|")+1,-1);
     //这里通过很low的方式拿到了发送人id  [shenfens](后台拼接字符串并前台截取得到)   
     //判断发送人id是否和页面初始化时的接收人id(jsrid)是否一致,相同则说明是对方回复的消息,并将该消息添加到自己的聊天框;如果收到的发送人id与“大白机器人”或者是和自己的id一致,也要将消息添加到自己的聊天框(自己发的消息嘛)
        if(jsrid==shenfens||shenfens=="大白机器人:"||shenfens==sb){
            //获取以后,在客户端显示
            messages.innerHTML+=message.data;
        }else{
            //不做任何操作
        }
	}
	//获取某个用户输入的聊天内容,并发送到服务端
	 function getMessage(){
		var inputMessage=document.getElementById("inputMessage").value;
		//alert(inputMessage);
		//获取消息以后,要发送给服务端,然后广播给所有用户
		if(typeof(inputMessage)==\'undefined\'){
			alert("请输入您要发送的消息!");
		}else{
			ws.send(inputMessage);
			//输入框消息清空
			inputMessage.value="";
		}	
	} 
	//当关闭页面或者用户退出时,会执行一个ws.close()方法
	window.onbeforeunload=function(){
		ws.close();
	}
	//按回车发送信息
	document.onkeyup=function(e){
		if(e.keyCode==13){
			getMessage();
			inputMessage.value="";
		}
	}
</script>

2,其次为后台系统客服聊天框的js代码

<script>
        var name="";
        $(function () {
            $.post(
                "/demo/getName",
                function (data) {
                    name=data.name;
                },
                "json"
            );
 //得到未读消息并展示到列表中(当点击其中一个未读消息时会得到该消息的账号(前台用户的userId唯一标识))
            $.post(
                "/demo/getweidu",
                function (data) {
                    var temp=\'\';
                    var count=0;
                    var account=\'\';
                    for(var i=0;i<data.length;i++){
                        temp+=\'<div class="yonghu" onclick="liaotian(\'+data[i].account+\')">\n\' +
                            \'            <span id="lalala">\'+data[i].account+\'</span>\n\' +
                            \'            <div id="\'+data[i].account+\'" class="weidu">\'+data[i].count+\'</div>\n\' +
                            \'        </div>\';
                    }
                    $("#nav").append(temp);
                }
            );
        });

//这是点击对应的未读消息之后将未读消息展示到客服的聊天框
        var sb=null;
        function liaotian(v) {  //下面的websocket内容在这个方法里面,执行这个点击事件之后触发websocket连接
            sb=v;
            $("#content").empty();
            $("#"+v).hide();
            $.post(
                "/demo/getxiaoxi",
                {account:v},
                function (data) {
                    // alert(JSON.stringify(data));
                    var str=\'\';
                    for(var i=0;i<data.length;i++){
                        var time=data[i].addtime.slice(0,10);
                        str+=\' <div class="message"><span>\'+data[i].account+\'</span>用户:<span>\'+time+\'</span>\'+data[i].message+\'</div>\';
                    }
                    $("#content").append(str);
                },
                "json"
            );
            //将点击后的未读消息改为已读消息
            $.post(
                "/demo/changeYD",
                {account:v},
                function (data) {
                    console.log(data);
                }
            );
            //发送人id(超级管理员写死的,上面说过了)
            var wsUrl="ws://127.0.0.1:8082/charRoomServer";
            var allUrl=wsUrl+"/"+"超级管理员";
            //客户端与服务端建立连接,建立连接以后,它会发出一个ws.open事件
            var ws=new WebSocket(allUrl);
            //接收人userId
            var jsrid=sb;
            //连接成功后,提示浏览器客户端输入名称
            ws.onopen=function(){
                ws.send(jsrid);
            }
            //客户端收到服务端发送的消息
            ws.onmessage=function(message){
                //截取到普通用户的手机号
                var shenfen=JSON.stringify(message.data).toString();
                var shenfens=shenfen.slice(shenfen.lastIndexOf("|")+1,-1);
                 console.log("身份:"+shenfens);
                // //判断发送人id是否和页面初始化时的接收人id(jsrid)是否一致,相同则说明是对方回复的消息
                if(jsrid==shenfens||"超级管理员"==shenfens){
                    //获取以后,在客户端显示
                    content.innerHTML+=message.data;
                }else{
                    //不做任何操作
                }
            }
            //获取某个用户输入的聊天内容,并发送到服务端
            function getMessage(){
                var inputMessage=document.getElementById("inputMessage").value;
                //alert(inputMessage);
                //获取消息以后,要发送给服务端,然后广播给所有用户
                if(typeof(inputMessage)==\'undefined\'){
                    alert("请输入您要发送的消息!");
                }else{
                    ws.send(inputMessage);
                    //输入框消息清空
                    inputMessage.value="";
                }
            }
            //当关闭页面或者用户退出时,会执行一个ws.close()方法
             window.onbeforeunload=function(){
                 ws.close();
             }
            //按回车发送信息
            document.onkeyup=function(e){
                if(e.keyCode==13){
                    getMessage();
                    inputMessage.value="";
                }
            }
        }
    </script>

3,最后为服务端的websocket实例

package com.qianlong.controller;

import com.qianlong.service.SelectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
/**
 * 聊天室的服务端程序
 * @author Administrator
 *
 */
//声明websocket某个服务端的地址
@ServerEndpoint(value = "/charRoomServer/{param}")
@Component
public class ChatRoomServer {
    @Autowired
    public static SelectService selectService;
    private boolean firstFlag=true;
    private Session session;
    private String userName;

    //发送人id
    private String userId;
    //key代表此次客户端的userId,value代表此次连接对象
    private static final HashMap<String, Object> connectMap=new HashMap<String, Object>();
    //保存所有用户昵称信息
    //key是session的id,value是用户昵称
    private static final HashMap<String, String> userMap=new HashMap<String, String>();
    //服务端收到客户端的连接请求,连接成功后会执行此方法
    @OnOpen
    public void start(@PathParam(value = "param") String param, Session session) {
        this.session=session;
        this.userId=param; //接收参数
        connectMap.put(param,this);
    }

    //客户端发来的信息,服务端接收
    @OnMessage              //接收人userId
    public void chat(String clientMessage,Session session) {
        //firstFlag为true是第一次进入,第二次进入之后设为false
        ChatRoomServer client=null;
        if(firstFlag) {
            this.userName=clientMessage;
            //将新进来的用户保存到用户map
            userMap.put(session.getId(), userName);

            try {
               if("超级管理员".equals(userId)){

               }else{
                   //构造发送给客户端的提示信息
                   String message=htmlMessage("大白机器人:","亲爱的"+userId+",您想了解点儿啥?");
                   client=(ChatRoomServer) connectMap.get(userId);
                   //给对应的web端发送一个文本信息
                   client.session.getBasicRemote().sendText(message);
               }
            } catch (IOException e) {
                e.printStackTrace();
            }
            firstFlag=false;
        }else {
            System.err.println("clientMessage:"+userName);
            //给对方发消息
            String message1=htmlMessage(userId,clientMessage);
            client  = (ChatRoomServer) connectMap.get(userName);
           if(client!=null){
               try {
                   client.session.getBasicRemote().sendText(message1);
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
            //给自己窗口发消息
            String message2=htmlMessage(userId,clientMessage);
            client  = (ChatRoomServer) connectMap.get(userId);
            try {
                client.session.getBasicRemote().sendText(message2);
            } catch (IOException e) {
                e.printStackTrace();
            }
           
            //这是将前台用户发送的消息存数据库并标记为未读,和上面通信没关系
            if("超级管理员".equals(userId)){

            }else{
                Map map=new HashMap();
                map.put("account",userId);
                map.put("message",clientMessage);
                map.put("addtime",new Date());
                int i = selectService.chatInsert(map);
                System.out.println(i);
            }
        }
        }
    
    /**
     * 前台js的ws.close事件,会触发后台的标注onClose的方法
     */
    @OnClose
    public void close() {
        userMap.remove(session.getId());
        connectMap.remove(userId);
    }
    /**
     * 渲染页面,把信息构造好标签再发送
     */
    public String htmlMessage(String userName,String message) {
        StringBuffer stringBuffer=new StringBuffer();
        SimpleDateFormat sf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        stringBuffer.append("<article>");
        stringBuffer.append("<span>"+sf.format(new Date())+"</span>");
        stringBuffer.append("<div class=\'avatar\'>");
        stringBuffer.append("<h3>"+userName+"</h3>");
        stringBuffer.append("</div>");
        stringBuffer.append("<div class=\'msg\'>");
        stringBuffer.append("<div class=\'tri\'></div>");
        stringBuffer.append("<div class=\'msg_inner\'>"+message+"</div>");
        stringBuffer.append("</div>");
        stringBuffer.append("</article>");
        //这里拼接了消息发送人的userId,在前台进行截取字符串接收发送人的userId
        stringBuffer.append("|"+userName);
        return stringBuffer.toString();
    }
}

三,依赖注入

websocket中进行依赖注入service并调用service方法进行数据库存储,如果按常规的方式是走不通的。

解决方式:

在该springboot项目中添加一个WebsocketConfig配置类,对service进行配置。

@Configuration
public class WebSocketConfig {
    /**
     * ServerEndpointExporter 用于扫描和注册所有携带 ServerEndPoint 注解的实例,
     * 若部署到外部容器 则无需提供此类。
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    /**
     * 因 SpringBoot WebSocket 对每个客户端连接都会创建一个 WebSocketServer(@ServerEndpoint 注解对应的对象,Bean 注入操作会被直接略过,因而手动注入一个全局变量
     */
    @Autowired
    public void setSelectService(SelectService selectService) {
        ChatRoomServer.selectService = selectService;
    }
}

然后在websocket中注入service

在这里插入图片描述

在这里插入图片描述

四,效果展示

在前台商城登录用户18838030468账号

点击联系客服

在这里插入图片描述

进到聊天框中(左下角的用户名忽略,因为上面通过截取字符串得到的发送人userId,所以这里多了一个userId)

在这里插入图片描述

然后发送消息给客服

在这里插入图片描述

然后退出18838030468账号并切换用户13333333333账号进行登录,然后点击客服,进入聊天页面

在这里插入图片描述

然后发送消息给客服

在这里插入图片描述

这个13333333333账号先不要退,直接登录后台超级管理员账号

登录之后会提示未读消息的信息。

点击客服管理,进入客服聊天平台,并显示消息未读数。

在这里插入图片描述

点击18838030468未读消息,查看消息并进行回复。(这是给18838030468用户发送的消息,其他用户是接收不到的,正规开发中,要把客服发送的消息存到缓存或数据库中,然后再在前台用户聊天框中进行显示聊天记录即可,这里忽略不做)

在这里插入图片描述

然后再点击13333333333未读消息进行查看,并回复消息。

在这里插入图片描述

这是给13333333333用户发送的消息,其他用户也是收不到的。

查看13333333333用户的聊天框内容,可以收到客服的消息,然后该用户可以和客服实现在线单聊。

在这里插入图片描述

本次websocket讲解到此结束,谢谢观赏!