Chat Application

Chat Application #

In this section we implement a more complex application using actors. The application can be used to chat on the command line allowing clients on different computers to connect to a remote chat server.

The messages for the protocol are defined in the classes ChatClient and ChatServer. We will only discuss selected parts of the chat client implementation. Implementing the chat server is left as an exercise. The command line applications for the client and server can be started using RemoteChatClient and RemoteChatServer, respectively.

Implementing the chat client #

Chat clients can be created by passing a user name and the path to a remote chat server.

public static Behavior<Event> create(String name, String path) {
    return Behaviors.setup(ctx -> new ChatClient(ctx, name, path));
}

private String name;
private final String path;
private ActorRef<ChatServer.Request> server;

private ChatClient(ActorContext<Event> ctx, String name, String path) {
    super(ctx);
    this.name = name;
    this.path = path;
    ctx.getSelf().tell(Created.INSTANCE);
}

When a chat client is created (or restarted) it sends itself a Created message, which is defined as an enum because it does not have any contents.

private enum Created implements Event { INSTANCE }

As it is only used internally, the type is declared private. The following method handles such messages by trying to login to the server.

private ChatClient receive(Created msg) {
    ActorContext<Event> ctx = getContext();
    ActorRef<Event> self = ctx.getSelf();
    ctx.getSystem().classicSystem().actorSelection(path) //
        .tell(new ChatServer.Login(name, self), Adapter.toClassic(self));
    return this;
}

We use actor selection again, to communicate with a remote actor. This time however, we do not resolve an actor reference immediately. Instead we use ActorSelection.tell to send a message to the server without resolving its reference. The server will respond with a message containing its reference so we can store it in the local state.

Another interesting aspect is when to logout from the server. Actors can not only receive messages but also signals used internally by Akka. In its createReceive method, the chat client uses the following calls on the receive builder.

    .onSignal(PreRestart.class, signal -> quit())
    .onSignal(PostStop.class, signal -> quit())

Those signals are sent before the actor restarts and after it was stopped. They are a good place to perform cleanup actions. We use them here to logout from the chat server.

Task: Implement the chat server #

The ChatServer class defines messages that clients can send to a chat server. Implement a corresponding behavior after a conscious decision for the functional or object-oriented API.

The behavior should react to each possible message with appropriate responses to clients as well as local state changes. More specifically, the behaviour should capture local state associating client references with names. Login requests should be answered with a LoggedIn event or with an Error event if the chosen name is already taken. Additionally, other users should be alerted to successful registrations via UserJoined. When a registered client requests to Send a message, other clients should be notified via NewMessage. Unregistered clients should not be able to send messages but receive an Error event. When a registered client quits, others should be notified via the UserLeft event.

In order to test your implementation, start a RemoteChatServer and several RemoteChatClients. The latter can be given a user name and a port as command line arguments to avoid conflicts. Are existing users notified about new users? What happens, if a client tries to login with an already exiting name? Are messages sent by one user displayed to other users correctly? Are existing users notified about leaving users?

As a bonus, write unit tests that document that the chat server responds to messages as intended.