Spring WebSocket @SendToSession:向特定会话发送消息

时间:2016-01-21 16:49:38

标签: java spring spring-mvc stomp spring-websocket

是否可以向特定会话发送消息?

我在客户端和Spring servlet之间有一个未经身份验证的websocket。当异步作业结束时,我需要向特定连接发送未经请求的消息。

@Controller
public class WebsocketTest {


     @Autowired
    public SimpMessageSendingOperations messagingTemplate;

    ExecutorService executor = Executors.newSingleThreadExecutor();

    @MessageMapping("/start")
    public void start(SimpMessageHeaderAccessor accessor) throws Exception {
        String applicantId=accessor.getSessionId();        
        executor.submit(() -> {
            //... slow job
            jobEnd(applicantId);
        });
    }

    public void jobEnd(String sessionId){
        messagingTemplate.convertAndSend("/queue/jobend"); //how to send only to that session?
    }
}

正如您在此代码中看到的,客户端可以启动异步作业,当它完成时,它需要结束消息。显然,我只需要向申请人发送信息而不是向所有人广播。 拥有@SendToSession注释或messagingTemplate.convertAndSendToSession方法会非常棒。

更新

我试过了:

messagingTemplate.convertAndSend("/queue/jobend", true, Collections.singletonMap(SimpMessageHeaderAccessor.SESSION_ID_HEADER, sessionId));

但这会向所有会话广播,而不仅仅是指定的会话。

更新2

使用convertAndSendToUser()方法进行测试。 这个测试是和正式的Spring教程一起测试的:https://spring.io/guides/gs/messaging-stomp-websocket/

这是服务器代码:

@Controller
public class WebsocketTest {

    @PostConstruct
    public void init(){
        ScheduledExecutorService statusTimerExecutor=Executors.newSingleThreadScheduledExecutor();
        statusTimerExecutor.scheduleAtFixedRate(new Runnable() {                
            @Override
            public void run() {
                messagingTemplate.convertAndSendToUser("1","/queue/test", new Return("test"));
            }
        }, 5000,5000, TimeUnit.MILLISECONDS);
    } 

     @Autowired
        public SimpMessageSendingOperations messagingTemplate;
}

这是客户端代码:

function connect() {
            var socket = new WebSocket('ws://localhost:8080/hello');
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function(frame) {
                setConnected(true);
                console.log('Connected: ' + frame);
                stompClient.subscribe('/user/queue/test', function(greeting){
                    console.log(JSON.parse(greeting.body));
                });
            });
        }

不幸的是,客户端没有像预期的那样每5000毫秒收到一次会话回复。我确定“1”是连接的第二个客户端的有效sessionId,因为我在调试模式下看到SimpMessageHeaderAccessor.getSessionId()

背景情景

我想为远程作业创建进度条,客户端要求服务器提供异步作业,并通过服务器发送的websocket消息检查其进度。这不是文件上传而是远程计算,因此只有服务器知道每个作业的进度。 我需要向特定会话发送消息,因为每个作业都是由会话启动的。 客户端要求远程计算 服务器启动此作业,并为每个作业步骤回复申请人客户端及其作业进度状态。 客户端获取有关其作业的消息并构建进度/状态栏。 这就是我需要每会话消息的原因。 我也可以使用每用户消息,但Spring 不为每个用户提供未经请求的消息。 (Cannot send user message with Spring Websocket

工作解决方案

 __      __ ___   ___  _  __ ___  _  _   ___      ___   ___   _    _   _  _____  ___  ___   _  _ 
 \ \    / // _ \ | _ \| |/ /|_ _|| \| | / __|    / __| / _ \ | |  | | | ||_   _||_ _|/ _ \ | \| |
  \ \/\/ /| (_) ||   /| ' <  | | | .` || (_ |    \__ \| (_) || |__| |_| |  | |   | || (_) || .` |
   \_/\_/  \___/ |_|_\|_|\_\|___||_|\_| \___|    |___/ \___/ |____|\___/   |_|  |___|\___/ |_|\_|

从UPDATE2解决方案开始,我必须使用last param(MessageHeaders)完成convertAndSendToUser方法:

messagingTemplate.convertAndSendToUser("1","/queue/test", new Return("test"), createHeaders("1"));

其中createHeaders()是这种方法:

private MessageHeaders createHeaders(String sessionId) {
        SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
        headerAccessor.setSessionId(sessionId);
        headerAccessor.setLeaveMutable(true);
        return headerAccessor.getMessageHeaders();
    }

5 个答案:

答案 0 :(得分:31)

无需创建特定目的地,自Spring 4.1开始就已经开箱即用(参见SPR-11309)。

鉴于用户订阅了/user/queue/something队列,您可以通过以下方式向单个会话发送消息:

作为stated in the SimpMessageSendingOperations Javadoc,因为您的用户名实际上是一个sessionId,您必须将其设置为标题,否则DefaultUserDestinationResolver无法路由该消息并将其删除

SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor
    .create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);

messagingTemplate.convertAndSendToUser(sessionId,"/queue/something", payload, 
    headerAccessor.getMessageHeaders());

您不需要对用户进行身份验证。

答案 1 :(得分:3)

这很复杂,在我看来,不值得。 您需要按会话ID为每个用户(甚至是未经身份验证的用户)创建订阅。

假设每个用户只为他订阅一个唯一的队列:

stompClient.subscribe('/session/specific' + uuid, handler);

在服务器上,在用户订阅之前,您需要通知并发送特定会话的消息并保存到地图:

    @MessageMapping("/putAnonymousSession/{sessionId}")
    public void start(@DestinationVariable sessionId) throws Exception {
        anonymousUserSession.put(key, sessionId);
    }

之后,当您想要向用户发送消息时,您需要:

messagingTemplate.convertAndSend("/session/specific" + key); 

但我真的不知道你要做什么,以及你将如何找到特定的会话(谁是匿名的)。

答案 2 :(得分:2)

您只需在

中添加会话ID
  • 服务器端

    convertAndSendToUser(sessionId,apiName,responseObject);

  • 客户端

    $ stomp.subscribe('/ user / + sessionId +'/ apiName',handler);

注意:
不要忘记在服务器端的端点中添加'/user'

答案 3 :(得分:1)

最简单的方法是利用@SendToUser 中的广播参数。 文档:

<块引用>

是将消息发送到与用户关联的所有会话还是仅发送到正在处理的输入消息的会话。 默认情况下,这设置为 true,在这种情况下,消息将广播到所有会话。

对于您的具体情况,它看起来像这样

    @MessageMapping("/start")
    @SendToUser(value = "/queue/jobend", broadcast = false)
    //...

答案 4 :(得分:0)

我一直在同一个问题上挣扎,提出的解决方案对我没有用,因此我不得不采用不同的方法:

  1. 修改Web套接字配置,以便通过会话ID标识用户:
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws-endpoint")
                .setHandshakeHandler(new DefaultHandshakeHandler() {
                    
                    @Override
                    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
                        if (request instanceof ServletServerHttpRequest) {
                            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
                            HttpSession session = servletRequest.getServletRequest().getSession();
                            return new Principal() {
                                @Override
                                public String getName() {
                                    return session.getId();
                                }
                            };
                        } else {
                            return null;
                        }
                    }
                }).withSockJS();
    }
  1. 将消息发送到该会话ID(不包含标题):
    simpMessagingTemplate.convertAndSendToUser(sessionId, "/queue", payload);