Подтвердить что ты не робот

Spring WebSocket @SendToSession: отправить сообщение на определенный сеанс

Можно ли отправить сообщение на определенный сеанс?

У меня есть неавторизованный веб-узел между клиентами и сервлет Spring. Мне нужно отправить незапрашиваемое сообщение на конкретное соединение при завершении работы async.

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

UPDATE

Я пробовал это:

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

Но это транслируется на все сеансы, а не только на указанные.

ОБНОВЛЕНИЕ 2

Протестируйте метод convertAndSendToUser(). Этот тест и взлом официального учебника Spring: 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()

ПРЕДПОСЫЛКА СЦЕНАРИИ

Я хочу создать индикатор выполнения для удаленного задания, клиент запрашивает сервер для работы async и проверяет его прогресс посредством сообщения websocket, отправленного с сервера. Это НЕ загрузка файлов, а удаленное вычисление, поэтому только сервер знает ход выполнения каждого задания. Мне нужно отправить сообщение на конкретный сеанс, потому что каждое задание запускается сеансом. Клиент запрашивает удаленное вычисление Сервер запускает это задание и отвечает за каждый ответ шага задания клиенту-заявителю с его статусом выполнения задания. Клиент получает сообщения о своей работе и создает строку прогресса/состояния. Вот почему мне нужны сообщения за сеанс. Я мог бы также использовать сообщения для каждого пользователя, но Spring не предоставляет для каждого незатребованного сообщения пользователя. (Не удается отправить сообщение пользователя с помощью Spring Websocket)

РАБОЧЕЕ РЕШЕНИЕ

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

Начиная с решения UPDATE2, мне пришлось завершить метод convertAndSendToUser с последним параметром (MessageHeaders):

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();
    }
4b9b3361

Ответ 1

Не нужно создавать определенные адресаты, это уже сделано из коробки с 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());

Для этого вам не требуется аутентификация пользователей.

Ответ 2

Это очень сложно и, на мой взгляд, не стоит. Вам необходимо создать подписку для каждого пользователя (даже неаутентифицированных) по их идентификатору сеанса.

Скажем, что каждый пользователь подписывается на уникальную очередь только для него:

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

Но я действительно не знаю, что вы пытаетесь сделать, и как вы найдете конкретный сеанс (анонимный).