Minimizing Client/Server communication

* Basic issues - what needs to be sent *
* How do we minimize the net traffic *
* Implementing the client *
* Implementing the remote interfaces *
* Summary of layers *

Basic issues - what needs to be sent?

Item references

Item references represent the most commonly transferred data.  In the simplest form they are simply integer IDs, that the server can use as a hashtable key to retrieve the real Item when necessary.  Item references are transferred from client->server and back again in a number of different forms, and in a number of different situations.  Here are server->client examples: And client -> server examples:

Item properties

Item references in themselves are not so useful to the client, since it cannot present the item in any way in the GUI without knowing at least the name or the graphical representation of the Item. The client can retrieve any item property from the server.

Keeping item references updated

Let's say a client has received an Item reference containing only the integer ID.  The server now has an update responsibility for this particular Item - in the simplest case this means the server must tell the client if this item is destroyed, nothing else.  Since the client has not received any properties (such as location), the server does not need to send updates about this.

But what if the client retrieves a property value, such as the item name.  If this name is displayed in the GUI in some persistent form, the client may or may not want to keep this value updated.  In a scrolling test area, for example, updates might not be desired, while in a persistent label component updates might be desired, so that the label component always shows the current name of the item.

So when retrieving a property from the server, the client must also tell the server if this is a property that should be tracked.

How do we minimize the traffic?

The server-side ItemDictionary

The server must maintain a dictionary of items that have been sent to the client, and which properties should be tracked for which items.  Items will only be removed from this dictionary when they go "out of scope" in the client, i.e when they are not visible any more and therefore do not need to be updated.

Minimizing the number of property requests

In some cases the server will be know in advance that a certain item property is desired from start.  For example the map display component will always want the icon of the item, so it is unneccesary for this map component to have to constantly ask the server for icon property values every time a new item reference is received.

It must therefore be possible for the server to send item references that include an initial set of property values, as well as the item ID.

The client-agent contract

A client-agent relationship is an independent connection between a client-side component and a server-side "agent" that works on the behalf of that client.  The main job of these two is to minimize the traffic between them.  One way of doing this is to make the agent as "smart" as possible.  Consider the MapAgent-MapClient relationship for example.

The MapClient will receive updates from the MapAgent.  The MapClient client will also receive scroll commands, when the creature who's view the map represents moves around.  In this case the MapAgent knows quite a lot about the state of the MapClient, which means the MapClient will rarely have to make calls to the MapAgent.  In fact, the only call necessary is to tell the MapAgent which item it should use as a centerpoint.

The MapAgent will automatically include the icon of each item reference, and will automatically assume that icon updates are desired for all items in its ItemDictionary, and will also know when items go out of scope, since it knows then scrolling takes place.  So in this case the client does not even need to tell the agent when the items go out of scope and should be removed from the dictionary.

Client-agent configuration

In some cases a client can make "prearrangments" with it's agent.  For example upon initialization a MapClient might tell the MapAgent that the item names should be included automatically.  This means the user can configure his client to his own taste, in this case deciding that it's OK with slighly slower map scrolling, given the benifit of being able to check an item's name without any delay.

Item reference classes

So there are two ways of sending item references over the net: one with only the item ID, and one with the ID plus one or more properties.  In some cases either of these are allowed, in other cases only the ID will be allowed.  When sending an action, for example, the server most definitely does not want anything else but the IDs of the items involved.  With this in mind, I suggest the following inheritance hierarchy for item references: The DetailedItemHeader is used in cases where the client and agent have not predefined which item properties are to be cached.  This is a way for the server to tell the client things like "for THIS item I have included the name and icon properties, but I have will only send updates on the icon property").  In most cases this will not be necessary, since the client and agent will usually predefine which properties are to be cached.

The ItemID might seem redundant, but it's purpose is to ensure that no one sends an ItemHeader when only an ItemID is requested (for example in action parameters).  ItemReference is used when you don't care which one is to be used.  Event parameters, for example, will be ItemReferences since in some cases they might include properties, in other cases they might not.

An EventClient might, for example, preconfigure it's EventAgent so that the item names are always included.  This means if the client recieves an event saying that #23 says hello, it does not need to explicitely retrieve the item name from the server - this will be included from start.

Implementing the client

Asynchronous GUI components

Most of the independent GUI components (PropertyPanel, EventPanel, etc) should work asynchronously with respect to each other.  This means that if the PropertyPanel is "busy" fetching the properties of an item, the other GUI components should not be frozen as well.

The Tile class - raising the abstraction level

The item reference behaviour is a bit complex since some properties are cached locally (and kept updated by the server) while other properties have to be requested from the server explicitely.  And when requesting a property explicitely you also have to decide whether or not you want updates on this property in the future.  I suggest we encapsulate all this into a Tile class.

