websocket+springboot+vue3实现服务器向客户端推送数据

之前一直对websocket非常好奇,今天花了差不多半个下午的时间来实践,还是学会了websocket基本的使用,同时也做了一个简单的demo。可供之后复习回顾使用,哈哈哈

websocket介绍

在单个TCP连接上进行全双工通信的协议,通讯双方建立长链接

  • http与websocket通讯方式示意图

1. 连接方式

  • HTTP: 是一种请求-响应协议。客户端发送请求,服务器处理并返回响应。每个请求都是独立的,连接在完成后会关闭。
  • WebSocket: 是一种持久连接协议。建立连接后,客户端和服务器可以随时互相发送数据,直到连接关闭。

2. 连接建立

  • HTTP: 连接每次请求都要重新建立,增加了延迟。
  • WebSocket: 通过一次 HTTP 请求建立连接后,后续数据传输不再需要重新建立连接。

3. 数据传输

  • HTTP: 采用文本格式,通常使用 JSON 或 XML 传输数据。
  • WebSocket: 可以传输文本和二进制数据,支持更高效的数据传输。

4. 实时性

  • HTTP: 不支持实时通信,无法实现服务器主动推送数据到客户端。
  • WebSocket: 支持实时双向通信,适合需要即时交互的应用,如在线聊天、游戏等。

5. 性能

  • HTTP: 每次请求和响应都需要开销,影响性能。
  • WebSocket: 由于连接持久化,减少了连接开销,提高了性能。

6. 适用场景

  • HTTP: 适用于静态网页、API 请求等场景。
  • WebSocket: 适用于需要实时更新的应用,如在线聊天、实时数据推送、多人游戏等。

总结

  • 如果需要实时、双向的通信,选择 WebSocket。
  • 如果是传统的请求-响应模式,使用 HTTP 更为合适。

后端部分

依赖导入

xml 复制代码
<!--websocket-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

websocketConfig类配置

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

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

实体类 (感觉没啥必要,可以选择不写)

java 复制代码
import lombok.Data;

import javax.websocket.Session;

@Data
public class SocketDomain {
    private Session session;

    private String uri;
}

WebsockerServer类实现(多阅读,思考)

在Spring中,@ServerEndpoint注解用于定义WebSocket端点,类似于Servlet的URL映射。"重点:每次客户端建立一个WebSocket链接都会创建一个新的WebSocketServer对象",

每次新的WebSocket连接会创建一个新的WebSocketServer对象

详细解释:

  1. @ServerEndpoint的行为

    • 根据JSR-356规范,@ServerEndpoint注解的类是按每个连接创建一个实例的。也就是说,每个WebSocket连接都会创建一个新的实例来处理该连接。
  2. Spring中的@ServerEndpoint

    • 虽然WebSocketServer类上使用了@Component注解,但@ServerEndpoint的行为不会受其影响。Spring支持JSR-356规范,因此会按照规范为每个WebSocket连接创建一个新的实例。
  3. 实例变量的影响

    • WebSocketServer类中,sessionclientName是实例变量,每个连接会有自己独立的实例变量,不会互相干扰。
    • 静态变量如onlineCountwebsocketMap是共享的,所有实例可以访问和修改这些变量,适用于统计在线用户数量和管理连接。
  4. 代码设计的合理性

    • 当前的设计是合理的,因为每个连接有独立的实例变量,而静态变量用于共享数据。
    • 如果WebSocketServer是单例,实例变量如sessionclienName会被多个连接共享,导致数据混乱。

结论:

每次新的WebSocket连接会创建一个新的WebSocketServer实例,每个实例独立处理各自的连接,静态变量用于共享数据

具体代码实现

java 复制代码
package com.example.crucialfunctiontest.server;

import com.example.crucialfunctiontest.model.entity.SocketDomain;
import io.netty.util.internal.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

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;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


/**
 * 在Spring中,@ServerEndpoint注解用于定义WebSocket端点,
 * 类似于Servlet的URL映射。对于问题"每次建立一个WebSocket链接是否会创建一个新的WebSocketServer对象",
 * 答案是:是的,每次新的WebSocket连接会创建一个新的WebSocketServer对象。
 *
 *
 */
@Component
@Slf4j
@ServerEndpoint("/websocket/{sid}")
public class WebSocketServer {//每次建立一个websocket链接 就会创建一个WebSocketServer对象

