Уведомлять только конкретных пользователей через WebSockets, когда что-то изменяется в базе данных

чтобы уведомить всех пользователей через WebSockets, когда что-то изменяется в выбранных объектах JPA, я использую следующий базовый подход.

@ServerEndpoint("/Push")
public class Push {

    private static final Set<Session> sessions = new LinkedHashSet<Session>();

    @OnOpen
    public void onOpen(Session session) {
        sessions.add(session);
    }

    @OnClose
    public void onClose(Session session) {
        sessions.remove(session);
    }

    private static JsonObject createJsonMessage(String message) {
        return JsonProvider.provider().createObjectBuilder().add("jsonMessage", message).build();
    }

    public static void sendAll(String text) {
        synchronized (sessions) {
            String message = createJsonMessage(text).toString();

            for (Session session : sessions) {
                if (session.isOpen()) {
                    session.getAsyncRemote().sendText(message);
                }
            }
        }
    }
}

при изменении выбранного объекта JPA возникает соответствующее событие CDI, которое должно наблюдаться следующим наблюдателем CDI.

@Typed
public final class EventObserver {

    private EventObserver() {}

    public void onEntityChange(@Observes EntityChangeEvent event) {
        Push.sendAll("updateModel");
    }
}

наблюдатель / потребитель вызывает статический метод Push#sendAll() определено в конечной точке WebSockets, которая отправляет сообщение JSON в качестве уведомления всем связанным пользователи / соединения.

логика внутри sendAll() метод должен быть изменен каким-то образом, когда только выбранные пользователи должны быть уведомлены.

  • только уведомляет пользователя, ответственного за изменения лица (это может быть администратор или зарегистрированный пользователь, который может изменить что-то только после успешного входа в систему).
  • уведомлять только определенных пользователей (не всех). "Конкретный" означает, например, когда сообщение проголосовало на этом сайте, только сообщение владелец уведомляется (сообщение может быть проголосовано любым другим пользователем с достаточными привилегиями).

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

каков наиболее приемлемый / канонический способ уведомления только выбранных пользователей, как указано выше? Некоторое условное утверждение(ы) в sendAll() метод или что-то еще где-то требуется. Похоже, что он должен делать что-то другое, а не толькоHttpSession.

Я использую GlassFish Server 4.1 / Java EE 7.

1 ответов


сессии?

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

кажется, вас укусила двусмысленность слова "сессия". Время жизни сеанса зависит от контекста и клиента. Ля сеанс websocket (WS) не имеет того же времени жизни, что и сеанс HTTP. Как и то, что сеанс EJB не имеет того же времени жизни, что и сеанс HTTP. Например, устаревший сеанс Hibernate не имеет того же времени жизни, что и сеанс HTTP. Этсетера. Сеанс HTTP, который вы, вероятно, уже понимаете, объясняется здесь как работают сервлеты? Создание экземпляров, сеансы, общие переменные и многопоточность. Сеанс EJB объясняется здесь JSF запрос области Боб продолжает воссоздавать новые компоненты сеанса Stateful по каждому запросу?

жизненный цикл WebSocket

сеанс WS привязан к контексту, представленному HTML-документом. Клиент-это в основном код JavaScript. Сеанс WS начинается, когда JavaScript делает new WebSocket(url). Сеанс WS останавливается, когда JavaScript явно вызывает на WebSocket экземпляр или когда связанный HTML-документ выгружается в результате навигации по странице (щелчок ссылка / закладка или изменение URL-адреса в адресной строке браузера), или обновление страницы, или закрытие вкладки/окна браузера. Обратите внимание, что вы можете создать несколько WebSocket экземпляры в пределах одного и того же DOM, обычно каждый с разными параметрами URL-адреса или строки запроса.

каждый раз, когда начинается сеанс WS (т. е. каждый раз, когда JavaScript делает var ws = new WebSocket(url);), то это вызовет запрос рукопожатия, в котором у вас, таким образом, есть доступ к связанному сеансу HTTP через ниже Configurator класс, как вы уже узнали:

public class ServletAwareConfigurator extends Configurator {

    @Override
    public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        config.getUserProperties().put("httpSession", httpSession);
    }

}

это, таким образом, не вызывается только один раз за сеанс HTTP или HTML-документ, как вы, казалось, ожидали. Это называется каждый раз это.

тогда совершенно новый экземпляр @ServerEndpoint будет создан аннотированный класс и его @OnOpen аннотированный метод будет вызван. Если вы знакомы с управляемыми бобами JSF/ CDI, просто относитесь к этому классу как к @ViewScoped и метод, как будто это @PostConstruct.

@ServerEndpoint(value="/push", configurator=ServletAwareConfigurator.class)
public class PushEndpoint {

    private Session session;
    private EndpointConfig config;

    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        this.session = session;
        this.config = config;
    }

    @OnMessage
    public void onMessage(String message) {
        // ...
    }

    @OnError
    public void onError(Throwable exception) {
        // ...
    }

    @OnClose
    public void onClose(CloseReason reason) {
        // ...
    }

}

