41

Is it possible to send a message to specific session?

I have an unauthenticated websocket between clients and a Spring servlet. 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.

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

Test with convertAndSendToUser() method. This test is and hack of the official Spring tutorial: 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. I'm sure that "1" is a valid sessionId for the 2nd client connected because I see it in debug mode with 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. 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. (Cannot send user message with Spring Websocket)

WORKING SOLUTION

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

Starting from the UPDATE2 solution I had to complete convertAndSendToUser method with last param (MessageHeaders):

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

where createHeaders() is this method:

private MessageHeaders createHeaders(String sessionId) {
        SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
        headerAccessor.setSessionId(sessionId);
        headerAccessor.setLeaveMutable(true);
        return headerAccessor.getMessageHeaders();
    }
Community
  • 1
  • 1
Tobia
  • 8,015
  • 24
  • 90
  • 182
  • I found this: https://jira.spring.io/browse/SPR-12143 – Tobia Jan 21 '16 at 16:50
  • I have wrote you down an example.. How can you tell who is the specific session you want to send a message to? – Aviad Jan 24 '16 at 22:02
  • This is a remote job management, when controller receive a remote job request, it should store applicant's session to route future job status replies – Tobia Jan 25 '16 at 08:58
  • What is Return("test")? the model object? – e-info128 Jan 19 '17 at 22:22
  • Hi @Tobia I tried the solution which you have written, but it's not working in my case. I have the exact same situation that you have with an async task to run in the background and send the messages on websocket to show the progress. With an authenticated user it works fine but not with a unauthenticated user. Could you please help? – edeesan Jun 14 '17 at 07:31

5 Answers5

42

No need to create specific destinations, it's already done out of the box as of Spring 4.1 (see SPR-11309).

Given users subscribe to a /user/queue/something queue, you can send a message to a single session with:

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.

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.

Brian Clozel
  • 46,620
  • 12
  • 129
  • 152
  • Would be fantastic. Tomorrow I will try it. – Tobia Jan 26 '16 at 18:16
  • This doesn't work for me... I tried to schedule every 5000ms this run: messagingTemplate.convertAndSendToUser("1","/queue/something", payload); But the client doesn't receive the message. I'm sure that it is the "1" session (2nd client conntected) because I get it in debug mode. – Tobia Jan 27 '16 at 08:08
  • I added an UPDATE to my question with this test – Tobia Jan 27 '16 at 08:17
  • 2
    Finally I got it... I should pass also a header param to convertAndSendToUser() to let it work. Please update your answer with the last param then I will accept it as answer! – Tobia Jan 27 '16 at 08:28
  • I don't know if you should be required to do that or if this is a bug in Spring. I'll look into it. – Brian Clozel Jan 27 '16 at 09:28
  • You were right, headers are mandatory in this case, I just updated my response. I'm wondering though is this could be an improvement or if we had a strong reason to enforce this. Please open an issue if you feel something can be improved here: https://jira.spring.io – Brian Clozel Jan 27 '16 at 10:21
  • Very helpful but I'm not sure if this can be classed as 'working out of the box' when you have to fashion you're own headers to get it to work. – alex.p Sep 25 '16 at 12:17
  • [http://stackoverflow.com/questions/43536507/spring-session-spring-web-socket-send-message-to-specific-client-based-on-ses](http://stackoverflow.com/questions/43536507/spring-session-spring-web-socket-send-message-to-specific-client-based-on-ses). I followed your instructions but still, my client are not receiving events sent from the server – Akshada Apr 25 '17 at 10:12
  • it works for me. this should be better than create a queue for every session. – SalutonMondo Oct 23 '18 at 01:55
  • Umm, looks weird to specify the sessionid two times. In the first parameter and also in the header. It would be cool if Spring put the sessionId in the header automatically, i wonder what's the reason they didn't do this. – GabrielBB Sep 04 '19 at 22:21
  • @BrianClozel I tried with convertAndSendToUser without the headers parameters and didn't work, so I tried your solution and also didn't work. In my case I don't know exactly why, but I need to add any "native header" if I don't do that it doesn't work, for example: headerAccessor.addNativeHeader("any", "any"); Do you have any idea why? – Guilherme Bernardi Dec 23 '19 at 14:32
  • Great. Thank you... I spent 3 days browsing different manuals that claimed to tell me how to send message to the user's queue. And only your answer actually mentioned that some headers HAS TO BE PROVIDED in order to succeed in doing it. Also was able to achieve the desired behavior with this: simpMessagingTemplate.convertAndSend("/queue/hello-me-user" + sessionId, helloMessage); The path for the CLIENT to subscribe to is: "/user/queue/hello-me" – Pasha Feb 18 '21 at 23:18
4

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.

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

vphilipnyc
  • 6,607
  • 6
  • 44
  • 70
Aviad
  • 1,469
  • 1
  • 9
  • 22
  • 1
    This is my actual solution, and seems to be a workaround. Looking into Spring Websocket sources seems possibile to get the session id for every active socket, and moreover, seems theoricallypossibile to route a message to a specific connection. I will update my question with a scenario backgroud to tell you what I want to achieve with websockets. – Tobia Jan 25 '16 at 08:48
  • The problem is that you never know what is the exact sesion you want to send to.. And in that case you just need to subscribe client to specific channel – Aviad Jan 25 '16 at 11:08
  • It can do it because job starts with client message, then server should starts this job storing the applicant session-id and when this job advances a message should be send from server to client with per-job stored session-id – Tobia Jan 26 '16 at 07:23
  • I understand.. So unfortunately there is no such feature. You will have to save jobs in map and client should subscribe a channel. Sorry i couldn't help more – Aviad Jan 26 '16 at 09:27
2

You need to simply add the session id in

  • Server Side

    convertAndSendToUser(sessionId,apiName,responseObject);

  • Client Side

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

Note:
Dont forget to add '/user' in your end point in server side.

Jens Gustedt
  • 72,200
  • 3
  • 92
  • 164
Sandy
  • 111
  • 2
  • 10
1

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:
    @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):
    simpMessagingTemplate.convertAndSendToUser(sessionId, "/queue", payload);
mi0
  • 136
  • 8
1

The simplest way is to take advantage of the broadcast parameter in @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.

For your exact case it would look something like this

    @MessageMapping("/start")
    @SendToUser(value = "/queue/jobend", broadcast = false)
    //...
Cosmin Stoian
  • 57
  • 1
  • 8