Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Play2.5 Java WebSockets

Play 2.5 Highlights states

Better control over WebSocket frames The Play 2.5 WebSocket API gives you direct control over WebSocket frames. You can now send and receive binary, text, ping, pong and close frames. If you don’t want to worry about this level of detail, Play will still automatically convert your JSON or XML data into the right kind of frame.

However https://www.playframework.com/documentation/2.5.x/JavaWebSockets has examples around LegacyWebSocket which is deprecated

  1. What is the recommended API/pattern for Java WebSockets? Is using LegacyWebSocket the only option for java websockets?
  2. Are there any examples using new Message types ping/pong to implement a heartbeat?
like image 870
chifer Avatar asked Mar 23 '16 00:03

chifer


1 Answers

The official documentation on this is disappointingly very sparse. Perhaps in Play 2.6 we'll see an update to this. However, I will provide an example below on how to configure a chat websocket in Play 2.5, just to help out those in need.

Setup

AController.java

@Inject
private Materializer materializer;
private ActorRef chatSocketRouter;

@Inject
public AController(@Named("chatSocketRouter") ActorRef chatInjectedActor) {
    this.chatSocketRouter = chatInjectedActor;
}


// Make a chat websocket for a user
public WebSocket chatSocket() {

    return WebSocket.Json.acceptOrResult(request -> {
        String authToken = getAuthToken();

        // Checking of token
        if (authToken == null) {
            return forbiddenResult("No [authToken] supplied.");
        }

        // Could we find the token in the database?
        final AuthToken token = AuthToken.findByToken(authToken);
        if (token == null) {
            return forbiddenResult("Could not find [authToken] in DB. Login again.");
        }

        User user = token.getUser();
        if (user == null) {
            return forbiddenResult("You are not logged in to view this stream.");
        }

        Long userId = user.getId();

        // Create a function to be run when we initialise a flow.
        // A flow basically links actors together.
        AbstractFunction1<ActorRef, Props> getWebSocketActor = new AbstractFunction1<ActorRef, Props>() {
            @Override
            public Props apply(ActorRef connectionProperties) {

                // We use the ActorRef provided in the param above to make some properties.
                // An ActorRef is a fancy word for thread reference.
                // The WebSocketActor manages the web socket connection for one user.
                // WebSocketActor.props() means "make one thread (from the WebSocketActor) and return the properties on how to reference it".
                // The resulting Props basically state how to construct that thread.
                Props properties = ChatSocketActor.props(connectionProperties, chatSocketRouter, userId);

                // We can have many connections per user. So we need many ActorRefs (threads) per user. As you can see from the code below, we do exactly that. We have an object called
                // chatSocketRouter which holds a Map of userIds -> connectionsThreads and we "tell"
                // it a lightweight object (UserMessage) that is made up of this connecting user's ID and the connection.
                // As stated above, Props are basically a way of describing an Actor, or dumbed-down, a thread.

                // In this line, we are using the Props above to
                // reference the ActorRef we've just created above
                ActorRef anotherUserDevice = actorSystem.actorOf(properties);
                // Create a lightweight object...
                UserMessage routeThisUser = new UserMessage(userId, anotherUserDevice);
                // ... to tell the thread that has our Map that we have a new connection
                // from a user.
                chatSocketRouter.tell(routeThisUser, ActorRef.noSender());

                // We return the properties to the thread that will be managing this user's connection
                return properties;
            }
        };

        final Flow<JsonNode, JsonNode, ?> jsonNodeFlow =
                ActorFlow.<JsonNode, JsonNode>actorRef(getWebSocketActor,
                        100,
                        OverflowStrategy.dropTail(),
                        actorSystem,
                        materializer).asJava();

        final F.Either<Result, Flow<JsonNode, JsonNode, ?>> right = F.Either.Right(jsonNodeFlow);
        return CompletableFuture.completedFuture(right);
    });
}

// Return this whenever we want to reject a 
// user from connecting to a websocket
private CompletionStage<F.Either<Result, Flow<JsonNode, JsonNode, ?>>> forbiddenResult(String msg) {
    final Result forbidden = Results.forbidden(msg);
    final F.Either<Result, Flow<JsonNode, JsonNode, ?>> left = F.Either.Left(forbidden);
    return CompletableFuture.completedFuture(left);
}

ChatSocketActor.java

public class ChatSocketActor extends UntypedActor {

    private final ActorRef out;
    private final Long userId;
    private ActorRef chatSocketRouter;


    public ChatSocketActor(ActorRef out, ActorRef chatSocketRouter, Long userId) {
        this.out = out;
        this.userId = userId;
        this.chatSocketRouter = chatSocketRouter;
    }

    public static Props props(ActorRef out, ActorRef chatSocketRouter, Long userId) {
        return Props.create(ChatSocketActor.class, out, chatSocketRouter, userId);
    }

    // Add methods here handling each chat connection...

}

ChatSocketRouter.java

public class ChatSocketRouter extends UntypedActor {

    public ChatSocketRouter() {}


    // Stores userIds to websockets
    private final HashMap<Long, List<ActorRef>> senders = new HashMap<>();

    private void addSender(Long userId, ActorRef actorRef){
        if (senders.containsKey(userId)) {
            final List<ActorRef> actors = senders.get(userId);
            actors.add(actorRef);
            senders.replace(userId, actors);
        } else {
            List<ActorRef> l = new ArrayList<>();
            l.add(actorRef);
            senders.put(userId, l);
        }
     }


     private void removeSender(ActorRef actorRef){
         for (List<ActorRef> refs : senders.values()) {
             refs.remove(actorRef);
         }
     }

    @Override
    public void onReceive(Object message) throws Exception {
        ActorRef sender = getSender();

        // Handle messages sent to this 'router' here

        if (message instanceof UserMessage) {
            UserMessage userMessage = (UserMessage) message;
            addSender(userMessage.userId, userMessage.actorRef);
            // Watch sender so we can detect when they die.
            getContext().watch(sender);
        } else if (message instanceof Terminated) {
            // One of our watched senders has died.
            removeSender(sender);

        } else {
            unhandled(message);
        }
    }
}

Example

Now whenever you want to send a client with a websocket connection a message you can do something like:

ChatSenderController.java

private ActorRef chatSocketRouter;

@Inject
public ChatSenderController(@Named("chatSocketRouter") ActorRef chatInjectedActor) {
    this.chatSocketRouter = chatInjectedActor;
}

public static void sendMessage(Long sendToId) {
    // E.g. send the chat router a message that says hi
    chatSocketRouter.tell(new Message(sendToId, "Hi"));
}

ChatSocketRouter.java

@Override
public void onReceive(Object message) throws Exception {
    // ...

    if (message instanceof Message) {
         Message messageToSend = (Message) message;
         // Loop through the list above and send the message to
         // each connection. For example...
         for (ActorRef wsConnection : senders.get(messageToSend.getSendToId())) {
              // Send "Hi" to each of the other client's
              // connected sessions
              wsConnection.tell(messageToSend.getMessage());
         }
    }

    // ...
}

Again, I wrote the above to help out those in need. After scouring the web I could not find a reasonable and simple example. There is an open issue for this exact topic. There are also some examples online but none of them were easy to follow. Akka has some great documentation but mixing it in with Play was a tough mental task.

Please help improve this answer if you see anything that is amiss.

like image 97
gurpreet- Avatar answered Oct 17 '22 11:10

gurpreet-