srpingboot+vue整合websocket实现在线群聊系统

一、websocket 介绍

websocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

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

二、效果示意图

在这里插入图片描述

三、功能实现

要看的懂下面的代码,首先你必须学会springboot和vue的相关知识。

话不多说,直接上代码。

后端代码

maven坐标

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

WebSocket配置类

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

pojo实体类:负责封装发送消息的实体类

package com.jia.pojo.chat;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 * @author 侠客
 * Date 2022/8/15 20:56
 * Description: 服务器发给浏览器的webSocket数据
 */
 
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain=true)
public class MsgResult {
    private boolean systemMsgFlag;
    private String from;
    private Object sendMsg;
}

封装消息工具类:负责判断系统或者群聊消息,并转换成json格式

package com.jia.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jia.pojo.chat.MsgResult;

/**
 * @author 侠客
 * Date 2022/8/15 20:58
 */
public class MessageUtils {

    /**
     * 封装响应的消息,系统消息的发送fromName为null;
     * 封装好的响应如下,例如
     *   系统消息: {“systemMsgFlag”: true, "fromName": null, "message": ["Name1", "Name2"]}.
     *   非系统消息 {“systemMsgFlag”: false, "fromName": "YYJ", "message": “你在哪里呀?”}.
     * @param systemMsgFlag 是否是系统消息
     * @param from 发送方名称
     * @param message 发送的消息内容
     * @return java.lang.String json字符串
     */
    public static String getMessage(boolean systemMsgFlag, String from, Object message) {
        MsgResult resultMessage = new MsgResult();
        resultMessage.setSystemMsgFlag(systemMsgFlag);
        resultMessage.setSendMsg(message);
        if (!systemMsgFlag) {
            resultMessage.setFrom(from);
        }
        ObjectMapper objectMapper = new ObjectMapper();
        String repStr = null;
        try {
            repStr = objectMapper.writeValueAsString(resultMessage);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return repStr;
    }
}

核心来了,ChatEndPoint类,就是websocket业务核心类了

package com.jia.webSocket;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jia.utils.MessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author 侠客
 * Date 2022/8/15 21:01
 * Description webSocket核心业务类(用户间私信业务)
 */
@ServerEndpoint("/chat/{name}")
@Component
@Slf4j
public class ChatEndPoint {

    public static Map<String, ChatEndPoint> onlineUsers = new ConcurrentHashMap<>();

    private Session session;

    private String name;

    /**
     * 获取所有用户姓名列表
     * @return 用户姓名list
     */
    public Set<String> getAllUser() {
        return ChatEndPoint.onlineUsers.keySet();
    }


    /**
     * 广播发送消息
     * @param message 发送消息
     */
    private void broadcastMsgToAllOnlineUsers(String message) {
        Set<String> names = onlineUsers.keySet();
        for (String name : names) {
            ChatEndPoint chatEndpoint = onlineUsers.get(name);
            RemoteEndpoint.Basic basicRemote = chatEndpoint.session.getBasicRemote();
            try {
                log.info("广播发送消息:"+message);
                basicRemote.sendText(message);
            } catch (IOException e) {
                log.error("broadcastMsgToAllOnlineUsers错误");
                e.printStackTrace();
            }
        }
    }

    /**
     * 上线通知展示
     * @param message
     */
    private void onlineToUsers(String message) {
        Set<String> names = onlineUsers.keySet();
        for (String name : names) {
            ChatEndPoint chatEndpoint = onlineUsers.get(name);
            RemoteEndpoint.Basic basicRemote = chatEndpoint.session.getBasicRemote();
            try {
                log.info("广播发送消息:"+message);
                basicRemote.sendText(message);
            } catch (IOException e) {
                log.error("broadcastMsgToAllOnlineUsers错误");
                e.printStackTrace();
            }
        }
    }

    @OnOpen
    public void onOpen(Session session,@PathParam(value = "name") String name){
        if (name==null) {
            try {
                session.close();
            } catch (IOException e) {
                log.error("onOpen错误");
                e.printStackTrace();
            }
        }
        else {
            this.name=name;
            log.info(name + "已经上线!");
            this.session = session;
            onlineUsers.put(name, this);
            String message = MessageUtils.getMessage(true, null, getAllUser());
            onlineToUsers(message);
        }
    }

    @OnClose
    public void onClose(){
        log.info(this.name+"已下线!");
        onlineUsers.remove(this.name);
        String message = MessageUtils.getMessage(true, null, getAllUser());
        broadcastMsgToAllOnlineUsers(message);
    }

    @OnMessage
    public void onMessage(String msg){
        if (msg == null) {
            return;
        }
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            String  message= objectMapper.readValue(msg,String.class);
            log.info("服务器接收到消息"+message);
            //获取发送的消息
            String sendMsg=MessageUtils.getMessage(false, this.name,message);
            broadcastMsgToAllOnlineUsers(sendMsg);
        } catch (JsonProcessingException e) {
            log.error("onMessage错误");
            e.printStackTrace();
        }
    }

    @OnError
    public void onError(Throwable error) throws IOException {
        log.error("聊天系统错误:"+error.getMessage());
        String message = MessageUtils.getMessage(true, null, getAllUser());
        broadcastMsgToAllOnlineUsers(message);
    }
}

vue前端代码

