Как хранить пользовательские данные для каждого WebSocket соединения в Helidon SE4

Задача: Есть сервер реализованный с помощью фреймворка Helidon SE4. Общение с сервером выполнятеся по протоколу WebSocket. Для каждого нового соединения мне нужно сохранять ID пользователя. Когда соединение закрывается, нужно эти данные удалять.

Проблема: В Helidon SE4 я не нашел для этого никаких механизмов похожих на те, что есть в Jakarta WebSockets. У WsSession нет методов вроде Session.getUserProperties(), а класс имплементирующий WsListener переиспользуется для всех устанавливаемых соединений.

Как я пробовал решить проблему: Но для каждого WebSocket соединения предоставляется свой уникальный виртуальный поток.

  1. Я пробовал использовать ThreadLocal для хранения пользовательских данных. Но это приводит к утечкам памяти, т.к. клиент может закрыть соединение не уведомив сервер -> ни одни из метдов WsListener никогда не будет вызван из нужного потока -> не получится вызвать ThreaLocal.remove() из правильного потока.
  2. Я также пробовал использовать ConcurrentHashMap. Однако это вызвало проблемы с производительностью, т.к. все потоки отвечающие за соединение вынуждены постоянно ждать друг-друга.

Ответы (4 шт):

Автор решения: Sandro

Скорее всего - никак. Хотя WebSocket и является statefull протоколом, его можно использовать и как stateless. Скорее всего, разработчики этого фреймворка хотели подтоклнуть именно к такому его использованию, когда каждое входящее сообщение имеет некий токен, по которому можно однозначно идентифицировать клиента. Видимо без условного ConcurrentHashMap никак не обойтись.

→ Ссылка
Автор решения: PavelGalkin

Для решения вашей задачи в Helidon SE4 можно использовать подход с ConcurrentHashMap, комбинируя его с использованием WeakReference для предотвращения утечек памяти.

Это позволит вам ассоциировать пользовательские данные с соединением, а также гарантировать, что эти данные будут автоматически удалены при закрытии соединения, или при отсутствии ссылок на соединение.

Ниже приведен пример, как можно реализовать эту идею:

  1. Создание карты для хранения данных с использованием WeakReference:

    import io.helidon.webserver.websocket.WsListener;
    import io.helidon.webserver.websocket.WsSession;
    
    import java.util.concurrent.ConcurrentHashMap;
    import java.lang.ref.WeakReference;
    
    public class WebSocketListener implements WsListener {
        private final ConcurrentHashMap<WsSession, WeakReference<String>> userSessionMap = new ConcurrentHashMap<>();
    
        @Override
        public void onOpen(WsSession session) {
            // Здесь вы можете добавить ID пользователя в карту при открытии соединения.
            String userId = "someUserId"; // Получите userId из ваших данных
            userSessionMap.put(session, new WeakReference<>(userId));
        }
    
        @Override
        public void onMessage(WsSession session, String message) {
            // Обработка сообщения
        }
    
        @Override
        public void onClose(WsSession session, int status, String reason) {
            // Удаление данных при закрытии соединения
            userSessionMap.remove(session);
        }
    
        @Override
        public void onError(WsSession session, Throwable throwable) {
            // Обработка ошибок
        }
    }
    
  2. Использование карты для получения данных пользователя:

    public String getUserId(WsSession session) {
        WeakReference<String> ref = userSessionMap.get(session);
        return (ref != null) ? ref.get() : null;
    }
    

В этом примере при открытии соединения onOpen добавляет ID пользователя в карту, ассоциируя его с сессией WsSession. При закрытии соединения onClose удаляет данные из карты.

Использование WeakReference помогает предотвратить утечки памяти, так как данные будут автоматически удалены, если на сессию больше не будет ссылок.

Этот подход позволяет избежать проблем с производительностью, связанных с использованием ConcurrentHashMap, так как операции добавления и удаления в карте будут выполняться только при открытии и закрытии соединений, а не при каждом обращении.

→ Ссылка
Автор решения: PavelGalkin

В таком случае давайте рассмотрим более надежный способ хранения данных о пользователях с использованием ConcurrentHashMap и гарантированного удаления данных при закрытии соединения.

В данном примере мы используем WsSession как ключ в ConcurrentHashMap, при условии, что каждый экземпляр WsSession уникален для каждого соединения (что, как предполагается, обычно верно для WebSocket соединений).

Вот пример кода, который решает описанные вами проблемы:

  1. Создание карты для хранения данных о пользователях:
import io.helidon.webserver.websocket.WsListener;
import io.helidon.webserver.websocket.WsSession;

import java.util.concurrent.ConcurrentHashMap;

public class WebSocketListener implements WsListener {
    private final ConcurrentHashMap<WsSession, String> userSessionMap = new ConcurrentHashMap<>();

    @Override
    public void onOpen(WsSession session) {
        // Здесь вы можете добавить ID пользователя в карту при открытии соединения.
        String userId = "someUserId"; // Получите userId из ваших данных
        userSessionMap.put(session, userId);
    }

    @Override
    public void onMessage(WsSession session, String message) {
        // Обработка сообщения
    }

    @Override
    public void onClose(WsSession session, int status, String reason) {
        // Удаление данных при закрытии соединения
        userSessionMap.remove(session);
    }

    @Override
    public void onError(WsSession session, Throwable throwable) {
        // Удаление данных при ошибке
        userSessionMap.remove(session);
    }

    public String getUserId(WsSession session) {
        return userSessionMap.get(session);
    }
}
  1. Использование карты для получения данных пользователя:
public String getUserId(WsSession session) {
    return userSessionMap.get(session);
}

В этом примере при открытии соединения onOpen добавляет ID пользователя в карту, ассоциируя его с сессией WsSession. При закрытии соединения onClose удаляет данные из карты. Аналогично, в случае ошибки соединения onError также удаляет данные.

Этот подход гарантирует, что данные пользователя будут очищены сразу же при закрытии соединения или возникновении ошибки, что предотвращает утечки памяти. Использование ConcurrentHashMap обеспечивает потокобезопасность и высокую производительность при работе с данными.

Если вам необходимо удостовериться, что WsSession уникален для каждого соединения, можно также провести тесты или обратиться к документации или сообществу Helidon для получения подтверждения. В большинстве реализаций WebSocket соединений каждый экземпляр сессии уникален для каждого нового соединения.

→ Ссылка
Автор решения: Sandro

Те функции, которых вам не хватает - планируют добавить в фреймворк. На данный момент WebSocket server находится в статусе prototype. Переводя на человеческий - это просто недоделаный фреймворк. И неизвестно, будет ли он доделан хоть когда-нибудь. Возможно в вашем случае лучшим решением будет мигрировать на другой фреймворк.

→ Ссылка