WebSocket-STOMP应用于路径广播实现配置

WebSocket-STOMP方式用户,大屏通信

一、pom.xml引入web-socket包和jedis包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>websocket</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>urlWebsocket</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

        <!-- socket依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- Fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <!-- redis依赖,也可用到自己的starter封装 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

二.application.yml启动配置文件
server:
  port: 7001
  servlet:
    session.timeout: 300

logging:
  level:
    #日志优先级(只会输出指定优先级及以上的日志信息):trace<debug<info<warn<error
    org.springframework.web: debug
    cn.zifangsky: debug
#  file: logs/stomp-websocket.log


  #Thymeleaf
  thymeleaf:
    mode: LEGACYHTML5
    prefix: classpath:/templates/
    suffix: .html
    template-resolver-order: 0
    cache: false

auth:
  aes:
    key: **************

#redis连接
spring.redis.host= 192.168.0.9
三、java代码实现

1.WebSocketConfig.java配置

@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Autowired
    private MyChannelInterceptor myChannelInterceptor;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/stomp-websocket")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //客户端需要把消息发送到/message/xxx地址
        registry.setApplicationDestinationPrefixes("/message");
        //服务端广播消息的路径前缀,客户端需要相应订阅/topic/xxx这个地址的消息
        registry.enableSimpleBroker(Constant.BROKER_DESTINATION_USER_PREFIX,Constant.BROKER_DESTINATION_PREFIX);
        //给指定用户发送消息的路径前缀,默认值是/user/
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        //配置连接用户关系保存
        registration.interceptors(myChannelInterceptor);
    }
}

2、MyChannelInterceptor.java拦截保存获取token中的userId会员id设置到socket中

@Component
@Slf4j
public class MyChannelInterceptor implements ChannelInterceptor {

    @Value("${auth.aes.key}")
    private String aesKey;

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            Object raw = message.getHeaders().get(SimpMessageHeaderAccessor.NATIVE_HEADERS);
            if (raw instanceof Map) {
                Object token = ((Map) raw).get("token");
                if(token != null){
                    // 设置当前访问的认证用户
                    String tokenString = ((LinkedList)token).get(0).toString();
                    String tokenValue = AESUtils.decrypt(tokenString, aesKey);
                    String[] tokenValues = tokenValue.split("##");
                    String userId = tokenValues[1];//todo 小程序登录时取0
                    CustomPrincipal customPrincipal = new CustomPrincipal(userId);
                    accessor.setUser(customPrincipal);
                }

            }
        }
        return message;
    }

    @Override
    public boolean preReceive(MessageChannel channel){
        return true;
    }

    @Override
    public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        StompCommand command = accessor.getCommand();

        //用户已经断开连接
        if(StompCommand.DISCONNECT.equals(command)){
            String user = "";
            Principal principal = accessor.getUser();
            if(principal != null && !StringUtils.isEmpty(principal.getName())){
                user = principal.getName();
            }else{
                user = accessor.getSessionId();
            }
            log.info(MessageFormat.format("用户{0}的WebSocket连接已经断开", user));
        }
    }
}

3.CustomPrincipal.java,socket用户凭证

public class CustomPrincipal implements Principal {
    private String userId;

    public CustomPrincipal(String userId) {
        this.userId = userId;
    }

    @Override
    public String getName() {
        return userId;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }
}

4.RedisConfig.java 配置redis监听

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

@Configuration
public class RedisConfig {

    @Autowired
    private LettuceConnectionFactory lettuceConnectionFactory;

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer() {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(lettuceConnectionFactory);
        return container;

    }
}

5.RedisChannelListener.java实现redis订阅自定义的channel,用户接受代码中向通道发送的信息,接受到后根据消息体的不同发送到广播,或者指定用户

@Slf4j
@Component
public class RedisChannelListener implements MessageListener {

    @Autowired
    SimpMessagingTemplate simpMessagingTemplate;

    @Override
    public void onMessage(Messachange message, byte[] pattern) {
        String channel = new String(pattern);
        log.info("channel:" + channel + "receives message :" + message.getBody());
        if (!StringUtils.isEmpty(message) && Constant.STOMP_MESSAGE_CHANNEL.equals(channel)) {
            try {
                StompMessage msg = JSON.parseObject(message.getBody(), StompMessage.class);
                //如果消息包含会员id表示单独发送给小程序会员,小程序会员响应广播,否则发送到指定用户
                if(msg.getMemberId() == null){
                    String destination = Constant.BROKER_DESTINATION_PREFIX + "/" + msg.getMarketId();
                    simpMessagingTemplate.convertAndSend(destination, JSON.toJSON(msg));
                }else{
                    String destination = Constant.BROKER_DESTINATION_USER_PREFIX + "/" + msg.getMarketId();
                    simpMessagingTemplate.convertAndSendToUser(msg.getMemberId().toString(),destination,JSON.toJSON(msg));
                }

            } catch (Exception e) {
                log.info("onMessage error:{}", e.getMessage());
            }
        }
    }

}

