SpringBoot Websocket Stomp 实现单设备登录(顶号) ①

39 篇文章 3 订阅

单设备登录方式直接使用websocket实现比较容易实现,通常自己维护session会话列表管理即可。

当集成spring-messaging的stomp后,它封装的比较封闭,stomp有维护session会话列表,但是外部无法通过正常方式获取到,如果不想自己再维护一个可以尝试通过下面方式实现单设备登录功能。

本案例场景:同账号登录时,存在已在线通同账号,发送一个消息给在线账号告知顶号,然后将连接断开。

Stomp通过消息中继实现消息发送,下面使用SimpleBrokerMessageHandler说明:

配置:


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

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/wse").setHandshakeHandler(new MyHandleShakeHandler());
//        .withSockJS(); // 本例不使用sockjs客户端,所以不启用
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        ThreadPoolTaskScheduler pool = new ThreadPoolTaskScheduler();
        pool.setPoolSize(Runtime.getRuntime().availableProcessors());
        pool.setThreadNamePrefix("WsHeart");
        pool.initialize();

        // 心跳最好配置上,不配置会导致无法感知连接状态,掉了也不知道,
        // 一方面占用资源,另一方面影响业务功能
        // 不配置时,由于系统环境等缘故长连接长期无读写操作可能会失效
        // 注意配置了心跳要配置一个心跳执行线程池
        registry.enableSimpleBroker("/topic/")
                .setHeartbeatValue(new long[]{1000 * 60, 1000 * 30}) // 心跳读写间隔,
                .setTaskScheduler(pool)
        ;
        // user点对点通讯时,/user是UserDestinationMessageHandler使用的topic前缀名,
        // /queue是一个broker消息中继,如果没有消息中继,那么无法最终将消息发出去。
        // user消息最终也是转为simp消息发送,最终使用SimpleBrokerMessageHandler处理发送消息
        // 所以如果使用点对点消息,配置消息中继时最好为/user配置一个中继,            
        // 当然也可以只配置一个中继,都用一个中继如topic,此时convertAndSendToUser时,destination为/topic/xxx, 
        // 很多例子中使用/queue代表用户点对点中继,如果配置了/queue单独使用则就变成了/queue/xxx
        // 本例没有为用户单独配置一个中继,都是用topic, 实际使用最好分开
        // 用户订阅普通广播消息为:/topic/xxx, 订阅点对点消息为 /user/topic/xxx
        registry.setUserDestinationPrefix("/user/");
    }
    // 握手后自定义用户token解析
    static class MyHandleShakeHandler extends DefaultHandshakeHandler {

        @Override
        protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
            if (log.isDebugEnabled()) {
                log.debug("request handshake: {} remote: {}, headers: {}", request.getURI(), request.getRemoteAddress(), JsonUtil.toJsonStringQuit(request.getHeaders()));
            }
            List<String> tkHeaders = request.getHeaders().get("token");
            if (Objects.nonNull(tkHeaders) && !tkHeaders.isEmpty()) {
                String tk = tkHeaders.get(0);
                JWT jwtAuthToken = MyJwtUtil.parseToken(tk);
                if (Objects.isNull(jwtAuthToken)) {
                    log.error("handshake token not parsable: {}", tk);
                } else {
                    return MyJwtUtil.extractJwtUser(jwtAuthToken);
                }
            }
            return super.determineUser(request, wsHandler, attributes);
        }
    }
}

单设备检测及消息发送

package com.tom;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.user.SimpSession;
import org.springframework.messaging.simp.user.SimpUser;
import org.springframework.messaging.simp.user.SimpUserRegistry;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.socket.messaging.AbstractSubProtocolEvent;
import org.springframework.web.socket.messaging.SessionConnectEvent;

import java.security.Principal;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
 // 当前bean会在simpleBrokerMessageHandler之前被创建,
// 构造函数注入需要特别指明这个被依赖的bean要完成之后再创建本类bean
@DependsOn("simpleBrokerMessageHandler")
@RequiredArgsConstructor
public class WsConnectEventHandler implements SmartApplicationListener {

    private final SimpUserRegistry simpUserRegistry;
    private final SimpMessageSendingOperations sendingOperations;

