1. 常见的消息推送方式

1.1 轮询

浏览器以指定的时间(如js的定时任务)间隔向服务器发送请求,服务器将数据返回给浏览器

如果客户端第一次发送请求到服务端,但是服务端没有新的数据,此时服务端返回一个空的数据给前端。恰好此时服务端有新的数据了,那这些新的数据也只能等待下一次发起请求才能返回数据,这就导致数据有一定的延时。


1.2 长轮询

浏览器向服务端发送请求,服务端收到请求后,将请求挂起,就会出现两种情况,一种是直到有新的数据时给客户端响应;第二种事超时才会给客户端响应

相对于短轮询,长轮询在服务端有挂起了,将发送请求的时间间隔设置长一些就可以减少数据库的压力。


1.3 SSE(server-sent event): 服务器发送事件

  • SSE在服务器和客户端之间打开的单向通信通道
  • 服务端响应的不再事一次性的数据包,而是 text/event-stream 流类型的数据流信息
  • 服务端有数据变更时将数据流传递到客户端

1.4 websocket 

websocket 是一种基于TCP连接上进行全双工通信的协议

全双工(Full Duplex):允许数据在两个方向上同时传输

半双工 (Half Duplex):允许数据在两个方向上传输,但同一个时间段内只允许一个方向传输

2. Websocket介绍

1. 概述

Websocket 是基于TCP的一种新的网络协议。允许在单个 TCP 连接上进行全双工通信。与传统的 HTTP 请求-响应模式不同,WebSocket 提供了持久的连接,服务器和客户端可以随时主动推送数据,无需反复建立连接,可以进行双向数据传输。客户端可以向服务器发送数据,服务器也可以向客户端发送消息。

HTTP协议与WebSocket协议对比

不同点

HTTP是短连接,WebStocket是长连接。

HTTP通信是单向的,WebSocket通信是持久双向的

相同点

二者的底层都是TCP连接

HTTP 连接响应 

客户端请求一次,服务端响应一次。

WebStocket 连接响应

客户端与服务端建立连接后,服务端可随时向客户端发送消息

应用场景

建立一次连接后,客户端与服务器持久连接,客户端可以向服务器发送数据,服务器也可以向客户端发送消息。

  • 视频弹窗,如视频弹幕
  • 网页聊天,如百度上的一对一客服
  • 股票基金报价实时更新

2. WebSocket 前端 API

websocket对象创建

let ws = new WebSocket(URL格式)

URL格式:协议://ip地址/访问路径

                   协议:协议名称为 ws

WebSocket 事件

事件事件处理程序描述
openws.onopen连接建立时触发
messagews.onmessage客户端接收服务端数据时触发
errorws.onerror通信发生错误时触发
closews.onclose连接关闭时触发
<srcipt>
    let ws = new WebSocket("ws://localhost/chat")
    
    ws.onopen = function() {  }

    ws.onmessage = function(evt) {  
        // 通过 evt.data 可以获取服务器发送的数据
    }
    
    // websocket 连接断开后触发
    ws.onclose = function() { }
</srcipt>

 WebSocket 方法

方法描述
ws.send()连接成功后,关闭前,发送消息
ws.close()关闭连接

3. WebSocket 后端API

tomcat 7.0.5 版本开始支持websocket 

java websocket 应用由一系列的 Endpoint 组成,Endpoint 是个java对象,代表websocket 链接的一端,对于服务端,可以视作处理具体websocket消息的接口。可以理解为两个人面对面聊天所建立的一对一关系。

两种方式定义Endpoint 

第一种编程式: 即集成类 java.websocket.Endpoint 并实现其方法

第二种注解式:定义要给POJO, 并添加@serverEndpoint 相关注解

方法描述注解
onOpen()当开启一个新的会话时调用,该方法是客户端和服务端连接后调用@OnOpen
onClose()会话关闭时调用@OnClose
onError()链接异常时调用@OnError

4. WebSocket 心跳机制

为什么要心跳机制