A Tile represents an item in the server.  It hides all the client-server communication stuff and presents a clear, intuitive method interface that the client GUI components can use.  It's key methods are:

Tile sharing - the client-side TileDictionary

Even though the different client GUI components should work independently, they will share certain resources.  This includes the connection to the server, certain shared GUI components such as the menu bar and status bar, and the client-side TileDictionary.

If I have two different icon representations of the same item, in different parts of the client GUI, they should still be considered to be the same in many cases.  So if I click on one of them the other might also be highlighted, to make it clear that they are the same item.

This way there will never be more than one tile representing a single item in the server.  This avoids things like having 5 tiles representing the same item, and each one receiving property updates of it's own.  Sending a single property update for an item more than once is a waste of time.

The ClientContext will ensure this.  When someone receives an ItemReference (for example an event client receiving a "#27 says hello" event) a Tile should be created or retrieved immediately.  This is done using the following ClientContext method:

This method is simple, since it doesn't have to worry about item properties.  Here is what will happen To retrieve a Tile for an ItemHeader, these methods must be used instead: These two work similar to getTile(ItemID item), but they also take into account the caching of properties.  In the first method of the two, the "cachedProperties" variable applies to the whole set of properties included in the ItemHeader.  It cached=true, the Tile will be notified that these properties are already cached, i.e the server will send updates.

The second method uses an array of booleans, specifying exactly which properties are cached and which are not.  This could later replaced by something more compact, like a bitstring.

So there must be a way for the ClientContext to tell a Tile (regardless of whether it is pre-existing or newly created) that "properties A and B will be monitored by the server, so treat them as cached".  I will call this pre-caching, i.e caching initiated by the server.  The following Tile method will be added for this:

This will not result in any server calls, since it is a way of notifying the Tile that the caching has already been arranged.  This means that not all cachings in the Tile have been explicitely requested by a TileListener - some are added directly by the server using the above method.

How do Tiles communicate with the server?

A Tile does not communicate directly with the server - the server communication is encapsulated by the ClientContext.  This means that any requests from the tile to the server will go via the ClientContext, and all property updates from the server will reach the ClientContext first - then the ClientContext will forward it to the correct Tile.  The ClientContext will need the following methods to receive updates from the server: The Tile needs a similar set of methods for receiving these updates from the ClientContext:

Destroying Tiles - TileDestroyedException

If an item is destroyed in the world, any GUI representation of this item should be notified.  This is also done via Tile and TileListener.  The event of a tile being destroyed is first of all forwarded to the ClientContext from the server (just like property updates).  The ClientContext will then tell the representing Tile that it's item has been destroyed.  The Tile will in turn notify all TileListeners, which hopefully will result in GUIs being closed or disabled or whatever.

After that, the Tile will know that it is "dead", and all methods will result TileDestroyedExceptions.  So when dealing with a Tile you always have to be ready for the possibility of TileDestroyedExceptions.

So the propagation of the ItemDestroyedEvent is:

Item ---> Agent ---> ClientContext ---> Tile ---> TileListeners

The TileListener interface

A TileListener wants to know everything that happens to a Tile, which amounts to these methods: Note that the first three methods will only be used if appropriate caching or precaching has been set in the Tile (although not necessarily set by this TileListener).

Implementing the remote interfaces

The Agent interfaces

The MainAgent is the interface that the ClientContext communicates with.  As few calls to this as possible should be made, with as small arguments as possible.  The ClientContext can also use the MainAgent to retrieve other Agents, for example EventAgents and MapAgents.

The decision of which agent should be spawned seperately is a matter of performance.  I have not thought that deeply about this, but it feels like the most important consideration is how many times the new agent will be requested.  For this reason, the PropertyAgent will be merged into the MainAgent interface, since otherwise new PropertyAgents would have to be opened very often.  EventAgents and MapAgents, however, are usually only opened when you first start your client, and perhaps in special cases like if you possess someone or cast a spy spell on something.

MainAgent property-related methods

The following property handling methods will be suitable for MainAgent.  Note that these can be changed quite easily, since only ClientContext will be talking to the MainAgent.

Summary of layers

The important thing here is the dependency arrows.  This means that the server itself is totally independent of the internet communication, the client abstraction layer is immune to changes in the client implementation layer, etc.

Also, the layers can limit "change propagation".  For example if we were to completely redo the remote interface layer, this change will hopefully be "absorbed" by the client abstraction layer and not affect the client implementation layer.  On the server side it will affect the agent abstraction layer, but not the server itself.

Wow, did I write all that?  Wow, is it evening already?  Where did all day go...?

This design stuff is really the hard part.  Implementation is easy in comparison!  If the design is good, that is... :o)


Henrik Kniberg

Last updated: