CMSC 433, Spring 2006

Programming Language Technologies and Paradigms

Project 6

Due May 10, 2006

Updates

Overview

This project completes the distributed gaming system you started in Project 5. Specifically, you will be adjusting part 2 slightly, and completing parts 3 and 4 of the project:

  1. Transactional State. While there is conceptually a single shared state managed by the state server, the player servers have their own copies for performance reasons. In project 5, you implemented transactional state using boolean grids. In this project, you will slightly extend your implementation to support an additional command for synchronizing new players into a running game. We will discuss this change while explaining part 3 in the detailed description (overviewed next).
  2. Concurrent communication patterns. With transactional state in hand, you will implement the communication patterns for maintaining and updating state within a multi-threaded context. There will be one thread for the state server, and one thread to represent each player server.
  3. Distributed communication. With multithreaded player support in place, you must extend the code to actually work with distributed messages. This is done by creating "stubs" and "skeletons" to encapsulate the communication between processes, in the style of RMI (but you will implement it "by hand").

2. Transactional State

You can reuse your project 5 implementation for this project, with a couple of changes: Only the last point is not self-explanatory; we explain further in the next section.

If you struggled with Project 5, we will guide you to complete this project both in class and during office hours. Get started soon!

3. Concurrent communication patterns

You will implement a client-server system that supports using different threads for the clients and the server, but runs locally on one site. (In part 4, you will provide support for distributing the application to different sites.) The relevant classes for this part of the project are in the package cmsc433.p6.clientserver.

In this package, you will implement ServerImpl which represents the server.

The server implementation will use a TransactionalState to keep track of the state of the game and a ConcurrentPublisher to notify clients of certain events, such as changes to the game state. To join a game, a client will call the server's register function, which returns a RemoteServerConnection to the client. Thereafter, all communication from the client goes through this object. In particular, the client sends messages to the server using the receive method in the server connection.

The messages that are passed between the clients and the server are called Commands and are located in the cmsc433.p6.commands package. Each of the commands are described in the API. All commands can be sent from the server to the client, while only two types of commands, TransactionCommand and Logout, go from client to the server. Here's the basic communication flow between the client and the server:

A client first invokes register on a Server, which then sends a Welcome command to all registered clients (including the new one just registered) and returns a RemoteServerConnection to the new client.

At any time, a registered client can send a TransactionCommand to the Server, proposing a change to the server's state. (You implemented GridCommand as an example of a TransactionCommand in part 2). If this change is successful, the Server sends the same TransactionCommand to all registered clients so they can update their states. Otherwise, it sends an ExceptionMsg back to the client that initially sent the command. When a client first joins a game, it will send a special transaction command that returns the server's current state from which the client initializes its own state. More on this below.

When a client is ready to leave, it sends a Logout command to the server, which responds by sending a Logout command back to the client as a confirmation and a Goodbye command to all the other registered clients.

Sample Clients

We have provided you with some client implementations to help you understand how everything fits together, and to test your implementation. Each client maintains its own 5 by 5 BooleanGrid and connects remotely to a server which maintains a central 5 by 5 BooleanGrid. When the clients attempt to make changes to their grids, these changes should be sent as GridCommands to the central server which either rejects the changes or passes the commands on to all registered clients.

The UIGridClient provides a Swing-based GUI that allows users (even at different sites) to update the grid. Grid updates are done by clicking on a button that corresponds to the cell to be updated. This changes the cell from red/green to orange. It turns to the opposite color (green/red) when the update is successful. UIGridClient has a main() method that can be used to start a server and two clients, both of which connect to the server. Here is a code snippet:

final Server server = 
  new ServerImpl(new BooleanGrid(gridsize, gridsize));

... new UIGridClient("Bill", server) ...
... new UIGridClient("John", server) ...
Thus we start a server, initialized with the game's boolean grid, and then create two clients, both of which will be communicating with the given server. Looking at the constructor for UIGridClient, we see the line
connection = server.register(getName(), this);
This connection object is then used to send commands to the server based on user actions. Likewise, the UIGridClient.update() method is invoked by the server to send information back to the client based on commands it receives.

The other sample client is discussed in the next section.

TransactionCommand types

To use this sample client correctly, you will need to modify your implementation of GridCommand (which implements TransactionCommand) from project 5. In particular, there are now two types of commands: an update command (just as in project 5), and a sync command. This sync variety of the command is sent by a client that has just registered with the server. This can be seen in the sample UI client (the sendSyncCommand method). When the server receives this command, it will modify the command's contents to be the difference between the server's current state and the empty grid, and the command type is modified to be a normal update command. This modified command will then get sent to all clients as a matter of course; those clients having just registered (and thus currently having the empty grid as their state) will thus update their grids to be the server's current state. See the API documentation for BooleanGrid and GridCommand for more information.

