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:
-
New items that the character sees
-
Items referred to in an Event that the client receives
-
Items included as part of a property of another item (if I ask for
the "location" property of item A, the value of this property will in itself
be a new item reference). Another example could be if I ask for the
contents of a bag - these will of course be item references as well
-
When the server tells the client that an item has been destroyed.
And client -> server examples:
-
Items included as parameters in a new action (like pick up item
#23).
-
When the client asks the server for an item property (name of item
#23 for example)
-
When the client tells the server that an item is no longer visible in
the client. This means the server will no longer send updates about
that item.
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:
-
ItemReference - includes integer ID
-
ItemID - nothing extra added
-
ItemHeader - array of property values added
-
DetailedItemHeader - includes info about which properties are cached
and which aren't
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:
-
Object getProperty(int property) - retrieves a property value.
The Tile may or may not have these cached locally. This is a one-shot
call and will not result in any new dependencies.
-
ItemProperties getProperties(int[] properties) - same as above,
but for multiple properties. ItemProperties is a hashtable-like class
that maps property constants (int) to property values (Object).
-
ItemProperties getProperties() - same as above, but for all
properties the item has.
-
Tile[] getChildren() - returns an array of the children of this
Tile. This may or may not be cached locally.
-
void setPropertyCaching(TileListener caller, int property, boolean cache)
- This allows a component to explicitely ask the tile to cache (or not
to cache) a property. If cache=true then this property will be fetched
immediately and cached for the future, i.e. the tile will ask the server
to send updates on that property. The caller parameter is used to
make sure that multiple users of a single Tile do not mess things up for
each other. If one caller says "cache the location property", and
another says "stop caching the location property" then the second request
will be ignored. As long as at least one TileListener wants the property
cached, it will be cached.
-
void setPropertyCaching(TileListener caller, int[] properties, boolean
cache) - same as above, but for multiple properties.
-
void setPropertyCaching(TileListener caller, boolean cache) - this
will set the caching for all properties of the item to true or false.
-
void setChildrenCaching(TileListener caller, boolean cache) - This
is similar to above, but applies to the children of this Tile. If
cache=true then the Tile will make sure it is kept up-to-date on which
children this Tile has, so that it can answer getChildren() quickly without
having to ask the server. Note - the properties of the children
have nothing to do with this.
-
void addTileListener(TileListener listener) - The TileListeners
will be notified when the item that this tile represents is destroyed.
Any updates on properties cached will also be reported to the TileListeners.
Note - this means even if I only told a tile that I want to cache the name
property, I might receive a location property update because another TileListener
requested that (or because one of the agents assumed this from start).
So a TileListener should not be surprised if it receives "extra" property
updates that hasn't asked for.
-
void removeTileListener(TileListener listener) - This will remove
the given TileListener, and also remove all caching requests originating
from this TileListener. That means if this TileListener was the only
one that wants the item name cached, that caching will now be removed (the
server will be notified that we no longer want this cached).
-
ItemID getItemID() - used when you for example want to include this
Tile as an action parameter. All Action parameters will be ItemIDs
since this is all the server needs or wants to know.
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:
-
Tile clientContext.getTile(ItemID item) - creates a new tile or
retrieves an existing one, if found in the TileDictionary.
This method is simple, since it doesn't have to worry about item properties.
Here is what will happen
-
If a Tile for this item is already present in the TileDictionary then:
-
Return a reference to the existing tile
-
If a Tile for this item is not already present in the TileDictionary
then:
-
Create a new Tile for this item
-
Return a reference to this new tile
To retrieve a Tile for an ItemHeader, these methods must be used instead:
-
Tile clientContext getTile(ItemHeader item, boolean cachedProperties)
-
Tile clientContext getTile(ItemHeader item, boolean[] cachedProperties)
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:
void setPrecaching(int property, boolean cache).
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:
-
void itemPropertyChanged(ItemID item, int property, Object newValue)
- used when an item property changes in the server. ItemID is used
because the server knows that the client must already have that item in
it's TileDictionary.
-
void itemChildAdded(ItemID item, ItemReference child) - used when
an item receives a new child. The item is assumed to already be in
the TileDictionary, but the child may be something new, so the child is
sent as an ItemReference.
-
void itemChildRemoved(ItemID item, ItemReference child) - used when
an item loses a child.
The Tile needs a similar set of methods for receiving these updates
from the ClientContext:
-
void propertyChanged(int property, Object newValue)
-
void childAdded(Tile child);
-
void childRemoved(Tile child);
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:
-
void TilePropertyChanged(Tile tile, int property, Object newValue);
-
void TileChildAdded(Tile tile, Tile child);
-
void TileChildRemoved(Tile tile, Tile child);
-
void TileDestroyed(Tile tile);
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.
-
Object getItemProperty(ItemID item, int property, boolean cache) -
If cache=true then the server will update it's ItemDictionary so that further
changes in that particular property will be forwarded to the client.
-
ItemProperties getItemProperties(ItemID item, int[] properties, boolean
cache) - Same as above, but for multiple properties.
-
ItemProperties getItemProperties(ItemID item, boolean cache) - Same
as above, but for all properties.
-
ItemReference[] getItemChildren(ItemID item, boolean cache) - Returns
the collection of children that the given item has. The children
that previously existed in the ItemDictionary will be sent as ItemIDs,
while "new" children will be sent as ItemHeaders.
-
ItemHeader[] getItemChildren(ItemID item, boolean cacheChildren, int[]
properties, boolean cacheProperties) - Same as above, but ItemHeaders
(i.e children that are "new" to the client) will include the given properties.
If cacheChildren=true then the client will be notified when children are
added or removed in the future. If cacheProperties=true then the
client will be notified of property changes within the given set of children
in the future. Not that ItemHeaders are returned here, because the
properties will be included even if the client already had that item reference.
-
ItemHeader[] getItemChildren(ItemID item, boolean cacheChildren, boolean
cacheProperties) - Same as above, but for will include all properties
of the children.
-
PropertyViewer getPropertyViewer(ItemID item) - Returns a property
viewer for the given item, to be used when you want to display the properties
of an item in a panel of it's own, with a specific GUI for that type of
item.
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.
-
Client implementation - These are the GUI components, mostly implemented
as JavaBeans. They use the ClientContext and Tiles to interact with
the server, and are independent of the client interface layer. If
you look at the dependency arrows, you will see that this layer has no
dependency arrows pointing to it, which means it can be modified without
affecting any other code.
-
Client abstraction layer - the purpose of this layer is to make
implementation of the client easier. All optimization and minimizing
of net traffic from the client side is done here, including caching.
It also isolates the client implementation layer from possible changes
in the client interface layer, which may be necessary for performance fine-tuning.
-
Remote interface layer - this is a set of remote Interfaces that
represent the functionality that the client and agents expose to each other.
The job of this layer is to translate method calls into either RMI or Socket
calls, depending on which communication layer is used. Other alternatives
are UDP, for some purposes. The actual translation of method calls
in this layer is done using stubs and skeletons that implement the remote
interfaces. The stubs and skeletons represent remote surrogates of
the "real" clients and agents that they represent.
-
Communication layer - this is where the actual tranport of data
over internet is done. We will not have to implement this, but we
will have to select which communication layer interface we want to use.
The options are RMI, Sockets, UDP, or a mix of these. If we use RMI,
then the remote interface layer is already done for us (except the actual
interfaces), otherwise we have to implement the remote interface layer
ourselves (which means creating stubs and skeletons ourselves).
-
Agent abstraction layer - this is where server-side optimization
is done, such as keeping track of the state of the client and exactly what
information should be sent when. It also translates client requests
into actual operations upon items in the server, and translates events
from items into remote interface method calls. This effectively isolates
the server from internet-related issues. This layer has no dependency
arrows pointed at it, so it can be changed in any way without affecting
or invalidating any other code.
-
Server implementation - This is the actual world, which runs independently
from internet related issues and does not care about any of the other layers.
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: