vue项目使用websocket实现前后端即时通信(并且加入心跳检测机制)

485 阅读4分钟

自己项目中websocket使用场景:

1.单点登录

2.后端通知前端无感刷新用户信息,比如用户信息中的是否会员的更新、还有用户状态等一些后端状态更新了而前端需要通过退出登录后重新登录才可以获取到最新的数据

3.消息个数时时更新显示在前端页面

4.系统中使用了微信支付的功能,微信支付成功后,后端可以接受到微信支付是否成功的消息,但是这个消息无法发给前端的问题;之前的做法是前端去轮询判断微信支付是否成功的接口,加入websocket之后这个做法可以调整成通过后端发送消息来通知前端

websocket的心跳检测机制:

心跳和重连的目的就是客户端和服务需要保证彼此都还正常连接着(alive||health),避免丢包发生。

websocket连接断开有以下两种情况:

前端断开:在使用websocket过程中,可能会出现网络断开的情况,比如信号不好,或者网络临时关闭,这时候websocket的连接已经断开,而不同浏览器有不同的机制,触发onclose的时机也不同,并不会理想地执行websocket的onclose方法,我们无法知道是否断开连接,也就无法进行重连操作。

后端断开:如果后端因为一些情况需要断开ws,在可控情况下,会下发一个断连的消息通知,之后才会断开,我们便会重连。如果因为一些异常断开了连接,我们是不会感应到的,所以如果我们发送了心跳一定时间之后,后端既没有返回心跳响应消息,前端又没有收到任何其他消息的话,我们就能断定后端主动断开了。

因此需要一种机制来检测客户端和服务端是否处于正常连接的状态。通过在指定时间间隔发送心跳包来保证连接正常,如果连接出现问题,就需要手动触发onclose事件,这时候变可进行重连操作。

新建工具方法utils/socket.js

// mitt实现组件通信,相当于vue2项目中的全局事件总线,直接在package.json引入"mitt": "^3.0.0"后重新安装依赖
import mitt from "mitt";
import store from "@/store";
import router from "@/router";
import { Message } from 'element-ui';
import { INFRA_ENUM } from "@/const/enum";
import {WX_PAY_SUCCESS,REFRESH_MESSAGECOUNT} from "@/const/eventName"
const websocketUrl = import.meta.env.VITE_APP_WEBSOCKET_URL;
const ActionMethod = {
  Undefined: { value: 0, label: "" },
  Open: { value: 1, label: "开启连接" },
  NoticeComplete: { value: 2, label: "通知完成" },
  HeartHealth: { value: 3, label: "心跳检测" },
};
// 本项目使用mitt插件来实现组件间的通信问题,也可以用其他方式来处理
export const socketMitt = new mitt();

