Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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. 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();
    }
like image 301
Tobia Avatar asked Jan 21 '16 16:01

Tobia


People also ask

What is SimpMessagingTemplate?

public class SimpMessagingTemplate extends AbstractMessageSendingTemplate<String> implements SimpMessageSendingOperations. An implementation of SimpMessageSendingOperations . Also provides methods for sending messages to a user.

What is setApplicationDestinationPrefixes?

setApplicationDestinationPrefixes is used as prefix to message mapping while sending messages from client using STOMP. So if your client sending data using STOMP for @MessageMapping("add") then the stompClient. send("/app/add"..) Follow this answer to receive notifications.

Is WebSocket a message broker?

The MessageBroker WebSocket Subprotocol (MBWS) is a WebSocket Subprotocol used by messaging clients to send messages to, and receive messages from an internet message broker (herein called a message broker).


2 Answers

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.

like image 194
Brian Clozel Avatar answered Oct 12 '22 09:10

Brian Clozel


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

like image 4
Aviad Avatar answered Oct 12 '22 09:10

Aviad