Как хранить пользовательские данные для каждого WebSocket соединения в Helidon SE4
Задача: Есть сервер реализованный с помощью фреймворка Helidon SE4. Общение с сервером выполнятеся по протоколу WebSocket. Для каждого нового соединения мне нужно сохранять ID пользователя. Когда соединение закрывается, нужно эти данные удалять.
Проблема: В Helidon SE4 я не нашел для этого никаких механизмов похожих на те, что есть в Jakarta WebSockets. У WsSession нет методов вроде Session.getUserProperties()
, а класс имплементирующий WsListener
переиспользуется для всех устанавливаемых соединений.
Как я пробовал решить проблему: Но для каждого WebSocket соединения предоставляется свой уникальный виртуальный поток.
- Я пробовал использовать
ThreadLocal
для хранения пользовательских данных. Но это приводит к утечкам памяти, т.к. клиент может закрыть соединение не уведомив сервер -> ни одни из метдов WsListener никогда не будет вызван из нужного потока -> не получится вызватьThreaLocal.remove()
из правильного потока. - Я также пробовал использовать
ConcurrentHashMap
. Однако это вызвало проблемы с производительностью, т.к. все потоки отвечающие за соединение вынуждены постоянно ждать друг-друга.
Ответы (4 шт):
Скорее всего - никак. Хотя WebSocket и является statefull протоколом, его можно использовать и как stateless. Скорее всего, разработчики этого фреймворка хотели подтоклнуть именно к такому его использованию, когда каждое входящее сообщение имеет некий токен, по которому можно однозначно идентифицировать клиента. Видимо без условного ConcurrentHashMap
никак не обойтись.
Для решения вашей задачи в Helidon SE4 можно использовать подход с ConcurrentHashMap
, комбинируя его с использованием WeakReference
для предотвращения утечек памяти.
Это позволит вам ассоциировать пользовательские данные с соединением, а также гарантировать, что эти данные будут автоматически удалены при закрытии соединения, или при отсутствии ссылок на соединение.
Ниже приведен пример, как можно реализовать эту идею:
Создание карты для хранения данных с использованием
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) { // Обработка ошибок } }
Использование карты для получения данных пользователя:
public String getUserId(WsSession session) { WeakReference<String> ref = userSessionMap.get(session); return (ref != null) ? ref.get() : null; }
В этом примере при открытии соединения onOpen
добавляет ID
пользователя в карту, ассоциируя его с сессией WsSession
. При закрытии соединения onClose
удаляет данные из карты.
Использование WeakReference
помогает предотвратить утечки памяти, так как данные будут автоматически удалены, если на сессию больше не будет ссылок.
Этот подход позволяет избежать проблем с производительностью, связанных с использованием ConcurrentHashMap
, так как операции добавления и удаления в карте будут выполняться только при открытии и закрытии соединений, а не при каждом обращении.
В таком случае давайте рассмотрим более надежный способ хранения данных о пользователях с использованием ConcurrentHashMap
и гарантированного удаления данных при закрытии соединения.
В данном примере мы используем WsSession
как ключ в ConcurrentHashMap
, при условии, что каждый экземпляр WsSession
уникален для каждого соединения (что, как предполагается, обычно верно для WebSocket соединений).
Вот пример кода, который решает описанные вами проблемы:
- Создание карты для хранения данных о пользователях:
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);
}
}
- Использование карты для получения данных пользователя:
public String getUserId(WsSession session) {
return userSessionMap.get(session);
}
В этом примере при открытии соединения onOpen
добавляет ID пользователя в карту, ассоциируя его с сессией WsSession
. При закрытии соединения onClose
удаляет данные из карты. Аналогично, в случае ошибки соединения onError
также удаляет данные.
Этот подход гарантирует, что данные пользователя будут очищены сразу же при закрытии соединения или возникновении ошибки, что предотвращает утечки памяти. Использование ConcurrentHashMap
обеспечивает потокобезопасность и высокую производительность при работе с данными.
Если вам необходимо удостовериться, что WsSession
уникален для каждого соединения, можно также провести тесты или обратиться к документации или сообществу Helidon для получения подтверждения. В большинстве реализаций WebSocket соединений каждый экземпляр сессии уникален для каждого нового соединения.
Те функции, которых вам не хватает - планируют добавить в фреймворк. На данный момент WebSocket server находится в статусе prototype. Переводя на человеческий - это просто недоделаный фреймворк. И неизвестно, будет ли он доделан хоть когда-нибудь. Возможно в вашем случае лучшим решением будет мигрировать на другой фреймворк.