Short Version
We solved it with three classes:
- Scheduler class: the clock which send an Event
- Websocket endpoint: the basic websocket session handler
- 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:
- 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.
- 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.
- 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.
- 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.
- 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
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.