<template>
<div style="display: flex;justify-content: center;align-items: center;background: #404847;height: 630px;">
  <div class="main">
    <div class="left">
      <div style="position:relative;height:100%">
        <el-card style="position:relative;height:100%;overflow: auto">
          <template #header>
            <div class="header">
              <span style="font-weight: bolder">在线博友</span>
            </div>
          </template>
          <div v-for="o in friend" :key="o" style="font-weight: bold;margin-bottom: 5px;">{{o}}</div>
        </el-card>

      </div>
    </div>
    <div class="right">
      <div class="msg">
          <div class="name">
            <el-card>
              <span style="position:relative;margin-top: 30px;font-weight: bolder;">欢迎来到【简印】星球</span>
              <span style="position: relative;float: right;font-weight: normal">
                你的昵称 : <span style="font-weight: bolder">{{myName}}</span>
              </span>

            </el-card>
          </div>
        <el-card class="text">
          <div v-for="o in otherMsg" :key="o" class="text item">
            <template v-if="o.from!==myName">
              <div style="text-align: left">{{o.from}}<br/>
                <div style="background: gainsboro;position: relative;float: left;width: 50%;text-align: left;padding:5px;word-break:break-all">
                  <span style="position: relative;left: 0">{{o.sendMsg}}</span>
                </div>
              </div>
            </template>
            <template v-else>
              <div style="text-align: right">{{o.from}}<br/>
                <div style="background: cornflowerblue;position: relative;float: right;width: 50%;text-align: left;padding:5px;word-break:break-all">
                  <span style="position: relative;left: 0">{{o.sendMsg}}</span>
                </div>

              </div>
            </template>
          </div>
      </el-card>
<!--          <div class="text">
            <div v-for="o in otherMsg" :key="o" class="text item">
              <template v-if="o.from!==myName">
                <div style="text-align: left">{{o.from}}<br/>{{o.sendMsg}}</div>
              </template>
              <template v-else>
                <div style="text-align: right">{{o.from}}<br/>{{o.sendMsg}}</div>
              </template>
            </div>
          </div>-->
      </div>
      <div class="send">
        <el-input v-model="text" :autosize="{ minRows: 5, maxRows: 10 }"     type="textarea"
                  placeholder="Please input" @keyup.enter.native="send"
                  style="position:relative;height:78%;overflow: auto"/>
        <el-button @click="send" type="primary" style="position:relative;float: right;right: 2%;" >发送</el-button>
      </div>
    </div>

  </div>

</div>
</template>

<script>
import Cookie from "_vue-cookies@1.8.1@vue-cookies";
import {ElMessageBox,ElMessage} from "element-plus";

export default {
  beforeRouteLeave (to, from, next) {
    ElMessageBox.confirm(
        '正在离开本页面,本页面内所有未保存数据都会丢失',
        '警告',
        {
          confirmButtonText: '离开',
          cancelButtonText: '取消',
          type: 'warning',
        }
    )
        .then(() => {
          this.exit();
          next();
        })
        .catch(() => {
          window.history.go(1);
          ElMessage({
            type: 'info',
            message: '取消操作!',
          })
        })
  },
  name: "AllChat",
  data(){
    return{
      ws:null,
      myName:'我的名字',
      friend:[],
      text:'',
      myMsg:[],
      otherMsg:[],
      list:{
        systemMsgFlag:'',
        from:'',
        sendMsg:''
      }
    }
  },
  update() {
    this.scrollToBottom()
  },
  mounted() {
    this.getConnect();
    this.scrollToBottom()
  },
  watch:{
/*    '$route.path'( to , from ){
      console.log( to , from )
      if( to !== '/chat'){
        this.exit();
      }
    }*/
  },
  methods:{
    scrollToBottom(){
      const container=document.getElementsByClassName("text");
      container.scrollTop=container.scrollHeight;
    },
    getConnect(){
          const tokenStr = Cookie.get("user");
          this.ws = new WebSocket(`ws://124.71.59.103:8090/chat/`+tokenStr.name);
          this.myName = tokenStr.name;
          this.ws.onopen = this.open
          this.ws.onclose = this.close
          this.ws.onerror = this.error
          this.ws.onmessage = this.getMessage
    },
    open(){
      window.onunload=function(){
        this.ws.close()
      };
      window.onbeforeunload=function(){
        this.ws.close()
      };
    },
    exit(){
      this.ws.close();
    },
    error(data){
      console.log("webSocket连接错误:",data);
      this.ws.close();
    },
    getMessage(msg) {
      this.list=JSON.parse(JSON.stringify(msg.data));
      if (typeof this.list ==='string'){
        this.list = JSON.parse(this.list)
      }
      console.log(this.list)
      if (this.list.systemMsgFlag){
        this.friend=this.list.sendMsg;
      }else {
        if (this.list.sendMsg!=null){
          // if (this.list.from!==this.myName) {
            this.otherMsg.push(this.list);
/*          }*/
        }
        this.scrollToBottom();
      }
    },
    close() {
      this.ws.close();
    },
    send(){
      const a=this.text;
      if (a.match(/^\s*$/)){
        ElMessage({
          showClose: true,
          message: '消息不可为空,发送失败!',
          type: 'warning',
        })
      }else {
        this.myMsg.push(this.text);
        this.ws.send(JSON.stringify(this.text));
        this.text='';
        this.scrollToBottom();
      }

    }
  }
}
</script>

<style scoped>
.main{
  display:flex;
  position: relative;
  width: 65%;
  margin: 0;
  height: 95%;
}

.left{
  height: 100%;
  float: left;
  width: 30%;

}
.right{
  -webkit-flex: 1;
  width: 70%;
  left: 30%;
  height: 100%;
}
.msg{
  background: cornsilk;
  height: 70%;
}
.name{
  position: relative;
  background: cornflowerblue;
  height: 15%;
  font-weight: bolder;
  left: 0
}
.text{
  height: 85%;
  overflow:auto;
}
.send{
  background: aliceblue;
  height: 30%;
}
</style>
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值