繁体   English   中英

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

[英]Spring WebSocket @SendToSession: send message to specific session

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

我在客户端和 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 教程的 hack: 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不提供每用户未经请求的消息。 无法使用 Spring Websocket 发送用户消息

工作解决方案

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

从 UPDATE2 解决方案开始,我必须使用最后一个参数(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();
    }

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

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

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

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

messagingTemplate.convertAndSendToUser(sessionId,"/queue/something", payload, 
    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); 

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

您只需将会话 ID 添加到

  • 服务器端

    convertAndSendToUser(sessionId,apiName,responseObject);

  • 客户端

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

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

我正在努力解决同样的问题并且提出的解决方案对我不起作用,因此我不得不采取不同的方法:

  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);

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM