WebSocket服务端数据推送及心跳机制(Spring Boot + VUE)

2023-10-29

一、WebSocket简介

HTML5规范在传统的web交互基础上为我们带来了众多的新特性,随着web技术被广泛用于web APP的开发,这些新特性得以推广和使用,而websocket作为一种新的web通信技术具有巨大意义。WebSocket是HTML5新增的协议,它的目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,比如说,服务器可以在任意时刻发送消息给浏览器。支持双向通信。

二、WebSocket通信原理及机制

websocket是基于浏览器端的web技术,那么它的通信肯定少不了http,websocket本身虽然也是一种新的应用层协议,但是它也不能够脱离http而单独存在。具体来讲,我们在客户端构建一个websocket实例,并且为它绑定一个需要连接到的服务器地址,当客户端连接服务端的时候,会向服务端发送一个消息报文

三、WebSocket特点和优点

1、支持双向通信,实时性更强。
2、更好的二进制支持。
3、较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
4、支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)
5、建立在tcp协议之上,服务端实现比较容易
6、数据格式比较轻量,性能开销小,通信效率高
7、和http协议有着良好的兼容性,默认端口是80和443,并且握手阶段采用HTTP协议,因此握手的时候不容易屏蔽,能通过各种的HTTP代理

四、WebSocket心跳机制

在使用websocket过程中,可能会出现网络断开的情况,比如信号不好,或者网络临时性关闭,这时候websocket的连接已经断开,而浏览器不会执行websocket 的 onclose方法,我们无法知道是否断开连接,也就无法进行重连操作。如果当前发送websocket数据到后端,一旦请求超时,onclose便会执行,这时候便可进行绑定好的重连操作。

       心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,同时客户端会确认服务器端是否还活着,如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了。需要重连~
 

五、在后端Spring Boot 和前端VUE中如何建立通信

1、在Spring Boot 中 pom.xml中添加 websocket依赖

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

2、创建 WebSocketConfig.java 开启websocket支持


 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
 
/**
 * 开启WebSocket支持
 * 
 */
@Configuration
public class WebSocketConfig {
 
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
 
        return new ServerEndpointExporter();
    }
 
}

3、创建 WebSocketServer.java 链接

package com.mes.dispatch.socket;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.HashMap;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;

/** @Author: best_liu
 * @Description:WebSocket服务
 * @Date: 13:05 2023/8/31
 * @Param
 * @return
 **/

@ServerEndpoint("/websocket/processSocket/{userId}")
@Slf4j
@Component
public class WebSocketServer {
    /**
     * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
     */
    private static int onlineCount = 0;
    /**
     * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
     */
    private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;
    /**
     * 接收userId
     */
    private String userId = "";

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.session = session;
        this.userId = userId;
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            //加入set中
        } else {
            webSocketMap.put(userId, this);
            //加入set中
            addOnlineCount();
            //在线数加1
        }

        log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount());

        try {
            HashMap<Object, Object> map = new HashMap<>();
            map.put("key", "连接成功");
            sendMessage(JSON.toJSONString(map));
        } catch (IOException e) {
            log.error("用户:" + userId + ",网络异常!!!!!!");
        }
    }


    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            //从set中删除
            subOnlineCount();
        }
        log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("用户消息:" + userId + ",报文:" + message);
        //可以群发消息
        //消息保存到数据库、redis
        if (StringUtils.isNotBlank(message)) {
            try {
                //解析发送的报文
                JSONObject jsonObject = JSONObject.parseObject(message);
                //追加发送人(防止串改)
                jsonObject.put("fromUserId", this.userId);
                String fromUserId = jsonObject.getString("fromUserId");
                //传送给对应toUserId用户的websocket
                if (StringUtils.isNotBlank(fromUserId) && webSocketMap.containsKey(fromUserId)) {
                    webSocketMap.get(fromUserId).sendMessage(jsonObject.toJSONString());
                    //自定义-业务处理

//                    DeviceLocalThread.paramData.put(jsonObject.getString("group"),jsonObject.toJSONString());
                } else {
                    log.error("请求的userId:" + fromUserId + "不在该服务器上");
                    //否则不在这个服务器上,发送到mysql或者redis
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 发生错误时候
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        //加入线程锁
        synchronized (session) {
            try {
                //同步发送信息
                this.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("服务器推送失败:" + e.getMessage());
            }
        }
    }

    /** @Author: best_liu
     * @Description:发送自定义消息
     * @Date: 13:01 2023/8/31
     * @Param [message, toUserId]
     * @return void
     **/
    public static void sendInfo(String message, String toUserId) throws IOException {
        //如果userId为空,向所有群体发送
        if (StringUtils.isEmpty(toUserId)) {
            //向所有用户发送信息
            Iterator<String> itera = webSocketMap.keySet().iterator();
            while (itera.hasNext()) {
                String keys = itera.next();
                WebSocketServer item = webSocketMap.get(keys);
                item.sendMessage(message);
            }
        }
        //如果不为空,则发送指定用户信息
        else if (webSocketMap.containsKey(toUserId)) {
            WebSocketServer item = webSocketMap.get(toUserId);
            item.sendMessage(message);
        } else {
            log.error("请求的userId:" + toUserId + "不在该服务器上");
        }
    }


    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }

    public static synchronized ConcurrentHashMap<String, WebSocketServer> getWebSocketMap() {
        return WebSocketServer.webSocketMap;
    }

}

