[Jaffa Software]

Writing Client-Server Apps in WimpWorks

An Introduction to WWHouse and WWBounce

Charles Talibard, 13-Jul-1998

Introduction

Whilst in a state of utter boredom, I decided to install BeOS on my PC. Amongst the many demo applications that came on the CD one impressed me because of its simplicity and elegance, and besides it was totally cool. It was a simple bouncy ball program. When run, it opened a window and a ball bounced around in it - not that impressive in itself. However, when a second copy was run, its window did not contain a ball and the ball from the original was free to bounce in and out of the two windows. Many windows could be opened, each one a different task, and the ball could bounce between them.

I then realised that this program would be really simple to implement in WimpWorks under RISC OS. The main emphasis would be on the method of communication between the different tasks. It was decided that the window or 'room' with the ball should always be on the top of the window stack, and when the ball hits the edge of the window, if another room was below, then the ball would 'fall' into it. In other words, the windows should overlap, and the position and size of a window on screen would have to be available to each room task.

One method would be to have the exact position of a room broadcast to every other room running at the time. When a new room was loaded it would have to receive all the information about all other rooms and their positions on screen. Which task(s) would send this information? Whenever a room window was dragged its position would need to be sent to every other room and as a result the thing could get very slow. Also, there would be a lot of redundant data present - every single task keeping the same information.

Another approach would be to have one room keeping all the information. The other rooms could ask it to decide if a ball could leave when it bounced, or not. The trouble with this method is obvious. What happens when the task with all the data is quit? Another room must be designated to hold the information, and it must then have all that data passed to it.

Another problem not addressed very adequately by either of these solutions is how to identify each room. The best method would be to number each room, but how does a new room establish what ID number it should have.

Enter the Client-Server Model

The Client-Server model is simple, and for situations like this it is also the best solution. In its simplest form the client server model consists of a single server application with a number of different client applications which connect to / communicate with it in order to send or receive information. Usually the information will be accessible by more than one of the clients, allowing them to share data. More complex client-server systems can exist over networks with different clients running on different machines.

[Diagram of a server topology]

Fig. 1 : A Server with four clients

For the purposes of our bouncy ball program the server can be seen as a house which contains a number of rooms. When each room moves it tells the house where it is. When the ball bounces on the side of the room the server receives a message to that effect and decides whether it can fall into another room. When a new room (the client in our model) is started it sends a message to the server informing it that a new room is present and the server can then reply, telling the new client if it has the ball or not and what room number it is.

Using this method the number of messages can be reduced and the data replication problems are removed. Also, because the server keeps a record of all the clients that are present it is very easy to assign unique IDs to the new clients.

Communications Protocol

Before coding it is always good to design and plan a piece of software (though few of us ever do). However, with client server applications it is more or less impossible to produce a good product without planning part of it. Most importantly, you should decide on a protocol for communicating between a client and the server. With !WWBounce and !WWHouse we decided for example that when the ball bounced it should send a bounce message to the server with the position of the ball and the velocity vector for its direction. The format for all commands and what arguments they require should be decided on before coding. That way each command that a task will receive can correspond to a function or procedure within that task. The design should also be made as flexible as possible so that mistakes in the design can be changed easily, and additions can be made without a total re-write.

Inter-task Communication using ActiveApps

WimpWorks includes direct support for ActiveApps, Jaffa Software's application communication system, so the message passing is easy. One thing to note is that applications are sent a message based on their name. When the server wants to communicate with a specific client all clients will receive the message (unless it is a reply). This means that in the communications protocol you should allow a client to determine if the message it receives is actually for it.

This behaviour is a deliberate feature of the way ActiveApps works. If you wished to reduce the number of messages leaving the server you would have to store the wimp task handles (unique identifiers) of all the clients and use a SYS "Wimp_SendMessage" for all communications instead of an ActiveApps command. To do this you would have to create a procedure to be called in response to 'POLL called' and catch poll codes 17,18 (and possibly 19).

As the number of commands that the server needs to send to the clients is very low volume in our example we used ActiveApps. All ActiveApps commands are sent and received as strings so we designed a protocol where all commands are represented as a string of the following format:

[Command format]

Fig. 2: The WWBounce and WWHouse Command Format

The command is a 5 letter word such as 'HELLO'; all arguments and the command name are separated by 1 or more spaces. So for example, when the client receives the command


'HELLO 4 N' 

...as a reply to its 'HELLO' command. It must get the number and the Y/N boolean character from the string. Note that 'HELLO 4 N' and 'HELLO    4  N' are equally valid in this example.