4. Distributed communication

The distributed version of this application is built upon the local version in part 3. The classes for this part are in the cmsc433.p6.remote package.

In a distributed setting, you have classes that handle marshalling (stub classes) and unmarshalling (skeleton classes) of method invocations. This means that the "local" client, server, and server connection objects still exist, but are wrapped in a way that they can be used in a distributed environment. Any class wishing to communicate with a remote version of these objects must use the stub class. Any class wishing to be accessed remotely must wrap itself with a skeleton class. A skeleton class has a thread that is always running and accepting remote method invocation requests for the object it is remoting. Port numbers must be decided between the client and server beforehand.

You may have noticed that the client, server, and server connections from Part 3 implement the RemoteClient, RemoteServer, RemoteServerConnection interfaces respectively. These interfaces provide the minimal tasks required by each object for use in a distributed application. They are implemented by the ClientStub, ServerStub, ServerConnectionStub respectively.

The individual operation of these classes is described in some detail in the API documentation. Here will try to provide a picture of how the classes fit together. First ServerSkel is started and listens on a specific port number. Then, a client (possibly running on a different machine) constructs a ServerStub object, providing it with the name of the remote host and the port number. Next, the client program must call register on the ServerStub, as in part 3. Doing this causes the stub to connect to the remote server, and this connection is accepted by the server's ServerSkel.

At this point, the stub needs to send a message to indicate that the local client wants to register with the remote server. To do this, we can use serialization, layering an ObjectOutputStream over the outgoing stream, and then using writeObject as necessary, and similarly layering an ObjectInputStream over the incoming stream and using readObject.

Once the connection is established in this way, the server stub needs to send along information so that the skeleton can register the local client. The tricky part is what to do with the Client object. The skeleton will need to create a ClientStub to pass into the server.register() function. This stub needs to be able to communicate back with the remote client. Thus, there should be a corresponding ClientSkel to receive these communications, which forwards them to actual Client object. We do this by reusing the ServerSkel's existing OutputStream for ClientStub and ServerStub's existing InputStream for ClientSkel. Likewise, we will need to create ServerConnectionStub and ServerConnectionSkel objects; these use the ServerStub's OutputStream and ServerSkel's InputStream, respectively. This is possible because Client and RemoteServerConnection communicate asynchronously: this is a crucial point to understand. The Javadoc has much more detail about this exchange.

So, just like in the local scenario, there is a server object, client objects, and server connection objects (one per client); however, communication from the client to the server (i.e. registration) goes through the ServerStub to the ServerSkel, communication from the client to the server connection goes through the ServerConnectionStub to the ServerConnectionSkel, and communication from the server to the client goes through the ClientStub to the ClientSkel. This is illustrated in the following diagram:

Distributed Design

Note that the cross-site communication depicted here is using the same connection in all cases. The communication initially takes place between ServerStub and ServerSkel. Once the register method from ServerStub returns, one direction of the connection is used for client to server communication via RemoteServerConnection stub and skeleton and the other direction is used for server to client via the Client stub and skeleton.

You will implement all the classes in the cmsc433.p6.remote package. Detailed descriptions of each class are in its API. Since you will be running in a distributed environment, make sure your code is thread-safe.

There is a thorny issue regarding how to handle exceptions and I/O errors. If a client logs out normally, its associated socket connections are closed and its associated threads are stopped. However, if a skeleton thread has an I/O error, it should still close its sockets. In general, if a client receives an I/O error, communication with that ServerConnection is no longer possible. In such a case, you should just perform cleanup -- i.e., no associated threads or sockets should remain running or open, respectively. Similarly for the server, if a client gives an I/O error, assume the client is gone and perform cleanup, taking care to update any data structures to a consistent state.

Sample Clients

To test your distributed server, we have provided an additional client and some helper classes. To start up a server, you can run the StartServerSkel class. To start up a UIGridClient that connects to this server, run the StartRemoteUIGridClient class. Finally, we have provided another client, AIClient (isn't really that intelligent!), that simply attempts at random intervals to change a random grid position and displays messages if it is successful or unsuccessful. You can start an AI client by running StartRemoteAIClient. Notice that this will create three running programs (if you do all three), while the sample for part 3, above, creates a single program with several threads (hidden within the GUI machinery). We recommend that you look at these example programs to see how the pieces fit together.

Note that the Console view under Eclipse will allow you to switch between multiple VMs. To avoid conflicts with other users (if you are not running on your own machine), use port 433XX where XX is last two digits of your class account login. You can change the default port in the example code, or you can specify the port as a command-line argument.