    //在线客户端数量
    private static int  onlineCount = 0; //类共享变量
    //Map用来存储已连接的客户端信息
    private static ConcurrentHashMap<String, SocketDomain> websocketMap = new ConcurrentHashMap<>();

    //当前连接客户端的Session信息
    private Session session; //实例变量 互不干扰
    //当前客户端名称
    private String clientName="";

    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid){

        if(!websocketMap.containsKey(sid)){
            WebSocketServer.onlineCount++;
        }
        this.session = session; //实例变量初始化
        this.clientName = sid;

        SocketDomain socketDomain = new SocketDomain();
        socketDomain.setSession(session);
        socketDomain.setUri(session.getRequestURI().toString());
        websocketMap.put(clientName, socketDomain); //clientName 唯一
        log.info("用户连接:"+ clientName + ",人数:"+onlineCount);
        try {
            sendMessage("服务端发送的消息,恭喜你连接成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    @OnClose
    public void onClose(){
        if(websocketMap.containsKey(clientName)){
            websocketMap.remove(clientName);
            onlineCount--;
            log.info("用户关闭:"+ clientName + ",人数:"+onlineCount);
        }
    }

    @OnMessage
    public void onMessage(String message,Session session){
        if(!StringUtil.isNullOrEmpty(message)){
            log.info("收到用户消息:"+clientName+",报文:"+message);
        }
    }

    //给当前客户端发消息
    private void sendMessage(String obj) {
        synchronized (session) {
            //嵌套代码块 锁住session 锁住 session 对象。
            //使用 session.getAsyncRemote().sendText(obj) 方法异步发送消息。
            this.session.getAsyncRemote().sendText(obj);
        }
    }

    //给指定客户端发送消息,通过clientName找到Session发送
    public void sendMessageTo(String clientName,String obj){
        SocketDomain socketDomain = websocketMap.get(clientName);
        try {
            if(socketDomain !=null){
                socketDomain.getSession().getAsyncRemote().sendText(obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }
    //给除了当前客户端的其他客户端发消息
    private void sendMessageToAllExpectSelf(String message, Session currentSession) {
        for(Map.Entry<String, SocketDomain> client : websocketMap.entrySet()){
            Session tempSeesion = client.getValue().getSession();
            if( !tempSeesion.getId().equals(currentSession.getId())&&tempSeesion.isOpen()){
                tempSeesion.getAsyncRemote().sendText(message);
                log.info("服务端发送消息给"+client.getKey()+":"+message);
            }
        }
    }
    //给包括当前客户端的全部客户端发送消息
    public void sendMessageToAll(String message){
        for(Map.Entry<String, SocketDomain> client : websocketMap.entrySet()){
            Session seesion = client.getValue().getSession();
            if(seesion.isOpen()){
                seesion.getAsyncRemote().sendText(message);
                log.info("服务端发送消息给"+client.getKey()+":"+message);
            }
        }
    }
    //给外部调用的方法接口
    public void sendAll(String Message){
        sendMessageToAll(Message);
    }

}

定时任务(每隔一段时间向前端推送数据,前端通过图表形式显示) spring corn表达式只能有六个参数

定时任务corn表达式 在线网站在线Cron表达式生成器 - 码工具

java 复制代码
@Component
@Slf4j
public class MyTask {

    @Autowired
    private WebSocketServer webSocketServer;
    @Scheduled(cron = "0/5 * * * * ?") //每隔五秒执行一次
    public void sendData(){
        //System.out.println("sendData");

        //发送一个数组 转为json字符串

        webSocketServer.sendAll("服务端发来数据hello world(所有人可以接收)");

        List<Integer> randomIntegers = new ArrayList<>();
        Random random = new Random();

        // 生成长度为 7 的随机整数列表
        for (int i = 0; i < 7; i++) {
            randomIntegers.add(random.nextInt(1000)); // 生成 0-99 之间的随机整数
        }
        String clientName="forerunner";
        //转为json字符串
        // 将 List 转换为 JSON 字符串
        String jsonString = JSON.toJSONString(randomIntegers);
        System.out.println(jsonString);
        webSocketServer.sendMessageTo(clientName,jsonString);

    }
}

前端vue3代码 -- 主要是onMessage(当接收到后端数据触发) onOpen(链接建立触发)函数

js 复制代码
<template>
  <div class="websocketTest">
  <div class="buttonArea">
    <el-button @click="webclick">给后台发送消息</el-button>
    <el-button type="danger"  plain @click="disConnected">断开链接</el-button>
  </div>

  <div id="myChart" >

  </div>

  </div>

</template>

<script setup>
import {useRoute} from "vue-router";
import {ref, onUnmounted, onMounted} from 'vue';
import api from "../../api/index.js";
import * as echarts from "echarts";

const ws = ref(null);
const route = useRoute();

/**
 * 网页路径应该是 http://127.0.0.1:5173/wsChart?name=forerunner
 */
let id
const paramInit=()=>{
  id=route.query['name'];
// 连接 WebSocket 服务端
  ws.value= new WebSocket(`ws://localhost:8099/websocket/${id}`);
}
// 定义事件处理函数
const onOpen = () => {
  ws.value.send(id+":服务已连接");
  console.log(id,":连接状态:", ws.value.readyState); // 1 表示连接打开
  let data=[1,2,3,4,5,6,7]
  initChart(data);
};

const onClose = () => {
  console.log("服务器关闭");
  console.log(id,":连接状态:", ws.value.readyState); // 3 表示连接关闭
};

const onMessage = (message) => {
  console.log(id,"收到服务器消息:", message.data);
  console.log(id,"消息类型", typeof message.data);
  console.log("连接状态:", ws.value.readyState);
  if(message.data.includes('[')){
    let data=JSON.parse(message.data);
    initChart(data);
  }
    // let data=JSON.parse(message.data);
    // initChart(data);
};

const onError = (error) => {
  console.error(id,"发生错误:", error);
  console.log("连接状态:", ws.value.readyState);
};

// 初始化 WebSocket 事件监听器
const wsInit = () => {
  ws.value.onopen = onOpen;
  ws.value.onclose = onClose;
  ws.value.onmessage = onMessage;
  ws.value.onerror = onError;
};


onMounted(() => {

  paramInit();
  wsInit();
  initChart()
})

const initChart = (data) => {
  const chart = echarts.init(document.getElementById('myChart'));
  chart.setOption({
        xAxis: {
          type: 'category',
          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
        },
        yAxis: {
          type: 'value'
        },
        series: [
          {
            data: data,
            type: 'line'
          }
        ]
      }
  );
}

// 关闭 WebSocket 连接
onUnmounted(() => {
  if (ws.value && ws.value.readyState === WebSocket.OPEN) {
    ws.value.close();
  }
});

// 发送消息方法
const webclick = () => {
  if (ws.value && ws.value.readyState === WebSocket.OPEN) {
    ws.value.send(id+"发送消息");
    console.log("消息已发送");
  } else {
    console.warn("WebSocket 连接未打开,无法发送消息");
  }
};

const disConnected = () => {
  if (ws.value && ws.value.readyState === WebSocket.OPEN) {
    ws.value.close();
  }
}


</script>

<style scoped>
.websocketTest{
  margin: auto;
  width: 60%;
  height: 600px;
  background-color: #fbfbfb;
  display: flex;
  flex-direction: column;
  align-items: center;

}
.buttonArea{
  margin-top: 10px;
  height: 10%;
}
#myChart{
  width: 80%;
  height: 90%;
  background-color: #FBFAFAFF;
}
</style>

具体效果

间隔五秒后端推送数据给前端 ,前端展示数据

相关推荐
zhglhy13 分钟前
springboot主要有哪些功能
java·spring boot·后端
华梦岚43 分钟前
Ruby语言的语法
开发语言·后端·golang
赵瑽瑾1 小时前
Groovy语言的物联网
开发语言·后端·golang
SomeB1oody3 小时前
【Rust中级教程】1.1. 指针概览(上):什么是指针、指针和引用的区别
开发语言·后端·rust
景天科技苑5 小时前
【Prometheus】如何通过prometheus监控springboot程序运行状态,并实时告警通知
spring boot·后端·prometheus·监控jvm·监控springboot
zhyhgx12 小时前
【Spring】什么是Spring?
java·后端·spring·java-ee
圆圆同学13 小时前
Springboot实现TLS双向认证
java·spring boot·后端
追逐时光者15 小时前
2025年推荐一些程序员常逛的开发者社区
后端·程序员
Benaso15 小时前
Rust unresolved import `crate::xxx` 报错解决
开发语言·后端·rust·actix
uhakadotcom15 小时前
JSONPath Plus Remote Code Execution (RCE) 漏洞
后端