在WebSocket使用心跳机制是为了保持 WebSocket 连接的活跃状态而设计的一种机制。在长时间没有数据传输的情况下,WebSocket 连接可能会被中间的网络设备(如路由器或防火墙)认为已经断开,从而导致连接失效。但是服务端和客户端并不知道连接失效了,还在发送数据,但这些数据都接收不到。心跳机制通过定期发送小的数据包来保持连接的活动状态,从而避免这种情况的发生。

心跳机制原理 

 心跳机制原理定期发送小的数据包,保证WebSocket 不被断开。

心跳机制的工作流程

初始化WebSocket 时开启心跳检测,就是定义一个定时器,定时向后端(服务端)发送一次信息,等待后端(服务端)给前端(客户端)一个响应消息,假设数据发到服务端没有响应消息,被视为断开连接,需要重新初始化websocket方法连接;假设数据发到服务端有响应信息,此时认为连接正常,重置定时器,重新开始计时。

<template>
  <!-- websocketceshi -->
  <div class="layout">
    <div class="msgBody">{{ msg }}</div>
    <input
      v-model="sendMsg"
      style="width: 200px; height: 30px; margin-top: 20px"
    />
    <button @click="websocketsend(sendMsg)" style="width: 100px; height: 30px">
      发送
    </button>
    <button @click="websocketclose" style="width: 100px; height: 30px">
      断开链接
    </button>
    <button @click="initWebSocket" style="width: 100px; height: 30px">
      建立链接
    </button>
  </div>
</template>

<script>
export default {
  name: "LayOut",
  data() {
    return {
      websock: null, //建立的连接
      lockReconnect: false, //是否真正建立连接
      timeout: 20 * 1000, //20秒一次心跳
      timeoutObj: null, //心跳心跳倒计时
      serverTimeoutObj: null, //心跳倒计时
      timeoutnum: null, //断开 重连倒计时
      msg: "", //显示的值
      sendMsg: "", //输入框的值
    };
  },
  created() {
    // //页面刚进入时开启长连接
    this.initWebSocket();
  },
  destroyed() {
    //页面销毁时关闭长连接
    this.websocketclose();
  },
  methods: {
          //建立连接,初始化weosocket
    initWebSocket() {
      //后台地址,前面的ws不动,后面是后台地址,我是本地运行的所以填的本地,自行更改。再后面ws是后端的接口地址,admin是参数
      const wsuri = "ws://localhost:8001/ws/admin";
      //建立连接
      this.websock = new WebSocket(wsuri);
      //连接成功
      this.websock.onopen = this.websocketonopen;
      //连接错误
      this.websock.onerror = this.websocketonerror;
      //接收信息
      this.websock.onmessage = this.websocketonmessage;
      //连接关闭
      this.websock.onclose = this.websocketclose;
    },
    reconnect() {
      //重新连接
      var that = this;
      //判断链接状态,true就是链接,false是断开,这里如果是链接状态就不继续执行了,跳出来。
      if (that.lockReconnect) {
        return;
      }
      //把链接状态改为true
      that.lockReconnect = true;
      //没连接上会一直重连,设置延迟避免请求过多
      that.timeoutnum && clearTimeout(that.timeoutnum);
      that.timeoutnum = setTimeout(function () {
        //初始化新连接
        that.initWebSocket();
        //把链接状态改为false
        that.lockReconnect = false;
      }, 5000);
    },
    reset() {
      //重置心跳
      var that = this;
      //清除时间
      clearTimeout(that.timeoutObj);
      clearTimeout(that.serverTimeoutObj);
      //重启心跳
      that.start();
    },
    start() {
      //开启心跳
      var self = this;
      //有延迟时间的就清除掉
      self.timeoutObj && clearTimeout(self.timeoutObj);
      self.serverTimeoutObj && clearTimeout(self.serverTimeoutObj);
      //从新创建计时器
      self.timeoutObj = setTimeout(function () {
        //这里发送一个心跳,后端收到后,返回一个心跳消息
        if (self.websock.readyState == 1) {
          //如果连接正常发送信息到后台
          self.websock.send("ping");
        } else {
          //否则重连
          self.reconnect();
        }
        self.serverTimeoutObj = setTimeout(function () {
          //超时关闭
          self.websock.close();
        }, self.timeout);
      }, self.timeout);
    },
    //链接成功时执行的方法
    websocketonopen() {
      //连接成功事件
      this.websocketsend("发送数据");
      //提示成功
      console.log("连接成功", 3);
      //开启心跳
      this.start();
    },
    //连接失败事件
    websocketonerror(e) {
      //错误
      console.log("WebSocket连接发生错误");
      //重连
      this.reconnect();
    },
    //连接关闭事件
    websocketclose(e) {
      this.websock.close();
      //提示关闭
      console.log("连接已关闭");
      //重连
      this.reconnect();
    },
    //接收服务器推送的信息
    websocketonmessage(event) {
      //打印收到服务器的内容
      console.log("收到服务器信息", event.data);
      this.msg = event.data;
      //收到服务器信息,心跳重置
      this.reset();
    },
    websocketsend(msg) {
      //向服务器发送信息
      this.websock.send(msg);
    },
  },
};
</script>
<style scoped>
.layout {
  position: relative;
  width: 100%;
  height: 100%;
}
.msgBody {
  width: 500px;
  height: 300px;
  border: 1px solid rgb(95, 79, 79);
}
</style>

 3. SpringBoot整合WebStocket

