1

I need to send a message a client, after the creation of item. The item is created an ApiRest. Then I created my WebSocket with @ApplicationScope and I Injected in serviceREST with @Inject. The Problem is when the webSocket was initialized, in my serviceRest this webSocket's session still is null. How I can use the web SOcket in my apirest?

@Path("citas")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class citaResource {

    @Inject
    com.softcase.citasmanager.websocket.ws websocket;

    @GET
    @Path("cita")
    @Produces("application/json")
    public Response cita() {
       websocket.onMessage("Your Item was created");//Session of webSocket is null
        return Response.ok("ItemCreated", MediaType.APPLICATION_JSON).build();
    }

}

@ApplicationScope
@ServerEndpoint("/item")
public class ws{   

    private Session session;

    @OnOpen
    public void open(Session session) {
        this.session = session;
    }

    @OnMessage
    public void onMessage(String message) {
            this.session.getBasicRemote().sendText(message);
    }
Brayme Guaman
  • 175
  • 2
  • 12
  • I think you are wrongly mixing websocket and REST. They are two different protocols to send information from a client to a server or vice-versa. It's like to do Paris-New York, you can take a boat or a plane. You are basically asking a passenger who bought a boat round trip ticket to go to New York by boat but coming back by plane. On a more serious note, one instance of `ServerEndpoint`, in Tyrus implementation, is created per client. How you can identify which client REST is currently talking to? – Al-un Oct 17 '18 at 17:21
  • But I supossed that when I injected a webSocket with ApplicationScoped, I will have the webSocket with the updated data(In case that a session has been created, before Injected), because when it is ApplicationScoped I have only one instance. – Brayme Guaman Oct 18 '18 at 13:42
  • You can check [this SO answer](https://stackoverflow.com/a/18500388/4906586). Basically, I prefer to have a dedicated `@ApplicationScope` which list all existing `@ServerEndpoint` – Al-un Oct 18 '18 at 14:01
  • In terms of code, how do I do that? – Brayme Guaman Oct 18 '18 at 15:13
  • I can try writing an answer for that but I need to know how you identify the websocket sessions you are trying to reach. Let's assume there are user A, B, C etc... A is sending a request to the `citas` path and received the `ItemCreated`. Let's assume A, B, C ... have their websocket sessions opened. Who will received the websocket message? and how you identify A, B, C ... ? with username? email? – Al-un Oct 18 '18 at 15:30
  • But the problem is that the object websocket.session is null!Even when I already opened the session, from any other client! It does not matter to identify the session, because I send the message to all the webSocket sessions. – Brayme Guaman Oct 19 '18 at 14:04
  • As all the explanation is a bit long, I posted it as an answer. Please let me know if something is unclear. – Al-un Oct 22 '18 at 13:34
  • does the answer fit your question or is there anything unclear? – Al-un Oct 29 '18 at 11:41

1 Answers1

1

A little context

Instances: there is a unique Session instance per client-server pair i.e. one instance of Session is created for a each client which connects to the WebSocket server endpoint. In short, the number of unique Session instances is equal to number of connected clients

Source: https://abhirockzz.gitbooks.io/java-websocket-api-handbook/content/lifecycle_and_concurrency_semantics.html

For more details: https://tyrus-project.github.io/documentation/1.13.1/index/lifecycle.html

A suggestion is to use a static variables like

// @ApplicationScope
@ServerEndpoint("/item")
public class ws{   
    // something like
    private static final Set<javax.websocket.Session> ALL_SESSIONS = new HashSet<>();

    // ...
}

An example can be found here. It's an option but I do not think it solves your injection issue.

Another option is to leverage the javax.websocket.Session#getOpenedSessions() method such as this chat example. But once again, it does not solve the injection issue.

Your example

You are using both websocket and REST. As I understand, the flow is:

  1. User A, B, C are connected
  2. User A submits a request to citas/cita and receives the REST response
  3. At the same time, A, B, C receive a websocket notification

So, as you wrote, on one hand, you have

@Path("citas")
// ...
public class CitaResource{
    // ...
}

and

// @ApplicationScope -> commented as irrelevant in your situation
@ServerEndpoint("/item")
public class ws{   
    // ...
}

In the example, there is one instance of CitaResource when user A made the request and three instances of ws as A, B, C are connected. However, you were right about the injection: you need to have something injected in CitaResource but you need a bean that is always available and as you noticed, websocket instances are not a good option and which session the container must inject?

A websocket sessions handler

The solution is to use an application scoped bean to handle all the existing sessions. I got it from Oracle tutorial. It goes like this:

// com.softcase.citasmanager.websocket.SessionHandler
@ApplicatedScoped
@Named // optional
public class MySessionHandler{

    private final Set<Session> ALL_SESSIONS;
    // or use a map if you need to identify the
    // the session by a key. This example uses Set
    // private final Map<String, Session> ALL_SESSIONS;

    public MySessionHandler(){
        ALL_SESSIONS = new HashSet<>();
    }

    // manage sessions
    public void addSession(Session session){
        this.ALL_SESSIONS.add(session);
    }

    public void removeSession(Session session){
        this.ALL_SESSIONS.remove(session);
    }

    // send messages to all instances:
    public void sendMessage(String message){
        this.ALL_SESSIONS.stream()
            // optional
            .filter(s -> s.isOpen())
            // or whatever method you want to send a message
            .forEach( s -> s.getBasicRemote().sendText(message);
    }
    // or if you want to target a specific session
    // hence my questions in comments
    public void sendMessage(String message, String target){
        this.ALL_SESSIONS..stream()
            // identity the target session
            .filter(s -> s.equals(target))
            // optional
            .filter(s -> s.isOpen())
            .forEach( s -> s.getBasicRemote().sendText(message);
    }

}

Note:

  • I optionally check that the stored session is still opened. isOpen() is not mandatory but it might avoid some errors
  • Think the session handler as the "captain": it knows everything about the websocket sessions whereas the sessions themselves do not know about each other.

However, you need to adapt your endpoint to make the session handler efficient:

// com.softcase.citasmanager.websocket.WsCita
@ServerEndpoint
public class WsCita{

    // there is no need to declare the session as attribute
    // private Session session;

    // ApplicatedScoped so always defined
    @Inject
    private MySessionHandler handler;

    @OnOpen
    public void open(Session session){
        handler.addSession(session);    // "Aye cap'tain, reporting from duty!"
        // your stuff
    }

    @OnClose
    public void close(Session session, CloseReason closeReason){
        handler.removeSession(session); // "Cya cap'tain, that's all for today!"
        // your stuff
    }

    // your OnMessage and other stuff here
}

Now we have set our websocket architecture, what now?

  1. You have one instance of WsCita per client. At any time, there might be zero, one or more instances.
  2. MySessionHandler knows this information and is @ApplicatedScoped so it is safe to inject it

The REST endpoint then changes to:

@Path("citas")
// ...
public class citaResource {

    @Inject
    com.softcase.citasmanager.websocket.SessionHandler handler;

    @GET
    // ...
    public Response cita() {
        // REST processing
        // ...

        // Websocket processing:
        // - Handler is always here for you
        // - Handler knows which websocket sessions to send the message to.
        //   The RestController is not aware of the recipients
        handler.sendMessage("Your Item was created");
    }

}

Please note that I put the websocket processing after the REST processing as you may not always send the message (e.g. creation or whatever exception).

Misc

Unrelated to your questions but I have some comments about your code:

  • Classes name are CamelCase starting with capitalized letter per Oracle recommendation
  • Avoid generic name for your classes such as Ws. I renamed it WsCita for the example
Al-un
  • 3,102
  • 2
  • 21
  • 40