15

I'm working on an application that uses Websockets (Java EE 7) to send messages to all the connected clients asynchronously. The server (Websocket endpoint) should send these messages whenever a new article (an engagement modal in my app) is created.

Everytime a connection is established to the websocket endpoint, I'm adding the corresponding session to a list, which I could be able to access outside.

But the problem I had is, when I'm accessing this created websocket endpoint to which all the clients connected from outside (any other business class), I've get the existing instance (like a singleton).

So, can you please suggest me a way I can get an existing instance of the websocket endpoint, as I can't create it as new MyWebsocketEndPoint() coz it'll be created by the websocket internal mechanism whenever the request from a client is received.

For a ref:

private static WebSocketEndPoint INSTANCE = null;

public static WebSocketEndPoint getInstance() {
if(INSTANCE == null) {
// Instead of creating a new instance, I need an existing one
    INSTANCE = new WebSocketEndPoint ();
}
        return INSTANCE;
}

Thanks in advance.

Aryan Venkat
  • 679
  • 1
  • 10
  • 34

2 Answers2

23

The container creates a separate instance of the endpoint for every client connection, so you can't do what you're trying to do. But I think what you're trying to do is send a message to all the active client connections when an event occurs, which is fairly straightforward.

The javax.websocket.Session class has the getBasicRemote method to retrieve a RemoteEndpoint.Basic instance that represents the endpoint associated with that session.

You can retrieve all the open sessions by calling Session.getOpenSessions(), then iterate through them. The loop will send each client connection a message. Here's a simple example:

@ServerEndpoint("/myendpoint")
public class MyEndpoint {
  @OnMessage
  public void onMessage(Session session, String message) {
    try {  
      for (Session s : session.getOpenSessions()) {
        if (s.isOpen()) {
          s.getBasicRemote().sendText(message);
        }
    } catch (IOException ex) { ... }
  } 
} 

But in your case, you probably want to use CDI events to trigger the update to all the clients. In that case, you'd create a CDI event that a method in your Websocket endpoint class observes:

@ServerEndpoint("/myendpoint")
public class MyEndpoint {
  // EJB that fires an event when a new article appears
  @EJB
  ArticleBean articleBean;
  // a collection containing all the sessions
  private static final Set<Session> sessions = 
          Collections.synchronizedSet(new HashSet<Session>());

  @OnOpen
  public void onOpen(final Session session) {
    // add the new session to the set
    sessions.add(session);
    ...
  }

  @OnClose
  public void onClose(final Session session) {
    // remove the session from the set
    sessions.remove(session);
  }

