【问题标题】:Limit a @SendToUser broadcast on a specific browser tab限制特定浏览器选项卡上的 @SendToUser 广播
【发布时间】:2024-01-22 09:23:01
【问题描述】:

我在 Springboot 上使用 STOMP websocket 并希望将广播限制到特定页面。这是我的过程:

  1. User 将消息填充到 HTML 输入中。
  2. 浏览器会通过STOMP客户端发送消息。
  3. 服务器接收消息并对其进行验证。如果消息有效,它将广播到User 已发送消息的用户处理的所有选项卡。如果它无效,它将仅将错误消息发送回发送该消息的特定浏览器标签,而不是其他标签,即使这些标签具有相同的 User 登录。

虽然我不能限制将错误消息发送到特定选项卡,但我已经使它的某些部分工作,它总是将错误消息广播到共享相同User 的所有选项卡。这是我的初始代码:

@MessageMapping("/api/secure/message")
@SendToUser("/api/secure/broadcast")
public HttpEntity createMessage(Message message, Authentication authentication) throws Exception {
    Set<String> errors = TreeSet<String>();
    // Process Message message and add every exceptions encountered to Set errors.
    boolean valid = (errors.size() > 0);
    if(valid) {
        // Broadcast to all.
        return new ResponseEntity(message, HttpStatus.OK);
    }
    else {
        // Send the message to that particular tab only.
        return new ResponseEntity(errors, HttpStatus.UNPROCESSABLE_ENTITY);
    }
}

这可以通过websocket 实现吗?还是应该回XHR

【问题讨论】:

    标签: spring-boot websocket stomp


    【解决方案1】:

    用户@Srinivas 提供了一个很好的起点。我已经用我的工作代码修改了我的问题中的代码块:

    // inject the [messagingTemplate] bean.
    // class org.springframework.messaging.simp.SimpMessagingTemplate
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    @MessageMapping("/api/secure/message")
    // Remove the @SendToUser annotation and change return type to void.
    // @SendToUser("/api/secure/broadcast")
    // public HttpEntity createMessage(Message message…
    public void createMessage(Message message, Authentication authentication) throws Exception {
        Set<String> errors = TreeSet<String>();
        // Process Message message and add every exceptions encountered to Set errors.
        boolean valid = (errors.size() > 0);
        if(valid) {
            // Broadcast to all.
            
            // Instead of returning to send the message, use the [messagingTemplate] instead.
            // return new ResponseEntity(message, HttpStatus.OK);
            messagingTemplate.convertAndSendToUser("/api/secure/broadcast", errors);
        }
        else {
            // Send the message to that particular tab only.
            
            // Each STOMP WebSocket connection has a unique ID that effectively differentiate
            // it to the other browser tabs. Retrieve that ID so we can target that specific
            // tab to send our error message with.
    
            // class org.springframework.messaging.simp.stomp.StompHeaderAccessor
            StompHeaderAccessor stompHeaderAccessor = StompHeaderAccessor.wrap(message);
            String sessionId = stompHeaderAccessor.getSessionId();
    
            // class org.springframework.messaging.simp.SimpMessageHeaderAccessor
            // class org.springframework.messaging.simp.SimpMessageType
            // class org.springframework.util.MimeType
            // class java.nio.charset.StandardCharsets
            SimpMessageHeaderAccessor simpHeaderAccessor =
                SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
            simpHeaderAccessor.setSessionId(sessionId);
            simpHeaderAccessor.setContentType(new MimeType("application", "json",
                StandardCharsets.UTF_8));
            simpHeaderAccessor.setLeaveMutable(true);
    
            // Instead of returning to send the message, use the [messagingTemplate] instead.
            // It will ensure that it will only broadcast the message to the specific
            // STOMP WebSocket sessionId.
            // return new ResponseEntity(errors, HttpStatus.UNPROCESSABLE_ENTITY);
            messagingTemplate.convertAndSendToUser(sessionId, "/api/secure/broadcast",
                errors, simpHeaderAccessor.getMessageHeaders());
        }
    }
    

    如果您在控制器方法参数上使用@ResponseBody @Valid,则必须将逻辑线移动到您的ControllerAdviceexceptionHandler()

    【讨论】:

      【解决方案2】:

      对于每个选项卡,您都将创建一个新的 websocket 会话,因此您的 stomp session-id 也会有所不同。因此我们可以决定是发送到特定会话还是特定用户的所有会话。

      @Autowired
      private SimpMessagingTemplate template;
      ....
      @MessageMapping(...)
      public void sendMessage(Message<?> message...) {
       .....
       StompHeaderAccessor headerAccessor = 
       StompHeaderAccessor.wrap(message);
       String sessionId = headerAccessor.getSessionId();
       ....
       if(valid) {
         //Not specifying session Id so sends all users of 
          <user_name>
         template.cnvertAndSendToUser(<user_name>, 
         <destination>, <payload>)
       }
       else {
        SimpMessageHeaderAccessor headerAccessor = 
        
        SimpMessagingHeaderAccessor.create(SimpMessageType.MESSAGE);
        headerAccessor.setSessionId(sessionId);
      
        //This will send it to particular session.
        template.convertAndSendToUser(<user_name>, 
                         <destination>, <payload>,  
                headerAccessor.getMessageHeaders());
        }
      }
      

      有用的参考资料:

      1. Medium post on sending to particular session.
      2. convertAndSendToUser Documentation

      【讨论】:

      • 变量watchesSubscription在哪里声明?
      • 抱歉,我放错了,应该是之前得到的 sessionId 变量,请立即查看。