webSocket信息推送 实现客服聊天功能 群发和点对点

webSocket信息推送 实现客服聊天功能 群发和点对点

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

客服系统开发需要用到点对点聊天,如果是不断访问接口的形式获取数据,性能消耗大
用WebSocket通信技术,这样的话只需要连接一次,信息即可互相推送

解决思路

1.当用户或者客服进入聊天界面时,发起ws请求
“ws://127.0.0.1:8888/websocket/”+id
id为唯一标识符,辨别连接用户

2.发送信息 根据id发送信息

3.接收信息 前端接收信息包含接收人id,如果接收人是当前聊天对话框的用户,就把数据渲染到对话框里

步骤
1.引入pox

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
 </dependency>

<dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.4</version>
            <classifier>jdk15</classifier>
 </dependency>

2.

@Configuration
public class WebSocketConfig {

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


}

3.

@Component
@ServerEndpoint("/websocket/{id}")
public class WebSocket {

    private Session session;

    private static CopyOnWriteArraySet<WebSocket> webSockets = new CopyOnWriteArraySet<>();
    private static Map<String, Session> sessionPool = new HashMap<String, Session>();

    @OnOpen
    public void onOpen(Session session, @PathParam(value = "id") String id) {
        this.session = session;
        webSockets.add(this);
        sessionPool.put(id, session);
        System.out.println(id + "【websocket消息】有新的连接,总数为:" + webSockets.size());
    }

    @OnClose
    public void onClose() {
        webSockets.remove(this);
        System.out.println("【websocket消息】连接断开,总数为:" + webSockets.size());
    }

    @OnMessage
    public void onMessage(String message) {
        System.out.println("【websocket消息】收到客户端消息:" + message);
    }

