如果使用Spring + SockJS + STOMP,是否会缓存Websocket消息?如果没有,如何检测网络断开?

时间:2019-05-20 13:39:17

标签: angular websocket spring-websocket stomp sockjs

我有网页的不同部分在不同时间通过websocket消息进行更新。如果到服务器的网络连接由于任何原因(几秒钟到几天)都失败了,我需要使页面回到正确的状态。 我在后端使用Spring websockets,在前端(内置Angular)中使用SockJS和STOMP.js。

Q 1.该部分是否缓存正在发送的websocket消息(我仅使用一种方法,从服务器到客户端),然后检测到网络故障并在恢复连接时发送已存储的消息? (因此,这种情况会自动将页面放回正确的状态)

Q 2.否则,我需要以某种方式检测到网络连接丢失-究竟该怎么做? (然后我会从前端触发整个页面的重新加载-这是我可以轻松完成的工作)

我的后端是使用Spring Websockets的Groovy,即:

import org.springframework.messaging.simp.SimpMessagingTemplate
SimpMessagingTemplate brokerMessagingTemplate
brokerMessagingTemplate.convertAndSend('/topic/updatepage', pageComponentMessage)

为此配置:

@CompileStatic
@Configuration
@EnableWebSocketMessageBroker
class MySocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    void configureMessageBroker(MessageBrokerRegistry messageBrokerRegistry) {
        messageBrokerRegistry.enableSimpleBroker "/queue", "/topic"
        messageBrokerRegistry.setApplicationDestinationPrefixes "/app"
    }

    @Override
    void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
        stompEndpointRegistry.addEndpoint("/stomp").setAllowedOrigins("*").withSockJS()
    }

    @Bean
    GrailsSimpAnnotationMethodMessageHandler grailsSimpAnnotationMethodMessageHandler(
        SubscribableChannel clientInboundChannel,
        MessageChannel clientOutboundChannel,
        SimpMessageSendingOperations brokerMessagingTemplate
    ) {
        def handler = new GrailsSimpAnnotationMethodMessageHandler(clientInboundChannel, clientOutboundChannel, brokerMessagingTemplate)
        handler.destinationPrefixes = ["/app"]
        return handler
    }

    @Bean
    GrailsWebSocketAnnotationMethodMessageHandler grailsWebSocketAnnotationMethodMessageHandler(
        SubscribableChannel clientInboundChannel,
        MessageChannel clientOutboundChannel,
        SimpMessageSendingOperations brokerMessagingTemplate
    ) {
        def handler = new GrailsWebSocketAnnotationMethodMessageHandler(clientInboundChannel, clientOutboundChannel, brokerMessagingTemplate)
        handler.destinationPrefixes = ["/app"]
        return handler
    }

}

和前端Angular代码:

export class MyWSService {
  private sockjsclient = null; // SockJS socket that connects to the server (preferably using a WebSocket)
  private stompClient = null; // Stomp client that handles sending messages over the WebSocket

  subscribeToTopic(topic: string, subInstance: any, callbackfn): any { 

    // SockJS socket connection does not exist yet, set it up:
    if(!this.sockjsclient) {
      this.sockjsclient = new SockJS(myWebsocketUrl);
    }

    // If STOMP instance (to send messages over the socket) does not exist yet, set it up:
    if(!this.stompClient) {

      this.stompClient = Stomp.over(this.sockjsclient);

      this.stompClient.connect({}, () => {

        subInstance.wsSubscription = this.stompClient.subscribe(topic, (message) => callbackfn(message));
      })
    }
    // STOMP instance already exists, so use that existing connection:
    else {
        subInstance.wsSubscription = this.stompClient.subscribe(topic, (message) => callbackfn(message));
      }
  }

  unsubscribeFromTopic(subscription: any) {
    subscription.unsubscribe(); // Unsubscribe from topic
  }
}

谢谢

1 个答案:

答案 0 :(得分:1)

问题已经在理论层面上了。即使您要进行缓存,也无法知道需要保留多长时间。

我在项目中为获得此功能所做的工作是将所有用户都订阅到其专用用户队列中,并配置RabbitMQ,以使队列仅在一定时间后才会过期。使用这种方法-并且由于用户队列只有一个订户-未发送的消息将仅保留在队列中,直到各个用户将其提取为止。在客户端,一旦遇到连接丢失(由于缺少心跳),您可以建立新的Websocket连接和订阅。订阅后,您会收到错过的消息。

为了在RabbitMQ中配置用户队列,我实现了一个ChannelInterceptorSUBSCRIBE帧上做出反应的

public class SubscribeInterceptor implements ChannelInterceptor {

    private static final int EXPIRE_IN_MILLISECONDS = 30 * 1000;

    @Override
    public Message<?> preSend(@NonNull Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        if (accessor.getCommand() == StompCommand.SUBSCRIBE) {
            String username = accessor.getUser().getName();
            String queueName = Stream.of(accessor.getDestination().split("/"))
                    .reduce((f, s) -> s)
                    .orElseThrow(IllegalStateException::new);

            accessor.setNativeHeader("x-queue-name", String.format("%s_%s", username, queueName));
            accessor.setNativeHeader("x-expires", Integer.toString(EXPIRE_IN_MILLISECONDS));
            accessor.setNativeHeader("durable", "true");
            accessor.setNativeHeader("auto-delete", "false");
            return MessageBuilder.createMessage(message.getPayload(), accessor.getMessageHeaders());
        }
        return message;

    }
}