Skip to content

Cache and registration based communications (push)

Thorben Kuck edited this page Sep 19, 2018 · 8 revisions

Push, or: Sending from Server to Client without a request from the Client.

Of course, you can simply realize this, by adding sub-routines, that handle stuff and than send stuff, but you can also use the internal Registration. Now, what do i mean?

As stated in the previous section, the Client can send a "registration" to the Server. This is handled internaly and you are now free to use one of 2 thing. But let's start at the beginning.

Lets assume, the following happend:
The Client did the following

Sender sender = Sender.open(clientStart);
Sender.registrationToServer(TestObject.class, new TestObserver());

and the Test-Observer looks something like this:

class TestObserver extends AbstractCacheObserver<TestObject> {

        public TestObserver() {
            super(TestObject.class);
        }

	@Override
	public void newEntry(TestObject testObject, Observable observable) {
		System.out.println("[NEW ENTRY] Received push from Server about: " + testObject);
	}

	@Override
	public void updatedEntry(TestObject testObject, Observable observable) {
		System.out.println("[UPDATE] Received push from Server about: " + testObject);
	}

	@Override
	public void deletedEntry(TestObject testObject, Observable observable) {
		System.out.println("[DELETED] Received push from Server about: " + testObject);
	}
}

Here you see one new thing, the AbstractCacheObserver-class. This class provides you 3 functions, which sections an event in the cache into 3 separate events. Think about it, you can add something new to the cache, update something existing or delete something existing.

This CacheObserver will be maintained within the ClientStart. In our example, it does nothing else than outputting the events to System.out, this is why we do not save this Observer anywhere else.

And now let's imagine, we wanted to send a new instance of TestObject to all Clients, that wanted to receive said class. Now we have to ways of doing it.

  1. Distributor.open(serverStart).toRegistered(new TestObject());
  2. serverStart.cache().addAndOverride(new TestObject());

Both result in the Client receiving the new TestObject. Whilst the first seems to be an clean and easy to understand method, the second one is to be preferred. Why?

If we saved an instance of TestObject into the Cache before the Client send a "registerToServer" request, he will automatically receive the last known object of this. Try to think about those Registrations as a sort of Chanel. What ever happend beforehand will be send to the Client.

The first one is to be used in Resource-poor environments. As the Cache used is a low-level-cache, meaning a cache that is not handling the resource (this has to be done outside of this module), it can, if used wrong, be very very resource hungry.

Considerations

Both methods used for registration based distribution can be combined. For example:

Let's imagine you wanted to implement a chat system. The system want's to greet every new registration to the chat (like saying "Welcome to the global chat"). The can be done, the following way:

First, set an arbitrary chat message from the System user.

ServerStart serverStart = ...
serverStart.cache().addAndOverride(new GlobalChatMessage("Welcome to the global chat", SYSTEM_USER.getUserName(), true));

This would store a new instance of the GlobalChatMessage in the Cache. It would contain the message as the fist parameter, the username of the sender as the second parameter (which in our case is a constant) and a boolean flag, to signal that the user the message is from the system, not from a user. True means "I am not an connect user, but the server" and false would mean "I am a connected user". This design has some flaws, but it does for the moment.

Now, at the Client-Side, we have a CacheObserver, which looks like this:

class CacheObserver extends AbstractCacheObserver<GlobalChatMessage> {
    
    private final ChatManagement chatManagement;

    public TestObserver(ChatManagement chatManagement) {
        super(GlobalChatMessage.class);
        this.chatManagement = chatManagement;
    }

    private void handleMessage(GlobalChatMessage message) {
        if(message.isSystemMessage()) {
            chatManagement.systemMessage(message);
        } else {
            this.handleMessage(message);
        }
    }

    @Override
    public void newEntry(GlobalChatMessage message, Observable observable) {
        this.handleMessage(message);
    }

    @Override
    public void updatedEntry(GlobalChatMessage message, Observable observable) {
        this.handleMessage(message);
    }

    @Override
    public void deletedEntry(GlobalChatMessage message, Observable observable) {
        // Ignore this case. We don't care
    }
}

We have some sort of ChatManagement, which (in what ever way) handles the provided message. It may print it to a GUI or log it. The we have to register it like this:

ClientStart clientStart = ...
Sender sender = Sender.open(clientStart);
sender.registrationToServer(GlobalChatMessage.class, new CacheObserver());

The last thing we do, is that we receive a GlobalChatMessage at the Server-Side, we send it to all registered. Let's expand our previous ServerStart example for that

ServerStart serverStart = ...
Distributor distributor = Distributor.open(serverStart);
serverStart.cache().addAndOverride(new GlobalChatMessage("Welcome to the global chat", SYSTEM_USER.getUserName(), true));

serverStart.getCommunicationRegistration()
     .register(GlobalChatMessage.class)
     .addLast((session, message) -> {
         // How ever it is done, this should not be taken from the Client itself.
         String username = getUsername(session);
         distributor.toAllRegistered(new GlobalChatMessage(message.getText(), username, false));
     });

after both snippets, you would launch both Network modules. The result would be, that once the Client connects, he would receive the message "Welcome to the global chat" and once it or another client sends a GlobalChatMessage to the Server, all Clients connected would receive the message.

This is only an example. The problem with that is, that you normally want to register later to a GlobalChatMessage (maybe only after the client has logged in). But as always, this is only an example to show the function. You may send an registration at any point in time, after the ClientStart is launched.


Push without cache or registration requirements

Of course, NetCom2 provides multiple other ways of sending something to multiple/all clients.

The first (and most clean) way, would be to use and maintain a custom "User" Object, which is decoupled using an Inteface. This User would aggregate the (Session)[https://github.com/ThorbenKuck/NetCom2/wiki/Session] and provide functions to interact with a certain user. The Object would be created (once the client connects)[https://github.com/ThorbenKuck/NetCom2/wiki/Client-Handling].

Some design choices however do not allow for that. You could do several other things in this case. But most of them follow this principle:
Check the ServerStart ClientList and filter for specific criteria.

You could implement this behaviour manually. It would look like the following:

ServerStart serverStart = ...
for(Client client : serverStart.clientList()) {
    if(matchesCriteria(client)) {
        // Do something with the client
    }
}

This however is already implemented. You could (in theory) use the Distributor interface.

The Distributor interface is reached by calling ServerStart#distribute and provide multiple functions to send Objects to certain clients. Following is an example of only some of the methods provided by the Distributor:

ServerStart serverStart = ...
TestObject object = ...
Distributor distributor = Distributor.open(serverStart);

// Send to all connected Clients
distributor.toAll(object);
// Send to all connected Clients which have a session, that returns on if Session#isIdentified 
distributor.toAllIdentified(object);
// Send to all connected Clients which DO NOT meat the criteria
distributor.toAllExcept(object, Session::isIdentified);
// Send to all connected Clients which DO meat all provided criteria
distributor.toAllExcept(object, session -> session.getIdentifier().equals("Test"));

As you might have realized, the wording is chosen in a way, which allows for more readability whilst using the functional style.

Problems

The distributor is not designed to handle other connections. The interface only allows you to send a message over the DefaultConnection, which is automatically established and only closed once the Client disconnects, or if you specifically close it. This is a design choice which might be enhanced in the near future. Currently this framework wants to discourage Connections, this is the reason that the Distributor is limited to the DefaultConnection.

Clone this wiki locally