| |
Client/Server
TicTacToe
In this assignment, we'll build on what we did in the
earlier TicTacToe assignment. This time, we'll turn it into client/server
version of TicTacToe!
Requirements
We would like to create a TicTacToe server,
which provides the ability to play TicTacToe to a number of TicTacToe
clients.
The TicTacToe server runs on some machine in the network,
and each TicTacToe client connects to it from another
(possibly the same) machine within the same network. The
server pairs up the clients, and decides which one will play
cross and which one nought. The server is capable of running
multiple simultaneous TicTacToe games (subject to resource
limitations, such as memory).
The TicTacToe Client
A single client for this version of the TicTacToe game
should look something like this:
(that's an image, not a live applet, in case you tried to
click on it!)
This should look very reminiscent of the way things were
in the earlier TicTacToe assignment. The way the game is
played is the same -- it's just that the two Boards are now totally
separated from each other. You should be able to reuse lots
of the code that you created earlier, with relatively minor
modifications.
Note that the above image shows an applet
version. However, you will almost certainly wish to use an application
instead, to avoid the problems related to applet security
restrictions. An unsigned applet is restricted to making
network connections only to the machine from which it was
downloaded. Unless you have a web server available to you on
which you can place the TicTacToe server program, it will be
difficult to make things work with applets. (Even if you do
have such a web server available to you, it's still more of a
pain to set up, so I still recommend you use applications).
Of course, you can create applets that you intend only to run
via appletviewer, which does
not enforce the security restrictions that a true browser
does.
The TicTacToe Server
The TicTacToe server has no graphical user interface. Its
source code will initially look very much like one of the
multi-threaded server examples in the networking section of
this course. You can start off with one of those examples and
work from there -- be sure to use a multi-threaded version of
the server!
The TicTacToe Protocol (TTTP)
In order for the TicTacToe clients to communicate with the
TicTacToe server, and vice versa, there must be a
well-defined protocol that they must follow to ensure that
the communication is reliable and properly understood. The
use of a protocol is a common requirement for such
client/server communication. For example, a mail client
typically talks to a mail server using the SMTP (Simple Mail
Transport Protocol), a browser communicates with a web server
using the HTTP (HyperText Transport Protocol), and so on.
We'll use the TicTacToe Protocol (TTTP), which is a simple
(probably too simple, but it's OK for this assignment!)
protocol whose features are described in the TicTacToeProtocol
interface:
package tictactoe;
/**
* Interface to define the TicTacToe protocol
* used by client/server TicTacToe.
*
* @author Bryan J. Higgs, 24 February 2000
* @version 1.0
*/
public interface TicTacToeProtocol
{
/////// Client to server commands ///////
/**
* Client connect request.
* <p>
* Expected response:
* <br>CONNECTED
* <br>When both players have connected,
* START_GAME to both players.
*/
public static final int CONNECT = 1;
/**
* Client disconnect request.
* <p>
* Expected response: DISCONNECTED
*/
public static final int DISCONNECT = 2;
/**
* Client Autoplay request.
* <p>
* Expected response:
* <br>Autoplay events
*/
public static final int AUTOPLAY = 3;
/**
* Client cross move request.
* <p>
* Followed by row and column of square.
* <p>
* Expected response:
* <br>CROSS_IN_SQUARE, if valid move
* <br>nothing, otherwise.
*/
public static final int CROSS_MOVE = 4;
/**
* Client nought move request.
* <p>
* Followed by row and column of square.
* <p>
* Expected response:
* <br>NOUGHT_IN_SQUARE, if valid move
* <br>nothing, otherwise.
*/
public static final int NOUGHT_MOVE = 5;
/**
* Client get game status request.
* <p>
* Expected response:
* <br>IN_PROGRESS, CROSS_WINS,
* NOUGHT_WINS, or STALEMATE
*/
public static final int GET_STATUS = 6;
/**
* Client restart game request.
* <p>
* Expected response: CLEAR_BOARD
*/
public static final int RESTART_GAME = 7;
/////// Server to client responses ///////
/**
* Server response to client CONNECT request.
*/
public static final int CONNECTED = 101;
/**
* Server response to client DISCONNECT request.
*/
public static final int DISCONNECTED = 102;
/**
* Server response to start game.
* <p>
* Sent when both players in a game have connected.
* <p>
* Followed by a boolean value:
* <br>true if player plays cross
* <br>false if player plays nought
* <p>
* Client is expected to set itself to play X or O.
*/
public static final int START_GAME = 103;
/**
* Cross selected square.
* <p>
* Followed by row and column of square.
* <p>
* Client is expected to display a cross in the
* specified square.
*/
public static final int CROSS_IN_SQUARE = 104;
/**
* Nought selected square.
* <p>
* Followed by row and column of square.
* <p>
* Client is expected to display a cross in the
* specified square.
*/
public static final int NOUGHT_IN_SQUARE= 105;
/**
* Game status response.
* <p>
* Followed by an int containing the status value
* (IN_PROGRESS, WIN, LOSE, STALEMATE)
*/
public static final int RETURN_STATUS = 106;
/**
* Clear Board response (request to client).
* <p>
* Client is expected to clear the board.
*/
public static final int CLEAR_BOARD = 107;
//////// Server to client errors ////////
/**
* Error encountered.
* <p>
* Followed by explanatory string.
*/
public static final int ERROR = 901;
/////////////////////////////////////////////////////
/**
* Default port to use for client/server communications.
*/
public static final int DEFAULT_PORT = 5000;
} |
Note that this interface simply defines a number of
constants, and no methods whatsoever. While you could have
one or more of your classes implement this protocol if you
wish, it isn't necessary; you can simply have your code refer
to the its constants directly using TicTacToeProtocol.CROSS_IN_SQUARE,
etc.
Let's illustrate how the protocol works by following a
typical TicTacToe game session between two clients and the
server:
Client
A |
Client
B |
Server |
Connects to server socket |
|
Accepts client connection. |
Sends a CONNECT
request to the server. |
|
Receives the CONNECT
request, and sends a CONNECTED
response to client A. |
|
Connects to server socket |
Accepts client connection. |
|
Sends a CONNECT
request to the server. |
Receives the CONNECT
request, and sends a CONNECTED
response to client B.
Pairs up the two clients, creates a game for them to
play, then sends a START_GAME
to both clients, telling client A to play X and
client B to play O. |
Receives START_GAME ,
determines that is playing X, and enables the user
interface for play. |
Receives START_GAME ,
determines that is playing O, and enables the user
interface for play. |
|
User clicks on square. This causes a
CROSS_MOVE
(followed by row and column of square) to be sent to
server. |
|
Receives CROSS_MOVE
and row and column information, and calls the game
appropriately. The game determines whether the move
is legal; if it is, a CROSS_IN_SQUARE
message (with row and column) is sent to both
clients. |
Receives CROSS_IN_SQUARE
(with row and column), and causes the cross image to
display in the appropriate square. |
Receives CROSS_IN_SQUARE
(with row and column), and causes the cross image to
display in the appropriate square. |
|
|
User clicks on square. This causes a
NOUGHT_MOVE
(followed by row and column of square) to be sent to
server. |
Receives NOUGHT_MOVE
and row and column information, and calls the game
appropriately. The game determines whether the move
is legal; if it is, a NOUGHT_IN_SQUARE
message (with row and column) is sent to both
clients. |
Receives NOUGHT_IN_SQUARE
(with row and column), and causes the nought image to
display in the appropriate square. |
Receives NOUGHT_IN_SQUARE
(with row and column), and causes the nought image to
display in the appropriate square. |
|
|
The process
repeats until one of the clients wins, or stalemate
occurs. |
Detects that X or O has won, or
stalemate has occurred. Sends a RETURN_STATUS
message to both clients, with the appropriate code. |
Receives RETURN_STATUS
message and displays the appropriate message in the
text field. |
Receives RETURN_STATUS
message and displays the appropriate message in the
text field. |
|
User clicks on Restart button. This
causes a RESTART_GAME
message to be sent to the server. (Either client
could do this.) |
|
Receives the RESTART_GAME
message. Tells the game to restart, which causes a CLEAR_BOARD
message to be sent to each client. |
Receives CLEAR_BOARD
message, and clears its board. |
Receives CLEAR_BOARD
message, and clears its board. |
|
|
User clicks on Auto Play button.
This causes an AUTOPLAY
message to be sent to the server (either client could
to this). |
Receives AUTOPLAY
message, tells the game to start autoplay, which
causes the appropriate CROSS_IN_SQUARE ,
NOUGHT_IN_SQUARE
messages to be sent, to both clients until the game
ends, and then sends the appropriate RETURN_STATUS
message, indicating stalemate has occurred to both
clients. |
Receives the CROSS_IN_SQUARE ,
NOUGHT_IN_SQUARE ,
and RETURN_STATUS
messages, and updates its user interface
appropriately. |
Receives the CROSS_IN_SQUARE ,
NOUGHT_IN_SQUARE ,
and RETURN_STATUS
messages, and updates its user interface
appropriately. |
|
Threading Requirements
You will find that both the server and the client will
need to be multi-threaded.
It should be clear why the server must be multi-threaded:
As each client connects to the server's port, the server will
accept the connection, and will then create an instance of a
class (I called mine GamePlayer)
to represent the client within the server program. It will
start a thread for each of these instances, and the instance
will be solely responsible for communicating with its client.
After the first client connects, its GamePlayer instance is
not yet ready to play, because it does not yet have an
opponent, so it does not yet create a game. Once the second
client connects, its GamePlayer instance takes responsibility
for creating the game (it's a GameImpl on the server), and
then associating the two GamePlayer instances together (I had
an m_opponent field to keep track of who each GamePlayer's
opponent was). Finally, once the GameImpl is created, and
everything is set up between the two GamePlayer instances,
the appropriate messages can be sent to each client to tell
them that the game has started.
It may be less clear why you need the client to be
multi-threaded, until you try to implement it. You will find
that you will need to have code that stays in a loop, waiting
for input from the client. If you do this in a
straightforward way, you'll likely find that the GUI won't
respond, because the loop, which mostly blocks waiting for a
message from the server, doesn't allow events to be handled
properly. I wrote my client so that the loop was running in a
separate thread, and it worked pretty well.
Note: If
you decide to use JFC/Swing for your client GUI, you
need to be aware that, unlike AWT, JFC
is not generally thread safe.For
details, see the JavaSoft articles on this
topic. My implementation used AWT.
Changes Required to Your Earlier Classes
You'll need to make some changes to your earlier classes:
First, we're changing who determines which player plays X
and which plays O. Earlier, this was determined by passing a
boolean as a parameter in the constructor for Board. Now,
it's the server who decides, so we need to make some changes
to accommodate that:
- Change the constructor for the Board class to be:
public Board(Game game, Image noughtImage, Image crossImage)
That is, eliminate the playerIsCross
parameter.
- Add two new methods to the Board class:
public void playsCross()
public void playsNought()
which set the value of a boolean instance variable
to the appropriate state (you probably already have
this boolean variable from earlier). These methods
are used to set the state of the Board when the
server tells the client which of X or O to play.
- Add two more event types to the GameEvent class:
public void playsCross()
public void playsNought()
to the GameListener interface, to the GameAdapter
class, to the GameEventMulticaster class, and to any
other GameListeners you wrote for the earlier version.
- Add the necessary code to the GameImpl class to
dispatch these two new event types to their
appropriate methods. (This is optional for the
GameImpl class; I didn't find it necessary to do
this.)
Second, you'll need to change the GUI from the earlier
TicTacToe game to look show a single Board and text area (see
the image above for how I did it).
Implementation
Here's how we're going to set things up:
So, here is the general strategy:
GUI code/Board
calls: |
GameClient
which sends messages to: |
GamePlayer
which calls: |
GameImpl |
Board
receives GameEvents from: |
GameClient
which receives messages from: |
GamePlayer
which receives GameEvents from: |
GameImpl |
In other words, we're replacing the previous interactions
that Board (and other GUI code) had directly with GameImpl with
a mechanism that looks the same as before to Board, and to
GameImpl. However, the new mechanism uses a
message-based protocol (TTTP) to communicate between the
client and the server. (The mechanism is represented by the
gray background cells, above.)
Note: The fact that this message-based protocol is being
used should be totally transparent to the Board class
(and to other GUI components), and also totally
transparent to the GameImpl class!
This transparency is achieved by providing a GameClient
class on the client side, and a GamePlayer class on the
server side, and by having these two classes be totally
responsible for the messages passed between them:
- GameImpl fires GameEvents, which GamePlayer listens
for.
- GamePlayer responds to each GameEvent by sending its
GameClient an appropriate message
- GameClient receives the message, and fires the
appropriate GameEvent, which Board listens for.
- Board receives the GameEvent as if it had received it
directly from the GameImpl. (It doesn't know the
difference.)
- Board (and other parts of the GUI), on detecting AWT
events (such as a mouse or button click) invokes
various methods in GameClient, which causes an
appropriate message to be sent to its GamePlayer.
- GamePlayer receives the message, and calls its
GameImpl at the appropriate entry point.
- GameImpl was invoked as if it was called directly
from Board (It doesn't know the difference.)
The GameClient Class
Implementing GameClient is relatively easy, since it
involves cutting and pasting, and then stripping out some
code:
- Make a copy of GameImpl.java into GameClient.java,
and change every occurrence of the string
"GameImpl" to "GameClient".
- Change the GameClient constructor to:
public GameClient(String host, int port)
where the host and the port are used to connect to
the server on that host, via that port.
- Ensure that the two new events, PLAYS_CROSS
and PLAYS_NOUGHT,
are properly dispatched (use the original code as a
guide).
- Remove the isWon() and bestMove() methods.
- Remove everything in the private data section, except
m_gameListener.
- For every method present in GameClient that is
required because it implements the Game interface,
remove the body of the method (the code between the
open curly and close curly). Do not touch
addGameEventListener or removeGameEventListener.
You will have to retain some code (like
return
true; ) to get GameClient to
compile. (Note that the computedMove
and status
methods will not be used in GameClient, although they
must be present to satisfy the requirements of
implementing Game.)
- Remove the body of the GameClient's
run
method.
This should give you a class that you can compile.
However, it won't do anything yet, of course.
You will then have to add the necessary code to cause the
GameClient to:
- Connect to the server and send the CONNECT message
(Where should this code go?)
- Loop, waiting for messages from the server (Where
should this code go?)
- Cause appropriate messages to be sent to the server
when certain actions are requested.
The GamePlayer Class
GamePlayer is the class that acts on behalf of a GameClient, on the
server side.
Here, you'll have to do more original work. You can start
by cloning the Board class, if you like (with appropriate
name changes, of course), and stripping out unneeded code
along similar lines to above. You won't need the Square
class, nor any GUI-related stuff in GamePlayer.
- GamePlayer should implement GameListener, and should
be capable of running in a thread.
- The GamePlayer constructor won't have the same
parameters as Board's constructor, because GamePlayer
doesn't deal with Images, and also the associated
Game won't be in existence yet when the server
creates the GamePlayer -- it gets created and
associated later. You should add methods getGame()
and setGame() so that the Game can be set later, and
then retrieved.
- Essentially, any code in Board that involves GUI
changes should be converted to the appropriate
sending of a message to the client.
- It may just be simpler to start afresh, rather than
doing major surgery on a copy of the Board class.
This should give you a class that will compile, but won't
do anything, yet.
You will then have to add the necessary code to cause the
GamePlayer to:
- Be constructed with the appropriate parameters to
allow an association with the opposing GamePlayer,
and to create the associated GameImpl when both
clients have sent their CONNECT requests.
Hint: I
had the GameServer do the work to pair the
GamePlayers up, and to pass in the appropriate
parameters to their constructors.
- Be fired up (by the GameServer) in a thread
- Loop, waiting for messages from the associated client
(Where should this code go?)
- Cause appropriate messages to be sent to the client
when certain events are encountered.
Hint:
I suggest using DataInputStream and DataOutputStream to
communicate between client and server.
Putting it All Together
The first warning I want to give you is:
Don't try to do
everything at once!
To maintain your sanity, you should take small steps, get
something working, and keep building on your successes until
you're done. (There's nothing like a long string of failures
-- or even one very long failure -- to increase your
frustration level, raise your blood pressure, and generally
make you want to hit something -- or someone!)
This may sound obvious, but it gets harder to do when
you're working on both a client and a server, where you can't
really test something until you've implemented the necessary
support on both ends! So, don't try to add too much
functionality at once, or you'll likely go crazy!
I suggest a development
sequence like:
- Write the bare minimum
server that will accept a connection from your bare
minimum client, and get that working. This will
probably involve getting both your GameClient and
your GamePlayer classes working, in their appropriate
threads, at a minimum level.
- Add the CONNECT --
CONNECTED functionality and get it working
- Add the START_GAME
functionality and get it working.
- Try getting AUTOPLAY
working. This is a bigger step, because it involves
implementing support for the CROSS_IN_SQUARE,
NOUGHT_IN_SQUARE, and RETURN_STATUS messages, and
getting them to display the images in the client, and
the appropriate text for the end of the game in the
text area on the client.
- Add support for
RESTART_GAME, and the resulting CLEAR_BOARD messages.
- Then add support for
CROSS_MOVE and NOUGHT_MOVE; once Auto Play is
working, this should be relatively simple to add.
- Finally, add support
for GET_STATUS, which should be relatively trivial at
this point.
Some more hints:
- Using a debugger on
both sides of a client/server link can be difficult. You may wish to
use a debugger only on one side
at a time, to avoid getting confused. It may be that your IDE can't support more than one
executable running in
its debugger at a time, anyway. You can always switch
from running the client in a debugger to running the
server in a debugger, as required.
- Make a habit of using System.out.println(...) calls in appropriate parts
of your code. They can be very helpful, and give you
faster feedback than often is possible in a debugger. This is
especially beneficial when you're dealing with multi-threaded programs,
where using the debugger is often not very practical.
- I ran my server process
from one MS/DOS window on my Windows NT system, one
client from another MS/DOS window, and the second
client from my Visual Cafe environment, with the
debugger. (All on the same machine.) That, together
with useful output on the MS/DOS windows from System.out.println() calls, worked well.
Results
At the end of this assignment, you should have a server
which supports connections from pairs of clients. Each client
should be independent of all other clients (other than
interacting with the client's opponent). Each pair of clients
should be playing a game independent of all other games.
I'd like to see some simple trace output (those
System.out.println() statements) to show which clients
connected, were paired up, and the various moves that were
made.
I'd like to see that your server can support more than
just one pair of clients playing a game.
|