Springboot Websocket Stomp 消息订阅推送实战

需求背景

闲话不扯,直奔主题。需要和web前端建立长链接,互相实时通讯,因此想到了websocket,后面随着需求的变更,需要用户订阅主题,实现消息的精准推送,发布订阅等,则想到了STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的简单文本协议。

websocket协议

想到了之前写的一个websocket长链接的demo,也贴上代码供大家参考。

  1. pom文件
    直接引入spring-boot-starter-websocket即可。
    	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

  1. 声明websocket endpoint
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @ClassName WebSocketConfig
 * @Author scott
 * @Date 2021/6/16
 * @Version V1.0
 **/
@Configuration
public class WebSocketConfig {

    /**
     * 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}
  1. websocket实现类,其中通过注解监听了各种事件,实现了推送消息等相关逻辑
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.ruoyi.common.core.domain.AjaxResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @ClassName: DataTypePushWebSocket
 * @Author: scott
 * @Date: 2021/6/16
**/
@ServerEndpoint(value = "/ws/dataType/push/{token}")
@Component
public class DataTypePushWebSocket {

    private static final Logger log = LoggerFactory.getLogger(DataTypePushWebSocket.class);

    /**
     * 记录当前在线连接数
     */
    private static AtomicInteger onlineCount = new AtomicInteger(0);

    private static Cache<String, Session> SESSION_CACHE = CacheBuilder.newBuilder()
            .initialCapacity(10)
            .maximumSize(300)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("token")String token) {
        String sessionId = session.getId();
        onlineCount.incrementAndGet(); // 在线数加1
        this.sendMessage("sessionId:" + sessionId +",已经和server建立连接", session);
        SESSION_CACHE.put(sessionId,session);
        log.info("有新连接加入:{},当前在线连接数为:{}", session.getId(), onlineCount.get());
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session,@PathParam("token")String token) {
        onlineCount.decrementAndGet(); // 在线数减1
        SESSION_CACHE.invalidate(session.getId());
        log.info("有一连接关闭:{},当前在线连接数为:{}", session.getId(), onlineCount.get());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session,@PathParam("token")String token) {
        log.info("服务端收到客户端[{}]的消息:{}", session.getId(), message);
        this.sendMessage("服务端已收到推送消息:" + message, session);
    }

    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误");
        error.printStackTrace();
    }

    /**
     * 服务端发送消息给客户端
     */
    private static void sendMessage(String message, Session toSession) {
        try {
            log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);
            toSession.getBasicRemote().sendText(message);
        } catch (Exception e) {
            log.error("服务端发送消息给客户端失败:{}", e);
        }
    }

    public static AjaxResult sendMessage(String message, String sessionId){
        Session session = SESSION_CACHE.getIfPresent(sessionId);
        if(Objects.isNull(session)){
            return AjaxResult.error("token已失效");
        }
        sendMessage(message,session);
        return AjaxResult.success();
    }

    public static AjaxResult sendBroadcast(String message){
        long size = SESSION_CACHE.size();
        if(size <=0){
            return AjaxResult.error("当前没有在线客户端,无法推送消息");
        }
        ConcurrentMap<String, Session> sessionConcurrentMap = SESSION_CACHE.asMap();
        Set<String> keys = sessionConcurrentMap.keySet();
        for (String key : keys) {
            Session session = SESSION_CACHE.getIfPresent(key);
            DataTypePushWebSocket.sendMessage(message,session);
        }

        return AjaxResult.success();

    }

}

至此websocket服务端代码已经完成。

stomp协议
  1. 前端代码.这个是在某个vue工程中写的js,各位大佬自己动手改改即可。其中Settings.wsPath是后端定义的ws地址例如ws://localhost:9003/ws
import Stomp from 'stompjs'
import Settings from '@/settings.js'

export default {
  // 是否启用日志 默认启用
  debug:true,
  // 客户端连接信息
  stompClient:{},
  // 初始化
  init(callBack){
    this.stompClient = Stomp.client(Settings.wsPath)
    this.stompClient.hasDebug = this.debug
    this.stompClient.connect({},suce =>{
      this.console("连接成功,信息如下 ↓")
      this.console(this.stompClient)
      if(callBack){
        callBack()
      }
    },err => {
      if(err) {
        this.console("连接失败,信息如下 ↓")
        this.console(err)
      }
    })
  },
  // 订阅
  sub(address,callBack){
    if(!this.stompClient.connected){
      this.console("没有连接,无法订阅")
      return
    }
    // 生成 id
    let timestamp= new Date().getTime() + address
    this.console("订阅成功 -> "+address)
    this.stompClient.subscribe(address,message => {
      this.console(address+" 订阅消息通知,信息如下 ↓")
      this.console(message)
      let data = message.body
      callBack(data)
    },{
      id: timestamp
    })
  },
  unSub(address){
    if(!this.stompClient.connected){
      this.console("没有连接,无法取消订阅 -> "+address)
      return
    }
    let id = ""
    for(let item in this.stompClient.subscriptions){
      if(item.endsWith(address)){
        id = item
        break
      }
    }
    this.stompClient.unsubscribe(id)
    this.console("取消订阅成功 -> id:"+ id + " address:"+address)
  },
  // 断开连接
  disconnect(callBack){
    if(!this.stompClient.connected){
      this.console("没有连接,无法断开连接")
      return
    }
    this.stompClient.disconnect(() =>{
      console.log("断开成功")
      if(callBack){
        callBack()
      }
    })
  },
  // 单位 秒
  reconnect(time){
    setInterval(() =>{
      if(!this.stompClient.connected){
        this.console("重新连接中...")
        this.init()
      }
    },time * 1000)
  },
  console(msg){
    if(this.debug){
      console.log(msg)
    }
  },
  // 向订阅发送消息
  send(address,msg) {
    this.stompClient.send(address,{},msg)
  }
}
  1. 后端stomp config,里面都有注释,写的很详细,并且我加入了和前端的心跳ping pong。
