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

Java Spring WebSocket@SendToSession:向特定会话发送消息,java,spring,spring-mvc,stomp,spring-websocket,Java,Spring,Spring Mvc,Stomp,Spring Websocket,是否可以向特定会话发送消息 我在客户端和Spring servlet之间有一个未经验证的websocket。当异步作业结束时,我需要向特定连接发送未经请求的消息 @Controller public class WebsocketTest { @Autowired public SimpMessageSendingOperations messagingTemplate; ExecutorService executor = Executors.newSingle

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

我在客户端和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教程的一部分:

这是服务器代码:

@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不提供按用户发送的未经请求的消息。()

工作解决方案

 __      __ ___   ___  _  __ ___  _  _   ___      ___   ___   _    _   _  _____  ___  ___   _  _ 
 \ \    / // _ \ | _ \| |/ /|_ _|| \| | / __|    / __| / _ \ | |  | | | ||_   _||_ _|/ _ \ | \| |
  \ \/\/ /| (_) ||   /| ' <  | | | .` || (_ |    \__ \| (_) || |__| |_| |  | |   | || (_) || .` |
   \_/\_/  \___/ |_|_\|_|\_\|___||_|\_| \___|    |___/ \___/ |____|\___/   |_|  |___|\___/ |_|\_|
其中
createHeaders()
是此方法:

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

这是非常复杂的,在我看来,是不值得的。 您需要根据每个用户(即使是未经验证的用户)的会话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); 

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

无需创建特定的目的地,从Spring 4.1开始,它已经开箱即用了(请参阅)

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

由于您的用户名实际上是sessionId,因此必须将其设置为标头,否则
DefaultUserDestinationResolver
将无法路由消息并将其丢弃

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

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

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

您只需在中添加会话id即可

  • 服务器端

    convertAndSendToUser(会话ID、apiName、responseObject)

  • 客户端

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

注:

别忘了在服务器端的端点添加
'/user'

我也遇到了同样的问题,提出的解决方案对我不起作用,因此我不得不采取不同的方法:

  • 修改web套接字配置,以便通过会话ID标识用户:

  • 最简单的方法是利用@SendToUser中的broadcast参数。 文件:

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

    对于你的确切情况,它看起来像这样

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

    我发现:我给你写了一个例子。。您如何知道您要向谁发送消息?这是一个远程作业管理,当控制器接收到远程作业请求时,它应该存储申请者的会话以路由未来作业状态应答返回什么(“测试”)?模型对象?嗨@Tobia我试过你写的解决方案,但在我的情况下不起作用。我的情况与您在后台运行异步任务并在websocket上发送消息以显示进度的情况完全相同。对于经过身份验证的用户,它可以正常工作,但对于未经身份验证的用户则不行。你能帮忙吗?这是我的实际解决方案,似乎是一个解决办法。查看SpringWebSocket源代码似乎可以获得每个活动套接字的会话id,而且从理论上讲,似乎可以将消息路由到特定连接。我将用一个场景背景来更新我的问题,告诉你我想用WebSocket实现什么。问题是你永远不知道你想发送给什么样的服务器。。在这种情况下,您只需要将客户端订阅到特定的通道。它可以这样做,因为作业从客户端消息开始,然后服务器应启动此作业,存储申请者会话id,当此作业前进时,应使用每个作业存储的会话id从服务器发送到客户端。。因此,不幸的是,没有这样的功能。您必须在地图中保存作业,客户端应订阅一个频道。对不起,我帮不了你,那太好了。明天我会试试。这对我不起作用。。。我试着每运行5000毫秒安排一次:messagingTemplate.convertAndSendToUser(“1”,即“/queue/something”,有效负载);但是客户端没有收到消息。我确信这是“1”会话(第二个客户端连接),因为我在调试模式下得到它。。。我应该通过考试
        @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();
        }
    
        simpMessagingTemplate.convertAndSendToUser(sessionId, "/queue", payload);
    
        @MessageMapping("/start")
        @SendToUser(value = "/queue/jobend", broadcast = false)
        //...