使用Spring和SockJS进行WebSocket的Stomp协议,消息丢失了。

10

在客户端的JavaScript中我有:

    stomp.subscribe("/topic/path", function (message) {
        console.info("message received");
    });

并且在服务器端执行

public class Controller {
  private final MessageSendingOperations<String> messagingTemplate;
  @Autowired
  public Controller(MessageSendingOperations<String> messagingTemplate) {
      this.messagingTemplate = messagingTemplate;
  }
  @SubscribeMapping("/topic/path")
  public void subscribe() {
     LOGGER.info("before send");
     messagingTemplate.convertAndSend(/topic/path, "msg");
  }
}

从这个设置中,我偶尔会遇到消息丢失的情况(大约每30次页面刷新一次),这意味着我在客户端看不到“消息已收到”的消息,也看不到来自Chrome调试工具的WebSocket流量。

无论何时,在服务器端都会记录“发送前”。

看起来像是当我在subscribe()方法中调用MessageSendingOperations时它还没有准备好。(如果我在调用messagingTemplate.convertAndSend之前加上Thread.sleep(50);,问题就会消失(或者很少出现))

我想知道是否有人之前也遇到过这种情况,并且是否有事件可以告诉我MessageSendingOperations是否准备好了。


stomp.subscribe 在 DOM 准备就绪后执行吗? - guido
@ᴳᵁᴵᴰᴼ 是的,没错。我可以看到订阅消息是从Chrome调试中发送的websocket网络流量。所以我不认为这是客户端的问题。 - user2001850
3个回答

4
您面临的问题在于clientInboundChannel的本质,它默认是ExecutorSubscribableChannel。它有3个subscribers:
0 = {SimpleBrokerMessageHandler@5276} "SimpleBroker[DefaultSubscriptionRegistry[cache[0 destination(s)], registry[0 sessions]]]"
1 = {UserDestinationMessageHandler@5277} "UserDestinationMessageHandler[DefaultUserDestinationResolver[prefix=/user/]]"
2 = {SimpAnnotationMethodMessageHandler@5278} "SimpAnnotationMethodMessageHandler[prefixes=[/app/]]"

这些方法被在taskExecutor内调用,因此是异步的。

第一个方法 (SimpleBrokerMessageHandler) (或如果你使用的是 broker-relay,则为 StompBrokerRelayMessageHandler) 负责为topic注册subscription

您的messagingTemplate.convertAndSend(/topic/path, "msg")操作可能会在WebSocket会话的订阅注册之前执行,因为它们是在不同线程中执行的。因此,Broker处理程序不知道将消息发送到哪个会话。

@SubscribeMapping可以配置在带有return的方法上,该方法的结果将作为答复发送到客户端的subscription函数。

希望这能帮到你。


在我的subscribe方法中,我异步调用了服务层,例如subscriableService.registerAndHandleWith(new Handler(){}). 因此,我无法立即在此方法中返回。在这种情况下,您有什么建议?谢谢。 - user2001850
registerAndHandleWith中获取Future.get()CountDownLatch。从另一方面,您可以访问SimpleBrokerMessageHandler并填充一些自定义的DefaultSubscriptionRegistry实现来覆盖addSubscriptionInternal以引发一些自定义的ApplicationEvent以便从其他组件监听它并将该消息发送到主题,当subscription已经存在时。这是为了在您真正需要异步处理的情况下使用,并且不会过载clientInboundChannel执行程序以等待该Future - Artem Bilan
1
再次感谢你 Artem,我认为我不能使用现有的 SessionSubscribeEvent,因为它只告诉我客户端已经请求,但并不意味着订阅注册已经完成(如果有类似于 SessionSubscribedEvent 的东西就好了)。 - user2001850
1
是的,你是正确的:在真正订阅之前会发出一个SessionSubscribeEvent事件。欢迎为SessionSubscribedEvent提出JIRA工单。 - Artem Bilan

1
这是我的解决方案。它和之前的类似。添加了一个ExecutorChannelInterceptor并发布了一个自定义的SubscriptionSubscribedEvent。关键是在AbstractBrokerMessageHandler处理消息后发布事件,这意味着订阅已经被注册到代理程序。
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.interceptors(new ExecutorChannelInterceptorAdapter() {

        @Override
        public void afterMessageHandled(Message<?> message, MessageChannel channel, MessageHandler handler, Exception ex) {
            SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.wrap(message);
            if (accessor.getMessageType() == SimpMessageType.SUBSCRIBE && handler instanceof AbstractBrokerMessageHandler) {
                /*
                 * Publish a new session subscribed event AFTER the client
                 * has been subscribed to the broker. Before spring was
                 * publishing the event after receiving the message but not
                 * necessarily after the subscription occurred. There was a
                 * race condition because the subscription was being done on
                 * a separate thread.
                 */
                applicationEventPublisher.publishEvent(new SessionSubscribedEvent(this, message));
            }
        }
    });

}

0
有点晚了,但我想分享我的解决方案。我遇到了订阅在发送消息模板之前未注册的问题。由于与DefaultSubscriptionRegistry竞争,这个问题很少发生且不可预测。
不幸的是,我不能只使用@SubscriptionMapping的返回方法,因为我们使用的是自定义对象映射器,根据用户类型(属性过滤)动态更改。
我查看了Spring代码,并发现SubscriptionMethodReturnValueHandler负责发送订阅映射的返回值,并且具有与我的异步控制器中自动装配的SimpMessagingTemplate不同的messagingTemplate!
因此,解决方案是将MessageChannel clientOutboundChannel自动装配到我的异步控制器中,并使用它来创建一个SimpMessagingTemplate。(您不能直接进行连接,因为您只会得到连接到代理的模板)。
在订阅方法中,我使用直接模板,而在其他方法中,我使用连接到代理的模板。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接