6.MyApplicationRunner.java项目启动则订阅redis通道

@Component
public class MyApplicationRunner implements ApplicationRunner {

    @Autowired
    private SubscribeListener subscribeListener;

    @Autowired
    RedisMessageListenerContainer redisMessageListenerContainer;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        this.subWebsocketChannel();
    }

    /**
     * 订阅redis频道
     */
    private void subWebsocketChannel(){
        redisMessageListenerContainer.addMessageListener(subscribeListener,new ChannelTopic(webSocketChannel));
    }
}

7.PublishService 将消息发布到通道

/**
 * 通道发布
 */
@Component
public class PublishService {

    @Autowired
    StringRedisTemplate redisTemplate;

    /**
     * @param channel 消息发布订阅 主题
     * @param message 消息信息
     */
    public void publish(String channel, Object message) {
        redisTemplate.convertAndSend(channel, message);
    }
}

8.根据实际业务处理后需要向socket推送消息,则直接推送到redis通道,由redis订阅后再根据消息向订阅路径推送

@Autowired
private PublishService publishServerice;

//推送到广播
@GetMapping("/publish/topic")
    public void toTopic(long marketId){
        StompMessage stompMessage = new StompMessage();
        stompMessage.setMarketId(marketId);
        stompMessage.setMessageType(2);
        stompMessage.setData("from publish topic");
        publishServerice.publish(Constant.STOMP_MESSAGE_CHANNEL, JSON.toJSONString(stompMessage));
    }

//推送到指定用户
    @GetMapping("/publish/user")
    public void toUser(long marketId,long memberId){
        StompMessage stompMessage = new StompMessage();
        stompMessage.setMarketId(marketId);
        stompMessage.setMemberId(memberId);
        stompMessage.setMessageType(1);
        stompMessage.setData("from publish user *");
        publishServerice.publish(Constant.STOMP_MESSAGE_CHANNEL, JSON.toJSONString(stompMessage));
    }

    @MessageMapping("/send/to/{marketId}")
    public void sendToUser(Principal principal,@DestinationVariable Long marketId){
	//从principal.getName()可以获取连接时设置到socket中的userId,参数中获取marketId则可以处理对应业务,比如摇一摇计数       simpMessagingTemplate.convertAndSendToUser(principal.getName(),"/queue","from to user");
    }

9.StompMessage.javapublish到redis再推送到socket订阅路径的消息体,订阅者根据messageType处理相应业务

@Data
public class StompMessage {

    /**
     * 活动id
     */
    private Long marketId;

    /**
     * 推送消息类型
     */
    private Integer messageType;

    /**
     * 用户会员id
     */
    private Long memberId;

    /**
     * 消息内容
     */
    private Object data;
}

10.前端socket连接、订阅和发送部分代码

	var stompClient = null;
//连接
        function connect() {
            var target = $("#target").val();
            //target = http://localhost:7001/stomp-websocket
            var ws = new SockJS(target);
            stompClient = Stomp.over(ws);

//建立连接是将token作为header中参数,用于后端获取userId保存关系指定用户发送消息            stompClient.connect({"token":"40A070633494FFEF9050390AEF5C51761E067B8F4C9C446954785E25ED687B8D882C5F33A63DCBAA5605FA6431E60CBB"}, function () {
                setConnected(true);
                log('Info: STOMP connection opened.');

                //订阅广播 /topic/marketId ,根据接收消息类型相应处理
                stompClient.subscribe("/topic/1", function (greeting) {
                    log('Received topic: ' + greeting.body);
                });
				
				//订阅指定用户发送消息 /user/queue/marketId ,接收后端对这个用户的指定发送,这里默认/user为前缀,会接收到后端根据保存在socket中的Principal中的userName的消息
                stompClient.subscribe("/user/queue/1", function (greeting) {
                    log('Received to user: ' + greeting.body);
                });
            },function () {
                //断开处理
                setConnected(false);
                log('Info: STOMP connection closed.');
            });
        }

        //断开连接
        function disconnect() {
            if (stompClient != null) {
                stompClient.disconnect();
                stompClient = null;
            }
            setConnected(false);
            log('Info: STOMP connection closed.');
        }

        //向服务端发送姓名
        function sendName() {
            if (stompClient != null) {
                var username = $("#username").val();
                var mapping = $("#mapping").val();
                log('Sent: ' + username);
                stompClient.send("/message/"+mapping, {}, JSON.stringify({'name': username}));
            } else {
                alert('STOMP connection not established, please connect.');
            }
        }

        //日志输出
        function log(message) {
            console.log(message);
        }