    private final ScheduledExecutorService se = Executors.newScheduledThreadPool(4);
    private final Object sessionLock = new Object();
    private final SimpleBrokerMessageHandler simpleBrokerMessageHandler;

    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        return AbstractSubProtocolEvent.class.isAssignableFrom(eventType);
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        AbstractSubProtocolEvent subProtocolEvent = (AbstractSubProtocolEvent) event;
        if (event instanceof SessionConnectEvent) {
            Principal user = subProtocolEvent.getUser();
            if (user == null) {
                return;
            }
            String name = user.getName();
            synchronized (this.sessionLock) {
                SimpUser u = simpUserRegistry.getUser(name);
                if (Objects.nonNull(u)) {
                    Message<?> message = subProtocolEvent.getMessage();
                    MessageHeaders headers = message.getHeaders();
                    String sessionId = SimpMessageHeaderAccessor.getSessionId(headers);
                    Assert.state(sessionId != null, "No session id");
                    Set<SimpSession> sessions = u.getSessions();
                    log.info("User: {} has online sessions: {}", name, sessions.size());
                    for (SimpSession session : sessions) {
                        if (!sessionId.equals(session.getId())) {
                            // /user/topic/kick-out
                            log.info("kick-out: {}, user: {}", session.getId(), session.getUser());
                            // 此处监听connect事件, 通常情况下新session还未加入到simpSession中,
                            // 所以可以直接给当前用户发消息,不会影响到新session, 
                            // 但是由于消息在队列中不会即时发送,可能存在新session加入后,消息被发送,导致新设备也收到该消息,所以不用此方法
//                            sendingOperations.convertAndSendToUser(u.getName(), "/topic/kick-out"
//                                    , "当前帐号已在其他设备登录!"); //

                            // 指定session操作,否则是发给同一个用户下所有session
                            // 注意:一个websocket session对应多个stomp session,此处的session不是WebSocketSession
                            String sId = session.getId();
                            SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create();
                            headerAccessor.setSessionId(sId);
                            headerAccessor.setLeaveMutable(true);
                            // 点对点不需要加/user前缀
                            // 第一个参数user既可以是用户name也可以是sessionId, sessionId时只会指定session收到。
                            sendingOperations.convertAndSendToUser(sId, "/topic/kick-out"
                                    , "当前帐号已在其他设备登录!", headerAccessor.getMessageHeaders());
                            // 延迟断开连接
                            se.schedule(() -> {
                                try {
                                    simpleBrokerMessageHandler.handleMessage(createDisconnectMsg(sId));
                                    log.info("handle-disconnect: {}, user: {}", sId, session.getUser());
                                } catch (Exception e) {
                                    log.error("Handle disconnect error", e);
                                }
                            }, 3, TimeUnit.SECONDS);
                        }
                    }
                }
            }
        }
    }

    private Message<?> createDisconnectMsg(String id) {
        return MessageBuilder.withPayload("")
                .setHeader(SimpMessageHeaderAccessor.SESSION_ID_HEADER, id)
                .setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.DISCONNECT)
                .build();
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

测试方法:

package com.tom;

import com.alibaba.fastjson.JSONArray;
import com.guangyu.utils.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.converter.CompositeMessageConverter;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.StringMessageConverter;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler;
import org.springframework.web.socket.WebSocketHttpHeaders;
import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.messaging.WebSocketStompClient;
import org.springframework.web.socket.sockjs.client.SockJsClient;
import org.springframework.web.socket.sockjs.client.WebSocketTransport;

import java.lang.reflect.Type;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

@Slf4j
class StompClientTest {

    // 启动服务器后,分别运行两个test方法,没对返回消息做处理,可能会报错,正常忽略即可

    @Test
    void testStandardWebSocket0() throws ExecutionException, InterruptedException {
        // 标准wsClient,服务端不能使用withSockJs(): The HTTP response from the server [400] did not permit the HTTP upgrade to WebSocket
        // // ws://localhost:8800/wse
        StandardWebSocketClient webSocketClient = new StandardWebSocketClient();
        // user点对点通讯时,/user是UserDestinationMessageHandler使用的topic前缀名,
        // spring文档中的/queue 是一个broker消息中继,如果没有消息中继,那么无法最终将消息发出去。user消息最终也是转为simp消息发送
        testWebSocket(webSocketClient, "ws://localhost:8800/wse", "/user/topic/kick-out");
    }