4、创建一个测试调用websocket发送消息 TimerSocketMessage.java (用定时器发送推送消息


 
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
 
import java.util.HashMap;
import java.util.Map;
 
 
@Component
@EnableScheduling
public class TimerSocketMessage {
 
    /**
     * 推送消息到前台
     */
    @Scheduled(cron = "*/5 * * * * * ")
    public void SocketMessage(){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Map<String, Object> maps = new HashMap<>();
        maps.put("type", "sendMessage");
        maps.put("data", sdf.format(new Date()));
        WebSocketServer.sendInfo(maps);
    }
}

5、在VUE中创建和后端 websocket服务的连接并建立心跳机制。

<template>
  <div>
    <el-row :gutter="$ui.layout.gutter.g10">
      <el-col :span="$ui.layout.span.one" style="background-color: #FFFFFF; padding: 10px;">
        <el-form ref="form" :model="form" label-width="80px" size="small" :inline="true">
          <el-form-item label="生成数量">
            <el-input-number v-model="form.number" :min="1" :max="999" label="描述文字" />
          </el-form-item>

          <el-form-item label="数值范围">
            <el-input-number v-model="form.start" :min="1" :max="9999999999" label="描述文字" /> ~
            <el-input-number v-model="form.end" :min="1" :max="9999999999" label="描述文字" />
          </el-form-item>

          <el-form-item>
            <el-button size="mini" type="primary" @click="spawn">生成</el-button>
          </el-form-item>
        </el-form>

      </el-col>
    </el-row>
    <h1> websocket 消息推送测试:{{data}}</h1>

  </div>
</template>

<script>
export default {
  name: 'Index',
  data() {
    return {
      form: {
        number: 1,
        start: 1,
        end: 100
      },
      data:0,
      timeout: 28 * 1000,//30秒一次心跳
      timeoutObj: null,//心跳心跳倒计时
      serverTimeoutObj: null,//心跳倒计时
      timeoutnum: null,//断开 重连倒计时
      websocket: null,

    }
  },
  created () {
    // 初始化websocket
    this.initWebSocket()
  },
  methods: {
    spawn() {
      
    },
    //socket--start
    initWebSocket() {
        let url = 'ws://localhost/dev-api/process/websocket/processSocket/zkawsystem'
        this.websocket = new WebSocket(url)
        // 连接错误
        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.onbeforeunload
      },
      reconnect() { // 重新连接
        if (this.lockReconnect) return;
        this.lockReconnect = true;
        //没连接上会一直重连,设置延迟避免请求过多
        this.timeoutnum && clearTimeout(this.timeoutnum);
        this.timeoutnum = setTimeout(() => {
          //新连接
          this.initWebSocket();
          this.lockReconnect = false;
        }, 5000);
      },
      reset() { // 重置心跳
        // 清除时间
        clearTimeout(this.timeoutObj);
        clearTimeout(this.serverTimeoutObj);
        // 重启心跳
        this.start();
      },
      start() { // 开启心跳
        this.timeoutObj && clearTimeout(this.timeoutObj);
        this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
        this.timeoutObj = setTimeout(() => {
          // 这里发送一个心跳,后端收到后,返回一个心跳消息,
          if (this.websocket && this.websocket.readyState == 1) { // 如果连接正常
            let actions = { "heartbeat": "12345" };
            this.websocketsend(JSON.stringify(actions));
          } else { // 否则重连
            this.reconnect();
          }
          this.serverTimeoutObj = setTimeout(() => {
            //超时关闭
            this.websocket.close();
          }, this.timeout);

        }, this.timeout)
      },
      setOnmessageMessage(event) {
        let obj = JSON.parse(event.data);
        console.log("obj", obj)
        switch (obj.type) {
          case 'heartbeat':
            //收到服务器信息,心跳重置
            this.reset();
            break;
          case 'sendMessage':
            this.data = obj.data
            console.log("接收到的服务器消息:", obj.data)
        }

      },
      setErrorMessage() {
        //重连
        this.reconnect();
        console.log("WebSocket连接发生错误" + '   状态码:' + this.websocket.readyState)
      },
      setOnopenMessage() {
        //开启心跳
        this.start();
        console.log("WebSocket连接成功" + '   状态码:' + this.websocket.readyState)
      },
      setOncloseMessage() {
        //重连
        this.reconnect();
        console.log("WebSocket连接关闭" + '   状态码:' + this.websocket.readyState)
      },
      onbeforeunload() {
        this.closeWebSocket();
      },
      //websocket发送消息
      websocketsend(messsage) {
        this.websocket.send(messsage)
      },
      closeWebSocket() { // 关闭websocket
        this.websocket.close()
      },
      //socket--end

  }
}
</script>

6、启动项目开始测试结果

 7、vue文件连接websocket的url地址要拼接 context-path: /demo

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

WebSocket服务端数据推送及心跳机制(Spring Boot + VUE) 的相关文章

随机推荐

  • 【opencv4.3.0教程】01之opencv介绍与配置(win10+VS2015+OpenCV4.3.0)

    目录 一 前言 二 OpenCV介绍 1 介绍 2 OpenCV版本简介 3 OpenCV4 3 0下载 三 OpenCV安装与配置 1 安装 2 环境变量配置 四 配置VS2015 1 包含目录与库目录 2 链接器配置 五 测试及效果 一
  • Ajax vs Willem II,Feyenoord on top after beating Ajax 2-1

    Feyenoord on top after beating Ajax 2 1 Soccer Updated 2005 08 29 11 07 AMSTERDAM Netherlands Dirk Kuijt and Salomon Kal
  • 【概率论与数理统计】猴博士 笔记 p3-4 事件的概率、事件的独立性

    事件的概率 引入 画图 假设方块面积为1 那么P A 的数值就是点落在A上的概率 我们可以通过画图求出很多概率 如 P A B 0 25 P B A 0 23 P A B 0 58 一些概念 例1 解 0 3 画个图就行 例2 解 5 12
  • Windows平台下安装与配置MySQL ,配置环境变量,详细图解,

    1 安装检查 下载之前要看一下Windows版本 如果是专业版我们在安装之前需要多一步检查操作 如果是专业版我们需要在计算机管理中检查管理员属性中是否添加网络服务的属性 红框部分 计算机管理 gt 本地用户和组 gt 组 gt 双击Admi
  • C++复数运算

    C 复数运算探究 题目说明 抽象数据类型 ADT 的定义与实现 复数a bi a为实部 b为虚部 请用C或C 语言定义和实现复数抽象数据类型 要求能够输入两个实数作为实部和虚部 用于初始化 创建 一个复数 对任意的两个复数 能够按照复数运算
  • TypeC 基础

    type C接口形式 PD最大支持20V 5A 100W功率 通过CC线来协商Power供给 由于Type C的扩展功能 SBU1 SBU2 大部分配件诸如耳机 视频接口 debug接口等都可以实现兼容设计 在USB2 0端口 USB根据输
  • C++学习之路-构造函数的初始化列表

    构造函数 初始化列表 一 何为初始化列表 二 初始化列表的本质 三 初始化列表的优势 四 初始化列表中列表顺序问题 五 初始化列表与默认参数的配合使用 六 初始化列表的注意之处 七 构造函数的声明和实现分离时 初始化列表需写在实现里 八 子
  • 回归与分类区别

    回归与分类的根本区别在于输出空间是否为一个度量空间 我们不难看到 回归问题与分类问题本质上都是要建立映射关系 对于回归问题 其输出空间B是一个度量空间 即所谓 定量 也就是说 回归问题的输出空间定义了一个度量 去衡量输出值与真实值之间的 误
  • AGX Xavier使用记录

    整理了遇到的一些问题 Jetson AGX Xavier上查看版本 格格 gloria 博客园 Nvidia agx xavier TX2 无法查看opencv版本问题 Cc CSDN博客 Project cv bridge specifi
  • hdu1799(用递推公式求组合的个数)

    题目意思 我们知道 在编程中 我们时常需要考虑到时间复杂度 特别是对于循环的部分 例如 如果代码中出现 for i 1 i lt n i OP 那么做了n次OP运算 如果代码中出现 fori 1 i lt n i for j i 1 j l
  • Windows平台如何查看一个dll依赖的其他dll

    好多开发者在做windows开发的时候 容易遇到dll依赖的问题 VS自带一个小工具dumpbin 这个工具挺好用 可以查看dll相关依赖库 还可以看dll导出接口 下面演示下查依赖库用法 运行 dumpbin dependents nm
  • axios相应拦截器弹窗的实现

    在axios中同一封装 将请求之后code不等于0的数据进行弹窗显示 在封装axios的时候 通过require导入elementUI 之后调用message方法 import axios from axios var ui require
  • 【Jmeter线程组及报告解析】

    前言 一 线程组解析 1 含义 2 案例 3 各类线程执行顺序 二 报告解析 1 常用的压测报告 2 View Results Tree 介绍 3 Aggregate Report 聚合报告 介绍 前言 本章主要针对压测时常用的 线程组 压
  • 【FPGA IP系列】FIFO的通俗理解

    FPGA厂商提供了丰富的IP核 基础性IP核都是可以直接免费调用的 比如FIFO RAM等等 本文主要介绍FIFO的一些基础知识 帮助大家能够理解FIFO的基础概念 一 FIFO介绍 FIFO全称是First In First Out 即先
  • Unity热更新 ILRuntime 从零开始 继承 Inheritance(五)

    Unity热更新 ILRuntime 从零开始 继承 Inheritance 五 前言 一 继承分类 二 跨域继承的用法 1 继承适配器 2 实际应用 总结 版权声明 前言 我们继续来一起看下ILRuntime的第四个案例 Inherita
  • Java一键授权方案 离线授权 日期授权 代码授权 代码混淆

    Java软件部署到客户端 有时没外网 有时需要对模块时效进行控制 但是通常一般性的lic号注册 很容易被破解 屏蔽 不能保证软件的版权和收益 中小型软件又不能再安全方面投入太大 这时该如何做授权功能呢 我现在向您介绍的是一套具体的授权加密方
  • Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by

    使用node js连接mysql数据库报如下错误 Error ER NOT SUPPORTED AUTH MODE Client does not support authentication protocol requested by s
  • 8、抽象类、接口、多态、向上转型、向下转型

    一 final关键字 1 可以修饰变量 方法 类 2 修饰变量时 变量的值不能再改变 成为一个常量 3 修饰方法时 被修饰的方法不能被修改 4 修饰类时 这个类不能被继承 并且类中的成员方法会隐式地被final修饰 5 当final修饰一个
  • stm32 ucos/ii移植,程序执行到OSStart()内部的OSStartHighRdy()语句时跑飞问题解决方法之一

    stm32 ucos ii移植 程序执行到OSStart 内部的OSStartHighRdy 语句时跑飞问题解决办法之一 网络上的一些解决办法 stm32程序遇到OSStartHang的问题解决方法总结 但并不适合我遇到的情况 我的情况是已
  • WebSocket服务端数据推送及心跳机制(Spring Boot + VUE)

    一 WebSocket简介 HTML5规范在传统的web交互基础上为我们带来了众多的新特性 随着web技术被广泛用于web APP的开发 这些新特性得以推广和使用 而websocket作为一种新的web通信技术具有巨大意义 WebSocke