<think>好的,用户让我介绍一下基于STOMPWebSocket配置。首先,我需要回忆一下WebSocketSTOMP的基本概念。WebSocket是一种全双工通信协议,允许客户端和服务器之间建立持久连接,而STOMP是简单文本导向的消息协议,常用于在WebSocket之上进行消息传递。 用户可能正在开发一个需要实时通信的Web应用,比如聊天应用或者实时数据更新。他们可能已经知道WebSocket的基础,但想了解如何用STOMP来简化配置和使用。STOMP的好处在于它提供了更高级的抽象,比如消息的订阅和发布机制,不需要处理底层的帧结构。 接下来,我需要整理配置步骤。通常,在Spring Boot中配置WebSocketSTOMP需要几个关键点:添加依赖、配置WebSocket处理器、启用STOMP、定义消息代理、处理CORS、以及编写客户端代码。可能还需要考虑点对点消息和广播的区别,以及如何安全地处理这些消息。 用户可能遇到的问题包括依赖缺失、配置错误、CORS限制或者消息无法正确路由。需要确保在配置中正确设置允许的源和端点,并且消息代理正确配置。另外,客户端需要使用类似SockJS和Stomp.js的库来连接和处理消息。 我还需要检查是否有遗漏的部分,比如心跳机制、消息转换器或者安全性配置。例如,Spring Security可能需要额外的配置来保护WebSocket端点。不过用户的问题主要集中在配置上,可能暂时不需要深入安全细节,但可能需要提到这一点作为扩展考虑。 最后,示例部分需要简明扼要,展示服务端和客户端的代码片段,帮助用户快速上手。同时,要解释每个配置的作用,比如`@EnableWebSocketMessageBroker`注解的作用,以及消息代理的设置对性能的影响。 总结下来,回答需要结构清晰,分步骤解释配置过程,强调关键点,并提供示例代码。同时,预见到用户可能的疑问点,比如依赖项、CORS、消息路由等,提前做出说明,确保用户能顺利实现基于STOMPWebSocket配置。</think>以下是将基于STOMP协议的WebSocket配置实现方案,采用分步骤的结构化说明: --- ### **一、基础依赖配置(Spring Boot)** ```xml <!-- pom.xml 添加依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <!-- 可选安全配置 --> </dependency> ``` --- ### **二、服务端核心配置类** ```java @Configuration @EnableWebSocketMessageBroker // 启用STOMP协议支持 public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { // 注册STOMP端点(客户端连接地址) @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") // 连接端点路径 .setAllowedOrigins("*") // 允许跨域 .withSockJS(); // 支持SockJS回退 } // 配置消息代理 @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic", "/queue"); // 内置代理目标前缀 registry.setApplicationDestinationPrefixes("/app"); // 应用请求前缀 registry.setUserDestinationPrefix("/user"); // 用户私聊前缀 } } ``` --- ### **三、关键配置说明** #### 1. **消息代理模式** - **广播模式**:`/topic` 开头的地址向所有订阅者广播 - **点对点模式**:`/queue` 或 `/user` 开头的地址定向发送 #### 2. **消息路由规则** ```java @MessageMapping("/chat.send") // 接收/app/chat.send的请求 @SendTo("/topic/public") // 转发到代理目标 public MessageDto sendMessage(MessageDto message) { return message; } ``` #### 3. **安全控制(可选)** ```java @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/ws/**").permitAll() // 开放WebSocket端点 .anyRequest().authenticated(); } } ``` --- ### **四、客户端实现示例** ```javascript // 使用SockJS + Stomp.js const socket = new SockJS('http://localhost:8080/ws'); const stompClient = Stomp.over(socket); stompClient.connect({}, (frame) => { // 订阅公共频道 stompClient.subscribe('/topic/public', (message) => { console.log(JSON.parse(message.body)); }); // 订阅私有频道(用户隔离) stompClient.subscribe('/user/queue/private', (message) => { console.log('私密消息:', message.body); }); }); // 发送消息 stompClient.send("/app/chat.send", {}, JSON.stringify({content: "Hello"})); ``` --- ### **五、性能优化参数** ```properties # application.properties spring.websocket.sockjs.client-library-url=/webjars/sockjs-client/1.5.1/sockjs.min.js spring.websocket.sockjs.stream-cache-size=1024 # 缓存大小 spring.websocket.sockjs.heartbeat-time=30000 # 心跳间隔(ms) ``` --- ### **六、核心时序分析** 1. 握手阶段:通过HTTP Upgrade头建立WebSocket连接 2. STOMP帧传输: - CONNECT/CONNECTED帧建立协议会话 - SUBSCRIBE帧注册消息监听 - SEND帧发送应用消息 3. 消息代理处理:通过`@MessageMapping`注解方法处理消息 --- **实现优势**: 该配置方案通过STOMP协议实现了消息路由的标准化,相比原生WebSocket- 支持消息的发布/订阅模式(对应公式:$P = \prod_{i=1}^{n} Sub_i$,其中$P$为发布者,$Sub_i$为订阅者) - 自动处理消息转换(JSON/二进制转换) - 内置心跳保持机制(超时时间$T = 2 \times heartbeat$) 可根据具体需求扩展消息压缩、集群代理等高级特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值