export default  {
  webSocket:null,
  lockReconnect: false,// 是否真正建立连接
  timeout:30000,//30s一次心跳
  num:3,// 3次心跳均未响应重连
  timeoutObj:null,
  serverTimeoutObj: null, //心跳倒计时
  timeoutNum: null, //断开 重连倒计时
  getInstance(){
    if(this.webSocket == null) {
      return this.initWebsocket()
    } else {
      return this.webSocket;
    }
  },
  destroy(){
    this.webSocket&&this.webSocket.close();
    this.timeoutNum &&clearTimeout(this.timeoutNum);
    this.timeoutObj&&clearTimeout(this.timeoutObj);
    this.webSocket=null;
    this.lockReconnect=false;
    this.timeoutNum=null;
    this.num=3;
  },
  initWebsocket(){
    let self=this;
    if (typeof WebSocket === "undefined") {
      // 浏览器不支持WebSocket
      return;
    }
    let url = "";
    let protocol = "ws";
    
    if (window.location.protocol === "https:") {
      protocol = "wss";
    }
    url = `${protocol}${websocketUrl}?UserNo=${store.getters["user/currentUser"].UserNo}&ActionMethod=${ActionMethod.Open.value}&Token=${store.getters["user/accessToken"]}`; //连接地址
    console.log('连接的url为:',url);
    //打开websocket
    this.webSocket =  new WebSocket(url);
    this.webSocket.onopen = function(res){
      console.log("连接成功...");
      //启动心跳检测
      self.start();
    }
    this.webSocket.onmessage =async function(res){
      let webSocketOutput=JSON.parse(res.data);
      console.log('接收的消息:',webSocketOutput);
      // 收到服务器信息,心跳重置
      self.reset();
      let noticeComplete={
        Token:store.getters["user/accessToken"],
        UserNo:store.getters["user/currentUser"].UserNo,
        ActionMethod:ActionMethod.NoticeComplete.value,
        WebSocketNoticeID:webSocketOutput.ID,
      }
      self.webSocket.send(JSON.stringify(noticeComplete));
      // 通知用户退出 
      if (webSocketOutput.NoticeType==INFRA_ENUM.WebSocketNoticeType.UserExit.value) {
        store.dispatch("user/logout");
        router.push("/");
        Message({
          offset: 60,
          showClose: true,
          message: "此账号已在另一台客户端登录,您已退出登录。请重新登录!",
          type: "error",
          dangerouslyUseHTMLString: true,
          duration:0,
        });
      }
      // 通知刷新用户
      else if(webSocketOutput.NoticeType==INFRA_ENUM.WebSocketNoticeType.RefreshUser.value){
        await store.dispatch("user/getNewInfo");
        await self.destroy();
        await self.getInstance();
      }
      // 通知微信支付成功
      else if(webSocketOutput.NoticeType==INFRA_ENUM.WebSocketNoticeType.WxPaySuccess.value){
        socketMitt.emit(WX_PAY_SUCCESS,{
          success: async() => {
            await store.dispatch("user/getNewInfo");
            await self.destroy();
            await self.getInstance();
          },
        });
      }
      // 通知刷新消息数量
      else if(webSocketOutput.NoticeType==INFRA_ENUM.WebSocketNoticeType.RefreshMessagePushCount.value){
        socketMitt.emit(REFRESH_MESSAGECOUNT);
      }
    }
    this.webSocket.onclose = function(res){
      
      self.destroy();
      // 登录状态&&有token的情况下重新连接
      if(store.getters["user/isLogin"]&&store.getters["user/accessToken"]){
        console.log("连接onclose 重连...");
        //重连
        self.reconnect();
      }
    }
    this.webSocket.onerror = function(res){
      
      // 登录状态&&有token的情况下重新连接
      if(store.getters["user/isLogin"]&&store.getters["user/accessToken"]){
        console.log("连接onerror 重连...");
        //重连
        self.reconnect();
      }
    }
    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
      self.destroy();
    }
  },
  reconnect() {
    //重新连接
    let self = this;
    if (this.lockReconnect) {
      return;
    }
    this.lockReconnect = true;
    //没连接上会一直重连,30秒重试请求重连,设置延迟避免请求过多
    this.timeoutNum &&clearTimeout(this.timeoutNum);
    this.timeoutNum = setTimeout(() => {
      //新连接
      self.getInstance();
      self.lockReconnect = false;
    }, 5000);
  },
  start(state) {
    console.log("开启心跳...");
    //开启心跳
    var self = this;
    this.timeoutObj &&clearTimeout(this.timeoutObj);
    this.timeoutObj = setTimeout(() => {
      //这里发送一个心跳,后端收到后,返回一个心跳消息,
      if (self.webSocket.readyState === 1) {
        //如果连接正常
        let noticeComplete={
          Token:store.getters["user/accessToken"],
          UserNo:store.getters["user/currentUser"].UserNo,
          ActionMethod:ActionMethod.HeartHealth.value,
          Message:"heartHealth",
        }
        self.webSocket.send(JSON.stringify(noticeComplete));
        self.num--;
        if(self.num===0){
          console.log('三次连接都未响应销毁...');
          self.destroy();
        }
      } else {
        console.log('心跳连接不正常重连...');
        //否则重连
        self.reconnect();
      }
    }, self.timeout);
  },
  reset() {
    //重置心跳
    //清除时间
    this.num=3;
    clearTimeout(this.timeoutObj);
    //重启心跳
    console.log('重启心跳...');
    this.start();
  },
}

登录后初始化websocket

// 这里监听登录状态,登录后再去初始化websocket,建立连接,根据自身项目调整 
watch:{ 
    isLogin: {     
        immediate: true,     
        handler: function (v) {       
            if (v) {         
            websocket.getInstance();       
            }       
            else{         
                websocket.destroy();       
            }     
        },   
    }, 
}

App.vue

socket01.png

例子:

比如监听到后端提醒前端需要去刷新页面上的消息个数时,在需要更新消息个数的组件上

import { socketMitt } from "@/utils/socket.js"; 

mounted() { 
// 监听是否需要刷新消息个数     
socketMitt.on(REFRESH_MESSAGECOUNT, () => {       
    this.getUserMessageCounts();     
    }); 
}

socket02.png

websocket参考:blog.csdn.net/qq_33666325…

心跳机制参考:wenku.baidu.com/view/69fbb7…