package com.cn.scott.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @ClassName: WebSocketStompConfig
 * @Author: scott
 * @Date: 2021/7/8
**/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {

    private static long HEART_BEAT=10000;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //允许使用socketJs方式访问,访问点为webSocket,允许跨域
        //在网页上我们就可以通过这个链接
        //ws://127.0.0.1:port/ws来和服务器的WebSocket连接
        registry.addEndpoint("/ws").setAllowedOrigins("*");
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        ThreadPoolTaskScheduler te = new ThreadPoolTaskScheduler();
        te.setPoolSize(1);
        te.setThreadNamePrefix("wss-heartbeat-thread-");
        te.initialize();
        //基于内存的STOMP消息代理来代替mq的消息代理
        //订阅Broker名称,/user代表点对点即发指定用户,/topic代表发布广播即群发
        //setHeartbeatValue 设置心跳及心跳时间
        registry.enableSimpleBroker("/user", "/topic").setHeartbeatValue(new long[]{HEART_BEAT,HEART_BEAT}).setTaskScheduler(te);
        //点对点使用的订阅前缀,不设置的话,默认也是/user/
        registry.setUserDestinationPrefix("/user/");
    }
}
  1. 后端stomp协议接受、订阅等动作通知
package com.cn.scott.ws;

import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @ClassName StompSocketHandler
 * @Author scott
 * @Date 2021/6/30
 * @Version V1.0
 **/
@RestController
public class StompSocketHandler {

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    /**
    * @MethodName: subscribeMapping
     * @Description: 订阅成功通知
     * @Param: [id]
     * @Return: void
     * @Author: scott
     * @Date: 2021/6/30
    **/
    @SubscribeMapping("/user/{id}/listener")
    public void subscribeMapping(@DestinationVariable("id") final long id) {
        System.out.println(">>>>>>用户:"+id +",已订阅");
        SubscribeMsg param = new SubscribeMsg(id,String.format("用户【%s】已订阅成功", id));
        sendToUser(param);
    }


    /**
    * @MethodName: test
     * @Description: 接收订阅topic消息
     * @Param: [id, msg]
     * @Return: void
     * @Author: scott
     * @Date: 2021/6/30
    **/
    @MessageMapping(value = "/user/{id}/listener")
    public void UserSubListener(@DestinationVariable long  id, String msg) {
        System.out.println("收到客户端:" +id+",的消息");
        SubscribeMsg param = new SubscribeMsg(id,String.format("已收到用户【%s】发送消息【%s】", id,msg));
        sendToUser(param);
    }
    
     @GetMapping("/refresh/{userId}")
    public void refresh(@PathVariable Long userId, String msg) {
        StompSocketHandler.SubscribeMsg param = new StompSocketHandler.SubscribeMsg(userId,String.format("服务端向用户【%s】发送消息【%s】", userId,msg));
        sendToUser(param);
    }

    /**
    * @MethodName: sendToUser
     * @Description: 推送消息给订阅用户
     * @Param: [userId]
     * @Return: void
     * @Author: scott
     * @Date: 2021/6/30
    **/
    public void sendToUser(SubscribeMsg screenChangeMsg){
        //这里可以控制权限等。。。
        simpMessagingTemplate.convertAndSendToUser(screenChangeMsg.getUserId().toString(),"/listener", JSON.toJSONString(screenChangeMsg));
    }

    /**
    * @MethodName: sendBroadCast
     * @Description: 发送广播,需要用户事先订阅广播
     * @Param: [topic, msg]
     * @Return: void
     * @Author: scott
     * @Date: 2021/6/30
    **/
    public void sendBroadCast(String topic,String msg){
        simpMessagingTemplate.convertAndSend(topic,msg);
    }