    @Test
    void testStandardWebSocket1() throws ExecutionException, InterruptedException {
        StandardWebSocketClient webSocketClient = new StandardWebSocketClient();
        testWebSocket(webSocketClient, "ws://localhost:8800/wse", "/user/topic/kick-out");
    }

    void testWebSocket(WebSocketClient webSocketClient, String url, String topic) throws InterruptedException, ExecutionException {
        WebSocketStompClient client = new WebSocketStompClient(webSocketClient);
        client.setMessageConverter(new CompositeMessageConverter(List.of(new MappingJackson2MessageConverter(),new StringMessageConverter())));

        client.setTaskScheduler(new DefaultManagedTaskScheduler());
        client.setDefaultHeartbeat(new long[]{30001, 60001});
        WebSocketHttpHeaders wsHeaders = new WebSocketHttpHeaders();
        wsHeaders.add("token", "xxx"); // 业务上自定义的ws连接token标识用户信息
        StompSession session = client.connect(url, wsHeaders, new StompSessionHandlerAdapter() {
            @Override
            public Type getPayloadType(StompHeaders headers) {
                log.info("getPayloadType {}", JsonUtil.toJsonString(headers));
                return super.getPayloadType(headers);
            }

            @Override
            public void handleFrame(StompHeaders headers, Object payload) {
                log.info("handleFrame {}, {}", JsonUtil.toJsonString(headers), payload);
            }

            @Override
            public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
                log.info("afterConnected {}, {}", session.getSessionId(), JsonUtil.toJsonString(connectedHeaders));
            }

            @Override
            public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) {
                log.error("handleException sessionId: " + session.getSessionId(), exception);
            }

            @Override
            public void handleTransportError(StompSession session, Throwable exception) {
                log.info("handleTransportError sessionId: " + session.getSessionId(), exception);
            }
        }).get();
        session.subscribe(topic, new StompSessionHandlerAdapter() {
            @Override
            public Type getPayloadType(StompHeaders headers) {
                log.info("subscribe getPayloadType {}", JsonUtil.toJsonString(headers));
                return String.class;
            }

            @Override
            public void handleFrame(StompHeaders headers, Object payload) {
                log.info("subscribe handleFrame {}, {}", JsonUtil.toJsonString(headers), JsonUtil.toJsonString(payload));
            }

            @Override
            public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
                log.info("subscribe afterConnected {}, {}", session.getSessionId(), JsonUtil.toJsonString(connectedHeaders));
            }

            @Override
            public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) {
                log.error("subscribe handleException sessionId: " + session.getSessionId(), exception);
            }

            @Override
            public void handleTransportError(StompSession session, Throwable exception) {
                log.info("subscribe handleTransportError sessionId: " + session.getSessionId(), exception);
            }
        });
        log.info("Connect status: {}", session.isConnected());
        while (true) {
            TimeUnit.SECONDS.sleep(10);
            log.info("Connect status: {}", session.isConnected());
        }
    }

}

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答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 Boot的WebSocketStomp支持,可以快速构建具有实时功能的应用程序。 在Spring Boot中使用WebSocketStomp,首先需要在pom.xml文件中添加相关依赖。然后,在应用程序的配置类中使用@EnableWebSocketMessageBroker注解启用WebSocketStomp的消息代理功能。接下来,使用@MessageMapping注解来定义处理WebSocket消息的方法。 在处理WebSocket消息的方法中,可以使用@SendTo注解将消息发送到指定的目的地,也可以使用SimpMessagingTemplate来主动推送消息给客户端。 另外,还可以使用@SubscribeMapping注解来定义处理订阅请求的方法。通过在订阅请求方法中返回需要订阅的数据,可以在客户端成功订阅后立即将数据发送给客户端。 通过使用Spring Boot的WebSocketStomp支持,我们可以轻松地实现实时通信功能,使应用程序能够实时传递消息和数据。这对于需要实时更新的应用程序非常有用,如聊天室、股票交易系统等。 总而言之,Spring Boot提供了对WebSocketStomp的支持,使开发人员能够方便地构建具有实时通信功能的应用程序。通过使用WebSocketStomp,我们可以实现实时传递消息和数据的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值