In order to aid the decoding of messages we wrote a string tokeniser. In general, a tokeniser takes an input of some kind and splits it up into tokens. In this context, tokens are considered to be the fundamental objects of a language. Depending on the application the tokens will be of different types and it is the job of the tokeniser to decide what token type is next in the input and what value it has. An example of this might be a program to calculate the value of a numerical expression is. For example:


((1 + 3) * 6) / 2

The tokeniser might well have to split the input up into tokens of type operator, number, or parenthesis. In the above example the tokeniser might return:

However, all we need to do is establish the individual components of a sentence. So our String Tokeniser will simply return us each word from a string. The tokeniser function removes and returns the first token from the command string. So if we had a variable, message$, equal to 'HELLO 4 N', and we passed to our tokeniser like this:


token$ = FN StringToken(message$)

token$ would become the first token (word) of message$ which is 'HELLO' and message$ would become '4 N'

This function is used to decode the commands sent to either the client or server. A simple call to FN StringToken will give us the 5 letter command and leave the next token in the input as the first argument.

Each successive use of the tokeniser strips off the next argument so that it can be used by the client or server. Integer or floating point arguments can be converted from strings using the BASIC command VAL or, for clarity purposes, a function ValToken could be written:


FN ValToken(RETURN c$)
=VAL(FN StringToken(c$))

Linked Lists and Client Reordering

The server must keep a list of all the clients that are currently active. To do this each client must tell the server when it starts and when it is dying. However, because the server will not know how many clients it will be storing data for, and because it is bad practice to limit your system to a maximum number, an array is not really a sensible solution. A linked list is the only real solution. For a detailed tutorial on linked lists see the Application Note "How to Implement Linked Lists in WimpWorks".

With !WWBounce and !WWHouse one further step was required. When a client or room dies, not only must it be removed from the linked list of clients but, as the room numbers are being displayed in the windows, each client after it in the list needs a new number. It would look wrong if there was a series of windows on screen with titles 'Room 1', 'Room 2', 'Room 5', 'Room 6', 'Room 7'. Also, if the room with the ball is quit then we need to give the ball to a different room/client.

It was decided that each client would be identified by its position in the list. That way the room number and the unique client-ID number could be combined and we could just use the client's room number as an index into the list. This also meant that all we had to do in order to renumber the rooms was to send another reply to their "HELLO" as if they had just loaded.

Because the expected reply to the HELLO command includes a field declaring whether that client should start the ball or not, we could also deal with the case where the room with the ball dies by telling one of the renumbered rooms to have the ball.

The command messages used in WWBounce and WWHouse

Instigated by the Client

command : "HELLO"
reply : "HELLO <number> <Y/N>"
<number> = the ID of the room
<Y/N> = Y if it has the ball
<Y/N> = N if it doesn't have the ball
use : The client sends this command to the server in order to establish what its ID is and whether it has the ball. The server may reply many time to this command in order to allow the rooms to renumber when one dies.
 
command : "DYING"
reply : N/A
use : the client sends this command to the server to indicate that it is no longer active and should be removed from the list of clients that the server knows about. The server will then renumber all of the remaining rooms by replying to the "HELLO" command again.
 
command : "BOUNC <id> <x> <y> <xvel> <yvel>"
<id> = the room that the ball bounced in
<x> = x coordinate of the ball
<y> = y coordinate of the ball
<xvel> = x velocity of the ball
<yvel> = y velocity of the ball
reply : "NOBLL"
use : The room/client sends this command to the server when the ball bounces on the edge of the window. The server will only reply with "NOBLL" if a transition from that window to another was possible.
 
command : "RESIZ <id> <xmin> <ymin> <xmax> <ymax>"
<id> = the room that is being resized
<xmin> = x coord of bottom left corner
<ymin> = y coord of bottom left corner
<xmax> = x coord of top right corner
<ymax> = y coord of top right corner
reply : N/A
use : When the user resizes the window of a client, the bounding box of the window on screen is sent to the server so that it can keep track of where all the windows are.

Instigated by the Server

command : "GETBL <id> <x> <y> <xvel> <yvel>"
<id> = the room that the ball bounced into
<x> = x coordinate of the ball
<y> = y coordinate of the ball
<xvel> = x velocity of the ball
<yvel> = y velocity of the ball
reply : N/A
use : When the server recieves the BOUNC command, if the ball can make a transition into another window then this command is sent to the room that receives the ball.
 
command : "HOUSE"
reply : N/A
use : Used to inform all clients that are waiting for a server that a server is now active.

Conclusion

"The Best Bouncy Ball Program In The World... Ever!" can be downloaded now - it requires RISC OS 3.10 or above.