    /**
     * @ClassName: SubMsg
     * @Author: scott
     * @Date: 2021/6/30
    **/
    public static class SubscribeMsg {
        private Long userId;
        private String msg;
        public SubscribeMsg(Long UserId, String msg){
            this.userId = UserId;
            this.msg = msg;
        }
        public Long getUserId() {
            return userId;
        }
        public String getMsg() {
            return msg;
        }
    }
}
  1. 连接展示
  • 建立连接成功,这里可以看出是基于websocket协议

在这里插入图片描述

  • 连接信息
    在这里插入图片描述
  • ping pong
    在这里插入图片描述
  • 调用接口向订阅用户1发送消息,http://localhost:9003/refresh/1?msg=HelloStomp,可以在客户端控制台查看已经收到了消息。这个时候不同用户通过自己的userId可以区分订阅的主题,可以做到通过userId精准的往客户端推送消息。
    在这里插入图片描述
  • 还记得我们在后端配置的时候还指定了广播的订阅主题/topic,这时我们前端通过js只要订阅了这个主题,那么后端在像这个主题推送消息时,所有订阅的客户端都能收到,感兴趣的小伙伴可以自己试试,api我都写好了。

在这里插入图片描述

至此,实战完毕,喜欢的小伙伴麻烦关注加点赞。

springboot + stomp后端源码地址:https://gitee.com/ErGouGeSiBaKe/stomp-server

  • 18
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论
### 回答1: Spring Boot WebSocket Stomp是一种基于Spring Boot框架的WebSocket协议的实现方式,它可以实现实时通信和消息推送功能。Stomp是一种简单的消息传输协议,它可以在WebSocket之上提供一个可靠的消息传输机制。使用Spring Boot WebSocket Stomp可以轻松地实现WebSocket通信,同时也可以使用Stomp协议来传输消息。这种方式非常适合实现实时通信和消息推送功能,例如在线聊天、实时监控等场景。 ### 回答2: springboot websocket stomp是一种基于Java的开源框架,它可以帮助我们实现实时通信功能。它采用了WebSocket协议作为底层通信协议,并结合了STOMP(Simple Text Oriented Messaging Protocol)协议来进行消息的传输和解析。 使用springboot websocket stomp可以很方便地实现客户端和服务器之间的实时通信,比如聊天室、实时数据展示等功能。它的好处是能够降低开发成本,提高开发效率,同时还可以提供较好的用户体验。 在使用springboot websocket stomp时,首先需要进行相关的配置和依赖,然后在代码中定义好相关的消息处理器,用于处理客户端发送过来的消息和服务器推送消息。接下来,我们可以使用JS等前端技术来调用WebSocket对象,连接到指定的WebSocket服务端,并发送和接收消息。 在WebSocket连接建立之后,我们可以使用STOMP协议进行消息的发送和订阅。我们可以使用STOMP协议中的几个关键命令,比如SEND、SUBSCRIBE、UNSUBSCRIBE等来进行消息的发送和订阅操作。 springboot websocket stomp还提供了一些注解,用于标识和定义消息的处理器、消息的目的地等属性。通过这些注解,我们可以很方便地控制消息的发送和接收。 总的来说,springboot websocket stomp提供了一种简单且效率高的方式来实现实时通信功能。它的易用性、扩展性和可靠性使得它在实际应用中得到广泛的应用。 ### 回答3: Spring Boot是一种用于简化Spring应用程序开发的框架,它提供了许多便利的功能和自动配置的特性。WebSocket是一种在客户端和服务器之间建立持久连接的协议,它为实时双向通信提供了一个解决方案。Stomp是一种在WebSocket之上建立消息传递协议的简单文本协议。 Spring Boot提供了对WebSocketStomp的支持,使开发人员能够轻松实现实时通信功能。通过使用Spring BootWebSocketStomp支持,可以快速构建具有实时功能的应用程序。 在Spring Boot中使用WebSocketStomp,首先需要在pom.xml文件中添加相关依赖。然后,在应用程序的配置类中使用@EnableWebSocketMessageBroker注解启用WebSocketStomp消息代理功能。接下来,使用@MessageMapping注解来定义处理WebSocket消息的方法。 在处理WebSocket消息的方法中,可以使用@SendTo注解将消息发送到指定的目的地,也可以使用SimpMessagingTemplate来主动推送消息客户端。 另外,还可以使用@SubscribeMapping注解来定义处理订阅请求的方法。通过在订阅请求方法中返回需要订阅的数据,可以在客户端成功订阅后立即将数据发送给客户端。 通过使用Spring BootWebSocketStomp支持,我们可以轻松地实现实时通信功能,使应用程序能够实时传递消息和数据。这对于需要实时更新的应用程序非常有用,如聊天室、股票交易系统等。 总而言之,Spring Boot提供了对WebSocketStomp的支持,使开发人员能够方便地构建具有实时通信功能的应用程序。通过使用WebSocketStomp,我们可以实现实时传递消息和数据的需求。
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码大师麦克劳瑞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值