0

I'm trying to implement Java EE websockets with a Glassfish server, and I've got the basics down already.

As for my current project, I want to implement server "ticks" like in a game, that update the server every few milliseconds so that all sessions connected to the server see the same thing.

However, I don't know where to start to make this work with Glassfish. I know that I create a server from scratch and implement a timer method, but I'd rather save myself some work here if possible.

Simply put, is it easy to implement a timer/tick mechanism in the glassfish server (assuming it is running)?

For example, I want to make the server periodically send the word "hello" to a client HTML page once they are connected via websocket.

Kevin Hu
  • 41
  • 9
  • [Something like this?](https://github.com/payara/Payara-Examples/blob/master/Java-EE/ejb-timer/simple-ejb-timer/src/main/java/fish/payara/examples/ejb/simple/SimpleTimerBean.java) – Mike May 30 '17 at 16:43
  • You could use `ManagedScheduledExecutorService`. – Marcos Zolnowski May 31 '17 at 16:52
  • @Kevin-Hu, if you think that my answer is correct, could you consider accepting it? or feel free to tell me what's missing – Al-un Jun 28 '17 at 13:57

1 Answers1

1

Short Version

We solved it with three classes:

  1. Scheduler class: the clock which send an Event
  2. Websocket endpoint: the basic websocket session handler
  3. Websocket session handler: the conductor which catches the Event and manages all the websocket sessions

Long version with code

Now for the code, 1) is the one Mike mentioned in comment: a classic Scheduler which fires some event. This class is basically a clock which knocks whoever is listening to its event. We have a EAR=EJB+WAR project. Our timer is in the EJB module but if you have a single WAR, you can put it there.

// some import
import javax.enterprise.event.Event;

@Singleton
@Startup
public class TimerBean {

    @Inject
    private Event<TickEvent> tickEvent;

    @Schedule(hour = "*", minute = "*", second = "*/5")
    public void printSchedule() {
        tickEvent.fire(new TickEvent(/*...your initialization...*/));
    }

    public class TickEvent{
        // we have some info here but you can 
        // leave it empty.
    }

}

For the 2) websocket endpoint, this is a very academic endpoint. Don't forget that one connection = one endpoint instance. You can find all opened sessions from a given session with session.getOpenedSessions() but I'll explain why I use a handler in the next part.

// some import
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;

@ServerEndPoint(value = "/myEndPointPath")
public class TickEndPoint{

    @Inject
    private TickSessionHandler sessionHandler;

    @OnOpen
    public void onOpen(Session session){
        sessionHandler.addSession(session);
    }

    @OnClose
    public void onClose(Session session, CloseReason reason){
         sessionHandler.removeSession(session);
    }

}

And now the keystone: the session handler. The session handler is an ApplicationScoped bean, so it's always available. The reason I used it:

  1. When TickEvent is fired, you don't know if a TickEndPoint instance exists or not so the event catching may raise some stupid errors. We fixed it with @Observes(notifyObserver = Reception.IF_EXISTS) but we met other EJB problems which I'll detail in 2.
  2. For a reason I haven't identified yet, when I used @Observes in my @ServerEndPoint, all my @EJB injection failed so I switched @EJB injection to @Inject. But as the ApplicationScoped is a CDI bean, @EJB and @Observes work very well together in the session handler class. We didn't really need the @EJB annotation by itself but for design consistency with other bean, we wanted all our EJB to be annotated with @EJB.
  3. Monitoring: you can do monitoring such as how many "hello" you sent at time T, giving you a time vs number of connected sessions graph.

Plus, in our particular situation, we have some point that you may meet later.

  1. We need to select a particular subset of SESSIONS, not all of them. Having a static Set in the @ServerEndPoint led to some-stuff-which-turned-me-into-Hulk behavior especially when you have EJB dependencies. In an example of a game, let's you have capture the flag scenario with blue team and red team. If red team capture the flag, you need to send "Oops, we lost the flag" to the blue team and "Yeah we got it" to the red team.
  2. Monitoring: apart from connection time and stuff, we need info such as "last activity time" and stuff. To properly put it in a JSF datatable, a list/set in a CDI bean is the best option
  3. I encountered some tutorial/detail where the @ServerEndPoint is annotated @ApplicationScoped. By design (one server endpoint = one websocket connection), I don't feel comfortable with it so I refused to implement this solution

    @Named
    @ApplicationScoped
    public class TickSessionHandler{
    
        // There is no need to have a static Set, worst,
        // containers don't like it
        public Set<Session> SESSIONS;
    
        // let's initialize the set
        public TickSessionHandler{
            this.SESSIONS = new HashSet<>();
        }
    
        // ---------- sessions management
        public void addSession(Session session){
            this.SESSIONS.add(session);
        }
    
        public void removeSession(Sesssion session){
            this.SESSIONS.remove(session);
        }
    
        // ---------- Listen to the timer
        public void onTick(@Observes TickEvent event){
    
            // if required, get the event attribute
            // and proceed
    
            // your request:
            this.SESSIONS.forEach(session -> {
                session.getBasicRemote().sendText("hello");
            });
        }
    
    }
    

Hope this help. The explanation seems long but the core implementation is actually very light. AFAIK, there is no shortcut for such requirement.

Kohei TAMURA
  • 4,970
  • 7
  • 25
  • 49
Al-un
  • 3,102
  • 2
  • 21
  • 40