    public void sendAllMessage(String message) {
        for (WebSocket webSocket : webSockets) {
            System.out.println("【websocket消息】广播消息:" + message);
            try {
                webSocket.session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void sendOneMessage(String id, String message) {
        System.out.println("【websocket消息】单点消息:" + message);
        Session session = sessionPool.get(id);
        if (session != null) {
            try {
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

发送

String msg="";
        String todeuser=""+touser;
        System.out.println("todeuser"+todeuser);
        JSONObject json;

        //1.将Bean转换为Json格式
        json = JSONObject.fromObject(chatVo);

        //2.将Json格式转换为String类型
        msg = json.toString();
        System.out.println("Bean转换为Json:" + msg);
        webSocket.sendOneMessage(todeuser,msg);

chatVo

package com.gdkm.vo;

import lombok.Data;

@Data
public class ChatVo {
    /**
     * 发送者
     */
    private Integer userId;
    /**
     * id
     */
    private Integer cId;
    /**
     * 头像
     */
    private String userIocn;
    /**
     * 内容
     */
    private String cltext;
    /**
     * 时间
     */
    private Data createtime;
    /**
     * 0是未读
     */
    private String clstate;
    /**
     * 0是自己  1是对方
     */
    private String tpye;
}

我的前端页面

<template>

    <div class="chat">

        <el-card class="box-card box-cards">
            <el-breadcrumb separator-class="el-icon-arrow-right">
                <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
                <el-breadcrumb-item>在线客服</el-breadcrumb-item>
            </el-breadcrumb>
        </el-card>

        <div class="box-card box-cards">
            <el-row class="con">
                <el-col :span="24">
                   <div class="righthead">
                       <h4>{{userNickname}}</h4>
                   </div>
                   <div class="context" id="liao">
                       <el-row class="itemchat" v-for="(item, index) in chatlogList" :key="index" >
                       <el-avatar class="chattouxiang" :class="item.tpye==0?'right':'left'" :size="40" :src="item.userIocn"></el-avatar>
                           <div class="send" :class="item.tpye==0?'right':'left'">
                               <span>{{item.cltext}}
                               </span>
                               <div class="arrow"  :class="item.tpye==0?'sendright':'sendleft'"/>
                           </div>
<!--                           <span :class="item.tpye==0?'right':'left'" class="clstate">{{item.clstate==0?'未读':'已读'}}-->
<!--                               </span>-->
                       </el-row>

                   </div>
                        <div class="rightbottom">
                          <textarea v-model="textarea" @keydown.13="tosendOneWebSocket()" ></textarea>
                        </div>
                </el-col>
            </el-row>
        </div>

        <log ref="child"   v-show="this.userNickname!=''"/>
    </div>
</template>

<script>
    import {appuser,chatlog,sendOneWebSocket,upchatlog} from "../../network/chat";
    import {userInfo} from "../../network/me";
    import Log from "./chatComps/Log";

    export default {
        name: "Chat",
        data(){
            return {
                touser:1,
                textarea:'',
                appuser:[],
                chatlogList:[],
                userNickname:'客服',
            }
        },
        beforeCreate() {

        },
        components:{
            Log
        },
        methods:{

            tosendOneWebSocket(){
                let text=this.textarea
                let id=this.touser
                sendOneWebSocket(id,text).then(res=>{
                    console.log(res)
                })
                const log = {
                    "userIocn": this.$store.state.user.userIcon,
                    "cltext": this.textarea,
                    "clstate": "1",
                    "tpye": "0"
                }
                this.chatlogList.push(log)
                this.textarea=''
            },
            tochatlog(touser){

                chatlog(touser).then(res=>{
                    console.log(res)
                    this.chatlogList=  res.data
                })
            },
            toappuser(){
                appuser().then(res=>{
                    console.log(res)
                    this.appuser=res.data
                })
            },
            initWebSocket() {
                // 连接错误
                this.websocket.onerror = this.setErrorMessage;

                // 连接成功
                this.websocket.onopen = this.setOnopenMessage;

                // 收到消息的回调
                this.websocket.onmessage = this.setOnmessageMessage;

                // 连接关闭的回调
                this.websocket.onclose = this.setOncloseMessage;

                // 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
                window.onbeforeunload = this.closeWebSocket;

            },
            setErrorMessage() {
                // eslint-disable-next-line no-console
                console.log(
                    "WebSocket连接发生错误   状态码:" + this.websocket.readyState
                );
            },
            setOnopenMessage() {
                // eslint-disable-next-line no-console
                console.log("WebSocket连接成功    状态码:" + this.websocket.readyState);
            },
            setOnmessageMessage(event) {
                let data=eval("("+event.data+")")
                upchatlog(data.CId)
                this.chatlogList.push(data);
            },
            setOncloseMessage() {
                // eslint-disable-next-line no-console
                console.log("WebSocket连接关闭    状态码:" + this.websocket.readyState);
            },
            closeWebSocket() {
                this.websocket.close();
            },
        },
        updated(){
            let ele= document.getElementById("liao");
            ele.scrollTop = ele.scrollHeight;
        },
        mounted() {
            window.onbeforeunload = this.closeWebSocket;
            this.websocket = new WebSocket(
                "ws://127.0.0.1:8888/linux/websocket/"+this.$store.state.user.userId
            )
            this.initWebSocket();
            this.tochatlog(1);
            userInfo(1).then(res=>{
                const user=res.data
                this.userNickname=user.userNickname
            })
            this.$refs.child.parentMsg(this.touser)
        },
        beforeRouteEnter:(to,from,next)=>{
            next(vm => {
                if (vm.$store.state.user.roleId!=1){
                    vm.$message.error('权限不通过');
                    next('/home');
                }
            })
        }

    }
</script>

<style scoped>

    .chat{
    }
    .clstate{
        font-size: 11px;margin-top: 20px;margin-left: 10px;margin-right: 10px
    }
    textarea{
        width: 100%;
        height: 100%;
        resize:none;
        border: none;
        outline: none;
    }
    .items{
        margin-left:5px ;
    }
    .item{
        height: 70px;
        background-color: #2E2E2E;
        border: 0;
        margin-top: 1px;
        line-height: 70px;
        color: white;
        border-radius: 5px;

    }
    .itemtouxiang{
        float: left;
         margin:15px 10px 0px 15px;
    }
    .touxiang{
       margin-top: 15px;
       margin-left: 15px;
        margin-right: 10px;
       float: left;
    }
    .con{
        width: 100%;
    }
    .box-cards {
        width: 65%;
        margin: 20px auto;
    }
    .lefthead{
        height: 80px;
        background-color: #2E2E2E;
        border-bottom: 1px white solid;
        line-height: 80px;
        color: white;
    }
    ::-webkit-scrollbar {
        width: 0 !important;
    }
    .leftbottom{
        height: 530px;
        background-color: #3D3D3D;
        overflow: auto;

    }
    .righthead{
        height: 60px;
        border-bottom: 1px #F5F5F5  solid;
        background-color: white;
        line-height: 60px;
        padding-left: 20px;
    }
    .context{
        height: 360px;
        border-bottom: 1px #E8E8E8  solid;
        overflow: auto;
        background-color: #F5F5F5;
        padding: 20px 20px;

    }
    .rightbottom{
        height: 130px;
        padding: 10px 10px;
        background-color: #FFFFFF;
    }
    .send {
        position:relative;
        background:#3399FF;
        border-radius:5px; /* 圆角 */
        padding:10px 10px;
        line-height: 20px;
        max-width: 60%;
    }
    .send .arrow {
        position:absolute;
        top:9px;
        width:0;
        height:0;
        font-size:0;
        border:solid 8px;
    }
    .send .sendright{
        right:-15px; /* 圆角的位置需要细心调试哦 */
        border-color:#F5F5F5 #F5F5F5 #F5F5F5 #3399FF;
    }
    .send .sendleft{
        left:-15px; /* 圆角的位置需要细心调试哦 */
        border-color:#F5F5F5 #3399FF #F5F5F5 #F5F5F5;
    }

    .send span{
      font-size: 15px;
      word-break:break-word;
    }
    .chattouxiang{
        margin-left: 20px;
        margin-right: 20px;
    }
    .right{
        float: right;
    }
    .left{
        float: left;
    }

    .itemchat{
        margin-top: 10px;
    }
</style>
<template>
    <div class="chat">
        <el-card class="box-card box-cards">
            <el-breadcrumb separator-class="el-icon-arrow-right">
                <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
                <el-breadcrumb-item>客服中心</el-breadcrumb-item>
            </el-breadcrumb>
        </el-card>

        <div class="box-card box-cards">
            <el-row class="con">
                <el-col :span="7">
                    <div class="lefthead">
                        <el-avatar class="touxiang" :size="50" :src="this.$store.state.user.userIcon"></el-avatar>
                        <h3>{{this.$store.state.user.userNickname}}</h3>
                    </div>
                    <div class="leftbottom">
                        <div class="item"   v-for="(item, index) in appuser" :key="index" @click="tochatlog(item.userId,item.userNickname)"
                             v-show="item.userId != $store.state.user.userId"
                        >
<!--                            this.$router.push('/user/'+item.userId)-->
                            <div @click="touserInfo(item.userId)">
                            <el-avatar class="itemtouxiang" :size="40" :src="item.userIcon"  ></el-avatar>
                            </div>
                            <h5>{{item.userNickname}}<el-badge is-dot class="items" v-if="item.xinxi"></el-badge></h5>
                        </div>

                    </div>
                </el-col>
                <el-col :span="17" v-if="showright">
                   <div class="righthead">
                       <h4>{{userNickname}}</h4>
                   </div>
                   <div class="context"  id="liao2">
                       <el-row class="itemchat" v-for="(item, index) in chatlogList" :key="index" >
                       <el-avatar class="chattouxiang" :class="item.tpye==0?'right':'left'" :size="40" :src="item.userIocn"></el-avatar>
                           <div class="send" :class="item.tpye==0?'right':'left'">
                               <span>{{item.cltext}}
                               </span>
                               <div class="arrow"  :class="item.tpye==0?'sendright':'sendleft'"/>
                           </div>
<!--                           <span :class="item.tpye==0?'right':'left'" class="clstate" v-show="item.tpye==0" >{{item.clstate==0?'未读':'已读'}}-->
<!--                               </span>-->
                       </el-row>

                   </div>
                        <div class="rightbottom">
                          <textarea v-model="textarea" @keydown.13="tosendOneWebSocket()" ></textarea>
                        </div>
                </el-col>
            </el-row>
        </div>

        <log ref="child"   v-show="this.userNickname!=''"/>
    </div>
</template>

<script>
    import {appuser,chatlog,sendOneWebSocket,upchatlog} from "../../network/chat";
    import Log from "./chatComps/Log";
    export default {
        name: "Chat",
        data(){
            return {
                textarea:'',
                appuser:[],
                chatlogList:[],
                userNickname:'',
                showright:true,
                touser:0,

            }
        },
        updated(){
            let ele= document.getElementById("liao2");
            ele.scrollTop = ele.scrollHeight;
        },
        computed:{
            logshow(){
                return false
            }
        },
        components:{
            Log
        },
        beforeCreate() {
        },

        methods:{
            touserInfo(userId){
                this.$router.push('/user/'+userId)
            },
            tosendOneWebSocket(){
                let text=this.textarea
                let id=this.touser
                sendOneWebSocket(id,text).then(res=>{
                    console.log(res)
                })
                const log = {
                    "userIocn": this.$store.state.user.userIcon,
                    "cltext": this.textarea,
                    "clstate": "1",
                    "tpye": "0"
                }
                this.chatlogList.push(log)
                this.textarea=''
            },
            //websocket
            initWebSocket() {
                // 连接错误
                this.websocket.onerror = this.setErrorMessage;

                // 连接成功
                this.websocket.onopen = this.setOnopenMessage;

                // 收到消息的回调
                this.websocket.onmessage = this.setOnmessageMessage;

                // 连接关闭的回调
                this.websocket.onclose = this.setOncloseMessage;

                // 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
                window.onbeforeunload = this.closeWebSocket;
            },
            setErrorMessage() {
                // eslint-disable-next-line no-console
                console.log(
                    "WebSocket连接发生错误   状态码:" + this.websocket.readyState
                );
            },
            setOnopenMessage() {
                // eslint-disable-next-line no-console
                console.log("WebSocket连接成功    状态码:" + this.websocket.readyState);
            },
            setOnmessageMessage(event) {
                let data=eval("("+event.data+")")
                console.log(data)
                if(data.userId == this.touser){
                    upchatlog(data.CId)
                    this.chatlogList.push(data);
                }
            },
            setOncloseMessage() {
                // eslint-disable-next-line no-console
                console.log("WebSocket连接关闭    状态码:" + this.websocket.readyState);
            },
            closeWebSocket() {
                this.websocket.close();
            },
            tochatlog(touser,userNickname){
                this.logshow=true
                this.touser=touser
                chatlog(touser).then(res=>{
                    this.chatlogList=  res.data
                    this.userNickname=userNickname
                    this.showright=true
                })
                // setInterval(() => {
                //     this.tochatlog();
                // }, 300);
                this.$refs.child.parentMsg(this.touser)
            },
            toappuser(){
                appuser().then(res=>{
                    this.appuser=res.data
                })
            },
            send(){
                console.log("发送")
                const log = {
                    "userIocn": "http://gdkmlzh.cn-gd.ufileos.com/img%2F5e3af915-83c8-4f23-9953-25ccb46363c1.jpg?UCloudPublicKey=TOKEN_0c9d0118-e892-4784-8702-34bf5b992604&Signature=8p462HhcXyhM%2FJmhhnygrZotLjw%3D&Expires=1891772663",
                    "cltext": this.textarea,
                    "clstate": "0",
                    "tpye": "1"
                }
                this.chatlogList.push(log)
                this.textarea=''
            },

        },
        mounted() {
            window.onbeforeunload = this.closeWebSocket;

            this.websocket = new WebSocket(
                "ws://127.0.0.1:8888/linux/websocket/"+this.$store.state.user.userId
            )
            this.initWebSocket();

            this.toappuser()
            setInterval(() => {
                this.toappuser();
            }, 3000);
        },
        beforeRouteEnter:(to,from,next)=>{
            next(vm => {
                if (vm.$store.state.user.roleId!=2){
                    vm.$message.error('权限不通过');
                    next('/home');
                }
            })
        },
    }
</script>

<style scoped>
    .page{
        margin:10px;
        text-align: center;
    }
    .chat{
    }
    .clstate{
        font-size: 11px;margin-top: 20px;margin-left: 10px;margin-right: 10px
    }
    textarea{
        width: 100%;
        height: 100%;
        resize:none;
        border: none;
        outline: none;
    }
    .items{
        margin-left:5px ;
    }
    .item{
        height: 70px;
        background-color: #2E2E2E;
        border: 0;
        margin-top: 1px;
        line-height: 70px;
        color: white;
        border-radius: 5px;

    }
    .itemtouxiang{
        float: left;
         margin:15px 10px 0px 15px;
    }
    .touxiang{
       margin-top: 15px;
       margin-left: 15px;
        margin-right: 10px;
       float: left;
    }
    .con{
        width: 100%;
    }
    .box-cards {
        width: 65%;
        margin: 20px auto;
    }
    .lefthead{
        height: 80px;
        background-color: #2E2E2E;
        border-bottom: 1px white solid;
        line-height: 80px;
        color: white;
    }
    ::-webkit-scrollbar {
        width: 0 !important;
    }
    .leftbottom{
        height: 530px;
        background-color: #3D3D3D;
        overflow: auto;

    }
    .righthead{
        height: 60px;
        border-bottom: 1px #F5F5F5  solid;
        background-color: white;
        line-height: 60px;
        padding-left: 20px;
    }
    .context{
        height: 360px;
        border-bottom: 1px #E8E8E8  solid;
        overflow: auto;
        background-color: #F5F5F5;
        padding: 20px 20px;
    }
    .rightbottom{
        height: 130px;
        padding: 10px 10px;
        background-color: #FFFFFF;
    }
    .send {
        position:relative;
        background:#3399FF;
        border-radius:5px; /* 圆角 */
        padding:10px 10px;
        line-height: 20px;
        max-width: 60%;
    }
    .send .arrow {
        position:absolute;
        top:9px;
        width:0;
        height:0;
        font-size:0;
        border:solid 8px;
    }
    .send .sendright{
        right:-16px; /* 圆角的位置需要细心调试哦 */
        border-color:#F5F5F5 #F5F5F5 #F5F5F5 #3399FF;
    }
    .send .sendleft{
        left:-16px; /* 圆角的位置需要细心调试哦 */
        border-color:#F5F5F5 #3399FF #F5F5F5 #F5F5F5;
    }

    .send span{
      font-size: 15px;
      word-break:break-word;
    }
    .chattouxiang{
        margin-left: 20px;
        margin-right: 20px;
    }
    .right{
        float: right;
    }
    .left{
        float: left;
    }

    .itemchat{
        margin-top: 10px;
    }
    .box-cards2 {
        width: 65%;
        margin: 20px auto;
    }
    .context2{
        border-bottom: 1px #E8E8E8  solid;
        background-color: #F5F5F5;
        padding: 20px 20px;
    }

</style>

原文链接:https://blog.csdn.net/weixin_44209584/article/details/103771533

Comments are closed.