1). 使用页面作为WebSocket客户端

2). 导入WebSocket的maven坐标

3). 新建WebSocket服务端组件WebSocketServer,用于和客户端通信

4). 新建配置类WebSocketConfiguration,注册WebSocket的服务端组件

5). 新建定时任务类WebSocketTask,定时向客户端推送数据

6). SpringBoot启动类开启定时任务注解

1. 前端页面

<template>
<!-- websocketceshi -->
  <div class="layout">
    <div class="msgBody">{{ msg }}</div>
    <input v-model="sendMsg" style="width:200px;height:30px;margin-top:20px"/>
    <button @click="sendMessage" style="width:100px;height:30px;">发送</button>
    <button @click="close" style="width:100px;height:30px;">断开链接</button>
    <button @click="init" style="width:100px;height:30px;">建立链接</button>
  </div>
</template>

<script>
export default {
  name: "LayOut",
  data() {
    return {
      msg: "",
      sendMsg: "",
      //后台的地址,只需要动localhost:8089部分,改成你后端的地址。
      //后面webSocket是后台设定的接口地址,uuid_test001是你前台的客户端唯一id(可以使用uuid生成)。
      //用于区分不同的客户端,比如你多个客户端连接后台,后台推送数据的时候需要根据这个id不同,给对应的人推送,不然就推送到所有建立链接的网页上了
      path: "ws://localhost:8089/qf/webSocket/uuid_test001",
      //存websocket实例化的
      socket: "",
    };
  },
  methods: {
    //用于前台发送数据到后台,调用websocket中的send方法把数据发过去。
    sendMessage() {
      this.socket.send(this.sendMsg);
    },
    //初始化建立前后台链接
    init() {
      if (typeof WebSocket === "undefined") {
        alert("您的浏览器不支持socket");
      } else {
        // 实例化socket
        this.socket = new WebSocket(this.path);
        // 监听socket连接
        this.socket.onopen = this.open;
        // 监听socket错误信息
        this.socket.onerror = this.error;
        // 监听socket消息
        this.socket.onmessage = this.getMessage;
        this.socket.onclose = this.close;
      }
    },
    //链接成功时的回调函数
    open() {
      console.log("socket连接成功");
    },
    //链接错误时的回调
    error(err) {
      console.log("连接错误" + err);
    },
    //后台消息推送过来,接收的函数,参数为后台推过来的数据。
    getMessage(msg) {
      this.msg = msg.data;
    },
    //链接关闭的回调
    close(event) {
      //socket是链接的实例,close就是关闭链接
      this.socket.close()
      console.log("断开链接成功");
    },
  },
  created() {
    //开局初始化建立链接
    this.init();
  },
};
</script>
<style scoped>
.layout {
  position: relative;
  width: 100%;
  height: 100%;
}
.msgBody {
  width: 500px;
  height: 300px;
  border: 1px solid rgb(95, 79, 79);
}
</style>
export default {
  name: 'test',
  data() {
    return {
      ws: null,   // ws变量
      uid: null,   // 用户id
      tableData: null,
    }
  },
  mounted() {
    this.initWebSocket()
  },
  destroyed: function() {
    //页面销毁时关闭
    this.ws.onclose =  this.websocketclose();
  },
  methods: {
    // 连接websocket
    initWebSocket() { 
      this.uid = this.$store.getters.username  
      this.ws = new WebSocket(`ws://localhost:8096/wsMessage/${this.uid}`)
      this.ws.onopen = this.websocketonopen;
      this.ws.onerror = this.websocketonerror;
      this.ws.onmessage = this.websocketonmessage; 
      this.ws.onclose = this.websocketclose;
    }, 
    websocketonopen() { //发送消息
      this.ws.send('我来了'); 
      console.log("WebSocket连接成功");
    },
    websocketonerror(e) { //连接错误
      console.log("WebSocket连接发生错误");
      // 此时需要重连
      setTimeout(() => {
        this.initWebSocket();
      }, 2000);
    },
    websocketonmessage(e){ //数据接收  
      //处理逻辑
      console.log(JSON.stringify(e.data))
      this.tableData = e.data
    }, 
    websocketclose(e){ //连接关闭 
      console.log("connection closed (" + e.code + ")"); 
    },
  }
}

