简体   繁体   English

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

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

Is it possible to send a message to specific session?是否可以向特定会话发送消息?

I have an unauthenticated websocket between clients and a Spring servlet.我在客户端和 Spring servlet 之间有一个未经身份验证的 websocket。 I need to send an unsolicited message to a specific connection when an async job ends.当异步作业结束时,我需要向特定连接发送未经请求的消息。

@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?
    }
}

As you can see in this code, the client can start an async job and when it finishes, it needs the end message.正如您在这段代码中看到的,客户端可以启动一个异步作业,当它完成时,它需要结束消息。 Obviously, I need to message only the applicant and not broadcast to everyone.显然,我只需要向申请人发送消息,而不是向所有人广播。 It would be great to have an @SendToSession annotation or messagingTemplate.convertAndSendToSession method.这将是巨大的,有一个@SendToSession注释或messagingTemplate.convertAndSendToSession方法。

UPDATE更新

I tried this:我试过这个:

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

But this broadcasts to all sessions, not only the one specified.但这会广播到所有会话,而不仅仅是指定的会话。

UPDATE 2更新 2

Test with convertAndSendToUser() method.使用 convertAndSendToUser() 方法进行测试。 This test is and hack of the official Spring tutorial: https://spring.io/guides/gs/messaging-stomp-websocket/这个测试是官方 Spring 教程的 hack: https : //spring.io/guides/gs/messaging-stomp-websocket/

This is the server code:这是服务器代码:

@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;
}

and this is the client code:这是客户端代码:

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

Unfortunately client doesn't receive its per-session reply every 5000ms as expected.不幸的是,客户端没有按预期每 5000 毫秒收到一次会话回复。 I'm sure that "1" is a valid sessionId for the 2nd client connected because I see it in debug mode with SimpMessageHeaderAccessor.getSessionId()我确定“1”是连接的第二个客户端的有效 sessionId,因为我在调试模式下看到它SimpMessageHeaderAccessor.getSessionId()

BACKGROUND SCENARIO背景场景

I want to create a progress bar for a remote job, client asks server for an async job and it checks its progress by websocket message sent from server.我想为远程作业创建一个进度条,客户端向服务器询问异步作业,并通过从服务器发送的 websocket 消息检查其进度。 This is NOT a file upload but a remote computation, so only server knows the progress of each job.这不是文件上传而是远程计算,因此只有服务器知道每个作业的进度。 I need to send a message to specific session because each job is started by session.我需要向特定会话发送消息,因为每个作业都是由会话启动的。 Client asks for a remote computation Server starts this job and for every job step reply to applicant client with its job progress status.客户端请求远程计算服务器启动此作业,并针对每个作业步骤回复申请人客户端及其作业进度状态。 Client gets messages about its job and build up a progress/status bar.客户端获取有关其工作的消息并建立进度/状态栏。 This is why I need a per-session messages.这就是为什么我需要每个会话的消息。 I could also use a per-user messages, but Spring does not provide per user unsolicited messages.我也可以使用每用户消息,但 Spring不提供每用户未经请求的消息。 ( Cannot send user message with Spring Websocket ) 无法使用 Spring Websocket 发送用户消息

WORKING SOLUTION工作解决方案

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

Starting from the UPDATE2 solution I had to complete convertAndSendToUser method with last param (MessageHeaders):从 UPDATE2 解决方案开始,我必须使用最后一个参数(MessageHeaders)完成 convertAndSendToUser 方法:

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

where createHeaders() is this method:其中createHeaders()是这个方法:

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

No need to create specific destinations, it's already done out of the box as of Spring 4.1 (see SPR-11309 ).无需创建特定的目的地,从 Spring 4.1 开始,它已经开箱即用(参见SPR-11309 )。

Given users subscribe to a /user/queue/something queue, you can send a message to a single session with:鉴于用户订阅了/user/queue/something队列,您可以使用以下命令向单个会话发送消息:

As stated in the SimpMessageSendingOperations Javadoc , since your user name is actually a sessionId, you MUST set that as a header as well otherwise the DefaultUserDestinationResolver won't be able to route the message and will drop it.SimpMessageSendingOperations Javadoc 中所述,由于您的用户名实际上是 sessionId,因此您必须将其设置为标头,否则DefaultUserDestinationResolver将无法路由消息并将其丢弃。

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

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

You don't need users to be authenticated for this.您不需要为此对用户进行身份验证。

It is very complicated and in my opinion, isn't worth it.这非常复杂,在我看来,不值得。 You need to create a subscription for every user (even unauthenticated ones) by their session id.您需要通过会话 ID 为每个用户(甚至未经身份验证的用户)创建订阅。

Let's say that every user subscribes to a unique queue only for him:假设每个用户都为他订阅了一个唯一的队列:

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

On the server, before the user subscribes you will need to notify and send a message for the specific session and save to a map:在服务器上,在用户订阅之前,您需要通知并发送特定会话的消息并保存到地图:

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

After that, when you want to send message to the user you will need to:之后,当您想向用户发送消息时,您需要:

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

But I don't really know what you are trying to do and how you will find the specific session (who is anonymous).但我真的不知道您要做什么以及您将如何找到特定会话(谁是匿名的)。

You need to simply add the session id in您只需将会话 ID 添加到

  • Server Side服务器端

    convertAndSendToUser(sessionId,apiName,responseObject); convertAndSendToUser(sessionId,apiName,responseObject);

  • Client Side客户端

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

Note:笔记:
Dont forget to add '/user' in your end point in server side.不要忘记在服务器端的端点添加'/user'

I was struggling with the same problem and presented solution didnt work for me, therefore I had to take different approach:我正在努力解决同样的问题并且提出的解决方案对我不起作用,因此我不得不采取不同的方法:

  1. modify web socket config so user will be identified by session ID:修改 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. send message to that session id (without headers):向该会话 ID 发送消息(不带标题):
    simpMessagingTemplate.convertAndSendToUser(sessionId, "/queue", payload);

The simplest way is to take advantage of the broadcast parameter in @SendToUser.最简单的方法是利用@SendToUser 中的广播参数。 Docs:文档:

Whether messages should be sent to all sessions associated with the user or only to the session of the input message being handled.消息是应该发送到与用户关联的所有会话还是只发送到正在处理的输入消息的会话。 By default, this is set to true in which case messages are broadcast to all sessions.默认情况下,这设置为 true,在这种情况下,消息将广播到所有会话。

For your exact case it would look something like this对于您的确切情况,它看起来像这样

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

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

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