  public void broadcastArticle(@Observes @NewArticleEvent ArticleEvent articleEvent) {
    synchronized(sessions) {
      for (Session s : sessions) {
        if (s.isOpen()) {
          try {
            // send the article summary to all the connected clients
            s.getBasicRemote().sendText("New article up:" + articleEvent.getArticle().getSummary());
          } catch (IOException ex) { ... }
        }
      }
    }
  }
}

The EJB in the above example would do something like:

...
@Inject
Event<ArticleEvent> newArticleEvent;

public void publishArticle(Article article) {
  ...
  newArticleEvent.fire(new ArticleEvent(article));
  ...
}

See the Java EE 7 Tutorial chapters on WebSockets and CDI Events.

Edit: Modified the @Observer method to use an event as a parameter.

Edit 2: wrapped the loop in broadcastArticle in synchronized, per @gcvt.

Edit 3: Updated links to Java EE 7 Tutorial. Nice job, Oracle. Sheesh.

Don Rhummy
  • 24,730
  • 42
  • 175
  • 330
Ian Evans
  • 1,357
  • 10
  • 10
  • That's certainly a great answer. I was already using the RemoteEndpoint.Async, n everything u have mentioned in the code snippet 1. But what my problem is, I don't have to send the created article to all the clients connected. I've do some filteration b4 sending instead. And our framework doesn't use EJB's,we've a serperate business class (say ArticleHandler) frm where I can know that an article is created. If I wanna access the session.getOpenSessions() frm there, I need the session object which was stored somewhere in the cache already. Help me reach there. – Aryan Venkat Aug 29 '13 at 07:21
  • The example above is just a general architecture. You don't have to use EJBs. In your case, `ArticleHandler` would inject the event and fire it. As far as accessing the sessions, move the `Set` collection to a singleton and call the singleton from your endpoint class to store the sessions there. The singleton would then have a `getSessions()` method to retrieve the current sessions. – Ian Evans Aug 29 '13 at 19:17
  • 2
    In `broadcastArticle` you should do `synchronized(sessions)` before the for-loop. – gcvt Oct 29 '14 at 10:35
  • Java EE 7 links are 404. – GoZoner Jun 18 '15 at 14:47
  • @GoZoner Fixed the links. – Ian Evans Jun 18 '15 at 19:49
  • @IanEvans is it possible to persist sessions? – Ignacio Garat Jan 19 '16 at 20:12
  • code typo: in `onOpen` it should be `sessions.add`, not `session`. (can't edit myself because of 6 symbols lower limit) – Alexander Malakhov May 02 '17 at 01:58
  • @AlexanderMalakhov Fixed. Thanks. – Ian Evans May 25 '17 at 19:40
  • @IanEvans Can I fire an event from a Servlet instead of a bean? Will my `@Observes` method in my websoket server endpoint still get called? – Don Rhummy May 28 '17 at 23:41
  • 1
    @DonRhummy Yes, you should be able to do that, assuming your container supports CDI. – Ian Evans May 31 '17 at 20:22
9

Actually, WebSocket API provides a way how you can control endpoint instantiation. See https://tyrus.java.net/apidocs/1.2.1/javax/websocket/server/ServerEndpointConfig.Configurator.html

simple sample (taken from Tyrus - WebSocket RI test):

    public static class MyServerConfigurator extends ServerEndpointConfig.Configurator {

        public static final MyEndpointAnnotated testEndpoint1 = new MyEndpointAnnotated();
        public static final MyEndpointProgrammatic testEndpoint2 = new MyEndpointProgrammatic();

        @Override
        public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
            if (endpointClass.equals(MyEndpointAnnotated.class)) {
                return (T) testEndpoint1;
            } else if (endpointClass.equals(MyEndpointProgrammatic.class)) {
                return (T) testEndpoint2;
            }

            throw new InstantiationException();
        }
    }

You need to register this to an endpoint:

@ServerEndpoint(value = "/echoAnnotated", configurator = MyServerConfigurator.class)
public static class MyEndpointAnnotated {

    @OnMessage
    public String onMessage(String message) {

        assertEquals(MyServerConfigurator.testEndpoint1, this);

        return message;
    }
}

or you can use it with programmatic endpoints as well:

public static class MyApplication implements ServerApplicationConfig {
    @Override
    public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> endpointClasses) {
        return new HashSet<ServerEndpointConfig>
          (Arrays.asList(ServerEndpointConfig.Builder
            .create(MyEndpointProgrammatic.class, "/echoProgrammatic")
            .configurator(new MyServerConfigurator())
            .build()));
    }

    @Override
    public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
        return new HashSet<Class<?>>(Arrays.asList(MyEndpointAnnotated.class));
    }

Of course it is up to you if you will have one configurator used for all endpoints (ugly ifs as in presented snippet) or if you'll create separate configurator for each endpoint.

Please do not copy presented code as it is - this is only part of Tyrus tests and it does violate some of the basic OOM paradigms.

See https://github.com/tyrus-project/tyrus/blob/1.2.1/tests/e2e/src/test/java/org/glassfish/tyrus/test/e2e/GetEndpointInstanceTest.java for complete test.

GoZoner
  • 67,920
  • 20
  • 95
  • 145
Pavel Bucek
  • 5,304
  • 27
  • 44