2. 在pom.xml文件中导入webStocket依赖

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

3.  新建一个类名为WebSocketConfig 的配置类,用于注册WebSocket的服务端组件。

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket配置类,用于注册WebSocket的Bean,
 * 确保它们在应用程序启动时被正确初始化和注册,以便能够处理WebSocket连接和通信。
 */

@Configuration
public class WebSocketConfig {

    /**
     * 该方法用来创建并返回一个ServerEndpointExporter实例。
     * 这个实例的作用是扫描并自动配置所有使用@ServerEndpoint注解标记的WebSocket端点
     *
     * @return ServerEndpointExporter:这是一个用于自动检测和管理WebSocket端点的类。
     *          通过将其实例化并配置为Spring管理的Bean,可以确保所有WebSocket端点在应用程序启动时被自动初始化和注册。
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

4. WebSocket 服务类如下,与controller同级目录新建一个包,在该包下新建一个WebSocketServer 服务类。用于和客户端通信。

package com.example.websocket;

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.Collection;
import java.util.HashMap;
import java.util.Map;


/**
 * 该类负责监听客户端的连接、断开连接、接收消息、发送消息等操作。
 * */
@Slf4j
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    //存放会话对象
    private static Map<String, Session> sessionMap = new HashMap();

    /**
     * 连接建立成功调用的方法
     * @param session 第一个参数必须是session
     * @param sid   代表客户端的唯一标识
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        log.info("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        log.info("收到来自客户端:" + sid + "的信息:" + message);
    }

    /**
     * 连接关闭调用的方法
     *
     * @param sid
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        log.info("连接断开:" + sid);
        sessionMap.remove(sid);
    }

    /**
     * 群发
     *
     * @param message
     */
    public void sendToAllClient(String message) {
        Collection<Session> sessions = sessionMap.values();
        for (Session session : sessions) {
            try {
                //服务器向客户端发送消息 , 发送文本信息有两种方式,一种是getBasicRemote,一种是getAsyncRemote
                //区别:getAsyncRemote是异步的,发送消息后立即返回,不阻塞当前线程.
                // 而getBasicRemote是同步的,发送消息时会阻塞当前线程,直到消息发送完成
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

5. 新建一个定时任务类WebSocketTask,将 WebSocketServer 服务类注入进来。

package com.example.task;

import com.example.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
public class WebSocketTask {
    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 通过WebSocket每隔5秒向客户端发送消息
     */
    //@Scheduled(cron = "0/5 * * * * ?")
    public void sendMessageToClient() {
        webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
    }
}

6). 在SpringBoot启动类开启定时任务调度。

@EnableScheduling //开启定时任务调度

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