Spring Boot WebSocket: как узнать, что клиент отписался?

В настоящее время у меня есть простой WebSocket с настройкой STOMP, где клиент подключается к теме (с идентификатором). Контроллер немедленно отвечает тем, что было запрошено, и устанавливается переменная, указывающая, на что подписался клиент. Метод, помеченный @Scheduled, теперь отправляет клиенту то, что он запросил, каждые несколько секунд.

Запланированный метод ничего не делает, пока клиент не подключится в первый раз. Однако после первой подписки он продолжит публикацию независимо от того, подписан клиент или нет.

@Controller
public class ServiceWebSocketController {

    @Autowired
    private ServiceService serviceService;

    @Autowired
    WebSocketSessionController webSocketSessionController;

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    private Set<Long> services = new HashSet<>();

    @SubscribeMapping("/service/{serviceId}")
    public ServiceDTO subscribe(@DestinationVariable("serviceId") final Long serviceId) throws SQLException {
        System.out.println("Subscribed to Service with ID: " + serviceId);
        services.add(serviceId);
        return serviceService.getServiceWithProperties(serviceId).orElseThrow(() -> new ResourceNotFoundException("Service", "id", serviceId));
    }

    @Scheduled(fixedDelay = 2000)
    public void service() throws SQLException {
        services.removeIf(serviceId -> !webSocketSessionController.hasSubscriptionTo("/topic/service/" + serviceId));

        // Publish specified Service data to each anonymously subscribed client.
        services.forEach(serviceId -> {
            try {
                System.out.println("Publishing Service with ID: " + serviceId);
                // We don't use .convertAndSendToUser here, because all our clients are anonymous.
                simpMessagingTemplate.convertAndSend("/topic/service/" + serviceId, serviceService.getServiceWithProperties(serviceId));
            } catch (SQLException e) {
                e.printStackTrace();
            }
        });
    }
}

Как узнать, отписался ли клиент? Если что-то похожее на @UnsubscribeMapping существует, я мог бы просто снова установить переменную currentSubscriptionServiceId в null, чтобы предотвратить непрерывную публикацию данных запланированным методом.

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

chrylis -cautiouslyoptimistic- 01.03.2019 15:18

@chrylis Вы абсолютно правы. Я начинающий программист, который только начинает работать с Spring/WebSockets, поэтому я все еще учусь. В итоге я использовал ConcurrentHashMap<String, Long> для хранения того, какие клиенты подписаны на какие услуги. Используя ответ @Bertrand Pestre ниже, я смог добиться того, чего хотел :)

AnonymousAngelo 03.03.2019 15:05

Рад, что помогает. Обратите внимание, что это будет хранить состояние навсегда, если вы не очистите его, поэтому в производственной системе вы обычно будете делать что-то вроде сохранения этой информации в Redis или аналогичном бэкэнде ключ-значение, который может истечь записи.

chrylis -cautiouslyoptimistic- 03.03.2019 22:07

@chrylis Я обновил свой ответ, чтобы отразить ваши предложения. Я отслеживаю подключения/отключения/подписки/отписки (используя ApplicationListeners, как предложено в ответе Бертрана Пестре ниже), чтобы определить, какие клиенты подключены и подписаны на определенные услуги. Если клиент отписывается/отключается, я удаляю его подписки и прекращаю публиковать данные.

AnonymousAngelo 05.03.2019 13:41
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
4
1 807
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Прослушать событие SessionUnsubscribeEvent можно так:

@Controller
public class SessionUnsubscribeListener implements ApplicationListener<SessionUnsubscribeEvent> {

   @Override
   public void onApplicationEvent(SessionUnsubscribeEvent event) {
       GenericMessage message = (GenericMessage) event.getMessage();

       String simpDestination = (String) message.getHeaders().get("simpDestination");

       if ("/topic/service".equals(simpDestination)) {
           // do stuff
       }
   }
}

Это именно то, что я искал! Большое спасибо :)

AnonymousAngelo 03.03.2019 14:16

К сожалению, message.getHeaders().get("simpDestination")` возвращает null, когда происходит событие SessionUnsubscribeEvent.

AnonymousAngelo 03.03.2019 16:38

@AnonymousAngelo Ну, по крайней мере, слушатель работает! Что касается заголовков, возможно, структура данных немного отличается. Вы можете запустить это в режиме отладки и попытаться заглянуть в объект event, чтобы найти нужную тему.

Bertrand P 04.03.2019 10:10

В итоге я пошел с ConcurrentMap<String, String>, который я назвал subscriptions. Всякий раз, когда клиент подписывается на публикацию, я сохраняю subscriptionId и simpDestination. Таким образом, когда клиент отписывается, я просто удаляю запись карты на основе subscriptionId (которую вы получаете из SessionUnsubscribeEvent).

AnonymousAngelo 04.03.2019 10:40

Другие вопросы по теме