本文介绍了在数据库中修改某些内容时,仅通过WebSockets通知特定用户的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧! 问题描述 29岁程序员,3月因学历无情被辞! 为了通过WebSockets通知所有用户,当在选定的JPA实体中修改某些内容时,我使用以下基本方法。In order to notify all users through WebSockets, when something is modified in selected JPA entities, I use the following basic approach.@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观察者观察。When a selected JPA entity is modified, an appropriate CDI event is raised which is to be observed by the following CDI observer.@Typedpublic final class EventObserver { private EventObserver() {} public void onEntityChange(@Observes EntityChangeEvent event) { Push.sendAll("updateModel"); }}观察者/消费者调用静态方法在WebSockets端点中定义的推送#sendAll(),它将JSON消息作为通知发送给所有关联的用户/连接。The observer/consumer invokes the static method Push#sendAll() defined in the WebSockets endpoint which sends a JSON message as a notification to all the associated users/connections.当只有选定的用户被通知时,需要以某种方式修改 sendAll()方法中的逻辑。The logic inside the sendAll() method needs to be modified somehow, when only selected users are to be notified. 仅通知负责修改相关实体的用户(可能是管理员用户或只有在成功登录后才能修改内容的注册用户)。 仅通知特定用户(不是全部)。 具体是指,例如,当在此网站上投票时,只会通知帖子所有者(该帖子可能由具有足够权限的任何其他用户投票)。当建立初始握手时,可以按照 HttpSession 。 com / a / 23405830/1391249>这个答案,但仍然不足以完成上面提到的两个子弹的任务。由于在第一次握手请求时可用,因此之后设置为该会话的任何属性在服务器端点中都不可用,换句话说,在建立握手后设置的任何会话属性都将不可用。When an initial handshake is establised, HttpSession can be accessed as stated in this answer but it is still insufficient to accomplish the tasks as mentioned above by two bullets. Since it is available when the first handshake request is made, any attribute set to that session afterwards will not be available in the server endpoint i.e. in other words, any session attribute set after a handshake is established will not be available.如上所述,仅通知所选用户的最可接受/规范方式是什么?某些条件语句在 sendAll()方法或其他地方是必需的。它似乎必须做除了用户的 HttpSession 以外的其他事情。What is the most acceptable/canonical way to notify only selected users as mentioned above? Some conditional statement(s) in the sendAll() method or something else somewhere is required. It appears that it has to do something other than only the user's HttpSession.我使用GlassFish Server 4.1 / Java EE 7。I use GlassFish Server 4.1 / Java EE 7.推荐答案 会话?Session?似乎你被会话这个词含糊不清所困扰。会话的生命周期取决于上下文和客户端。 websocket(WS)会话与HTTP会话的生命周期不同。就像EJB会话与HTTP会话的生命周期不同。就像传统的Hibernate会话与HTTP会话的生命周期不同。等等。您可能已经理解的HTTP会话在此处解释如何servlet有用吗?实例化,会话,共享变量和多线程。这里解释了EJB会话 JSF请求作用域bean继续在每个请求上重新创建新的有状态会话bean吗?It seems that you got bitten by the ambiguity of the word "session". The lifetime of a session depends on the context and the client. A websocket (WS) session does not have the same lifetime as a HTTP session. Like as that an EJB session does not have the same lifetime as a HTTP session. Like as that a legacy Hibernate session does not have the same lifetime as a HTTP session. Etcetera. The HTTP session, which you likely already understand, is explained here How do servlets work? Instantiation, sessions, shared variables and multithreading. The EJB session is explained here JSF request scoped bean keeps recreating new Stateful session beans on every request? WS会话与HTML文档表示的上下文相关联。客户端基本上是JavaScript代码。当JavaScript执行新的WebSocket(url)时,WS会话开始。当JavaScript在 WebSocket 实例上显式调用 close()函数时,或者当关联的HTML文档获得时,WS会话停止由于页面导航(单击链接/书签或修改浏览器地址栏中的URL)或页面刷新或浏览器选项卡/窗口关闭而卸载。请注意,您可以在同一个DOM中创建多个 WebSocket 实例,通常每个实例都有不同的URL路径或查询字符串参数。The WS session is tied to the context represented by the HTML document. The client is basically the JavaScript code. The WS session starts when JavaScript does new WebSocket(url). The WS session stops when JavaScript explicitly invokes close() function on the WebSocket instance, or when the associated HTML document gets unloaded as result of a page navigation (clicking a link/bookmark or modifying URL in browser's address bar), or a page refresh, or a browser tab/window close. Do note that you can create multiple WebSocket instances within the very same DOM, usually each with different URL path or query string parameters.每次WS会话开始时(即每当JavaScript执行 var ws = new WebSocket(url); )时,这将触发一个握手请求,其中因此,您可以通过以下 Configurator 您已经找到的课程:Each time when a WS session starts (i.e. each time when JavaScript does var ws = new WebSocket(url);), then this will fire a handshake request wherein you thus have access to the associated HTTP session via the below Configurator class as you already found out: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文档。每次创建新的WebSocket(url)时都会调用它。This is thus not called only once per HTTP session or HTML document as you seemed to expect. This is called every time a new WebSocket(url) is created.然后是一个全新的 @ServerEndpoint 将创建带注释的类及其 @OnOpen 带注释的方法。如果您熟悉JSF / CDI托管bean,只需将该类视为 @ViewScoped ,并将该方法视为 @PostConstruct 。Then a brand new instance of the @ServerEndpoint annotated class will be created and its @OnOpen annotated method will be invoked. If you're familiar with JSF/CDI managed beans, just treat that class as if it's a @ViewScoped and the method as if it's a @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) { // ... }}请注意,此类与例如不同一个servlet而不是应用程序作用域。它基本上是WS会话作用域。因此每个新的WS会话都有自己的实例。这就是为什么你可以安全地将 Session 和 EndpointConfig 指定为实例变量的原因。根据类设计(例如抽象模板等),您可以根据需要将 Session 添加回所有其他 onXxx 方法。这也是支持的。Note that this class is unlike e.g. a servlet not application scoped. It's basically WS session scoped. So each new WS session gets its own instance. That's why you can safely assign Session and EndpointConfig as an instance variable. Depending on the class design (e.g. abstract template, etc), you could if necessary add back Session as 1st argument of all those other onXxx methods. This is also supported. webSocket.send(some message)时,将调用rel =noreferrer> @OnMessage 带注释的方法。 @OnClose 注释方法。如有必要,确切的接近原因可以通过 CloseReason.CloseCodes 枚举。 @OnError 注释方法,通常是WS连接上的IO错误(断开管道,连接重置等)。The @OnMessage annotated method will be invoked when JavaScript does webSocket.send("some message"). The @OnClose annotated method will be called when the WS session is closed. The exact close reason can if necessary be determined by close reason codes as available by CloseReason.CloseCodes enum. The @OnError annotated method will be called when an exception is thrown, usually as an IO error on the WS connection (broken pipe, connection reset, etc).回到仅通知特定用户的具体功能要求,您应该在上述说明之后理解您可以放心地依赖 modifyHandshake()每次都从关联的HTTP会话中提取登录用户,前提是 new WebSocket(url) 在用户登录后创建。Coming back to your concrete functional requirement of notifying only specific users, you should after the above explanation understand that you can safely rely on modifyHandshake() to extract the logged-in user from the associated HTTP session, every time, provided that new WebSocket(url) is created after the user is logged-in.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端点类中有一个 @ServerEndpoint(configurator = UserAwareConfigurator.class),您可以在 @OnOpen 带注释的方法中获取它,如下所示:Inside the WS endpoint class with a @ServerEndpoint(configurator=UserAwareConfigurator.class), you can get hand of it in @OnOpen annotated method as below:@OnOpenpublic void onOpen(Session session, EndpointConfig config) { User user = (User) config.getUserProperties().get("user"); // ...}您应该在应用程序中收集它们范围。您可以在端点类的 static 字段中收集它们。或者,更好的是,如果您的环境中没有破坏WS端点中的CDI支持(在WildFly中工作,而不是在Tomcat + Weld中工作,不确定GlassFish),那么只需在应用程序范围的CDI托管bean中收集它们,然后再依次为 @Inject 。You should collect them in the application scope. You can collect them in a static field of the endpoint class. Or, better, if CDI support in WS endpoint is not broken in your environment (works in WildFly, not in Tomcat+Weld, not sure about GlassFish), then simply collect them in an application scoped CDI managed bean which you in turn @Inject in the endpoint class.当用户实例不是 null 时(即用户登录),然后记住用户可以有多个WS会话。因此,您基本上需要在 Map< User,Set< Session>> 结构中收集它们,或者可能是通过用户ID映射它们的更细粒度的映射或者组/角色,这毕竟可以更容易地找到特定用户。这一切都取决于最终的要求。这里至少是一个使用应用程序范围的CDI托管bean的启动示例:When User instance is not null (i.e. when an user is logged in), then remember that an user can have multiple WS sessions. So, you'd basically need to collect them in a Map<User, Set<Session>> structure, or perhaps a more fine grained mapping which maps them by user ID or group/role instead, which after all allows easier finding specific users. It all depends on the final requirements. Here's at least a kickoff example using an application scoped CDI managed bean:@ApplicationScopedpublic 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 中的特定用户:Finally you can send a message to specific user(s) as below in 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中注入,允许更容易的集成。The PushContext being a CDI managed bean has the additional advantage that it's injectable in any other CDI managed bean, allowing easier integration.在您的 EntityListener 中,您最有可能根据之前的相关问题触发CDI事件使用JSF / Java EE进行数据库实时更新,您已经掌握了已更改的实体,因此您应该能够通过模型中的关系找到与之关联的用户。In your EntityListener, where you fire the CDI event most likely as per your previous related question Real time updates from database using JSF/Java EE, you already have the changed entity at hands and thus you should be able to find the users associated with it via their relationships in the model.@PostUpdatepublic void onChange(Entity entity) { Set<User> editors = entity.getEditors(); beanManager.fireEvent(new EntityChangeEvent(editors));}@PostUpdatepublic void onChange(Entity entity) { User owner = entity.getOwner(); beanManager.fireEvent(new EntityChangeEvent(Collections.singleton(owner)));}然后在CDI事件观察员中,将其传递出去:And then in the CDI event observer, pass it forth:public void onEntityChange(@Observes EntityChangeEvent event) { pushContext.send(event.getUsers(), "message");} 参见: RFC6455 - WebSocket协议(描述 ws:// 协议) W3 - WebSocket API (描述JS WebSocket 界面) MDN - 编写WebSocket客户端应用程序(描述如何在客户端中使用WS API) Java EE 7教程 - WebSocket (描述 javax.websocket API以及如何使用它)See also:RFC6455 - The WebSocket Protocol (describes ws:// protocol)W3 - The WebSocket API (describes JS WebSocket interface)MDN - Writing WebSocket client application (describes how to use WS API in client)Java EE 7 tutorial - WebSocket (describes javax.websocket API and how to use it) 这篇关于在数据库中修改某些内容时,仅通过WebSockets通知特定用户的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持! 上岸,阿里云!
08-28 02:38