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:
If you struggled with Project 5, we will guide you to complete this project both in class and during office hours. Get started soon!
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:
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.
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 ServerImplThus 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(new BooleanGrid(gridsize, gridsize)); ... new UIGridClient("Bill", server) ... ... new UIGridClient("John", server) ...
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.
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:
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.
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.