обратите внимание, что этот класс не похож, например, на сервлет, не имеющий области приложения. Это в основном область сеанса WS. Таким образом, каждый новый сеанс WS получает свой собственный экземпляр. Вот почему вы можете смело назначить Session и EndpointConfig как переменная экземпляра. В зависимости от дизайна класса (например, абстрактный шаблон и т. д.) Вы можете при необходимости добавить back Session как 1-й аргумент всех остальных onXxx методы. Это также поддерживаемый.

на @OnMessage аннотированный метод будет вызываться, когда JavaScript делает webSocket.send("some message"). The @OnClose аннотированный метод будет вызываться при закрытии сеанса WS. Точная причина закрытия при необходимости может быть определена с помощью кодов причин закрытия, доступных CloseReason.CloseCodes перечисление. The @OnError аннотированный метод будет вызываться при возникновении исключения, обычно как ошибка ввода-вывода в соединении WS (сломанная труба, сброс соединения, и т. д.).

сбор сеансов WS вошедшим в систему пользователем

возвращаясь к вашему конкретному функциональному требованию уведомления только конкретных пользователей, вы должны после приведенного выше объяснения понять, что вы можете безопасно полагаться на modifyHandshake() чтобы извлечь вошедшего пользователя из связанного сеанса HTTP, каждый раз, при условии, что new WebSocket(url) создана после пользователь вошел в систему.

public class UserAwareConfigurator extends Configurator {

    @Override
    public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        User user = (User) httpSession.getAttribute("user");
        config.getUserProperties().put("user", user);
    }

}

внутри класса конечной точки WS с a @ServerEndpoint(configurator=UserAwareConfigurator.class), вы можете получить его в @OnOpen аннотированный метод, как показано ниже:

@OnOpen
public void onOpen(Session session, EndpointConfig config) {
    User user = (User) config.getUserProperties().get("user");
    // ...
}

вы должны собрать их в области приложения. Вы можете собрать их в static поле класса endpoint. Или, лучше, если поддержка CDI в WS endpoint не нарушена в вашей среде (работает в WildFly, а не в Tomcat+Weld, не уверен в GlassFish), то просто соберите их в приложении scoped CDI managed bean, который вы в свою очередь @Inject в классе endpoint.

, когда User экземпляр не null (т. е. когда пользователь вошел в систему), то помните, что пользователь может иметь несколько сессий с WS. Таким образом, вам в основном нужно будет собрать их в Map<User, Set<Session>> структура или, возможно, более тонкое отображение, которое отображает их по идентификатору пользователя или группе/роли, что в конце концов позволяет легче находить конкретных пользователей. Все зависит от конечных требований. Вот, по крайней мере, пример запуска с использованием приложения, управляемого CDI бин:

@ApplicationScoped
public class PushContext {

    private Map<User, Set<Session>> sessions;

    @PostConstruct
    public void init() {
        sessions = new ConcurrentHashMap<>();
    }

    void add(Session session, User user) {
        sessions.computeIfAbsent(user, v -> ConcurrentHashMap.newKeySet()).add(session);
    }

    void remove(Session session) {
        sessions.values().forEach(v -> v.removeIf(e -> e.equals(session)));
    }

}
@ServerEndpoint(value="/push", configurator=UserAwareConfigurator.class)
public class PushEndpoint {

    @Inject
    private PushContext pushContext;

    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        User user = (User) config.getUserProperties().get("user");
        pushContext.add(session, user);
    }

    @OnClose
    public void onClose(Session session) {
        pushContext.remove(session);
    }

}

наконец, вы можете отправить сообщение конкретному пользователю(ы), как показано ниже в PushContext:

public void send(Set<User> users, String message) {
    Set<Session> userSessions;

    synchronized(sessions) {
        userSessions = sessions.entrySet().stream()
            .filter(e -> users.contains(e.getKey()))
            .flatMap(e -> e.getValue().stream())
            .collect(Collectors.toSet());
    }

    for (Session userSession : userSessions) {
        if (userSession.isOpen()) {
            userSession.getAsyncRemote().sendText(message);
        }
    }
}

на PushContext будучи управляемым CDI bean имеет дополнительное преимущество, что он вводится в любой другой управляемый CDI bean, что позволяет упростить интеграцию.

пожар CDI событие с соответствующими пользователями

в своем EntityListener, где вы запускаете событие CDI, скорее всего, в соответствии с вашим предыдущим связанным вопросом реальном времени обновления из базы данных с использованием JSF / Java EE, у вас уже есть измененный объект на руках, и, таким образом, вы должны быть в состоянии найти пользователей, связанных с ним через их отношения в модели.

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

@PostUpdate
public void onChange(Entity entity) {
    Set<User> editors = entity.getEditors();
    beanManager.fireEvent(new EntityChangeEvent(editors));
}

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

@PostUpdate
public void onChange(Entity entity) {
    User owner = entity.getOwner();
    beanManager.fireEvent(new EntityChangeEvent(Collections.singleton(owner)));
}

и затем в наблюдателе событий CDI передайте его:

public void onEntityChange(@Observes EntityChangeEvent event) {
    pushContext.send(event.getUsers(), "message");
}

Читайте также: